一. 前言
本篇文章重點介紹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圖檔轉換功能:
- 右鍵圖檔,快速操作—>轉換圖像
- 格式選HEIF,圖像大小根據需求選擇
丨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],圖檔沒有經過任何其餘壓縮處理。
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:壓縮算法
我們可以得到以下結論:
- PNG圖檔轉HEIC圖檔體積會有所下降;
- PNG圖檔和HEIC圖檔經過actool處理後,car檔案裡的圖檔大小和實際大小不一緻;
- car檔案裡圖檔大小包體積和編碼方式和壓縮算法相關,PNG和HEIC圖檔的最終大小以SizeOnDisk字段資料為準。
- 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圖檔後會出現上述問題。以下是具體的分析過程和相關資料。
圖檔顯示為白色
透明顯示為綠色
丨4.1 問題分析思路
首先有幾個問題需要考慮:
- 為什麼同一張HIEC圖檔,iOS14和iOS15顯示正常,而iOS11、iOS12、iOS13會出現問題?
- 為什麼同樣是帶Alpha通道的HEIC圖檔,有的會在iOS11、iOS12、iOS13系統上出現問題,有的圖檔在所有系統都可以正常顯示?
第一步:确定圖檔編碼解碼資料,将PNG和問題HEIC圖檔轉Bitmap,檢視RGBA值
上述問題隻在帶有alpha通道的部分HEIC圖檔上出現,首先從編碼和解碼兩個角度分析alpha通道HIEC圖檔顔色失真問題。iOS裝置上所有的圖檔都會先解碼生成Bitmap位圖,然後渲染成圖檔,是以需要擷取一張圖檔的Bitmap資料和圖檔資訊。擷取Bitmap資料非常關鍵的一個結構體是CGImageRef, CGImageRef常見的有三種擷取方式:
- UIKit提供放UIImage的CGImage屬性,這是最常用的方式;
- ImageI/O提供的CGImageSourceCreateImageAtIndex 函數,這種适用于從檔案解析圖檔;
- 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的圖檔都是經過壓縮工具壓縮後才內建到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功能:
- 修改攔截門檻值,大資源和大圖檔準入攔截門檻值從50KB降低至20KB;
- 增加圖檔優化提示功能,送出新圖檔時自動對圖檔進行壓縮和格式轉換,給出圖檔最佳大小建議。
丨5.1 方案
- 在執行git commit時,檢測送出的檔案,如果為非代碼檔案,檢測檔案大小。大資源和大圖檔準入攔截門檻值從50KB降低至20KB;
- 計算各種優化後的圖圖檔在Bundle和Asset Catalog裡的大小,計算出最佳的優化方式,文字提示RD優化,不會攔截送出
- 圖檔優化方式分為兩類,一類是存放位置,放Bundle和放Asset Catalog;另一類是對圖檔進行處理,有壓縮和轉換格式兩種處理方式,兩種互相結合得到最佳方式:
- 放Bundle:不推薦,圖檔在安裝包内體積為圖檔本身大小,Xcode不會處理,在iOS11以下系統中無法相容HEIC圖檔;
- 放Asset Catalog:推薦,Xcode打包編譯時會用actool工具處理圖檔,優化圖檔大小,可以相容HEIC圖檔;
- pngquant壓縮:有損壓縮PNG圖檔,沿用百度APP之前的圖檔壓縮參數,是git hook原有邏輯;
- MozJPEG壓縮:有損壓縮JPG圖檔,新增的壓縮工具,https://calendar.perfplanet.com/2014/mozjpeg-3-0/;
- HEIC圖檔:無損轉換,百度APP中隻能在Asset Catalog中使用,需要回歸iOS11系統是否正常顯示;
需要注意: 由于圖檔放置在Asset Catalog中,Xcode打包編譯時會用actool工具處理圖檔,是以安裝包内的圖檔大小并不等于圖檔本身大小。腳本會将圖檔編譯生成Asset.car檔案,讀取在圖檔在安裝包内的實際大小。
圖檔送出檢測
六. 檢測無用類
丨6.1 無用類檢測原理分析
百度APP是一個非常大的工程,每個版本都會新上很多需求,但是随着人員的變動、營運活動和版本疊代,有些功能已經沒有入口,有些代碼已經不會被引用到,比如已經固化了AB實驗代碼,雲控開關代碼等,代碼重構過程中也會有備援的代碼忘記删除,活動下線後代碼依舊遺留在工程中等。從代碼角度優化包體積,無用代碼檢測是一種不錯的解決方案,代碼優化包含無用類、無用方法、重複代碼、營運代碼等緯度,下文重點介紹無用類的檢測和優化。無用類檢測的難點在于OC是一門動态語言,在檢測時誤報的機率會很大,會給RD造成人力浪費。無用類檢測總體思路分為兩部分,靜态檢測和動态檢測相結合。
靜态檢測是從編譯産物的角度分析代碼引用關系和結構。分析Linkmap檔案和Mach-O檔案,根據Segment裡的資料,查找出沒用被引用的Class和method。但是這種方式具有很大的局限性,比如某些能受營運影響,如何執行是Server端雲控決定的;還有些通過Runtime進行初試化的Class也無法被識别。
動态分析是從代碼運作的角度分析代碼是否被初始化,在APP運作期間,記錄APP生命周期中初始化過的Class,使用者使用到的功能所涉及的相關Class都會被記錄到,反之某些功能沒有被使用,那這些對應的類則不會被記錄。動态檢測是窮舉APP所有功能在使用過程中使用過的Class。
無用類分析與分發
七. 靜态分析
靜态分析需要用到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'
debug包
release包
丨7.2 注意點
- 在實際分析的過程中發現,如果一個類的子類被執行個體化,父類未被執行個體化,此時父類不會出現在__objc_classrefs這個段裡,在未使用的類中需要将這一部分父類過濾出去。
- 多個類中可能存在相同的方法名。因為MachO檔案中__cstring和__objc_methname這兩個代碼段記錄的是方法名字元的ASCII碼的十六進制表示。如果多個類中有相同的方法名,相同的方法名會進入link map的Dead Stripped Symbols中,最後隻留一個。
- 如果做了段遷移,可能導緻otool工具無法解析對應方法名,但是通過符号位址我們可以在linkmap裡還原出具體符号。
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();
九. 總結
- HEIC圖檔相較于PNG,對部分圖檔可以降低圖檔體積,收益從10%-70%不等,具體問題具體分析,編寫git hook檢查腳本提供指導;
- HEIC圖檔放Asset Catalog可以相容iOS10以上的所有機型和系統;
- HEIC圖檔放Bundle隻能在iOS12系統上解碼,這個和Apple給出的結論相悖。若APP最低支援系統小于iOS12,則HEIC圖檔禁止放Bundle。A9以上晶片的機型為硬解,速度更快;
- 帶有Alpha通道的PNG圖檔,未經過pngquant有損壓縮的,利用sips指令直接轉HEIC圖檔可以正常顯示;
- 帶有Alpha通道的PNG圖檔,已經被pngquant有損壓縮過的在iOS12,13,14系統上會顯示綠幕,iOS115,iOS16顯示正常。雖然顯示正常,但是RGB位圖顔色解碼錯誤,隻是因為alpha為0,綠色變成了透明;
- 無論是PNG還是HEIC圖檔,在Asset Catalog管理下,打包生成的體積和原圖檔不同,都會經過不同的處理壓縮,可能變大也可能變小,以最終産物為準;
- pngquant适合對Bundle裡的PNG壓縮,擷取收益,對Asset Catalog裡的圖檔不應該處理,因為這個收益其實是有損壓縮擷取的,并且會導緻壓縮過的帶Alpha通道的PNG無法轉HEIC;
- 無用類檢測結合動态檢測和靜态檢測,檢測較為嚴格,主要是為了降低誤報率,降低對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