为何对App包大小做瘦身优化?
- 节省用户流量,方便用户快速下载成功App
- 安装包超过150MB就不能使用OAT环境下载(也就是必须WiFi)
- 包过大,用户更新升级也有影响。
官方 App Thinning
App Thinning: 针对不同的设备来选择只适用于当前设备的内容以供下载。节约了用户下载App的流量问题 和 iOS设备的存储空间。
App Thinning原理:每个App包会包含多个芯片(如真机、模拟器)的指令集架构文件,App Thinning会使用户只下载一个适合自己设备的芯片指令架构文件。
App Thinning有三种方式:App Slicing、BitCode、on-Demand Resources。
-
App Slicing,会在向iTunes Connect上传App后,对App做切割,创建不同的变体,就可以适用到不同的设备。
-
on-Demand Resource 主要为游戏多关卡场景服务的。根据用户的关卡进度下载随后几个关卡的资源,并删除已过关的资源,减少初装App的包大小。
-
Bitcode,针对特定设备进行包大小优化,优化不明显。
如何使用App Thinning?
App Thinning的大部分工作都是由XCode 和 App Store来完成,所以只需要通过XCode添加xcassets目录,然后将图片添加进来即可。
也就是:新建一个文件选择Asset Catalog模板(在 iOS - Resource栏里),按照Asset Catalog的模板添加图片资源即可。
添加的2x和3x图片会在上传到App Store后被创建成不同的变体以减少App安装包的大小。
而芯片指令集架构文件只需要按照默认的设置,App Store就会根据设备创建不同的变体,每个变体里只有当前设备需要的那个芯片指令架构文件。
简单说:使用App Thinning之后苹果会自动帮你把App包按照型号分割成不同变体,保证每个型号只下载自己所需要的资源。而自己所需要做的,就是通过Asset Catalog模板创建xcassets目录,将2x与3x图片放进去。
图片资源的优化
图片资源的优化有两个方面:无用的图片、图片资源的压缩。
无用图片的删除大致为六步:
-
通过find命令获取App安装包中的所有资源文件,如 find /User/xxx/-name
-
设置用到的资源的类型,如jpg、gif、png、webp
-
使用正则匹配在源码中找出使用到的资源名,如pattern = @“@“(.+?)””。
-
使用find命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源。
-
对于按照规则设置的资源名,需要在匹配使用资源的正则表达式里添加相应的规则,如”image_%d“
-
确认无用资源后,就可以对这些无用资源执行删除操作了。这个删除操作,可以使用NSFileManager 系统类提供的功能来完成。
无用文件删除简单说:通过find命令与正则表达式找到所有代码中在使用的文件, 那么剩下的就是没有用的文件。关于没用文件的删除和有用文件的寻找,可以借助工具LSUnusedResources
图片的压缩处理
目前比较好的压缩方式是将图片转成WebP,原因如下:
-
WebP 压缩率高,而且肉眼看不出差异,同时支持有损和无损两种压缩模式。
-
WebP支持Alpha透明和24-bit颜色数,不会像PNG8那样因为色彩不够而出现毛边。
转发方式:使用图片压缩工具cwebp
cweb语法如下:
cwebp [options] input_file -o output_file.webp
//如,无损模式:
cwebp -lossless original.png -o new.webp
-lossless 表示转换方式为无损模式,不使用-lossless 则就是有损模式。
在cwebp语法中,还有一个关键参数 -q float 用来设置图片色值的,可以在不损失图片质量情况下进行最大化压缩:
-
小于256色适合无损压缩,压缩率高,参数使用 -lossless -q 100
-
大于256色使用75%有损压缩, 参数使用 -q 75
-
远大于256色使用75%以下压缩率,参数 -q 50 -m 6
除了cwebp之外,还有腾讯的iSparta
iSparta 提供了批量处理和记录操作配置的功能,不过只支持PNG转Webp,其它格式需要先转换成PNG格式。
而除了图片压缩,还可以在显示时使用libwebp来解析参考范例
Webp在CPU消耗和解码时间上会比PNG高两倍,性能与体积需要有个取舍。建议是如果超过100KB可以使用,如果没有超过就用网页工具TinyPng或GUI工具ImageOptim进行图片压缩。压缩率不高,但不会改变图片压缩方式,不会对性能有过多损耗。
代码瘦身
App的安装包主要由资源和可执行文件Mach-O组成。
Mach-O文件大小主要由代码量决定,所以对可执行文件的瘦身,就是找到并删除无用代码的过程。思路如下:
- 首先,找出所有代码
- 然后,找到使用过的代码
- 之后,取二者差得到无用代码
- 最后,人工确认没问题就删除
LinkMap结合Mach-O找无用代码
找到类和方法的全集,可以通过分析LinkMap来获得所有的代码类和方法的信息。获取LinkMap可通过将Build Setting里的Write Link Map File 设置为YES,然后指定Path to Link Map File的路径得到每次编译后的LinkMap文件。
LinkMap文件分为三部分:Object File、 Section 和 Symbols
-
Objec File包含代码工程的所有文件
-
Section描述了代码段在生成的Mach-O里偏移位置和大小
-
Symbols会列出每个方法、类、block 以及它们的大小
有了代码的全集信息后,还需要找到已使用的类和方法。牵涉到的原理如下:
- iOS的方法都会通过objc_msgSend来调用,而objc_msgSend通过__objc_selrefs这个section来获取selector这个参数。所以,__objc_selrefs里的方法一定是被调用了的。__objc_classrefs里是被调用过的类,__objc_superrefs是调用过super的类。
简单说:objc_msgSend通过_objc_selrefs获取selector, __objc_classrefs获取被调用的类,__objc_supperrefs获取调用的父类
查看方式,可以借助软件MachOView
具体方式:
- 编译一款App
- 将生成的GCDFetchFeed.app包解开,取出GCDFetchFeed
- 使用MachOView查看Mach-O里的信息
不过这个方法太繁琐了,而且若有部分方法是运行时动态调用就会没执行到则没法收集。比如A类的B方法没有被我直接调用,但是在运行到某一代码块时,我通过runtime的objc_msgSend手动调用了这个方法,则不会被记录。
通过AppCode找出无用代码
如果代码过百万行,则AppCode的静态分析会“歇菜”。如果没有超过,则适合直接使用。
AppCode使用:AppCode 选择Code -> Inspect Code 就可以进行静态分析了。
静态分析结束后,在Unused Code里就可以看到所有无用代码。主要类型为:
-
无用类:Unused class 是无用类,Unused import statement是无用类引入声明,Unused property是无用的属性
-
无用方法:Unused method 是无用的方法,Unused parameter是无用参数,Unused instance variable是无用的实例变量,Unused local variable是无用的局部变量, Unused value 是无用的值。
-
无用宏: Unused macro是无用的宏。
-
无用全局: Unused global declaration 是无用全局声明。
但是AppCode静态检查分析也有问题:
-
JSONModel里定义了未使用的协议会被判定为无用协议
-
子类使用了父类的方法,父类的方法不会被认为使用了。
-
通过点的方式使用属性,该属性会被认为没使用
-
使用performSelector方法调用的方法也检查不出来
-
运行时声明类的情况检查不出来。
所以还需要人工二次确认。
运行时检查类是否真的被使用过
有些代码是被调用过的,但是被调用场景的页面都已经不再使用了。这类代码,也算是无用代码。
通过Objc 的 isInitalized 里保存的结果,来判断类是否真的被初始化过。
元类:每个类都是元类,通过元类调用类方法。通过元类创建实例对象,用实例对象来调用实例方法。可以防止类方法与实例方法重名时,声明和调用失败。而实例对象的isa指针指向的是元类,也就是之前说的:每个对象都会指向一个类,每个类都有一个方法列表,方法列表里的每个方法都是由SEL、IMP和metadata组成。元类的isa指针,指向父类的元类,而根类就是NSObject。
所以,为什么要设计元类?为了区分实例方法和类方法的声明与调用。但是方法列表保存在元类里,通过元类来找到父类。
网友评论