天天看點

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

作者:閃念基因

一. 前言

本篇文章重點介紹HEIC圖檔和無用類檢測的優化實踐。HEIC是High Efficiency Image Format(高效圖像格式)的縮寫,是一種新的圖像檔案格式,它是2017年蘋果公司在iOS 11中引入,用于代替JPEG圖像格式,以更高效地壓縮圖像并減少存儲空間占用。HEIC支援多幀圖像、透明度和16位深度色彩,使得它成為高品質圖像和動畫的理想選擇。本文重點探究HEIC圖檔在百度APP中使用的可行性和包體積收益,驗證HEIC圖檔在Bundle和Asset Catalog的相容性,重點研究了Asset Catalog管理圖檔的機制,記錄了驗證過程中發現的特殊問題和解決思路。無用類則是詳細介紹了如何用靜态分析和動态分析相結合的方式,精簡代碼體積。

測試環境 基本工具
xcode:Version 14.1 (14B47b) sips、convert:圖檔格式轉換工具
Mac:macOS 12.5 (21G72) actool:Asset Catalog内圖檔打包工具
iPhone:iOS10及以上系統 assetutil:car檔案解析工具

二. HEIC圖檔格式轉換和使用方式

丨2.1 格式轉換

有三種常見的HEIC圖檔轉換方式:Mac圖檔轉換功能、Mac自帶sips指令、多平台支援的ImageMagick指令。

丨2.1.1 Mac圖檔轉換功能:

  1. 右鍵圖檔,快速操作—>轉換圖像
  2. 格式選HEIF,圖像大小根據需求選擇
百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

丨2.1.2 sips工具:

sips是一個MacOS自帶的指令行的圖檔處理工具,具有轉換圖檔格式、修改圖檔大小(擴充或者重新采樣縮小圖檔)、修改品質,設定版權資訊等功能。

舉例:sips -s format heic -s formatOptions default [email protected] --out [email protected]

丨2.1.3 ImageMagick工具:

ImageMagick 包括一個用于執行複雜圖像處理任務的指令行界面,以及用于将其功能內建到軟體應用程式中的 API。它是用 C 編寫的,可以在各種作業系統上使用,包括 Linux、Windows 和 macOS。

用法參考:https://imagemagick.org/index.php

需要手動安裝:

brew install imagemagick

convert [email protected] [email protected]

丨2.2 HEIC在iOS中使用

在iOS系統中,ImageIO、Core Image、UIKit、PhotoKit都支援HEIC圖檔。HEIC圖檔可以放Bundle裡也可以放Asset Catalog裡。使用原生方法就可以建立UIImage對象,和JPEG、PNG等圖檔使用方式一緻。

// 加載本地圖檔
UIImage *image = [UIImage imageNamed:@"heifFileName"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];


// 由 網絡請求的 NSData 解碼
UIImage *image = [UIImage imageWithData:heifImageData];           

丨2.3 HEIC圖檔相容性

編碼:

硬編:A10及以上晶片 iOS 裝置(iPhone7)

解碼:

硬解:A9 及以上晶片 iOS 裝置(iPhone6s),配備 6 代及以上 Inter Core 處理(Skylake)。

軟解:iOS12 和 macOS 支援軟解碼,(官方說是iOS11,實測iOS11并不能解碼)

可以調用ImageI/O相關函數擷取支援的圖檔編解碼支援的格式,這裡值得注意的是,在iPhone6p,iOS11.0.4實測不支援HEIC,即調用CGImageSourceCopyTypeIdentifiers();查詢可解碼格式包含public.heic,依舊是無法正常顯示出HEIC圖檔;在iPhone6p,iOS12.5.6測試機上可以正常顯示HEIC圖檔。
//擷取所支援的圖檔格式數組,解碼
CFArrayRef decodeArr = CGImageSourceCopyTypeIdentifiers();
NSArray *decodeUTI = (__bridge NSArray *)decodeArr;
NSLog(@"解碼支援%@", decodeUTI);


//擷取所支援的圖檔格式數組,編碼
CFArrayRef encodeArr = CGImageDestinationCopyTypeIdentifiers();
NSArray *encodeUTI = (__bridge NSArray *)encodeArr;
NSLog(@"編碼支援%@", encodeUTI);           

