iOS安装包瘦身

优化指标

itunes connect上有两种包大小显示:“Download Size”,“Install Size”。“Download Size”即下载包大小,超过150M需要使用无线网下载的限制就是这个大小(现在已经放宽到200M);“Install Size”即安装后占用的磁盘空间大小,在appstore上显示的也是这个大小,用户往往会误认为这是下载安装包消耗的流量大小。所以一开始我们就将“Install Size”作为了优化指标。“Install Size”减小后,“Download Size”自然也会减小。

Asset Catalog中的文件大小计算

Assest.car 做为 Asset Catalog 的编译产物,我们怎么获取到car文件中的图片大小呢?在以前查iOS9,P3格式图片问题的时候我们用过苹果提供的assetutil工具,使用assetutil就能获取到图片信息描述:

sudo xcrun --sdk iphoneos assetutil --info xxx/Assets.car > xxx/Assets.json

Assets.json 中的图片详细数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"AssetType" : "Image",
"BitsPerComponent" : 8,
"ColorModel" : "RGB",
"Colorspace" : "srgb",
"Compression" : "lzvn",
"Encoding" : "ARGB",
"Idiom" : "universal",
"Image Type" : "kCoreThemeOnePartScale",
"Name" : "1001", //xxx.imageset 的文件名
"NameIdentifier" : 11584,
"Opaque" : false,
"PixelHeight" : 48,
"PixelWidth" : 72,
"RenditionName" : "1001@3x.png",//工程文件中的实际图片名
"Scale" : 3,
"SHA1Digest" : "E34FCAC314E26DE7FF30442AA33E436B242AA4BA",
"SizeOnDisk" : 800,//占用的磁盘大小,Asset Catalog中的图片编译后的大小取该值。
"Template Mode" : "automatic"
},

最终我们使用SizeOnDisk 字段来获取图片大小。使用SizeOnDisk计算精度很高(所有图片的SizeOnDisk相加和car文件大小误差在1M以内)。

一个Asset Catalog的图片在Assets.car中实际上根据不同设备有4张对应的图片,大小不同,但名字相同。而cartool解压出的图片大小为其中某一张,这样大小计算就不太准确了。所以放弃原先使用cartool,改用assetutil。

App Slicing

比如企业包不经过appslicing:

App在打包过程中,会将同一张图片采用适合各个不同架构的压缩方式将图片压缩,并存储到asset.car中.

App Slicing只会对在Asset Catalog的资源文件进行分发,而放在根目录,bundle中的资源文件不会分发,所以在统计模块所使用资源文件之前我们需要注意到这个特性。如果以通用包来统计模块使用的资源文件大小、数量,其实并不能真正反映此模块对整个安装包大小的影响。所以我们决定使用单个设备来衡量资源文件使用情况。目前我们选择iPhone X,iOS11设备做为参考标准。 要统计单个设备的资源文件使用情况,一个方式是使用adhoc包导出支持单个设备的安装包统计,不过这样的方式需要每次集成后都需要单独打包,因为现在ci并不会出支持单个设备的包。后来我们发现assetutil除了可以导出car文件信息之外,还可以从通用包car文件导出指定设备的car文件,入参较多,经过尝试iPhone X的如下:

1
sudo xcrun --sdk iphoneos assetutil --idiom phone --subtype 570 --scale 3 --display-gamut srgb --graphicsclass MTL2,2 --graphicsclassfallbacks MTL1,2:GLES2,0 --memory 1 --hostedidioms car,watch xxx/Assets.car -o xxx/thinning_assets.car

从上文知道car以外的资源文件不会分发,获取指定设备的car文件后我们就可以计算出模块所用资源文件大小,数量。

其他的资源文件大小计算

其他的文件大小计算方式相对简单,苹果的APFS文件系统的最小存储单元为4KB,即使只有几十字节大小的文件,占用的空间也是4KB。对于安装包里面的独立文件我们使用4KB对齐的方式进行大小计算,有些大点的文件磁盘占用空间并不是4的整数倍,但大小相近,影响不大: Math.ceil(size/4000.0)4,size为文件实际大小,单位字节; 在 MB、KB、Byte 之间的换也是对齐 Apple 使用的是 1000 而不是 1024(即1MB = 1000 KB = 10001000 Byte)。

