查看原文
其他

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

百度Geek说 2023-04-28

The following article is from 百度App技术 Author RichardYang


一、前言

GEEK TALK

在上一篇文章,我们介绍了包体积优化的必要性、安装包组成部分和生成过程、国内外大厂APP包体积分析、百度APP包体积优化技术方案及各项收益,本文重点讲述图片优化,解压IPA包后发现,百度APP中asset和bundle里面图片共有94M,这是我们重点优化的对象。
本系列文章目录如下:
  《百度APP iOS端包体积50M优化实践(一)总览》(点击标题即可跳转)

百度APP采用如下方式对不同的图片资源进行了优化:

第一、无用图片优化,解决的是随着版本迭代,一些图片已经没有了引用关系,但还在IPA包中保留,挖掘这部分图片并删除,这个优化是所有包体积优化项目中ROI最高的,影响范围局限在单个组件内,质量可控,关键是提高查找无用图片的准确度;

第二、Asset Catalog优化:使用Asset Catalog管理的图片能被App Store工具App Thinning处理,处理后用户只会下载匹配其设备分辨率的图片资源,从而降低了用户下载的包体积;

第三、HEIC图片优化:跟PNG、JPEG、WebP相比,HEIC图片编码格式体积减少最小,从解码效率角度来说,跟WebP相比,HEIC硬解码效率高。

二、无用图片优化

GEEK TALK

2.1 方案综述

首先获取所有图片资源,然后开发工具获取Objective-C、Swift、xib、storyboard、html、js、css、json、plist文件可能引用图片的静态字符串,接着前面两个集合做diff即可排查未引用的图片,最后针对字符串拼接的常见case做二次过滤,覆盖的case越多准确度越高,当然也要考虑ROI。

2.2 获取所有图片

开启每个库的源码,用脚本检测所有图片及图片所属关系,为后续分发及落地优化提供方便。如果不开源码使用二进制库或者ipa包会带来很多麻烦,如获取asset.car里面的图片资源比较困难,同时只能知道图片名称,不能直接获取图片属于哪一个库。开启每个库的源码后,用脚本递归遍历可获取所有图片,从图片路径可知道所属关系,参考代码如下所示:

def findAllPictures(path): pathDir = os.listdir(path) for allDir in pathDir: child = os.path.join('%s%s' % (path, allDir)) if os.path.isfile(child): # 获取读到的文件的后缀 end = os.path.splitext(child)[-1] if end == ".png" or end == ".webp" or end == ".gif" or end == ".jpeg" or end == ".jpg": print("文件" + child + " 后缀 " + end) else: # 递归遍历子目录 child = child + "/" findAllPictures(child)

2.3 获取可能引用图片静态字符串

在这个环节,我们重点是要找到在代码中可能会引用图片的字符串集合,如在Objective-C的.m文件中,我们经常用如下代码去加载图片account_login来创建一个UIImageView对象, 针对Objective-C的.m文件内容,用正则过滤,匹配表达式为 @"(.*?)",即可获取所有可能加载图片的字符串集合。

imgView.image = [UIImageimageNamed:@"account_login"];br

对于Swift文件我们通常通过如下代码去加载图片account_login,加载方式完全不一样,针对Swift这种文件,正则表达式应为"(.?)"。

let imageView = UIImageView(frame: CGRectMake(100, 10, 200, 200))imageView.image = UIImage(named:"account_login.jpg")self.view.addSubview(imageView)

对于html文件我们通常用如下代码去加载图片,正则表达式应为img\s+src="'["']。

<html><body><img src="图片地址" alt="文本说明" width=** height=**></body></html>

不同的文件加载图片的方式不同,如Objective-C、Swift、xib、storyboard、html、js、plist、json和css都不尽相同,在下面的表格,我整理出常用文件过滤图片使用的正则表达式。

2.4 获取未引用图片

通过2.2章节我们获取的工程中的所有图片,通过2.3章节我们获取代码中所有可能引用图片的静态字符串,那么对于每张图片而言,如果不在引用图片的静态字符串集合中,这张图片可能就是未引用的图片。

2.5 字符串拼接的常见case做二次过滤

经过上面的环节,我们通过全字符串匹配获取了未引用的图片,实际开发过程,有一些常见情况,代码中引用图片的名称是通过字符串拼接生成的,因此我们需要进行二次过滤。第一种常见的case,就是当手机支持暗黑模式时,同一个位置的图片通常会有两套,通过后缀如light、dark或者day、night来做区分;第二种常见的case,就是后缀是数字的图片序列,iOS端有一种动画类型是ImageView加载多图动画,图片名称是由字符串和动态数字后缀拼接而成,过滤时需要覆盖如下后缀_%d,_%ld,_%zd,_%lu。

