目录
一、图片资源
1.1 清理无用图片
1.2 清理重复图片
1.3 压缩图片大小
二、代码瘦身
2.1 找寻无用代码
RealizeClass 的实现
RealizeClass的调用时机
如何遍历所有的类?
Class & MetaClass
小结
这是我个人的经验总结,如有更好的意见大家一起交流哈,共同进步。
为什么要管理包大小?相信iOS开发者们都知道,AppStore明文规定安装包大小超过150MB的App流量是无法下载的,只能在wifi环境下载。这对用户下载率和升级率有很大的影响。综合以上原因我们必须对包大小进行优化,以此节省用户流量,提高用户下载速度。
App的安装包主要是有资源和可执行文件组成,因此对于包体积的瘦身主要从图片和代码两个层面考虑。
怎么找到无用图片?
1)通过find命令获取App安装包中的所有资源文件。以下命令可能是你可能参考的。
find . -name "*.png"
find .
2)设置用到的资源类型,比如jpg,gif,png,webp。
3)使用正则匹配在源码中找出用到的资源名,比如pattern =@"@"+(.+?)"",还需要在正则表达式中添加响应的规则,比如@"image_%d"
4)使用find命令找到的所有资源文件,再去掉代码中使用到的资源文件,剩下的就是无用资源了。
5)人工确认是无用资源后,就可以对这些资源执行删除操作,可以使用NSFileManager系统类提供的功能来完成。
我推荐工具:LSUnusedResource
原理不是很清楚,大概只知道计算的MD5值,相同图片的MD5值会一样,这样就能找出重复图片了。
我们工程中用的最多的图片还是png格式,我们在考虑压缩图片大小的时候,是在不损失图片质量的前提下尽可能地做压缩。目前比较好的方案是:webP,google公司的一个开源项目。webP压缩率高,支持有损和无损压缩,拿一张gif图转为webP测试,有损压缩模式下可减少64%,无损压缩模式下可减少19%。
Google 公司在开源 WebP 的同时,还提供了一个图片压缩工具cwebp,只要根据图片情况设置参数就好了。
图片压缩完了我们还需要显示图片时使用libwebp进行解析,libwebp
总结:webP在cpu消耗和解码时间上会比png高两倍,需要在性能和体积上做取舍。我的建议是如果图片大小超过100KB,你可以考虑使用webP;而小于100KB你可以选择GUI工具ImageOptim进行图片压缩。
公司全部图片大小49MB,使用webp后降到了19MB
1)通过LinkMap获取所有的代码类和方法信息,LinkMap是什么?怎么获取?见博客LinkMap
2)通过Mach-O获取使用过的类和方法。
3)取差集得到无用代码
4)最后由人工进行确认无用代码后,进行删除。
第2)步是难点,关键在于如何检测有哪些类被使用过?
原理概述:
1. 其实原理比较简单,每个类在即将被使用之前,会进行一些惰性初始化工作,这个过程叫做 realize。类结构中有特定的 flag (一个 bit 位)标识当前类是否已经完成了 realize,一旦某个类被使用过,它的这个 flag 便已被置为了1
2. 在合适的时机遍历所有类,通过这个 bit 位判断是否是 realized,变可以得知有哪些类是使用过的了,将使用过的类进行上报
3. 上线一定时间后,汇总上报的数据便可以得到线上环境所有被使用过的类,用全部的类减掉使用过的类,便是无用类了
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
上述是Objc的runtime源码,判断一个类是否初始化过的函数。
isInitialized的结果会保存在元类的class_rw_t结构体的flags信息里,flags的1<<29位记录的就是这个类是否初始化的信息。
推荐看下左神的文章:深入解析Objc中方法的结构
先来看一下 objc runtime 源码
对类的 realize 的操作主要集中在 realizeClass 这个方法,以下代码移除了一些边界判断等不重要的逻辑
/***********************************************************************
* realizeClass
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClass(Class cls)
{
// 1. 分配可读写的 class_rw_t 来替换 class_ro_t
ro = (const class_ro_t *)cls->data();
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
// 2. 设置 Class 的 index
cls->chooseClassArrayIndex();
// 3. Realize superclass 和 metaclass
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()));
cls->superclass = supercls;
cls->initClassIsa(metacls);
// 4. 修正实例变量的偏移,实现 Non-fragile ivars 特性,保证二进制兼容
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// 5. 设置子类(维护一棵有第一个孩子和邻近兄弟关系的树,所以找一个类的子类也是可以的~)
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// 6. 将 class_ro_t 中的 method list, protocol list, property list 添加到 class_rw_t 中
// 将 Category 中的 method list, protocol list, property list 添加到 class_rw_t 中
methodizeClass(cls);
return cls;
}
总结realizedClass主要做了以下几件事情:
1.分配可读写的 class_rw_t 来替换 class_ro_t
2.设置 Class 的 index
3.Realize superclass 和 metaclass
4.修正实例变量的偏移,实现 Non-fragile ivars 特性,保证二进制兼容
5.设置子类(维护一棵有第一个孩子和邻近兄弟关系的树,所以找一个类的子类也是可以的~)
6.将 class_ro_t 中的 method list, protocol list, property list 添加到 class_rw_t 中,将 Category 中的 method list, protocol list, property list 添加到 class_rw_t 中
这里需要关注的第一点:
rw->flags = RW_REALIZED|RW_REALIZING;
这就是最初所提到的标志类 realize 的 flag,这个 flag 在类结构中位置如图所示:
所以给定一个 Class,便可以顺藤摸瓜的找到 realize 的 flag 来判断是否使用过了。
PS. 很多同学认为 Category 中的方法是在启动过程中添加到类方法列表中的,包括很多文章也有这个误解,其实只有极少数在很早的时机已经 realized 的类(如 NSObject)是这样的,对大部分类而言是在 realizeClass 的时候做的。启动过程中针对 Category 做的工作主要是遍历各个 image 中定义的 Category,得到类与 category_list 的映射并缓存下来。这里不做深入讨论,有兴趣的同学可以去读下源码。
前面讲到 RealizeClass 是在即类将使用的时候调用的,下面看一下具体的过程:
苹果提供的objc runtime源码是无法直接编译的,推荐看runtime源码:https://github.com/RetVal/objc-runtime,他帮我们配置了相关依赖环境,可以直接运行调试。
消息发送推荐左神的文章:从源码看Objc中的消息发送
lookUpImpOrForward 是在没有命中方法缓存的情况下(第一次调用方法肯定不会命中缓存),去寻找 IMP 的主要实现函数。在代码中可以看到,当前传入的 cache 参数为 false,所以首先执行的逻辑便是 realizeClass。
另外,从 realizeClass 做的事情也可以判断,在 realize 之前,类的 class_rw_t 结构还没准备好,category 中的方法也没有 attach 过来,这时是无法正常执行方法的。
所以,一个类可用的前提便是完成了 realize。
有了以上的理论,接下来主要待解决的问题就是判断每一个类是否 realize 了。了解一些 runtime 接口的同学可能会想到使用 objc_copyClassList 或者 objc_getClassList 函数来遍历所有类,如果这么做,你会发现所有的类都是已经 realize 了的,原因很简单,直接上代码,这两个方法第一件做的事情就是把所有的类都 realize。这也是这两个方法执行特别特别耗时的原因(之前发生过在线上调用此接口的情况,即便是在后台执行,仍造成了大面积的卡死问题)。这两个接口还有一个不适用的原因,这里得到的类是整个进程加载的 image 中所有的类,包括了系统动态库,而从找寻无用类的角度只需要关心自己实现的类。
在程序的可执行 Mach-O 文件中,有这样一个数据段:__objc_classlist,里面存储的便是程序中所有实现的类(当然不包括系统动态库的)
只需要读出 __objc_classlist 中所有类并判断是否 realize 了就可以得到所有使用过的类了,实现比较简单,直接贴代码:
剩下的事情就是将使用过的类进行上报了,不再具体讨论
细心的同学可能会发现,上文一直没有区分 Class 和 MetaClass,但这并不意味着不需要关心,有以下几点补充信息:
1. 使用一个类时,必然会先调用它的类方法(因为正常来讲创建一个类的实例,不论用 alloc 还是 new,都是类方法)
2. 第一次调用类方法,完成 MetaClass 的 realize 后,在 lookUpImpOrForward 方法内接下来处理 initialize 逻辑时也会 realize 其对应的 nonMetaClass(有兴趣的同学可以去看源码)
3. 即便先 realize Class(执行+load 方法前会先 realize 所有 non-lazy classes),在 realizeClass 函数内部也会去realize 它的 MetaClass
4. 遍历 __objc_classlist 拿到的都是 Class,MetaClass 是不存放在其中的
所以 Class 和 MetaClass 的 realize 状态是一致的,遍历判断无用类时只需要处理 Class 就可以了。
realize 是 objc runtime 中类的懒加载机制,在类真正要使用时再去做相应的准备工作,为确保程序的快速启动发挥了很大的作用无用类检查相当于基于 runtime 的内部实现完成的,但这部分实现相信在可预见的较长时间内苹果去修改的可能性是非常小的。
参考:main之前做了什么