可执行文件优化

  • 使用strip -x命令处理动态库。
  • 无用类和无用方法 。结合 linkMap映射出无用的方法和类归属的组件,并且初步量化大小。因为基础组件中的无用方法和类,不能确定是否被非商城的 App 使用,只能对业务组件优化,考虑到涉及组件众多,并且收益和工程量不成正比,并且删除方法风险比较大,将无用方法和类优化的优先级降低。
  • 内置的ReactNative业务 JDReact提供了预置和后装两种发布方式,而为了用户体验,大部分业务模块都选择使用预置包的方式。时间一长,文件的数量就越来越多。由于文件系统的4K对齐,对包大小的影响也是非常大。对内置的ReactNative业务优化如下:
    • 推动流量相对较低的模块(三级及三级以上页面)转后装方式;
    • 根据资源文件使用规范,推动业务整改; 这部分的工作量主要在和业务方的沟通,经过部分模块转后装后,瘦身效果也是很明显。
  • extension 在ipa包中我们也注意到了PlugIns目录,这里主要存放一些插件,比如today extension,share extension等,虽然这些插件在整个ipa包中的大小占比不大,但是我们还是决定梳理下有没有优化点。梳理后发现这些插件对于一些基础类库(网络框架,图片加载框架等)的使用都是以拷贝代码的方式加到工程中。我们知道这些类库完全可以和主app共享,因为主app中这些库是以动态库的形式使用的。经过优化后,成功的将today extension的大小减少了0.9M(嗯~蚊子虽小…)。

去除符号信息

  • Strip Linked Product / Deployment Postprocessing / Symbols Hidden by Default 在release版本应该设为yes,可以去除不必要的调试符号。Symbols Hidden by Default会把所有符号都定义成”private extern”,详细信息见官方文档。
  • 这些选项目前都是XCode里release的默认选项,但旧版XCode生成的项目可能不是,可以检查一下。其他优化还可以参考官方文档—Introduction to Code Size Performance Guidelines

无用类无用方法

扫描无用代码的基本思路都是查找已经使用的方法/类和所有的类/方法,然后从所有的类/方法当中剔除已经使用的方法/类剩下的基本都是无用的类/方法,但是由于 Objective-C 是动态语言,可以使用字符串来调用类和方法,所以检查结果一般都不是特别准确,需要二次确认。目前市面上的扫描的思路大致可以分为 3 种:

  • 基于 Clang 扫描
  • 基于可执行文件扫描
  • 基于源码扫描

重构重复代码

重复代码堆积太多,不仅意味着 Bad Code Smell,我们的包大小也会受到影响,我们可以使用 PMD 来检查项目中的重复代码,并且做选择性的重构。

jd做法

无用类通过 otool 逆向Mach-O文件 DATA.objc_classlist段和DATA.objc_classrefs 段获取所有 OC 类和被引用的类,两个集合差值为无用类集合,

  • 结合 nm -nm 得到地址和对应类名符号化无用类类名;根据商城的限制做过滤,规则如下:
  • otool 逆向 DATA.objc_nlclslist 获取实现 load 方法的类过滤(RN与原生的桥接类、Swizzle Method 类);
  • 通过 otool 逆向 TEXT.cstring 获取所有字符串常量,过滤通过 NSClassFromString 调用的;
  • 子类实例化,父类没有实例化,父类不会出现在中 __objc_classrefs,通过 otool -oV 逆向出类的继承关系,过滤出子类被实例化(NSClassFromString 调用),父类没有实例化(NSClassFromString 调用)的类;
  • 过滤使用 Plist 文件引用的类;

无用方法 通过 otool 逆向 DATA.objc_selrefs 段获取使用到的方法,通过 otool -oV 获取实现的所有方法取差值。然后过滤掉 setter、getter、系统方法和协议、自定义的协议、sel 调用。

静态库统计

项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。我们可以从linkmap中统计出这个信息,可以通过linkmap统计每个.o目标文件占用的体积和每个.a静态库占用的体积

ARC转MRC

有人提出用ARC写的代码编译出来的可执行文件是会比用MRC大的,原因大致是ARC代码会在某些情况多出一些retain和release的指令,例如调用一个方法,它返回的对象会被retain,退出作用域后会被release,MRC就不需要,汇编指令变多,机器码变多,可执行文件就变大了。还有其他细节实现的区别,先不纠结了。

那用ARC究竟会增大多少体积?我觉得从汇编指令的增多减少去算是很难算准确的,这东西涉及细节太多,还是得从统计的角度计算。做了几个对比试验,统计了几个同时支持ARC/MRC的开源项目在开启/关闭ARC的情况下TEXT代码段的大小对比。只对比TEXT代码段是因为:

ARC对可执行文件大小的影响几乎都是在代码段ARC大概会使代码段增加10%的size,考虑代码段占可执行文件大约有80%,估计对整个可执行文件的影响会是8%。

可以评估一下8%的体积下降是不是值得把项目里某些模块改成MRC,这样程序的维护成本上升了,一般不到特殊情况不建议这么做。

类/方法名长度

观察linkmap可以发现每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的,原因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来。

