写点什么

百度 APP iOS 端包体积 50M 优化实践 (五) HEIC 图片和无用类优化实践

作者:百度Geek说
  • 2023-08-01
    上海
  • 本文字数:12245 字

    阅读完需:约 40 分钟

百度APP iOS端包体积50M优化实践(五) HEIC图片和无用类优化实践

一、前言

之前的文章介绍了图片优化和代码优化的几种方式,本篇文章重点介绍 HEIC 图片和无用类检测的优化实践。HEIC 是 High Efficiency Image Format(高效图像格式)的缩写,是一种新的图像文件格式,它是 2017 年苹果公司在 iOS 11 中引入,用于代替 JPEG 图像格式,以更高效地压缩图像并减少存储空间占用。HEIC 支持多帧图像、透明度和 16 位深度色彩,使得它成为高质量图像和动画的理想选择。本文重点探究 HEIC 图片在百度 APP 中使用的可行性和包体积收益,验证 HEIC 图片在 Bundle 和 Asset Catalog 的兼容性,重点研究了 Asset Catalog 管理图片的机制,记录了验证过程中发现的特殊问题和解决思路。无用类则是详细介绍了如何用静态分析和动态分析相结合的方式,精简代码体积。


百度 APP iOS 端包体积优化实践系列文章回顾:


《百度APP iOS端包体积50M优化实践(一)总览》


《百度APP iOS端包体积50M优化实践(二) 图片优化》


《百度APP iOS端包体积50M优化实践(三) 资源优化》


《百度APP iOS端包体积50M优化实践(四) 代码优化》


二、HEIC 图片格式转换和使用方式

2.1 格式转换

有三种常见的 HEIC 图片转换方式:Mac 图片转换功能、Mac 自带 sips 命令、多平台支持的 ImageMagick 命令。

2.1.1 Mac 图片转换功能:

  1. 右键图片,快速操作—>转换图像

  2. 格式选 HEIF,图像大小根据需求选择


2.1.2 sips 工具:

sips 是一个 MacOS 自带的命令行的图片处理工具,具有转换图片格式、修改图片大小(扩充或者重新采样缩小图片)、修改质量,设置版权信息等功能。


举例:sips -s format heic -s formatOptions default guideview@3x.png --out guideview@3x.heic

2.1.3 ImageMagick 工具:

ImageMagick 包括一个用于执行复杂图像处理任务的命令行界面,以及用于将其功能集成到软件应用程序中的 API。它是用 C 编写的,可以在各种操作系统上使用,包括 Linux、Windows 和 macOS。


用法参考:


https://imagemagick.org/index.php


需要手动安装:


brew install imagemagick

convert guid‍eview@3x.png guideview@3x.heic

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 通道的图片和 guideview@3x.png 是不带 Alpha 通道的图片。然后分别生成对应的 HEIC 图片 log.heic 和 guideview@3x.heic,图片没有经过任何其余压缩处理。



△guideview@3x.png



△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)



图片大小:72,547(guideview@3x.png)33,589(guideview@3x.heic)


四、Alpha 通道兼容性问题

在实际操作过程中,我们发现某些带有 alpha 通道的 PNG 图片在转 HEIC 图片后,在 iOS11、iOS12、iOS13 系统上会出现图片无法显示、显示为白色、绿色等各种问题,但是也有一部分带 alpha 通道的图片显示完全正确。针对此类问题我们做了一系列的探索,最终确定问题原因。所有被 pngquant:https://pngquant.org 有损(60-90)压缩过的带有 alpha 通道的图片,转换成 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 的图片都是经过压缩工具压缩后才集成到 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 图片。


五、图片最佳实践

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;另一类是对图片进行处理,有压缩和转换格式两种处理方式,两种相互结合得到最佳方式:


  • 放 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 注意点

1、在实际分析的过程中发现,如果一个类的子类被实例化,父类未被实例化,此时父类不会出现在__objc_classrefs 这个段里,在未使用的类中需要将这一部分父类过滤出去。


2、多个类中可能存在相同的方法名。因为 MachO 文件中__cstring 和__objc_methname 这两个代码段记录的是方法名字符的 ASCII 码的十六进制表示。如果多个类中有相同的方法名,相同的方法名会进入 link map 的 Dead Stripped Symbols 中,最后只留一个。


3、如果做了段迁移,可能导致 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.2 技术实现

#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 的影响,实际操作过程中发现有些无用类会被漏检。准确度和覆盖度需要根据需求动态调整。


—— END——


参考文献


[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


推荐阅读


百度知道上云与架构演进


百度APP iOS端包体积50M优化实践(四)代码优化


百度App启动性能优化实践篇


扫光动效在移动端应用实践


Android SDK安全加固问题与分析


搜索语义模型的大规模量化实践

发布于: 刚刚阅读数: 3
用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
百度APP iOS端包体积50M优化实践(五) HEIC图片和无用类优化实践_ios_百度Geek说_InfoQ写作社区