燃烧app的卡路里--app瘦身之路

引言

app随着需求增加,体积逐步增大,影响用户的安装意愿。所以需要对app进行瘦身,轻装上阵~

我们的原则就是:

  • “知己知彼”
    我们需要了解app由哪些部门构成,哪些体积加起来构成了最终臃肿的app?
  • “高效打击”
    其次我们要对不同组成部分进行剖析,精确重点地对重要的模块进行优先处理,提高处理效率。为什么叫“高效打击”,因为我们的瘦身需求不是一项研究论文,可以长期深入的探索,通常是一个要在指定周期内完成的工作,所以我们需要有效率,快速地提升,所以本文的方式就是从最大可处理文件开始,依次处理更小的文件。毕竟对大文件处理得到的收益是最明显的。

所以本文主要方法就是按大小排序待处理的文件,分析每个待处理文件如何构成的,然后对构成文件递归分析构成、排序......


IPA文件构成

我们给用户或者appstore上传的应用文件,其实就是ipa文件,通过xcode中的archive而来。所以对于应用瘦身我们就是首先要对ipa文件进行瘦身。那么ipa文件是什么,包含什么? 我们先给出总图,再逐步说明:


ipa解析

ipa文件其实就是一个压缩文件,我们通过解压缩看到内部情况:

ipa文件 = app文件 + 图片文件 + meta信息 + plist文件

其中后面三项都是无法优化改动,是打包时生成的。所以现在我们ipa瘦身就等同于对app文件瘦身。


app文件瘦身:

app文件其实就是我们xcode中run出来的产出,也是一个文件夹,我们通过“查看包内容”,可以查看内部情况,主要包含:
app文件 = 可执行文件 + codingsign文件 + nib文件 + Assets.car+ 图片 +视频 + framework +语言包+ rn/ js/html文件 ......

每个app都不尽相同,可能与上述有所不同,但这不是重点,重点是我们要逐个对这些文件进行分析处理。我们目标是高效打击,那就首先对这些文件按照大小进行排序, 按照排序后文件进行逐个击破!但是本文描述的顺序是按照处理的难易程度来排列的,便于读者阅读。

  • _CodeSignature:

文件的 hash 列表。里面有一个文件 CodeResources 是一个属性列表,包含 bundle 中所有其他文件的列表。用来判断一个应用程序是否完好无损。

  • Assets.car

Assets.xcassets 好处是不同分辨率的图片好管理\工程打包后会对图片进行压缩. 如果将图片直接放在工程目录下面,打包后文件散落在包里面,不会对图片进行压缩,而如果放在xcassets中,会将这些图片(AppIcon和LaunchImage是直接放在包中的)统一压缩成一个Assets.car的文件。获取Assets.car里面的图片需要用到一个命令行工具叫cartool:https://github.com/steventroughtonsmith/cartool

  • 语言字体包

对于一些定制设计的语言或者字体包,可以考虑是否有必要?是否可以通过下载形式?

  • framework

如果我们的工程引入动态framework,这些framework会被引入到app文件夹的一个framework文件中,直接占用整个app的体积,所以对于framework的瘦身我们通常可以按照可执行文件相似的原理来处理。

  • 大视频文件

视频文件我们首先要考虑是否可以通过网络下载来使用,然后再考虑是否可以换成其他压缩编码效率更好的类型文件。

  • 大音乐文件

例如wav文件这种无损音频格式,虽然质量好,但是文件很大,我们可以选择合适的有损音频格式来有效减小体积。

  • 动图

gif格式的动图,是直接将图片放在一起播放,并没有去除时间冗余度,所以可压缩空间很大,我们可以考虑换成其他格式的动图。

  • 图片

对于图片我们需要从两方面处理,删减+压缩。
首先使用LSUnusedResources(https://github.com/tinymind/LSUnusedResources)工具找到无用图片,要注意代码中可能包含拼接文件名字的图片。
然后我们需要考虑是否对某些大图进行压缩,例如考虑是使用jpg还是png,还是WebP,当然更高的压缩编码可能带来图片质量的下降,需要权衡。

  • RN/js/html代码

对于RN/js/html代码,我们需要定期清理不用的静态代码,并按照oc源码相似原理来清除无用和过载的代码。

  • 可执行文件

经过排序后,排在第一位的通常就是与app同名的这个没有后缀的文件,这个文件是什么呢?它是怎么构成的呢?


可执行文件瘦身

首先我们通过命令看一下:

file 可执行文件

这个命令会输出:

xxx可执行文件: Mach-O executable arm_v7

可见这个文件是只包含arm_v7的executable文件。这样便可得到这个文件的架构信息,通常一个可执行文件可能包含armv7 arm64等架构。

1. 这里我们首先有一个可优化点:可执行文件是否包含不需要的架构?

例如模拟器的X86_64 I386首先是不能包含在其中的,因为appstore不允许,并且也是不需要提供给用的。另外是否需要支持armv7 也可以根据业务和需求来决定,如果可执行文件的size包含n个架构,通常每个架构的体积占到size/n,所以处理掉不需要的架构,带来的体积减小是最客观的!

处理的方式,一种是配置xcode的build settings中architecture,然后重新编译打包;另一种是使用lipo -thin命令,好处是不需要重新编译打包。(由于涉及到签名,这种方式无法提交appstore)

2. 架构处理之后,我们需要对最终保留的指定架构的可执行文件进一步处理:


可执行文件

可执行文件是有工程中的所有代码编译生成,通常由几部分组成: mach header + load command + segment1 + segment2 ......

  • header
    我们可以通过otool的一些指令查看这个可执行文件,例如查看header部分:
otool -h 可执行文件

输出结果如下:

Mach header

magic cputype cpusubtype caps filetype ncmds sizeofcmds flags

MH_MAGIC ARM V7 0x00 EXECUTE 55 5644 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

还可以使用otool -l xxx输出图中第二个部分的load command,其他命令可以通过otool命令自己查看。

  • 加载命令
    Mach-O中最重要的部分,它说明了操作系统应当如何加载文件中的各个segment数据。
  • 段数据(Segments)
    每一个segment定义了一些Mach-O文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了app所属进程的虚拟内存中。
    1). __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
    2). __TEXT: 包含了执行代码以及其他只读数据。 为了让内核将它 直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。
    3). __DATA: 包含了程序数据,该段可写;
    4). __OBJC: Objective-C运行时支持库;
    5). __LINKEDIT: 含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等。

