10 | 包大小:如何从资源和代码层面实现全方位瘦身?

特别是App Store 规定了安装包大小超过150MB的 App 不能使用 OTA(over-the-air)环境下载,也就是只能在WiFi 环境下下载。所以,150MB就成了 App 的生死线,一旦超越了这条线就很有可能会失去大量用户。

官方 App Thinning

App Thinning 是由苹果公司推出的一项可以改善 App 下载进程的新技术,主要是为了解决用户下载 App 耗费过高流量的问题,同时还可以节省用户 iOS 设备的存储空间。

无用图片资源
图片资源的优化空间,主要体现在删除无用图片和图片资源压缩这两方面。

删除无用图片的过程,可以概括为下面这6大步。
1.通过 find 命令获取App安装包中的所有资源文件,比如 find /Users/daiming/Project/ -name。
2.设置用到的资源的类型,比如 jpg、gif、png、webp。
3.使用正则匹配在源码中找出使用到的资源名,比如 pattern = @"@"(.+?)""。
4.使用find 命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源了。
5.对于按照规则设置的资源名,我们需要在匹配使用资源的正则表达式里添加相应的规则,比如 @“image_%d”。
6.确认无用资源后,就可以对这些无用资源执行删除操作了。这个删除操作,你可以使用 NSFileManger 系统类提供的功能来完成。


image.png

图5 删除无用图片资源的过程

如果你不想自己重新写一个工具的话,可以选择开源的工具直接使用。我觉得目前最好用的是 LSUnusedResources,特别是对于使用编号规则的图片来说,可以通过直接添加规则来处理。使用方式也很简单,你可以参看下面的动画演示:

image.png

图片资源压缩

目前比较好的压缩方案是,将图片转成 WebP。WebP 是 Google公司的一个开源项目。
Google公司在开源WebP的同时,还提供了一个图片压缩工具 cwebp来将其他图片转成 WebP。cwebp 使用起来也很简单,只要根据图片情况设置好参数就行。
我的建议是,如果图片大小超过了100KB,你可以考虑使用 WebP;而小于100KB时,你可以使用网页工具 TinyPng或者GUI工具ImageOptim进行图片压缩。这两个工具的压缩率没有 WebP 那么高,不会改变图片压缩方式,所以解析时对性能损耗也不会增加。

代码瘦身

App的安装包主要是由资源和可执行文件组成的,所以我们在掌握了对图片资源的处理方式后,需要再一起来看看对可执行文件的瘦身方法。

可执行文件就是 Mach-O 文件,其大小是由代码量决定的。通常情况下,对可执行文件进行瘦身,就是找到并删除无用代码的过程。而查找无用代码时,我们可以按照找无用图片的思路,即:
首先,找出方法和类的全集;
然后,找到使用过的方法和类;
接下来,取二者的差集得到无用代码;
最后,由人工确认无用代码可删除后,进行删除即可。
接下来,我们就看看具体的代码瘦身方法吧。

LinkMap 结合 Mach-O 找无用代码

我们可以通过分析 LinkMap 来获得所有的代码类和方法的信息
LinkMap文件分为三部分:Object File、Section 和 Symbols。如下图所示:

image.png

我们可以使用 MachOView 这个软件来查看Mach-O 文件里的信息。MachOView 同时也是一款开源软件,如果你对源码感兴趣的话,可以点击这个地址查看。

具体的查看方法,我将通过一个案例和你展开。

  • 首先,我们需要编译一个 App。在这里,我clone了一个GitHub上的示例 下来编译。
  • 然后,将生成的 GCDFetchFeed.app 包解开,取出 GCDFetchFeed。
  • 最后,我们就可以使用 MachOView 来查看Mach-O 里的信息了。


    image.png
通过 AppCode 找出无用代码

用 AppCode 做分析的方法很简单,直接在 AppCode 里选择 Code->Inspect Code 就可以进行静态分析。


image.png

静态分析完以后,我们可以在 Unused code 里看到所有的无用代码,如下图所示:
image.png

接下来,我和你说一下这些无用代码的主要类型。
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 是无用全局声明。
看似AppCode 已经把所有工作都完成了,其实不然。下面,我再和你列举下 AppCode 静态检查的问题:
1.JSONModel 里定义了未使用的协议会被判定为无用协议;
2.如果子类使用了父类的方法,父类的这个方法不会被认为使用了;
3.通过点的方式使用属性,该属性会被认为没有使用;
4.使用 performSelector 方式调用的方法也检查不出来,比如 self performSelector:@selector(arrivalRefreshTime);
5.运行时声明类的情况检查不出来。比如通过 NSClassFromString 方式调用的类会被查出为没有使用的类,比如 layerClass = NSClassFromString(@“SMFloatLayer”)。还有以[[self class] accessToken] 这样不指定类名的方式使用的类,会被认为该类没有被使用。像 UITableView 的自定义的 Cell 使用 registerClass,这样的情况也会认为这个 Cell 没有被使用。

基于以上种种原因,使用AppCode检查出来的无用代码,还需要人工二次确认才能够安全删除掉。

运行时检查类是否真正被使用过

define RW_INITIALIZED (1<<29)

bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
isInitialized 的结果会保存到元类的 class_rw_t 结构体的 flags 信息里,flags 的1<<29 位记录的就是这个类是否初始化了的信息。而flags的其他位记录的信息,你可以参看 objc runtime 的源码,如下:

// 类的方法列表已修复
#define RW_METHODIZED         (1<<30)

// 类已经初始化了
#define RW_INITIALIZED        (1<<29)

// 类在初始化过程中
#define RW_INITIALIZING       (1<<28)

// class_rw_t->ro 是 class_ro_t 的堆副本
#define RW_COPIED_RO          (1<<27)

// 类分配了内存,但没有注册
#define RW_CONSTRUCTING       (1<<26)

// 类分配了内存也注册了
#define RW_CONSTRUCTED        (1<<25)

// GC:class有不安全的finalize方法
#define RW_FINALIZE_ON_MAIN_THREAD (1<<24)

// 类的 +load 被调用了
#define RW_LOADED             (1<<23)

flags 采用位方式记录布尔值的方式,易于扩展、所用存储空间小、检索性能也好。所以,经常阅读优秀代码,特别有助于提高我们自己的代码质量。

小结

今天这篇文章,我主要和你分享的是App安装包的一些瘦身方案。

在我看来,可以把包瘦身方案根据App的代码量等因素,划分为两种。

对于上线时间不长的新 App 和那些代码量不大的 App来说,做些资源上的优化,再结合使用AppCode 就能够有很好的收益。而且把这些流程加入工作流后,日常工作量也不会太大。

但是,对于代码量大,而且业务需求迭代时间很长的 App来说,包大小的瘦身之路依然任道重远,这个领域的研究还有待继续完善。LinkMap 加 Mach-O 取差集的结果也只能作为参考,每次人工确认的成本是非常大的,只适合突击和应急清理时使用。最后日常采用的方案,可能还是用运行时检查类的方式,这种大粒度检查的方式精度虽然不高,但是人工工作量会小很多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355