iOS 安装包的瘦身 (持续完善中...)

前言

随着 app 版本的迭代,app 功能可能会越来越多,此时 app 打出来的包也会越来越大,由于 Apple 对安装包大小的有限制(具体参见 Apple文档),所以这就涉及到app安装包的瘦身。
App Store 安装包是由 资源可执行文件 两部分组成,安装包瘦身也是从这两部分进行。

资源瘦身

资源瘦身主要是去掉 无用资源压缩资源 ,资源包括 图片音视频文件配置文件 以及 多语言 wording。无用资源是指资源在工程文件里,但没有被代码引用。检查方法是,用资源关键字(通常是文件名,图片资源需要去掉 @2x @3x),搜索代码,搜不到就是没有被引用。当然,有些资源在使用过程中是拼接而成的(如 loading_xxx.png ),需要手工过滤。

经验:可以把工具使用和手工过滤想结合。

  • 如何优雅的清理掉那些无用资源
    一个项目开发得越久,添加的功能模块也就越多,相应地,也会慢慢引入大量图片等资源。但是,在移除一些不再使用的模块的时候,开发者往往会忘记把该模块所对应的图片资源一起删除,因为源码和资源是分离的。长久以来,项目中就会存在大量没被使用的资源文件。
    具体方法无非是,一个一个地复制资源文件名,然后在 XCode 中全局查找该字符串,如果结果为 0 ,那么这个资源很可能就没有被使用。为什么说很可能?因为在代码中,有可能通过字符串拼接的方式使用了这个资源,而这种情况是没办法通过字符串匹配查找出来的。
    于是,我们需要这么一款工具:能够迅速找出工程中所有没被使用的资源文件。
    工具一Unused

Use this useful utility tool to check what image resources are not being used in your Xcode projects. Very useful to reduce your bundle size by showing you what images are not used!

优点:工具 Unused脚本 的调用做了封装,通过界面可以配置一定的信息,然后比较清晰的输入结果。
缺点:不够智能,不够通用,速度太慢,结果不正确。

工具二LSUnusedResources
或直接下载 LSUnusedResources.app.zip


LSUnusedResources 很大程度上受 工具 Unused 的影响,比如界面、交互,以及部分代码。但是,本工具在核心代码上做了优化,使其在搜索的速度、结果的正确上都有了很大的提高。

LSUnusedResources工具核心思想,简述如下:

  • 查找:选定的目录下的所有资源文件。这一步与工具 Unused 区别不大,都是调用 find 命令查找指定后缀名的文件。
  • 匹配:与工具 Unused 不同,LSUnusedResources 不是对每个资源文件名都做一次全文搜索匹配,因为加入项目的资源太多,这里会导致性能快速下降。而 LSUnusedResources 只是针对源码、XibStoryboardplist 等文件,先全文搜索其中可能是引用了资源的字符串,然后用资源名和字符串做匹配。

LSUnusedResources匹配结果
工具 Unused 会把大量实际上有使用的资源,当做未使用的资源输出。LSUnusedResources 则不会出现这样的问题,并且使得结果更加优化。举例说明:

icon_tag_0.png
icon_tag_1.png
icon_tag_2.png
icon_tag_3.png

然后用字符串拼接的方式在代码中引用:

NSInteger index = random() % 4;
UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:@"icon_tag_%d", index]];

icon_tag_x.png 是不应该被当做未使用的资源的,只是以一种比较隐晦的方式间接引用了,所以不应该出现在结果列表中,LSUnusedResources 工具做到了。

  • 压缩资源
    资源压缩主要对 png 进行无损压缩,用的是 ImageOptim 工具和 compress 命令(需要安装 XQuartz-2.7.5.dm 插件)。不建议对资源做有损压缩,有损压缩需要设计一个个检查,通常压缩后效果不尽人意。
    ImageOptim压缩结果

插曲

在聊可执行文件的瘦身之前,先介绍一下 XcodeLink Map FileLinkMap 文件是 Xcode 产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括 代码段(__TEXT)数据段(__DATA) 的分布情况。只要设置 Project -> Build Settings -> Write Link Map FileYES ,并设置 Path to Link Map Filebuild 完后就可以在设置的路径看到 LinkMap 文件了:



注:想要了解更多,具体可以参见我的文章 Xcode的Link Map File

可执行文件瘦身

回到我们的可执行文件瘦身问题,LinkMap 文件可以帮助我们寻找优化点。

1. 无用方法检测思路

以往 C++ 在链接时,没有被用到的类和方法是不会编进可执行文件里。但 Objctive-C 不同,由于它的动态性,它可以通过类名和方法名获取这个类和方法进行调用,所以编译器会把项目里所有 OC 源文件编进可执行文件里,哪怕该类和方法没有被使用到。

结合 LinkMap 文件的 __TEXT.__text ,通过正则表达式( [+|-][.+\s(.+)] ),我们可以提取当前可执行文件里所有 objc类方法和实例方法( SelectorsAll )。再使用 otool 命令 otool -v -s __DATA __objc_selrefs 逆向 __DATA.__objc_selrefs 段,提取可执行文件里引用到的方法名( UsedSelectorsAll ),我们可以大致分析出 SelectorsAll 里哪些方法是没有被引用的( SelectorsAll-UsedSelectorsAll )。扫描脚本( py ):

import os
import re

outPath = "/Users/luph/Documents/sizetj/" #输出目录
mathoFilePaht = "/Users/luph/Documents/sizetj/Pro" #可执行文件
linkmapPath = "/Users/luph/Documents/sizetj/Pro-LinkMap-normal-arm64.txt"
selrefsFile =  outPath+"/selrefs.txt" #引用sel文件
cmd = "otool -v -s __DATA __objc_selrefs "+ mathoFilePaht +" >> "+selrefsFile
os.system(cmd) #逆向selrefs段

