为什么要给程序瘦身?可执行程序代码瘦身有哪些方法?

2019-05-16 16:43:11 来源:tuicool
标签:

为什么要给程序瘦身?

随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的移动网络流量等等。因此在这些众多的问题下需要对应用进行瘦身处理。

 

一个应用程序由众多资源文件和可执行程序文件组成,资源文件的优化不在本文探讨范围。本文主要讨论对可执行程序代码瘦身的方法。

 

对可执行程序代码瘦身主要就是想办法让程序中不会被调用的源代码不参与编译或链接。我们可以通过一些源代码分析工具来查找哪些函数或者类方法没有被调用并从代码中删除掉来解决编译链接前的瘦身问题。这些分析工具也不在本文的讨论范围内。应用程序在编译时会对工程中的所有代码都执行编译处理并生成目标文件。而在链接阶段则会根据程序代码中对符号的引用关系来将所有相关的目标文件链接为一个大的可执行程序文件,并且在链接阶段链接器会优化掉所有没被调用的C/C++函数代码,但是对于OC类中的没有调用的方法则不会被优化掉。所以为了对可执行程序在编译链接阶段进行瘦身处理就需要了解源代码的编译链接规则。这也是本文所要介绍的针对工程通过静态库的形式进行编译和链接的方式来减少可执行程序代码的尺寸。您可以从文章:《 深入iOS系统底层之静态库介绍 》中详细的了解到静态库的编译链接过程,以及相关的技术细节。

 

一个瘦身的例子!

 

为了验证和具体的实践,我在github上建立了一个项目: YSAppSizeTest 。您可以从这个项目中看到如何对工程进行构建以实现程序的瘦身处理。

 

在示例项目中同一个Workspace中分别建立ThinApp和FatApp两个工程,这两个工程实现的功能是一样。在整个应用程序中分别定义了CA、CB、CC、CD、CE一共5个OC类,定义了一个UIView(Test)分类,还有定义了两个C函数:libFoo1和libFoo1。

 

整个应用程序中只使用了CA和CC两个OC类,以及调用了UIView(Test)分类方法,以及调用了libFoo1函数,并且同时都采用导入静态库的形式。因为这两个工程对文件的定义和分布策略不同使得两个应用程序的最终可执行代码的尺寸是不相同的。

 

FatApp中的文件定义和分布策略

FatApp工程依赖并导入了FatAppLib静态库工程。

 

CA,CB两个类都定义在主程序工程中。

 

CC,CD,CE三个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在FatAppLib静态库工程中。

 

CC,CD两个类定义在同一个文件中,CE类则定义在单独的文件中。

 

FatApp工程的Other Linker Flags中设置了 -ObjC选项。

 

ThinApp中的文件定义和分布策略

ThinApp工程依赖并导入了ThinAppLib静态库工程。

 

主程序工程就是一个壳工程。

 

CA,CB,CC,CD,CE5个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在ThinAppLib静态库工程中。

 

上述的5个类都分别定义在不同的文件中。

 

ThinApp工程的Other Linker Flags中没有设置-ObjC选项。

 

上述两个工程的程序被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中的相关代码处调用静态库中定义的业务代码。

 

按业务组件对工程进行解耦每个组件是一个静态库工程。静态库中的每一个文件中最好只有一个类的实现,并且类的分类实现最好和类实现编写在同一个文件中,相同功能的代码以及可能都会被调用的代码尽量存放在一个文件中。

 

不要在主程序工程中使用-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);
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"
//其他头文件


5. 除了可以通过-force_load来加载指定静态库中的所有代码外。我们还可以在构建静态库时,在静态库的工程的Build Settings中将Perform Single-Object Prelink 中的开关选项打开。当这个开关打开时,系统会对生成的静态库的所有目标文件执行预链接操作,预链接操作会将所有的目标文件组合成为一个单独的大的目标文件。这样根据以文件为单位的链接规则就会将静态库中的所有代码全部都链接进可执行程序中去,但是这样带来的问题就是最后在dead code stripping时删除不掉已经链接进来的那些没有被任何地方使用过的OC类了。

 

