随着应用程序的功能愈来愈多,实现愈来愈复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增加,从而致使应用程序包的体积愈来愈大。当程序体积变大后不只会出现编译流程变慢,并且还会出现运行性能问题,会增长应用下载时长和消耗用户的移动网络流量等等。所以在这些众多的问题下须要对应用进行瘦身处理。git
一个应用程序由众多资源文件和可执行程序文件组成,资源文件的优化不在本文探讨范围。本文主要讨论对可执行程序代码瘦身的方法。github
对可执行程序代码瘦身主要就是想办法让程序中不会被调用的源代码不参与编译或连接。咱们能够经过一些源代码分析工具来查找哪些函数或者类方法没有被调用并从代码中删除掉来解决编译连接前的瘦身问题。这些分析工具也不在本文的讨论范围内。应用程序在编译时会对工程中的全部代码都执行编译处理并生成目标文件。而在连接阶段则会根据程序代码中对符号的引用关系来将全部相关的目标文件连接为一个大的可执行程序文件,而且在连接阶段连接器会优化掉全部没被调用的C/C++函数代码,可是对于OC类中的没有调用的方法则不会被优化掉。因此为了对可执行程序在编译连接阶段进行瘦身处理就须要了解源代码的编译连接规则。这也是本文所要介绍的针对工程经过静态库的形式进行编译和连接的方式来减小可执行程序代码的尺寸。您能够从文章:《深刻iOS系统底层之静态库介绍》中详细的了解到静态库的编译连接过程,以及相关的技术细节。bash
为了验证和具体的实践,我在github上创建了一个项目:YSAppSizeTest。您能够从这个项目中看到如何对工程进行构建以实现程序的瘦身处理。网络
在示例项目中同一个Workspace中分别创建ThinApp和FatApp两个工程,这两个工程实现的功能是同样。在整个应用程序中分别定义了CA、CB、CC、CD、CE一共5个OC类,定义了一个UIView(Test)分类,还有定义了两个C函数:libFoo1和libFoo1。函数
整个应用程序中只使用了CA和CC两个OC类,以及调用了UIView(Test)分类方法,以及调用了libFoo1函数,而且同时都采用导入静态库的形式。由于这两个工程对文件的定义和分布策略不一样使得两个应用程序的最终可执行代码的尺寸是不相同的。工具
上述两个工程的程序被Archive出来后,FatApp可执行程序的尺寸是367KB,而ThinApp可执行程序的尺寸是334KB。经过一些工具好比Mach-O View或者 IDA能够看出:FatApp中5个OC类的代码以及libFoo1函数还有UIView(Test)分类的代码都被连接进可执行程序中;而ThinApp中则只有CA,CC两个类以及libFoo1函数还有UIView(Test)分类的代码被连接进可执行程序中。在ThinApp中虽然没有使用-Objc连接选项,可是静态库中的分类也被连接进可执行程序中。性能
根据对项目中的文件定义和引用策略以及相关的理论基础咱们能够按照以下的规则来构建您的应用程序:测试
尽可能将全部代码都移植到静态库中,而主程序则保留为一个壳程序。具体操做方法是创建一个Workspace,而后主程序工程就只有默认建立工程时的代码,全部新加入的代码都创建并存放到静态库工程中去,而后经过工程依赖来引入这些静态库工程,或者借助一些工程化工具好比Cocoapods来实现这种拆分和引用处理。主程序工程中只保留AppDelegate的代码,其余代码都一致到静态库中。而后在AppDelegate中的相关代码处调用静态库中定义的业务代码。优化
按业务组件对工程进行解耦每一个组件是一个静态库工程。静态库中的每个文件中最好只有一个类的实现,而且类的分类实现最好和类实现编写在同一个文件中,相同功能的代码以及可能都会被调用的代码尽可能存放在一个文件中。ui
不要在主程序工程中使用-ObjC和-all_load两个选项而改成用-force_load 来单独指定要执行加载的静态库。-ObjC和-all_load选项会把主程序工程以及所依赖的全部静态库中的工程中的所有代码都连接到可执行程序中而无论代码是否有被调用过或者使用过。而force_load则只会将指定的静态库中的全部代码连接到可执行程序中,固然force_load若是没有必要也尽可能不要使用。
尽可能减小在静态库中定义OC类的分类方法,若是必定要定义分类方法则能够将分类方法定义在和类定义相同的文件中,或者将分类方法定义在一个必定会被调用和引用的实现文件中。由于根据连接规则静态库中的分类是不会被连接进可执行程序中的,除非使用了上述的三个连接选项。若是将分类代码单独的定义在一个文件中的话则能够经过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把整个分类的实现连接到可执行程序中去。通常状况下咱们在静态库中创建分类那就代表必定会被某个文件引用这个分类,从而实现整个文件的连接处理。在分类中定义的这两个函数则由于没有被任何地方调用,所以会在连接优化中将这两个函数给优化掉。这样就使得即便咱们不用-ObjC选项也能将静态库中的分类连接到可执行程序中去。最后须要注意的是在每一个分类中定义的这两个函数名最好可以惟一这样就不会出现符号重名冲突的问题了。
//分类文件的头文件UIView+XXX.h
@interface UIView (XXX)
//分类中定义的方法
@end
/*
经过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把
整个分类的实现连接到可执行程序中去。通常状况下咱们在静态库中创建分类那就代表必定会被某个文件引用这个分类,从而实现整个文件的连接处理。
而在分类中定义的这两个函数则由于没有被任何地方调用,所以会在连接优化中将这两个函数给优化掉。这样就使得即便咱们不用-ObjC选项也能
将静态库中的分类连接到可执行程序中去。最后须要注意的是在每一个分类中定义的这两个函数名最好可以惟一这样就不会出现符号重名冲突的问题了。
*/
extern void _cat_UIView_XXX_Impl(void);
inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}
------------------------------------------------------------
//分类文件的实现文件UIView+XXX.m
#import "UIView+XXX.h"
@implementation UIView (XXX)
//分类的实现代码
@end
void _cat_UIView_XXX_Impl(void){}
---------------------------------------------------------------
//最后把这个分类头文件放入到某个对外暴露的头文件中,好比本例中将分类代码放入到了ThinAppLib.h文件中
//ThinAppLib.h
#import "UIView+XXX.h"
//其余头文件
复制代码
Build Settings
中将Perform Single-Object Prelink 中的开关选项打开。当这个开关打开时,系统会对生成的静态库的全部目标文件执行预连接操做,预连接操做会将全部的目标文件组合成为一个单独的大的目标文件。这样根据以文件为单位的连接规则就会将静态库中的全部代码所有都连接进可执行程序中去,可是这样带来的问题就是最后在dead code stripping时删除不掉已经连接进来的那些没有被任何地方使用过的OC类了。总之一句话:为了让你的程序瘦身,尽可能将代码放到静态库中,不要使用-Objc和-all_load选项
为了验证上述方法的有效性,笔者对项目中的应用作了一个测试:分别是有带-ObjC选项和没有带-ObjC选项的状况下的应用程序包中可执行程序的大小从115M减小到95M,减小了20M的尺寸。
欢迎你们访问欧阳大哥2013的github地址