linkmapContent = open(linkmapPath,encoding="utf8", errors='ignore').read()
pattern = re.compile(r'[+|-]\[\w+ \w+\]') 
selall = pattern.findall(linkmapContent)

selrefsF = open(selrefsFile,encoding="utf8", errors='ignore')
selrefsList = []
for line in selrefsF.readlines():
    if '__objc_methname' in line:
        line = line.strip("\n");
        lineSplit = line.split(":")
        if  len(lineSplit)  > 0:
            selrefs = ""
            lineSplit.reverse()
            for subStr in lineSplit:
                if len(subStr) > 0:
                    selrefs = subStr
                    break
            if len(selrefs) > 0:
                selrefsList.append(selrefs)
selrefsF.close()   

output = open(outPath+"result.txt", 'w')
for sel in selall:
    print("正在扫描【{0}】".format(sel))
    selMth = sel.replace("+",'')
    selMth = selMth.replace("-",'')
    selMth = selMth.replace("[",'')
    selMth = selMth.replace("]",'')
    selL = selMth.split(" ")
    selMth = selL[1]
    isUse = False
    for selref in selrefsList:
        if  selref == selMth:
            isUse = True
            break 
    if not isUse:
        print("发现无用方法【{0}】".format(sel))
        output.write("{0}\n".format(sel))  
     
output.close()
print("扫描结束")

注:

  • 系统 APIProtocol 可能被列入无用方法名单里,如 UITableViewDelegate 的方法,我们只需要对这些 Protocol 里的方法加入白名单过滤即可。
  • 另外第三方库的无用 selector 也可以这样扫出来的。
  • 你也可以用一下 工具 来扫.
2. 查找无用oc类

查找无用oc类有三种方式:

  • 一种是类似于查找无用资源,通过搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]"等关键字在代码里是否出现;
  • 另一种是通过otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段来获取当前所有oc类和被引用的oc类,两个集合相减就是无用oc类;
  • 最后一种使用fui工具扫描;
3. 扫描重复代码

可以利用第三方工具simian扫描。

4. 其他

从安装包的瘦身角度来说,对于语言选择这块来说,不推荐使用 Swift,不论纯 Swift 还是 混编,任何一个包含有 Swift 代码的 App 都有的一个为了支持 Swift 的动态库集合,在10M 左右。如果你使用 Objective - C 完全不用这个东西。

更多相关

1. App Thinning

据Apple官方文档的介绍,App Thinning主要有三个机制:

  • Slicing
    开发者把App安装包上传到AppStore后,Apple服务会自动对安装包切割为不同的应用变体(App variant),当用户下载安装包时,系统会根据设备型号下载安装对应的单个应用变体。(你不需要做什么,iOS9.0.2以上就支持)


  • Bitcode
    开启Bitcode编译后,可以使得开发者上传App时只需上传Intermediate Representation(中间件),为二进制数据表示的格式的中间码,而非最终的可执行二进制文件。 在用户下载App之前,AppStore会自动编译中间件,产生设备所需的执行文件供用户下载安装。也就是当我们提交程序到 App Store上时, Xcode 会将程序编译为一个中间表现形式( bitcode )。然后 App store 会再将这个 Bitcode 编译为可执行的64位或32位程序。苹果会根据下载应用的用户的手机指令集类型生成只有该指令集的二进制,进行下发



    所以,通过这个方式,我们可以做到架构级别的App Slicing。

然而,一个很常见的误区是认为使用 bitcode 能优化包大小,其实启用 bitcode 作用并不大。实际上 bitcode 和包大小半毛钱关系都没有,它仅仅是把编译的最后一步留给苹果,这样苹果就可以在优化编译器后,再次将我们的应用打包,从而让历史应用也能享受到新技术
文档 里可看到

In fact, app slicing handles the majority of the app thinning process. ‘App Slicing’ feature finally switched on in iOS 9.0.2

说明slicing才是主要处理 app thinning的而且该功能需要在iOS9.0.2以上才支持(iOS9.0中被关闭了,因为一个iCloud的bug)。实际上Bitcode,做的事情是指令集优化。根据你设备的状态去做编译优化,进而提升性能。所以Bitcode对包的大小优化起不到什么本质上的作用。

注意点
1.开启 Bitcode 编译后,编译产生的 .app 体积会变大(中间代码,不是用户下载的包),且 .dSYM 文件不能用来崩溃日志的符号化(用户下载的包是 Apple 服务重新编译产生的,有产生新的符号文件)
2.通过 Archive 方式上传 AppStore 的包,可以在Xcode的Organizer工具中下载对应安装包的新的dSYM符号文件。或者iTunes Connect上下载对应构建包的dSYM(需消除混淆)
详情见 文档

  • On-Demand Resources
    On-Demand Resources(即按需资源)是指开发者对资源添加标签上传后,系统会根据App运行的情况,动态下载并加载所需资源,而在存储空间不足时,自动删除这类资源。
    这可能在游戏中应用场景会多一些。你可以用 tag 来组织像图像或者声音这样的资源,比如把它们标记为 level1,level2 这样。然后一开始只需要下载 level1 的内容,在玩的过程中再去下载 level2。或者也可以通过这个来推后下载那些需要内购才能获得的资源文件。



    这种机制对于大多数APP来讲,看起来更像是按需加载网络图片,并作缓存处理。而On-Demand Resources只是将这个服务交由苹果来处理, 个人觉得多少显得鸡肋。

Author

如果你有什么建议,可以关注我,直接留言,留言必回。

参考

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