6. 对于引入的一些第三方静态库或者第三方的开源库来说因为我们无法去改变其实现逻辑。如果这个静态库中没有任何分类代码的定义则正常引用即可,如果静态库中有分类方法的定义则单独对这个静态库采用-force_load选项。

 

总之一句话:为了让你的程序瘦身,尽量将代码放到静态库中,不要使用-Objc和-all_load选项
为了验证上述方法的有效性,笔者对项目中的应用做了一个测试:分别是有带-ObjC选项和没有带-ObjC选项的情况下的应用程序包中可执行程序的大小从115M减少到95M,减少了20M的尺寸。

 
关注与非网微信 ( ee-focus )
限量版产业观察、行业动态、技术大餐每日推荐
享受快时代的精品慢阅读
 

 

继续阅读
华为鸿蒙有多强大?谷歌无法再称霸安卓
华为鸿蒙有多强大?谷歌无法再称霸安卓

华为鸿蒙系统真的是让非常多人都很惊喜了。曾经大家都以为,手机只有IOS和安卓两个选项。似乎没有这两个系统就做不成手机了。

比尔·盖茨:打破 Android 和 iOS 称霸局面困难,已选择放弃?
比尔·盖茨:打破 Android 和 iOS 称霸局面困难,已选择放弃?

比尔·盖茨坦言,现在移动操作系统中,微软想要打破Android和iOS称霸的局面很困难了。

第七代 iPod touch 评测:不只是个播放器,有史以来最快
第七代 iPod touch 评测:不只是个播放器,有史以来最快

从2016年iPhone 7开始,苹果正式将高达50多岁的3.5mm接口从手机中剔除。紧接着,数字耳机接口、无线耳机、真无线耳机轮番上位,一连串的化学反应后,3.5mm接口在手机甚至平板上几乎消声绝迹。

语音助手前景不乐观,近一半用户从不使用

SUMO Heavy发布的一份新调查报告显示智能手机上语音助手使用率低于之前的预期。

华为“鸿蒙”究竟还有多久能抵达“战场”?

据界面数据公开资料统计,2019年5月21日,华为西欧业务部副总裁沃特金斯(Tim Waktins)接受采访时透露:华为操作系统早已开始研发。除此之外,华为曾被曝2012年就已开始规划自有操作系统。

更多资讯
微软推出全新通用预训练方法——MASS,效果比 BERT 和 GPT 更好?

自 2018 年以来,预训练无疑是自然语言处理(NLP)领域中最热门的研究课题之一。通过利用 BERT、GPT 和 XLNet 等通用语言模型,该领域的研究者们在自然语言理解方面已经取得了许多重大的突破。

基于神经网络的深度解析

本来想把题目取为“从炼丹到化学”,但是这样的题目太言过其实,远不是近期可以做到的,学术研究需要严谨。但是,寻找适当的数学工具去建模深度神经网络表达能力和训练能力,将基于经验主义的调参式深度学习,逐渐过渡为基于一些评测指标定量指导的深度学习, 是新一代人工智能需要面对的课题,也是在当前深度学习浑浑噩噩的大背景中的一些新的希望。

Google 开源的一个深度学习框架你了解多少?

想必大家都或多或少听过 TensorFlow 的大名,这是 Google 开源的一个深度学习框架,里面的模型和 API 可以说基本是一应俱全.

企业 AutoML会有何应用?可解释性真的成为了人与 AI 交互的必经之路?

AI Time第一期的主题是“论道AI安全与伦理”,当时我们向在场的三位老师提出了一个困扰大众已久的问题,即“我们有一天真的会达到电影里的那种智能吗?拥有情感,拥有爱?”

FPGA 的上电过程如何?在配置电路中又存在着怎样的配置方式?

目前,大多数FPGA芯片是基于 SRAM 的结构的, 而 SRAM 单元中的数据掉电就会丢失,因此系统上电后,必须要由配置电路将正确的配置数据加载到 SRAM 中,此后 FPGA 才能够正常的运行。