iOS 应用包大小瘦身解决办法
本文是借鉴 戴铭老师 iOS开发高手课 内容总结。
App 的安装包主要是由资源和可执行文件组成的。
App瘦身:无用图片、代码删除 + 图片压缩
目录
1、苹果 App Thinning 功能介绍
2、删除无用图片 方法
3、谷歌Webp图片资源压缩
4、腾讯公司开发的iSparta 工具进行图片压缩。
5、代码瘦身:对可执行文件的瘦身方法。(人工查找)
6、LinkMap 结合 Mach-O 找无用代码
7、通过 AppCode 找出无用代码
8、通过 ObjC 的 runtime 源码,我们可以找到怎么判断一个类是否初始化过的函数
1、充分利用 苹果官方自带 App Thinning 功能
1、iOS设备屏幕尺寸、分辨率越来越多,需要更多的资源匹配不同的尺寸和分辨率。而App Thinning会选择可用于当前设备的资源内容进行下载。iPhone6只会下载2x图片,iPhone6plus只会下载3x图片。并且下载适合自己设备的指令集架构文件。
2、App Thinning 有三种方式:
App Slicing:会在iTunes Connect上传App后,对App进行切割,创建不同的变体,适用不同的设备
On-Demand Resources:主要为游戏关卡场景服务的。它会根据用户的关卡进度下载随后几个关卡的资源,并且已经过关的资源也会被删掉,这样可以减少初装App的包大小
Bitcode:是针对特定设备进行包大小优化,优化不明显。
3、使用:Xcode 和 App Store已经帮我们完成大部分工作。我们通过Xcode添加 xcassets目录,然后将图片添加到这个目录既可。添加2x 和 3x分辨率图片。
2、无用图片资源优化:删除无用图片 和 图片资源压缩
LSUnusedResources使用1、删除无用图片:
1> 通过find 命令获取App安装包中所有的资源文件。比如 find /Users/..../工程文件
2>设置用到的资源的类型,比如jpg、gif、png、webp
3>使用正则匹配在源码中找出使用到的资源名,比如 pattern = @"@"(.+?)""
4>使用find命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源了
5>对于按照规则设置的资源名,我们需要在匹配使用资源的正则表达式里添加相应的规则,比如 @"imgge_%d"
6>确认无用资源后,就可以对这些无用资源执行删除操作了。这个删除操作,你可以使用NSFileManager系统类提供的功能来完成。
2、使用开源的工具来完成以上操作:LSUnusedResources 特别是对于使用编号规则的图片来说,可以通过直接添加规则来处理。使用方式也很简单
3、谷歌Webp图片资源压缩:对于 App 来说,图片资源总会在安装包里占个大头儿。对它们最好的处理,就是在不损失图片质量的前提下尽可能地作压缩。目前比较好的压缩方案是,将图片转成 WebP。WebP 是 Google 公司的一个开源项目。
1、WebP特点:
1>WebP 压缩率高,而且肉眼看不出差异,同时支持有损和无损两种压缩模式。比如,将 Gif 图转为 Animated WebP ,有损压缩模式下可减少 64% 大小,无损压缩模式下可减少 19% 大小。
2>WebP 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够而出现毛边。
2、图片转成 WebP 操作:
Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具 cwebp 来将其他图片转成 WebP。cwebp 使用起来也很简单,只要根据图片情况设置好参数就行。
cwebp 语法如下:cwebp [options] input_file -o output_file.webp
比如,你要选择无损压缩模式的话,可以使用如下所示的命令:cwebp -lossless original.png -o new.webp
其中,-lossless 表示的是,要对输入的 png 图像进行无损编码,转成 WebP 图片。不使用 -lossless ,则表示有损压缩。
在 cwebp 语法中,还有一个比较关键的参数 -q float。图片色值在不同情况下,可以选择用 -q 参数来进行设置,在不损失图片质量情况下进行最大化压缩:
小于 256 色适合无损压缩,压缩率高,参数使用 -lossless -q 100;
大于 256 色使用 75% 有损压缩,参数使用 -q 75;
远大于 256 色使用 75% 以下压缩率,参数 -q 50 -m 6。
4、腾讯公司开发的iSparta 工具进行图片压缩。
iSpart工具界面 webp图片加载使用1、优点:iSpart 是一个 GUI 工具,操作方便快捷,可以实现 PNG 格式转 WebP,同时提供批量处理和记录操作配置的功能。如果是其他格式的图片要转成 WebP 格式的话,需要先将其转成 PNG 格式,再转成 WebP 格式。
图片压缩完了并不是结束,我们还需要在 「显示图片」时使用 libwebp 进行解析。这里有一个 iOS 工程使用 libwebp 的范例,你可以点击 这个链接 查看。或者使用 pod 'SDWebImageWebPCoder' 框架或者 YYimage 框架。Webp图片加载使用方法
2、缺点:WebP 在 CPU 消耗和解码时间上会比 PNG 高两倍。所以,我们有时候还需要在性能和体积上做取舍。
建议:如果图片大小超过了 100KB,你可以考虑使用 WebP;而小于 100KB 时,你可以使用网页工具 TinyPng 或者 GUI 工具 ImageOptim 进行图片压缩。这两个工具的压缩率没有 WebP 那么高,不会改变图片压缩方式,所以解析时对性能损耗也不会增加。
5、代码瘦身:对可执行文件的瘦身方法。
1、可执行文件就是 Mach-O 文件,其大小是由代码量决定的。通常情况下,对可执行文件进行瘦身,就是 「找到并删除无用代码」的过程。
2、思路:
1>首先,找出方法和类的全集;
2>然后,找到使用过的方法和类;
3>接下来,取二者的差集得到无用代码;
4>最后,由人工确认无用代码可删除后,进行删除即可。
6、LinkMap 结合 Mach-O 找无用代码
LinkMap 文件获取方法1、怎么快速找到方法和类的全集:我们可以通过分析 LinkMap 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里的 Write Link Map File 设置为 Yes,然后指定 Path to Link Map File 的路径就可以得到每次编译后的 LinkMap 文件了。
Link-Map MachOView 软件打开样本2、LinkMap 文件分为三部分:Object File、Section 和 Symbols。
其中:1>Object File 包含了代码工程的所有文件;
2>Section 描述了代码段在生成的 Mach-O 里的偏移位置和大小;
3>Symbols 会列出每个方法、类、block,以及它们的大小。
通过 LinkMap ,你不光可以统计出所有的方法和类,还能够清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
3、得到了代码的全集信息以后,我们还需要找到已使用的方法和类,这样才能获取到差集,找出无用代码。所以接下来,我们要 通过 Mach-O 取到使用过的方法和类。
iOS 的方法都会通过 objc_msgSend 来调用。而,objc_msgSend 在 Mach-O 文件里是通过 __objc_selrefs 这个 section 来获取 selector 这个参数的。
所以,__objc_selrefs 里的方法一定是被调用了的。__objc_classrefs 里是被调用过的类,__objc_superrefs 是调用过 super 的类。通过 __objc_classrefs 和 __objc_superrefs,我们就可以找出使用过的类和子类。那么,Mach-O 文件的 __objc_selrefs、__objc_classrefs 和 __objc_superrefs 怎么查看呢?
我们可以使用 MachOView 这个软件来查看 Mach-O 文件里的信息。MachOView 同时也是一款开源软件,如果你对源码感兴趣的话,可以点击这个地址查看。
具体的查看方法,我将通过一个案例和你展开。
1>首先,我们需要编译一个 App。
2>然后,将生成的 DemoXXX.app 包解开,取出DemoXXX。
3>最后,我们就可以使用 MachOView 来查看 Mach-O 里的信息了。
可以看到 __objc_selrefs、__objc_classrefs 和、__objc_superrefs 这三个 section。但是,这种查看方法并不是完美的,还会有些问题。原因在于, Objective-C 是门动态语言,方法调用可以写成在运行时动态调用,这样就无法收集全所有调用的方法和类。所以,我们通过这种方法找出的无用方法和类就只能作为参考,还需要二次确认。
(缺点:这里只能看到已调用的方法,得去工程里查找为调用的方法,比较麻烦。建议结合 下面 的APPCode方法来进行过滤 找到 未使用的方法、类)
7、通过 AppCode 找出无用代码(当代码量过百万行时 AppCode 的静态分析会“歇菜”。但是,如果工程量不是很大的话,我还是建议你直接使用 AppCode 来做分析。)
AppCode 来做静态分析 Unused code 里看到所有无用代码1、AppCode 做分析的方法很简单,直接在 「AppCode 里选择 Code->Inspect Code 」就可以进行静态分析。
2、接下来,说一下这些无用代码的主要类型。
1>无用类:Unused class 是无用类,Unused import statement 是无用类引入声明,Unused property 是无用的属性;
2>无用方法:Unused method 是无用的方法,Unused parameter 是无用参数,Unused instance variable 是无用的实例变量,Unused local variable 是无用的局部变量,Unused value 是无用的值;
3>无用宏:Unused macro 是无用的宏。
4>无用全局:Unused global declaration 是无用全局声明。
【这里结合 上面工具 来进行筛选 ——> 没有用到的无用类、方法。】!!
3、APPCode静态检查的问题:
1>JSONModel 里定义了未使用的协议会被判定为无用协议;
2>如果子类使用了父类的方法,父类的这个方法不会被认为使用了;
3>通过点的方式使用属性,该属性会被认为没有使用;使用 performSelector 方式调用的方法也检查不出来,比如 self performSelector:@selector(arrivalRefreshTime);
4>运行时声明类的情况检查不出来。比如通过 NSClassFromString 方式调用的类会被查出为没有使用的类,比如 layerClass = NSClassFromString(@“SMFloatLayer”)。还有以[[self class] accessToken] 这样不指定类名的方式使用的类,会被认为该类没有被使用。像 UITableView 的自定义的 Cell 使用 registerClass,这样的情况也会认为这个 Cell 没有被使用。
基于以上种种原因,使用 AppCode 检查出来的无用代码,还需要人工二次确认才能够安全删除掉。
4、即使你使用 LinkMap 结合 Mach-O 或者 AppCode 的方式,通过静态检查已经找到并删除了无用的代码,那么就能说包里完全没有无用的代码了吗?
实际上,在 App 的不断迭代过程中,新人不断接手、业务功能需求不断替换,会留下很多无用代码。这些代码在执行静态检查时会被用到,但是线上可能连这些老功能的入口都没有了,更是没有机会被用户用到。也就是说,这些无用功能相关的代码也是可以删除的。
8、通过 ObjC 的 runtime 源码,我们可以找到怎么判断一个类是否初始化过的函数
1、isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里,flags 的 1<<29 位记录的就是这个类是否初始化了的信息。而 flags 的其他位记录的信息,你可以参看 objc runtime 的源码,
2、lags 采用位方式记录布尔值的方式,易于扩展、所用存储空间小、检索性能也好。所以,经常阅读优秀代码,特别有助于提高我们自己的代码质量。
既然能够在运行中看到类是否初始化了,那么我们就能够找出有哪些类是没有初始化的,即找到在真实环境中没有用到的类并清理掉。
总结:对于上线时间不长的新 App 和那些代码量不大的 App 来说,做些资源上的优化,再结合使用 AppCode 就能够有很好的收益。而且把这些流程加入工作流后,日常工作量也不会太大。但是,对于代码量大,而且业务需求迭代时间很长的 App 来说,包大小的瘦身之路依然任道重远,这个领域的研究还有待继续完善。LinkMap 加 Mach-O 取差集的结果也只能作为参考,每次人工确认的成本是非常大的,只适合突击和应急清理时使用。最后日常采用的方案,可能还是用运行时检查类的方式,这种大粒度检查的方式精度虽然不高,但是人工工作量会小很多。