百度APP最低支援的系統版本是iOS10,iOS10的5s和iPhone6、iPhone6p無法直接解碼HEIC圖檔,這三款機型會受到影響。但是這并不意味着這些機型不能相容HEIC圖檔。正常思路是引入三方SDK,如:SDWebImageHEIFCoder(https://github.com/SDWebImage/SDWebImageHEIFCoder),增加解碼支援,不過引入三方SDK變相增加了包體積,顧此失彼。在測試中發現,将HEIC圖檔放入Asset Catalog管理,是可以在上述三款機型上正常顯示圖檔的。

三. Bundle和Asset Catalog的相容性

在iOS系統中,APP内的圖檔資源可以放Bundle和Asset Catalog。若圖檔放Bundle中,ipa包安裝到裝置上後,圖檔占用的磁盤空間和圖檔實際大小一緻。不過放Bundle的缺點是需要針對不同分辨率的進行放不同大小的倍圖,明顯增加了包體積。

蘋果推薦使用Asset Catalog管理内置資源,包括圖檔資源、音視訊等,同樣也支援HIEC圖檔。Asset Catalog的好處顯然易見,支援app slicing、支援設定拉伸區域、給不同的機型配置不同的圖檔、配置渲染顔色等。最終所有的檔案最終會打包成.car壓縮檔案。

對此,我們選擇了兩張具有代表性的圖檔,log.png是帶有Alpha通道的圖檔和[email protected]是不帶Alpha通道的圖檔。然後分别生成對應的HEIC圖檔log.heic和[email protected],圖檔沒有經過任何其餘壓縮處理。

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

[email protected]

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

log.png

丨3.1 生成car檔案

從Xcode編譯的log中發現,系統使用自有actool工具對workspace内所有的.xcassets壓縮生成一個.car檔案,對于Xcode是否連接配接測試機,編譯Assets.car的參數又有所不同。

對于未連接配接測試機,選擇Any iOS Device(arm64),生成通用的Assets.car檔案,編譯參數如下:

// Any iOS Device(arm64)
/Applications/Xcode.app/Contents/Developer/usr/bin/actool --output-format human-readable-text --notices --warnings --export-dependency-info /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_dependencies --output-partial-info-plist /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_generated_info.plist --app-icon AppIcon --accent-color AccentColor --compress-pngs --enable-on-demand-resources YES --development-region en --target-device iphone --target-device ipad --minimum-deployment-target 16.0 --platform iphoneos --compile /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Products/Debug-iphoneos/ImageDemoS.app /Users/xxxxxR/baidu/personal-code/ImageDemoS/ImageDemoS/Assets.xcassets /Users/xxxxx/baidu/personal-code/ImageDemoS/Media.xcassets           

若連接配接了測試機,則根據測試機的機型和系統生成對應的Assets.car檔案。關鍵參數--filter-for-thinning-device-configuration iPhone7,1 --filter-for-device-os-version 11.4.1,這兩個參數可以解釋HEIC圖檔在iOS11的iPhone6p上的相容性問題。實測中發現HEIC圖檔放Asset Catalog中,實際上是可以在iOS11的iPhone6p上顯示的,隻不過這時候Asset Catalog裡的圖檔已經不是HEIC編碼了。具體的編譯參數如下:

// iPhone6p(iOS11.4.1)
/Applications/Xcode.app/Contents/Developer/usr/bin/actool --output-format human-readable-text --notices --warnings --export-dependency-info /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_dependencies --output-partial-info-plist /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Intermediates.noindex/ImageDemoS.build/Debug-iphoneos/ImageDemoS.build/assetcatalog_generated_info.plist --app-icon AppIcon --accent-color AccentColor --compress-pngs --enable-on-demand-resources YES --optimization space --filter-for-thinning-device-configuration iPhone7,1 --filter-for-device-os-version 11.4.1 --development-region en --target-device iphone --target-device ipad --minimum-deployment-target 9.0 --platform iphoneos --compile /Users/xxxxx/Library/Developer/Xcode/DerivedData/ImageDemoS-auwsocxqgbwbgmfoiguzuahzizre/Build/Products/Debug-iphoneos/ImageDemoS.app /Users/xxxxx/baidu/personal-code/ImageDemoS/ImageDemoS/Assets.xcassets /Users/xxxxx/baidu/personal-code/ImageDemoS/Media.xcassets           

同時我們發現,使用actool工具對.xcassets進行處理時,會出現以下warning,而不是error。從actool給的警告資訊看,HIEC圖檔隻在iOS11以後的系統上被支援,但是在生成Asset.car檔案時,actool工具會根據指定的最小系統版本,對iOS11以下的機型生成相容的圖檔,雖然圖檔體積可能有所變大,但是HIEC圖檔在Asset Catalog中對所有機型相容。而HIEC圖檔放Bundle則無法相容iOS11以下系統。進一步證明了actool自己對HEIC圖檔相容性的處理。

/* com.apple.actool.document.warnings */
/Media.xcassets:./logHEICAlpha.imageset/[universal][][][3x][][][][][][][][][][]: warning: You're targeting iOS 9.0, but HEIF files can only be accessed from an Asset Catalog in iOS 11.0 and later.           

丨3.2 解析car檔案

解析Assets.car檔案,可以使用Mac自帶工具assetutil,可以移除通用的Assets.car裡不需要的圖檔,也可以解析Assets.car的詳細内容。也可以使用Asset Catalog Tinkerer顯示圖檔,參考:https://github.com/insidegui/AssetCatalogTinkerer。在此我們使用assetutil對Assets.car内容進行解析。指令如下:

assetutil -I Assets.car > Assets.json           

下面的表格中對于通用Assets.car裡的檔案資訊進行分析,首先需要了解以下幾個字段的含義:

SizeOnDisk:這是圖檔在Assets.car裡實際的體積
Encoding:編碼方式,HEIF就是HEIC圖檔的編碼方式
Compression:壓縮算法           

我們可以得到以下結論:

  1. PNG圖檔轉HEIC圖檔體積會有所下降;
  2. PNG圖檔和HEIC圖檔經過actool處理後,car檔案裡的圖檔大小和實際大小不一緻;
  3. car檔案裡圖檔大小包體積和編碼方式和壓縮算法相關,PNG和HEIC圖檔的最終大小以SizeOnDisk字段資料為準。
  4. actool會對圖檔做iOS系統和裝置相容,在不支援HEIC的裝置上會将HEIC圖檔轉為其他可以顯示的格式

圖檔大小:15,444(log.png)10,867(log.heic)

PNG圖檔

{

"AssetType" : "Image",

"BitsPerComponent" : 8,

"ColorModel" : "RGB",

"Colorspace" : "srgb",

"Compression" : "deepmap2",

"Encoding" : "ARGB",

"Name" : "logPNGAlpha",

"NameIdentifier" : 62170,

"Opaque" : false,

"PixelHeight" : 258,

"PixelWidth" : 540,

"RenditionName" : "log.png",

"Scale" : 3,

"SizeOnDisk" : 9441,

"Template Mode" : "automatic"

}

HIEC圖檔

(支援HEIC的裝置)

{

"AssetType" : "Image",

"BitsPerComponent" : 8,

"ColorModel" : "RGB",

"DeploymentTarget" : "2017",

"Encoding" : "HEIF",

"Name" : "logHEICAlpha",

"NameIdentifier" : 2469,

"Opaque" : false,

"PixelHeight" : 258,

"PixelWidth" : 540,

"RenditionName" : "log.heic",

"Scale" : 3,

"SizeOnDisk" : 11239,

"Template Mode" : "automatic"

}

HIEC圖檔

(支援HEIC的裝置)

{

"AssetType" : "Image",

"BitsPerComponent" : 8,

"ColorModel" : "RGB",

"Colorspace" : "srgb",

"Compression" : "jpeg-lzfse",

"Encoding" : "ARGB",

"Name" : "logHEICAlpha",

"NameIdentifier" : 2469,

"Opaque" : false,

"PixelHeight" : 258,

"PixelWidth" : 540,

"RenditionName" : "log.heic",

"Scale" : 3,

"SizeOnDisk" : 38227,

"Template Mode" : "automatic"

}

圖檔大小:72,547([email protected])33,589([email protected]

PNG圖檔

{

"AssetType" : "Image",

"BitsPerComponent" : 8,

"ColorModel" : "RGB",

"Colorspace" : "srgb",

"Compression" : "deepmap2",

"Encoding" : "ARGB",

"Name" : "guideviewPNG",

"NameIdentifier" : 13256,

"Opaque" : true,

"PixelHeight" : 1026,

"PixelWidth" : 993,

"RenditionName" : "[email protected]",

"Scale" : 3,

"SizeOnDisk" : 89531,

"Template Mode" : "automatic"

}

HIEC圖檔

(支援HEIC的裝置)

{

"AssetType" : "Image",

"BitsPerComponent" : 8,

"ColorModel" : "RGB",

"DeploymentTarget" : "2017",

"Encoding" : "HEIF",

"Name" : "guideviewHIEC",

"NameIdentifier" : 30493,

"Opaque" : true,

"PixelHeight" : 1026,

"PixelWidth" : 993,

"RenditionName" : "[email protected]",

"Scale" : 3,

"SizeOnDisk" : 33963,

"Template Mode" : "automatic"

}

HIEC圖檔

(支援HEIC的裝置)

{

"AssetType" : "Image",

"BitsPerComponent" : 5,

"ColorModel" : "RGB",

"Colorspace" : "srgb",

"Compression" : "lzfse",

"Encoding" : "RGB555",

"Name" : "guideviewHIEC",

"NameIdentifier" : 30493,

"Opaque" : true,

"PixelHeight" : 1026,

"PixelWidth" : 993,

"RenditionName" : "[email protected]",

"Scale" : 3,

"SizeOnDisk" : 225090,

"Template Mode" : "automatic"

}

四. Alpha通道相容性問題

在實際操作過程中,我們發現某些帶有alpha通道的PNG圖檔在轉HEIC圖檔後,在iOS11、iOS12、iOS13系統上會出現圖檔無法顯示、顯示為白色、綠色等各種問題,但是也有一部分帶alpha通道的圖檔顯示完全正确。針對此類問題我們做了一系列的探索,最終确定問題原因。所有被pngquant:https://pngquant.org 有損(60-90)壓縮過的帶有alpha通道的圖檔,轉換成HEIC圖檔後會出現上述問題。以下是具體的分析過程和相關資料。

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

圖檔顯示為白色

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

透明顯示為綠色

丨4.1 問題分析思路

首先有幾個問題需要考慮:

  1. 為什麼同一張HIEC圖檔,iOS14和iOS15顯示正常,而iOS11、iOS12、iOS13會出現問題?
  2. 為什麼同樣是帶Alpha通道的HEIC圖檔,有的會在iOS11、iOS12、iOS13系統上出現問題,有的圖檔在所有系統都可以正常顯示?

第一步:确定圖檔編碼解碼資料,将PNG和問題HEIC圖檔轉Bitmap,檢視RGBA值

上述問題隻在帶有alpha通道的部分HEIC圖檔上出現,首先從編碼和解碼兩個角度分析alpha通道HIEC圖檔顔色失真問題。iOS裝置上所有的圖檔都會先解碼生成Bitmap位圖,然後渲染成圖檔,是以需要擷取一張圖檔的Bitmap資料和圖檔資訊。擷取Bitmap資料非常關鍵的一個結構體是CGImageRef, CGImageRef常見的有三種擷取方式:

  1. UIKit提供放UIImage的CGImage屬性,這是最常用的方式;
  2. ImageI/O提供的CGImageSourceCreateImageAtIndex 函數,這種适用于從檔案解析圖檔;
  3. Core graphics提供的CGBitmapContextCreateImage,這種适用于已知bitmap graphics context情況下使用;

此處直接從UIImage擷取CGImageRef。

/// 擷取圖檔資訊和像素
/// - Parameters:
///   - image: <#image description#>
 -(void)dumpImageInfo:(UIImage *)image
{
    // 擷取CGImageRef
    CGImageRef cgimage = image.CGImage;
 
    size_t width  = CGImageGetWidth(cgimage);
    size_t height = CGImageGetHeight(cgimage);
    size_t bpr = CGImageGetBytesPerRow(cgimage);
    size_t bpp = CGImageGetBitsPerPixel(cgimage);
    size_t bpc = CGImageGetBitsPerComponent(cgimage);
    size_t bytes_per_pixel = bpp / bpc;
    
    CGBitmapInfo info = CGImageGetBitmapInfo(cgimage);
    NSLog(
        @"\n"
//        "===== %@ =====\n"
        "CGImageGetHeight: %d\n"
        "CGImageGetWidth:  %d\n"
        "CGImageGetColorSpace: %@\n"
        "CGImageGetBitsPerPixel:     %d\n"
        "CGImageGetBitsPerComponent: %d\n"
        "CGImageGetBytesPerRow:      %d\n"
        "CGImageGetBitmapInfo: 0x%.8X\n"
        "  kCGBitmapAlphaInfoMask     = %s\n"
        "  kCGBitmapFloatComponents   = %s\n"
        "  kCGBitmapByteOrderMask     = %s\n"
        "  kCGBitmapByteOrderDefault  = %s\n"
        "  kCGBitmapByteOrder16Little = %s\n"
        "  kCGBitmapByteOrder32Little = %s\n"
        "  kCGBitmapByteOrder16Big    = %s\n"
        "  kCGBitmapByteOrder32Big    = %s\n",
//        file,
        (int)width,
        (int)height,
        CGImageGetColorSpace(cgimage),
        (int)bpp,
        (int)bpc,
        (int)bpr,
        (unsigned)info,
        (info & kCGBitmapAlphaInfoMask)     ? "YES" : "NO",
        (info & kCGBitmapFloatComponents)   ? "YES" : "NO",
        (info & kCGBitmapByteOrderMask)     ? "YES" : "NO",
        (info & kCGBitmapByteOrderDefault)  ? "YES" : "NO",
        (info & kCGBitmapByteOrder16Little) ? "YES" : "NO",
        (info & kCGBitmapByteOrder32Little) ? "YES" : "NO",
        (info & kCGBitmapByteOrder16Big)    ? "YES" : "NO",
        (info & kCGBitmapByteOrder32Big)    ? "YES" : "NO"
    );
    
    // 擷取位圖資料
    CGDataProviderRef provider = CGImageGetDataProvider(cgimage);
    NSData* data = (__bridge NSData *)CGDataProviderCopyData(provider);
//    [data autorelease];
    const uint8_t* bytes = [data bytes];
  
    printf("Pixel Data:\n");
    for(size_t row = 0; row < height; row++)
    {
        for(size_t col = 0; col < width; col++)
        {
            const uint8_t* pixel =
                &bytes[row * bpr + col * bytes_per_pixel];
  
            printf("(");
            for(size_t x = 0; x < bytes_per_pixel; x++)
            {
                printf("%.2d", pixel[x]);
                if( x < bytes_per_pixel - 1 )
                    printf(",");
            }
  
            printf(")");
            if( col < width - 1 )
                printf(", ");
        }
  
        printf("\n");
    }
}           

對有問題的HEIC圖檔,分析Bitmap值可以發現,對于以RGBA方式排列的Bitmap,白色透明應該為(0,0,0,0),sips工具将PNG轉HEIC的編碼,在iOS12、13、14上是(71,112,77,112),在iOS15和iOS16上是(71,112,77,00),明顯看出sips工具轉的HEIC圖檔之是以在iOS15和iOS16系統上保持無色透明,主要是alpha通道值為0,然而它的實際顔色是綠色。經過實驗得知,iOS12、13、14系統的[UIImage imageNamed:]對有Alpha的圖檔解析Bitmap會稍有誤差,導緻iOS12、13、14系統上Alpha值不為0,是以顯現出綠色。

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

第二步:确定轉碼工具和壓縮工具對圖檔的影響

第一步基本可以确定是圖檔本身的問題,百度APP的圖檔都是經過壓縮工具壓縮後才內建到APP中的,可能是原PNG圖檔被處理過導緻上述問題。用支援硬編的iOS12系統的測試機(iPhone7p iOS12),調用Image I/O提供的函數CGImageSourceCreateImageAtIndex先解碼PNG圖檔,然後調用CGImageDestinationCreateWithData 重新編碼為HEIC圖檔。轉換的代碼:

/// 從支援解碼的圖檔建立CGImageRef
/// - Parameter path: 圖檔路徑
CGImageRef createCGImageFromFile (NSString* path)
{
    // Get the URL for the pathname passed to the function.
    NSURL *url = [NSURL fileURLWithPath:path];
    CGImageRef        myImage = NULL;
    CGImageSourceRef  myImageSource;
    CFDictionaryRef   myOptions = NULL;
    CFStringRef       myKeys[2];
    CFTypeRef         myValues[2];
 
    // Set up options if you want them. The options here are for
    // caching the image in a decoded form and for using floating-point
    // values if the image format supports them.
    myKeys[0] = kCGImageSourceShouldCache;
    myValues[0] = (CFTypeRef)kCFBooleanTrue;
    myKeys[1] = kCGImageSourceShouldAllowFloat;
    myValues[1] = (CFTypeRef)kCFBooleanTrue;
    // Create the dictionary
    myOptions = CFDictionaryCreate(NULL, (const void **) myKeys,
                   (const void **) myValues, 2,
                   &kCFTypeDictionaryKeyCallBacks,
                   & kCFTypeDictionaryValueCallBacks);
    // Create an image source from the URL.
    myImageSource = CGImageSourceCreateWithURL((CFURLRef)url, myOptions);
    CFRelease(myOptions);
    // Make sure the image source exists before continuing
    if (myImageSource == NULL){
        fprintf(stderr, "Image source is NULL.");
        return  NULL;
    }
    // Create an image from the first item in the image source.
    myImage = CGImageSourceCreateImageAtIndex(myImageSource, 0, NULL);
 
    CFRelease(myImageSource);
    // Make sure the image exists before continuing
    if (myImage == NULL){
         fprintf(stderr, "Image not created from image source.");
         return NULL;
    }
 
    return myImage;
}           
/// 将任意一種格式的圖檔由UIImage編碼為HEIC圖檔存儲
/// - Parameters:
///   - image: <#image description#>
///   - path: <#path description#>
- (void)generateNewHEIC:(UIImage *)image savePath:(NSString *)path{
    
    NSMutableData *imageData = [NSMutableData data];
    // HEIC圖檔編碼格式
    CFStringRef imageUTType =  CFSTR("public.heic");
    CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
    if (!destination) {
        // 無法編碼,基本上是因為目标格式不支援
        NSLog(@"無法編碼");
        return;
    }
    CGImageRef imageRef = image.CGImage; // 待編碼的CGImage
    // 可選元資訊,比如EXIF方向
    CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationDown;
    NSMutableDictionary *frameProperties = [NSMutableDictionary dictionary];
//    imageProperties[(__bridge_transfer NSString *) kCGImagePropertyExifDictionary] = @(exifOrientation);
    // 添加圖像和元資訊
    CGImageDestinationAddImage(destination, imageRef, (__bridge CFDictionaryRef)frameProperties);
    if (CGImageDestinationFinalize(destination) == NO) {
        // 編碼失敗
        imageData = nil;
    }
    // 編碼成功,清理……
    CFRelease(destination);
    // 儲存新生成的HEIC圖檔
    if(imageData) {
        NSURL *url = [NSURL fileURLWithPath:path];
        [imageData writeToURL:url atomically:YES];
    }
}

           

iPhone7p使用ImageI/O将PNG轉HEIC的編碼則為(00,00,01,01),對測試機自己轉換生成的HEIC圖檔,調用[UIImage imageNamed:]擷取UIImage對象,顯示出的圖檔alpha通道沒有綠邊問題。經過以上操作,基本定位到帶Alpha通道圖檔綠邊問題是發生在sips将PNG圖檔轉HEIC圖檔這一過程中,而且由于這個問題隻發生在部分PNG圖檔上,是以可以得出結論:是原PNG圖檔被其他壓縮算法處理過,導緻sips轉HEIC圖檔發生問題。最終經過排查得出:被pngquant有損壓縮過的帶有Alpha通道的PNG圖檔,無法正确的轉為HEIC圖檔。

iOS16顯示效果 iOS12顯示效果
原圖PNG 正常 正常
原圖sips轉HEIC 正常 正常
pngquant壓縮PNG 正常 正常
壓縮PNG轉HEIC 沒有綠幕,邊緣稍微出現鋸齒 出現綠幕,邊緣明顯出現鋸齒

五. 圖檔最佳實踐

iOS包體積主要由代碼和資源組成,在包體積優化實踐中發現,相較于代碼,資源收益更容易落地。百度APP内常用的的資源優化方式有:PMS下發、ZIP壓縮、圖檔壓縮和格式轉換等。為了防止後續新增大資源和大圖檔問題,在RD送出代碼時,優化git hook功能:

  1. 修改攔截門檻值,大資源和大圖檔準入攔截門檻值從50KB降低至20KB;
  2. 增加圖檔優化提示功能,送出新圖檔時自動對圖檔進行壓縮和格式轉換,給出圖檔最佳大小建議。

丨5.1 方案

  1. 在執行git commit時,檢測送出的檔案,如果為非代碼檔案,檢測檔案大小。大資源和大圖檔準入攔截門檻值從50KB降低至20KB;
  2. 計算各種優化後的圖圖檔在Bundle和Asset Catalog裡的大小,計算出最佳的優化方式,文字提示RD優化,不會攔截送出
  3. 圖檔優化方式分為兩類,一類是存放位置,放Bundle和放Asset Catalog;另一類是對圖檔進行處理,有壓縮和轉換格式兩種處理方式,兩種互相結合得到最佳方式:
    1. 放Bundle:不推薦,圖檔在安裝包内體積為圖檔本身大小,Xcode不會處理,在iOS11以下系統中無法相容HEIC圖檔;
    2. 放Asset Catalog:推薦,Xcode打包編譯時會用actool工具處理圖檔,優化圖檔大小,可以相容HEIC圖檔;
    3. pngquant壓縮:有損壓縮PNG圖檔,沿用百度APP之前的圖檔壓縮參數,是git hook原有邏輯;
    4. MozJPEG壓縮:有損壓縮JPG圖檔,新增的壓縮工具,https://calendar.perfplanet.com/2014/mozjpeg-3-0/;
    5. HEIC圖檔:無損轉換,百度APP中隻能在Asset Catalog中使用,需要回歸iOS11系統是否正常顯示;

需要注意: 由于圖檔放置在Asset Catalog中,Xcode打包編譯時會用actool工具處理圖檔,是以安裝包内的圖檔大小并不等于圖檔本身大小。腳本會将圖檔編譯生成Asset.car檔案,讀取在圖檔在安裝包内的實際大小。

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

圖檔送出檢測

六. 檢測無用類

丨6.1 無用類檢測原理分析

百度APP是一個非常大的工程,每個版本都會新上很多需求,但是随着人員的變動、營運活動和版本疊代,有些功能已經沒有入口,有些代碼已經不會被引用到,比如已經固化了AB實驗代碼,雲控開關代碼等,代碼重構過程中也會有備援的代碼忘記删除,活動下線後代碼依舊遺留在工程中等。從代碼角度優化包體積,無用代碼檢測是一種不錯的解決方案,代碼優化包含無用類、無用方法、重複代碼、營運代碼等緯度,下文重點介紹無用類的檢測和優化。無用類檢測的難點在于OC是一門動态語言,在檢測時誤報的機率會很大,會給RD造成人力浪費。無用類檢測總體思路分為兩部分,靜态檢測和動态檢測相結合。

靜态檢測是從編譯産物的角度分析代碼引用關系和結構。分析Linkmap檔案和Mach-O檔案,根據Segment裡的資料,查找出沒用被引用的Class和method。但是這種方式具有很大的局限性,比如某些能受營運影響,如何執行是Server端雲控決定的;還有些通過Runtime進行初試化的Class也無法被識别。

動态分析是從代碼運作的角度分析代碼是否被初始化,在APP運作期間,記錄APP生命周期中初始化過的Class,使用者使用到的功能所涉及的相關Class都會被記錄到,反之某些功能沒有被使用,那這些對應的類則不會被記錄。動态檢測是窮舉APP所有功能在使用過程中使用過的Class。

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

無用類分析與分發

七. 靜态分析

靜态分析需要用到Linkmap檔案和Mach-O檔案,Linkmap檔案記錄着所有Symbol address、Symbol、Symbol Size的對應關系,Mach-O檔案記錄着類結構和位址。結合Linkmap檔案和Mach-O可以還原出每個Class的所有資訊。

Mach-o檔案中__DATA __objc_classrefs段記錄了引用類的位址,__DATA __objc_classlist段記錄了所有類的位址,取差集可以得到未使用的類的位址,然後進行符号化,就可以得到未被引用的類資訊。

丨7.1 解析Mach-O

可以通過Mac自帶的工具otool列印Mach-o中的段資訊。

% file -b BaiduBoxApp.app/BaiduBoxApp #擷取Mach-O架構
Mach-O 64-bit executable arm64


% otool -arch arm64 -oV BaiduBoxApp.app/BaiduBoxApp > ovrelease.txt #解析Mach-O内容           

輸出的内容主要包含以下幾個部分,其中__DATA,__objc_classlist就是類的全集,__DATA,__objc_classrefs是被引用到的類。如果是Debug包,可以直接擷取類名,但是release包一般隻有符号位址,利用這些位址可以在對應和Linkmap檔案中還原出符号,也就是可以得到具體的類名。

'Contents of (__DATA,__objc_classlist) section',  # classlist節辨別
'Contents of (__DATA,__objc_classrefs) section',  # classrefs節标
'Contents of (__DATA,__objc_superrefs) section',  # 父類節标
'Contents of (__DATA,__objc_catlist) section',  # category節标
'Contents of (__DATA,__objc_protolist) section',
'Contents of (__DATA,__objc_selrefs) section',
'Contents of (__DATA,__objc_imageinfo) section'           
百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

debug包

百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

release包

丨7.2 注意點

  1. 在實際分析的過程中發現,如果一個類的子類被執行個體化,父類未被執行個體化,此時父類不會出現在__objc_classrefs這個段裡,在未使用的類中需要将這一部分父類過濾出去。
  2. 多個類中可能存在相同的方法名。因為MachO檔案中__cstring和__objc_methname這兩個代碼段記錄的是方法名字元的ASCII碼的十六進制表示。如果多個類中有相同的方法名,相同的方法名會進入link map的Dead Stripped Symbols中,最後隻留一個。
  3. 如果做了段遷移,可能導緻otool工具無法解析對應方法名,但是通過符号位址我們可以在linkmap裡還原出具體符号。
百度APP iOS端包體積50M優化實踐(五) HEIC圖檔和無用類優化實踐

Symbol解析

八. 動态分析

丨8.1 動态分析原理

OC的類結構體中,都存在一個isa指針,指向對應類的meta-class。我們通過對meta-class的結構體的分析,能夠發現在meta-class的class_rw_t中,有一個flag标志位,通過flag标志位計算,可以獲知目前類在運作時中是否被初始化過。

// class is initialized
#define RW_INITIALIZED        (1<<29)


struct objc_class : objc_object {


    bool isInitialized() {
    return getMeta()->data()->flags & RW_INITIALIZED;
    }
};           

以上摘取objc-runtime源碼中,objc_class結構下擷取目前類是否已被初始化的函數。但在應用中,我們無法直接調用類結構體中的函數,是以在百度APP工程中,自定義與系統類相同的結構體,并實作相應isInitialized()函數。通過指派轉換,我們可以拿到指定類對應meta-class中的資料,即可以判斷指定類是否在目前生命周期中是否被初始化過(被使用過)。

丨8.1 技術實作

1. 模仿objc_class實作自定義結構體,用于擷取指定類結構體内部資料

#define RW_INITIALIZED        (1<<29)


# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif


struct lazyFake_objc_class : lazyFake_objc_object {
    //提供metaClass函數,擷取元類對象
    lazyFake_objc_class* metaClass() {
        #if __LP64__
        //isa指針需要經過一次 &ISA_MASK操作之後才得到真正的位址
            return (lazyFake_objc_class *)((long long)isa & ISA_MASK);
        #else
            return (lazyFake_objc_class *)((long long)isa);
        #endif
    }
    bool isInitialized() {
        return metaClass()->data()->flags & RW_INITIALIZED;
    }
};           

2. 首先擷取百度APP工程中所有自定義OC類

Dl_info info;
dladdr(&_mh_execute_header, &info);
classes = objc_copyClassNamesForImage(info.dli_fname, &classCount);           

3. 周遊自定義類,并逐個對其進行指派轉換為自定義結構體,并通過自定義類結構方法,擷取目前類是否被初始化過。

struct lazyFake_objc_class *objectClass = (__bridge struct lazyFake_objc_class *)cls;


BOOL isInitial = objectClass->isInitialized();           

九. 總結

  1. HEIC圖檔相較于PNG,對部分圖檔可以降低圖檔體積,收益從10%-70%不等,具體問題具體分析,編寫git hook檢查腳本提供指導;
  2. HEIC圖檔放Asset Catalog可以相容iOS10以上的所有機型和系統;
  3. HEIC圖檔放Bundle隻能在iOS12系統上解碼,這個和Apple給出的結論相悖。若APP最低支援系統小于iOS12,則HEIC圖檔禁止放Bundle。A9以上晶片的機型為硬解,速度更快;
  4. 帶有Alpha通道的PNG圖檔,未經過pngquant有損壓縮的,利用sips指令直接轉HEIC圖檔可以正常顯示;
  5. 帶有Alpha通道的PNG圖檔,已經被pngquant有損壓縮過的在iOS12,13,14系統上會顯示綠幕,iOS115,iOS16顯示正常。雖然顯示正常,但是RGB位圖顔色解碼錯誤,隻是因為alpha為0,綠色變成了透明;
  6. 無論是PNG還是HEIC圖檔,在Asset Catalog管理下,打包生成的體積和原圖檔不同,都會經過不同的處理壓縮,可能變大也可能變小,以最終産物為準;
  7. pngquant适合對Bundle裡的PNG壓縮,擷取收益,對Asset Catalog裡的圖檔不應該處理,因為這個收益其實是有損壓縮擷取的,并且會導緻壓縮過的帶Alpha通道的PNG無法轉HEIC;
  8. 無用類檢測結合動态檢測和靜态檢測,檢測較為嚴格,主要是為了降低誤報率,降低對RD的影響,實際操作過程中發現有些無用類會被漏檢。準确度和覆寫度需要根據需求動态調整;

十. 參考文獻

[1]、503_WWDC 2017 CMF_03_D:https://devstreaming-cdn.apple.com/videos/wwdc/2017/503i6plfvfi7o3222/503/503_introducing_heif_and_hevc.pdf

[2]、iOS代碼瘦身實踐:删除無用的類:httpshttps://juejin.cn/post/6844903922201526285

作者:TXT

來源:微信公衆号:百度App技術

出處:https://mp.weixin.qq.com/s/PU5rC0U0XZ1VYqLWSUBWLQ