三、Asset Catalog 图片优化

GEEK TALK

3.1 背景

Asset Catalog 是 Xcode 提供的在iOS7系统开始引入的资源管理工具,将分散在项目中大大小小的资源进行统一存放和集中管理,包括但不限于images、sprites、textures, ARKit resources和PDF,我们可以把之前放在bundle的图片或者其他资源放入Asset catalog中,XCode最后统一压缩成一个Assets.car的文件。

在Asset Catalog之前,我们通常将图片直接放到工程的bundle,这种方式存在一些缺点:第一、空间浪费,不同设备需要不同分辨率图片,所以在bundle里对同一张图片同时存在二倍图和三倍图,浪费资源;第二、图片压缩只能针对单个文件,没有统一的压缩功能;第三、信息冗余,每个图片资源都会存储自己的元数据和其他的一些属性信息,如果存在很多同类型的资源,这些相同的信息会产生冗余,造成空间浪费。

3.2 Asset Catalog优点

针对bundle存在的上述问题,Asset Catalog做了诸多优化,无论是包体积优化、统一的图片压缩和便利的资源管理,还是高效的IO操作,每一项优化都做到了极致,下面详细介绍:

第一、包体积瘦身:Asset Catalog为不同类型设备(分辨率不同)或者相同类型设备但不同配置(磁盘和内存不同)提供定制化资源下载,之前在bundle需要放二倍图和三倍图,同一张图片最后在用户手机上会有两份,有了Asset Catalog后,当用户下载App时,只有跟用户手机硬件设备参数相匹配的资源才会被下载,其他不会下载,比如说,iphone8手机用户只会下载二倍图片,iphone13的用户只会下载三倍图片,这样可明显减少下载包大小。

第二、统一的图片无损压缩:Asset Catalog默认对文件夹中的所有图片采用无损压缩,压缩方法是Apple Deep Pixel Image Compression,这是苹果新引入的一种压缩形式,会根据图片的色谱特性选择最优的算法进行压缩,压缩比能提高15~20%。WWDC2018:Optimizing App https://developer.apple.com/videos/play/wwdc2018/227/ 有详细介绍。体来说,针对不同类型的图片采用有不同的优化方式,一类是简单的图片资源,如很多icon图片,这类图片只有相对简单的配色和设计。另一类指的是复杂的图片资源,Apple Deep Pixel Image Compression针对这两种形式都做了不同形式的优化,图片资源体积越大使用Asset Catalog后优化的效果就越明显,统一的压缩更有利于实现包体积瘦身,下面这张图是苹果官方给出的Apple Deep Pixel Image Compression体积压缩比的优化。

第三、便利的资源管理:如果将图片直接放在工程目录下面,项目打包后图片文件是散落在iPA包里面,而如果用Asset Catalog来管理放在xcassets中,在打包后会将这些图片统一压缩成一个Assets.car的文件。

第四、高效的I/O操作:Assets Catalogs图片加载耗时比普通的bundle加载图片耗时要少两个数量级,这是因为编译最后生成Assets.car文件包含了BOM文件,BOM文件提供了图片加载时需要的的rendition、renditionKey和attribute属性值,rendition是 CoreUI.framework 对某一图像资源的不同样式的统称,如@2x,@3x,每一个rendition有一个renditionKey与之对应,通过renditionKey获取到对应的attribute,attribute中包含了各种属性,如图片的分辨率、垂直大小、水平大小等参数。

用Assets Catalogs管理的图片,通过imageNamed方法进行加载,因为car文件里面的上述资源信息是XCode编译时生成好的,当解析完car文件后,可以直接通过图片名称获取renditionKey和attribute属性并读取图片资源,没有任何多余操作,相反对于用bundle管理的图片,额外的操作太多导致耗时严重。对于bundle管理图片有两种加载方式,第一种通过imageName方式加载图片,需要先去Assets.car里面查询,由于图片资源并不在Assets.car里面,所以在获取rendition和renditionKey时多次调用canGetRenditionWithKey,canGetRenditionWithKey,最后再重新通过mmap加载读取图片属性和图片资源,形成rendition和renditionKey,总体耗时最大;第二种通过imageWithContentsOfFile加载图片,不需要去Assets.car里面查询,没有生成rendition和renditionKey的相关操作和缓存操作,只有读取图片属性和图片二进制的操作耗时,但是没有图片属性等相关缓存所以耗时比较长。

3.3 Assets.car生成过程

具体来说,Xcode 在处理Asset Catalog节点时, 构建 Asset Catalog 的工具 actool 会先对 Asset Catalog 中的 png 图片进行解码,得到 Bitmap 数据,然后再运用 actool 的压缩算法进行编码压缩处理生成Assets.car 文件,这就可以解释在Asset Catalog中放jpg格式图片,最后生成的Assets.car 文件中却是png格式图片。