对此我们可以考虑在编译前把所有类和方法名进行混淆,跟压缩js一样,把长名字替换成短名字,这样做的好处除了缩小体积外,还对安全性有很大提升,别人拿到可执行文件对它class-dump出来的结果都是混淆后的类和方法名,就无法从类和方法名中猜出某个方法是做什么的,就难以挂钩子进行hack。不过这样做有个缺点,就是crash堆栈反解出来的堆栈方法名会是混淆后的,需要再加一层混淆->原名的转换,实现和使用成本有点高。

实际上这部分占用的长度比较小,中型项目也就几百K,对安全性要求高的情况可以试试。

资源文件优化

这也是一期优化通过改造 Assets.car 中的 183 张图片能优化了近 30M 原因,千万不要将大图随意拖到工程中。 结合上文中负优化规律,改造处理方案如下:

  • 无用文件删除
    • 现在应该没有APP需要支持iPhone4以下的机型了,所以1X的图片可以全部删掉。3X的图片是保留还是删掉看具体情况。
  • 重复文件删除
  • 大文件压缩 apng
    • 不能转下载的使用压缩过的jpg格式图片。
    • 不能使用jpg的图片经过压缩后( 主要是tinypng有损压缩)后放到 bundle 中使用。
  • 图片管理方式规范
    • 优先转网络下载,使用默认图/纯色兜底,如楼层背景图;
  • 资源压缩
    • 首先是图片压缩,ImageOptim/TinyPNG/ImageAlapha 工具可以实现无损压缩。
      • 其实在我们使用这些图片的时候,我们其实是用的一些图片,但是像图片的创建日期,创建人之类的信息其实在我们项目中是没有用到的.这些信息我们可以通过一些工具进行删除,这里给大家推荐款工具ImageOptim.另外关于图片,建议使用Apple推荐的.xcassets来管理,它会把里边的所有png格式的图片压缩成一个Assets.car文件,压缩比率比其他方式管理图片要高。不过测试发现jpg图片不会在Assets.car文件里。
    • 尽量使用8-bit图片
      • 使用8-bit的PNG图片,比32-bit的图片能减少4倍的压缩率。由于8-bit的图片支持最多256种不同的颜色,所以8-bit的图片一般只应该用于一小部分的颜se图片。例如灰度图片最好使用8-bit。
    • 音频的压缩
      • 参考WWDC中的Audio Development for Games,里面介绍了如何有效的处理音频。常规来说,我们要使用AAC或MP3来压缩音频,并且可以尝试降低一下音频的比特率。有时候44.1khz的采样是没有必要的,稍微低一点的比特率也不会降低音频的质量。

无用图片筛查

通过分析安装包中使用图片可分为三类文件:

  • 可执行文件;
  • 可读文件(.plist、.js、.html);
  • 不可读文件(.nib、.storyboardc);

可执行文件通过 otool -v -s TEXT cstring 获取可执行文件中的 TEXT.cstring 段。__cstring 包含了可执行文件中的字符串常量(源码中的 @“xxx” 字符串);

iconfont

即使经过了图片转下载,无用图片删除,但是工程中的图片数量还是极为可观,其中各种各样的icon图标占了不少的数量。为了进一步减少图片数量,我们引入了iconfont方案, iconfont优点:

  • 矢量,缩放不失真。
  • 可以设置颜色。
  • 接入成本低,不需要引入额外的类库。

iconfont 可以解决因为icon大小,颜色不同而重新切图的窘境。从京东内部的quark平台了解到目前已经可以很好的支持iconfont,我们在一个模块就找到了55个icon并且成功转成了iconfont。不难看出iconfont是一个能减少图片数量的好方案。

注意: Apple 为了在优化 iPhone 设备读取 png 图片速度,将 png 转换成 CgBI 非标准的 png 格式
Apple自动优化: • 放在根目录下png格式的图片。 • 放在Asset Catalog中的png,jpg格式的图片,其中jpg会转成png。
不会处理: 放在根目录下的jpg, bundle中的png不会被优化

xcode编译参数优化

  • Generate Debug Symbols
  • Asset Catalog Compiler
  • Dead Code Stripping
  • Apple Clang - Code Generation - Optimization Level
  • Strip Symbol Information
  • Exceptions - 去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设为NO

工具

无用物品扫描 https://github.com/tinymind/LSUnusedResources

重复文件:https://github.com/arsenetar/dupeguru

图片压缩 : https://github.com/ImageOptim/ImageOptim

linkmap: https://github.com/huanxsd/LinkMap

查找相似代码 https://github.com/startry/SameCodeFinder

无用代码 无用imoprt https://github.com/dblock/fui

查找XCode工程中没被使用的图片资源

参考链接

干货!京东商城iOS App瘦身实践
干货|今日头条iOS端安装包大小优化
https://www.jianshu.com/p/c94dedef90b7
https://www.cnblogs.com/wdsunny/p/7486617.html
https://mp.weixin.qq.com/s/J_XYpIfDeeWJBlk9sRQMAA