综上可知,我们工程中的代码(包括常量、变量、字符串、代码)和动态库等都被链接到了这个可执行文件,所以这些元素都是我们的优化点。从哪入手呢?

工程配置

首先从工程配置入手:
我们通过对xcode的工程进行配置,也可以减小可执行文件:

  • Strip Link Product设成YES,通常默认开启;
  • 去掉异常支持,微信号称减重明显,但在我们的工程中不明显。配置过程中,遇到了坑,这里列出来:

Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions。 这种配置之后如果你的工程中没有使用try 或者 throw语句,可以编译通过,但是使用了try 或者 throw语句将会报错,大意是含有这些语句的文件不支持异常捕获。 这时我们需要对这些文件的编译选项进行配置,在build phases中查找到编译的源文件,然后添加compile flags。 这里需要注意我们需要根据这个文件类型添加不同的compile flag,对于oc文件需要使用下图的-fobjc-exceptions,而对于c++文件,需要使用-fexceptions,添加不当的话,编译还是会报错。


build settings
compile flag

代码

LinkMap文件是Xcode产生可执行文件(Mach-O)的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。首先需要在工程中打开:XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置

LinkMap里展示了整个可执行文件的全貌,分为三段,分别是:

  • 以# Object files:为分割标志,列出所有.o目标文件的信息(包括静态链接库.a里的),
  • 以# Sections:为分割标志,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值),字段的含义在Mach-o中已详细介绍。
  • 以# Symbols:为分割标志,列出具体的按每个文件列出每个对应字段的位置和占用空间

有个LinkMap分析工具:https://github.com/huanxsd/LinkMap,可以排序列出当前链接的所有的.o文件,这些就是我们需要优化的地方。不管是三方库还是我们自己代码中的o文件,都是我们需要逐个考虑的,尤其是排序靠前的文件。主要包含两类问题需要处理:

1. 过载
引入某些三方库,我们可能只是用其中一个或几个类,而其他所有类都是不会使用的,但是作为一个静态库,也会被链接到可执行文件中,这时我就需要将这些不需要的类在编译的工程中移除,使其不进入到最终的静态库。例如某加密解密三方库,我们可能只使用其中一种加密解密的类,那么其他的类文件都可以从工程中删除。

2. 无用类
例如afnetworking三方库,如果我们的代码中只是简单一个http请求,用处很少,那就可以考虑将afnetworking移除,因为afnetworking涉及到的o文件总体积可能将近1M。 如果我们在某个子工程可能只用到一个http请求,并且不需要复杂的处理,那么完全可以自己使用NSUrlsession来实现请求,而不必引入afnetworking。

由于这里是对app的可执行文件所有o文件进行排序,会将所有三方库的o文件混杂一起,为了便于查到精确处理,我们通常可以将其中大的三方库单独处理,可以使用ar -x命令,将库解压缩成o文件的集合,然后按照大小排序,再按照上述方式进行处理。

ar -x  静态库

对代码的处理我们只做到这个粒度,如果时间允许,可以进行更小粒度的处理,例如找到无用的方法、重复的方法进行屏蔽。当然更小粒度意味着对代码处理的时间更多,收益没有文件级别大。微信使用了下面的方法:

1. 查找无用selector
结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。

2. 查找无用oc类
通过otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段来获取当前所有oc类和被引用的oc类,两个集合相减就是无用oc类。

总结:方法很多,最重要是根据自己的需求、项目紧迫程度,选择合适的方法来进行,而不是盲目扎到某一方面。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,180评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,667评论 2 59
  • 不断的开发迭代,产品经理不断的添加需求,引入的资源文件几乎是只加不减,猛然回首,iOS包已经100多m,看来iOS...
    码农甲阅读 3,660评论 8 31
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,962评论 3 119
  • 一篇意味开始的文章做为开始,感觉特别一些。 要如何开始,我们当初如何开始走路,如何开始唱歌,如何开始生活,开始点点...
    Dalzial阅读 493评论 0 0