3.4 Assets.car操作

用Assets Catalogs管理的图片,XCode编译时并不是简单的拷贝操作,而是将所有资源打包生成的Assets.car文件,这是一种压缩文件,直接解压无法操作的,利用XCode 自带工具assetutil可以分析.car文件,分析命令如下所示:

sudo xcrun --sdk iphoneos assetutil --info ./Assets.car > ./AssetsInfo.js

通过AssetsInfo.json获取图片相关属性,但是无法获取里面的图片。

如果想将car文件中的图片提取出来,推荐一个开源工具叫Asset Catalog Tinkerer,可以从github下载,这里给出github地址:https://github.com/insidegui/AssetCatalogTinkerer

3.5 Asset Catalog的压缩算法

使用3.4节介绍的XCode 自带工具assetutil可以知道每张图片的压缩算法,Compression字段值代表不同图片的采用的不同压缩算法,通过实践发现actool支持的压缩算法有 deepmap2、deepmap_lzfse、zip、lzfse、palette_img,具体采用哪种压缩算法跟很多因素有关,如图片自身特性、打包的XCode版本、Framework支持的iOS最低版本、编译配置(Asset Catalog Compiler - Options Optimization),从实际效果来看,XCode会根据综合上述因素选择一个压缩比最优的算法,另外这些压缩算法都是无损的。

3.6 不要做无损压缩

开发者在图片放入Asset Catalog之前千万不要做无损压缩,无损压缩算法是通过改变图片的压缩编码算法达到减少体积大小的目的,不会改变解码后的Bitmap 数据,从3.3节中我们知道Assets.car 文件的生成过程中,Asset Catalog 的工具 actool先做解码得到Bitmap数据,然后再编码压缩处理,针对无损压缩算法actool接收的Bitmap 数据并没有改变,所以无损压缩无法优化包体积,UI设计师给出的PNG图片如果采用Asset Catalog优化就千万不要做无损压缩。

3.7 bundle多倍图片Asset优化

Asset Catalog是Apple在2013年发布的iOS7的系统开始引入的,从iOS 9 之后开始支持做资源管理,老代码(尤其是16年前的代码)都是用bundle的方式去管理图片,为此,我们开发脚本专门针对bundle多倍图片做检查,然后采用Asset优化,这种优化方式可以实现包体积立减一半,参考脚本如下所示:
def find_all_bundle_pic(app_package_path, all_pic_list): """ 将所有bundle图片存入list中 """ pathDir = os.listdir(app_package_path) for child_file in pathDir: child_path = os.path.join('%s/%s' % (app_package_path, child_file)) # isfile:如果child是一个存在的文件则返回true,否则(bundle、文件夹会等)返回false if os.path.isfile(child_path): if child_path.endswith(".png") or child_path.endswith(".jpg") or child_path.endswith(".jpeg") or child_path.endswith(".gif") or child_path.endswith(".webp"): if child_path.find(".bundle") > 0: all_pic_list.append(child_path) else: find_all_bundle_pic(child_path, all_pic_list)
def find_opt_pic(all_picture_list,final_opt_pic_list): """ 查找bundle中重复的多倍图片 """ for picture in all_picture_list: if picture.endswith("@2x.png"): prefix_2x = picture[0: len(picture) - 7] for picture1 in all_picture_list: # 前缀匹配 if picture1 != picture and picture1.startswith(prefix_2x): if (len(picture) == len(picture1) and picture1.endswith("@3x.png")) or len(picture) == len(picture1) + 3: final_opt_pic_list.append(picture) final_opt_pic_list.append(picture1)

3.8 有损图片压缩可减少Assets.car大小

从3.6节我们得知无损压缩对于Asset Catalog是没有体积优化效果的,但是有损压缩可减少Assets.car 大小,因为Asset Catalog自身也会对图片进行压缩优化,所以有损压缩图片的收益没有bundle转Asset Catalog收益明显,常用的有损压缩工具有TinyPng和pngquant。

TinyPng是一个网页版的工具,通过合并图片中相似的颜色,将 24 位的 PNG 图片压缩成 8 位色值的图片,并且去掉了图片中不必要的元数据来实现压缩。对于单张图片压缩使用非常方便,链接地址如下:https://tinypng.com/,但是如果要处理批量图片压缩,上传过程中容易出现上传不成功等问题,这个工具不支持自定义压缩配置。

pngquant是一个有损的PNG压缩开源库,提供了命令行和源码库两种形式。将24位或32位的RGBA PNG图转换成8位PNG图并保留透明度通道。通过这个库的转化可以显著减少png文件大小,pngquant采用的是本地脚本压缩,工具下载地址:https://pngquant.org,对批量压缩图片支持的比较友好,pngquant支持自定义压缩品质,配置压缩品质小于90后压缩率会高于TinyPng,并且pngquant是开源的,可以自定义,这是百度APP图片压缩的首选。


