百度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优化实践(一)总览》:https://mp.weixin.qq.com/s/ANbFzg7X932o-iDpa8FcxQ

《百度APP iOS端包体积50M优化实践(二) 图片优化》:https://mp.weixin.qq.com/s/RR7sjhkuTFgUp7S5E8ECMw

《百度APP iOS端包体积50M优化实践(三) 资源优化》:https://mp.weixin.qq.com/s/FQWCX0wkK6ifHQ8RhmmPsg

《百度APP iOS端包体积50M优化实践(四) 代码优化》:https://mp.weixin.qq.com/s/H9KtBPE67g3752OcnjXG7A
测试环境 基本工具
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,图像大小根据需求选择

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 guideview@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)

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(guideview@3x.png)33,589(guideview@3x.heic)

PNG图片 {

  “AssetType” : “Image”,

  “BitsPerComponent” : 8,

  “ColorModel” : “RGB”,

  “Colorspace” : “srgb”,

  “Compression” : “deepmap2”,

  “Encoding” : “ARGB”,

  “Name” : “guideviewPNG”,

  “NameIdentifier” : 13256,

  “Opaque” : true,

  “PixelHeight” : 1026,

  “PixelWidth” : 993,

  “RenditionName” : “guideview@3x.png”,

  “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” : “guideview@3x.heic”,

  “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” : “guideview@3x.heic”,

  “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 问题分析思路
首先有几个问题需要考虑:
  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: %dn"        "CGImageGetWidth:  %dn"        "CGImageGetColorSpace: %@n"        "CGImageGetBitsPerPixel:     %dn"        "CGImageGetBitsPerComponent: %dn"        "CGImageGetBytesPerRow:      %dn"        "CGImageGetBitmapInfo: 0x%.8Xn"        "  kCGBitmapAlphaInfoMask     = %sn"        "  kCGBitmapFloatComponents   = %sn"        "  kCGBitmapByteOrderMask     = %sn"        "  kCGBitmapByteOrderDefault  = %sn"        "  kCGBitmapByteOrder16Little = %sn"        "  kCGBitmapByteOrder32Little = %sn"        "  kCGBitmapByteOrder16Big    = %sn"        "  kCGBitmapByteOrder32Big    = %sn",//        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功能:

  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文件,读取在图片在安装包内的实际大小。

图片提交检测

六. 检测无用类

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.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

下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/13443,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?