四、HEIC图片编码优化

GEEK TALK

4.1 HEIC图片编码优点

HEIC(High Efficiency Image Coding)是一种图像编码标准,它可以极大提升压缩率,并有效减小储存占用,自iOS 11和macOS High Sierra(10.13)开始,苹果将HEIC设置为图片存储的默认格式,它由动态影像专家小组(MPEG)开发,并在MPEG-H Part 12(ISO/IEC 23008-12)中定义,以下是HEIC图片的特点:

压缩率高:HEIC图片比JPEG图片压缩率高1.5倍,比PNG图片压缩率高3倍,也比GIF图片压缩率高3倍。

节省内存:HEIC图片比JPEG图片节省20%的存储空间,比PNG图片节省50%的存储空间,比GIF图片节省80%的存储空间。

解码效率高:在iOS系统中,HEIC采用硬解码,解码效率高,跟WebP(软编码)相比,是其100倍,但略慢于JPEG。

保留原始图像质量:HEIC图片采用H.264和JEP格式压缩,可以保留原始图像质量。

支持无损放大:HEIC图片支持无损放大,可以将图片放大两倍而不失真。

色彩处理方面:HEIC图片可以根据像素点的亮度分布自动亮度、对比度和饱和度,从而更好地还原图像的真实色彩。

系统兼容性好:我们知道iOS 11开始HEIC是图片存储的默认格式,也就是iOS 11以后的系统都支持HEIC图片,但我们百度APP目前还支持iOS10系统,对这部分用户如何处理?在实践中发现,在iOS10系统上,当把HEIC图片放xcasset文件里,最后图片也是可以正常显示的,我们做了一番原因排查,用Asset Catalog Tinkerer工具解压出Assets.car 文件,发现在xcasset里的HEIC图片,对于iOS10的系统,在打包时会被系统转化为png格式图片,Asset Catalog解决了HEIC图片的兼容性问题。

4.2 如何生成HEIC图片

利用Mac自带功能实现png转HEIC方法:右键图片,快速操作-》转换图像, 格式选HEIF,选择原图像。

优化结果如下所示,左边是png原图(1.6M),右边是HEIC图片(106KB)。

4.3 HEIC图片使用方法

4.3.1 必须在Asset Catalog使用

HEIC图片必须放在Asset Catalog中才能使用,bundle方式不支持HEIC图片加载。

HEIC图片的加载使用方法和普通的asset图片一样,如下所示:
imgView.image = [UIImage imageNamed:@"account_login"];

4.3.2 对于大图HEIC格式明显体积小

理论上来说,HEIC格式图片的体积是PNG格式图片的三分之一,但实际过程发现对于大图,这个优化效果很明显,但是对于小图尤其是小于10K的图片,HEIC图片还有可能超过PNG格式图片,所以我们在做HEIC图片编码优化时,对于小图不建议用这种方式。


4.3.3 带有Alpha通道的PNG图片不要做有损压缩

在实践过程中发现,一张PNG原图,尤其是带有Alpha通道,经过有损压缩(TinyPng或ImageOptim)后,再生成HEIC图片时,在iOS12,13,14系统上会显示绿幕,所以带有Alpha通道的PNG图片不要做有损压缩,存在兼容性问题。


五、总结

GEEK TALK

图片优化是包体积优化的重头戏,百度APP经过两个Q的优化落地9.75M的收益解决了存量图片的问题,随后建立图片使用规范和无用图片检测流水线解决增量图片的问题。

本文详细介绍了无用图片检测方案、Asset Catalog图片优化和HEIC图片优化方案,后续我们会针对其他优化类型详细介绍其原理与实现,敬请期待。

 END

参考资料:

[1]Asset使用方法:https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/index.html#//apple_ref/doc/uid/TP40015170-CH18-SW1

[2]Asset介绍:https://help.apple.com/xcode/mac/current/#/dev10510b1f7

[3]WWDC2018:Optimizing App Assets:https://developer.apple.com/videos/play/wwdc2018/227/

[4]TinyPng:https://tinypng.com/

[5]pngquant:https://pngquant.org

[6]HEIC图片介绍:https://mobiletrans.wondershare.com/heic-convert/what-is-heic-file.html

推荐阅读:

浅论分布式训练中的recompute机制

剖析多利熊业务如何基于分布式架构实践稳定性建设

百度工程师的软件质量与测试随笔

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

基于FFmpeg和Wasm的Web端视频截帧方案

百度研发效能从度量到数字化蜕变之路



一键三连,好运连连,bug不见👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存