标签:
经过多次版本迭代,app中难免会产生不少冗余代码和无用资源,整个app越发臃肿。
再加上目前我们的app整体架构变成了hybrid,很多native代码都不需要了,考虑到要为客户的流量负责,减小应用的大小已经迫在眉睫。
整体改造方案分为三部分:
找到.ipa包,解压缩后得到应用程序安装包:
点击显示包内容后按大小给文件夹排序,减包之前:
减包之后(可执行文件变大是因为加入了环信的sdk,2M多):
应用中各个文件的大小一目了然,为了使瘦身效果明显,我们先从体积最大的文件开始。
在我们的项目中主要包含的资源类型是图片,在之前的迭代中,开发和设计人员对图片资源的优化、管理一直都不够重视,所以这部分是瘦身的重头戏,无用图片可以分为四类:
1.引入图片框架时错误导入了demo中的图片。
2.开发人员的疏忽,导入了无用的屏幕截图图片。
3.历史项目中残留的图片,未及时清理。
4.本期项目中由于删除大量native功能导致好多图片都失效了。
针对这些图片,我们编写了脚本,进行删除。使用脚本有一点要注意:脚本中的图片命名方式务必和项目中的一致,否则可能会导致忽略和误删图片。
删除方法:
1.进入终端,输入: brew install ack 安装ack库。
2.cd到项目路径下,执行脚本文件,即可进行查找、移动、删除项目中没有引用到的图片,脚本内容如下:
------------------------------------------------
for i in `find . -name "*.png"`; do
file=`basename -s .png "$i"| xargs basename -s @2x | xargs basename -s @3x`
result=`ack -i "$file"`
if [ -z "$result" ]; then
#mv "$i" /Users/apple/Desktop 移动文件到指定路径
#echo "$i" 输出文件名
#rm "$i" 删除文件
fi
done
------------------------------------------------
脚本注释:
1、2——声明接下来的语句中使用的命令存放的文件夹,当执行命令时,则直接进入该文件夹路径内寻找。(:号是文件夹分隔符)
3——是forin结构相信诸位iOS开发者是非常属性的,遍历find命令找到的文件.(find后面的.的含义是当前目录,-name是指定文件名,*号表示0-n个字符的通配,-o 是or的含义)
4——是为变量file赋值,其中使用了basename命令、xargs命令以及至关重要的 | 管道。(如果对bash shell感兴趣,对管道和xargs命令的深入学习是必不可少的)
5——使用了ack命令。
6——是bash shell里面的if使用方式,-z 表示判定后面的文件是否为空。
7——是打印
8,9——加了#号表明是注释的语句,rm 是删除命令。
10——是if fi 的搭配。(bash shell语法)
11——是for in ; do done 的 for循环语句搭配(bash shell语法)
脚本的原理是用关键字(通常是文件名,图片资源需要去掉@2x @3x),搜索代码,搜不到就是没有被引用。当然,有些资源在使用过程中是拼接而成的,需要手工过滤。
图片的处理主要由设计部的同事来处理
1.不需要透明的地方,使用jpg而不是png。比如:背景图片、引导图、bundle里面的png文件。但是启动画面只能是png格式否则审核时可能被拒。
2.可拉伸素材。界面导航条背景、弹框背景、按钮等都可以考虑用拉伸素材。
3.压缩素材。可以进一步减小20%~40%左右的图片大小。在不影响视觉效果的情况下尽可能的压缩。
修改相关代码,让程序识别jpg格式的图片:
对于一张pattern image或者是有圆角的图片,考虑到有更大的屏幕,你需要重新调整图片的大小,以免图片拉伸出现失真。Natasha发布了一篇很棒的文章来说明如何编程解决这个问题,但是我们也可以在Xcode 6的Asset Catalog中搞定它。顺便说一下,我强烈建议你在继续往下读之前,看一下Natasha的文章,这样你就能理解到底发生了什么。免责声明:下面的图片等是直接从Natasha的文章中拷贝过来的。Sorry!
好了,我们继续。
在之前,一般用类似下面的代码来获得可改变大小的图片:
1
2
3
4
|
let edgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) let backgroundButtonImage = UIImage(named: "purple_button" )?.resizableImageWithCapInsets(edgeInsets) purpleButton.setBackgroundImage(backgroundButtonImage, forState: .Normal) |
这将会得到一张和下面类似的图片:
在运行时,会拉伸距离UIImageView的container的边框8像素的中间部分,这样就能保留圆角,得到下面这样的:
多亏了Xcode中Asset Catalog的slice和dice,我们不需要代码也能拉伸图片。首先在Xcode中选中图片,然后点击右下角的Show Slicing:
你现在应该能看到slicing 面板和一个按钮"Start Slicing"。
在你点击按钮之后,会显示下面的三个选项:
左边的按钮用于horizontal edge insets,右边的按钮用于vertical edge insets,中间的则是两个都有。在我们的例子中要保留圆角,所以我们按中间的按钮,告诉系统我们想要按钮的中间在水平和垂直方向拉伸。在按下按钮之后,就能看到一些可以拖动的细条,这可以设置从哪里开始拉伸图片。
系统会保留深紫色的区域,浅紫色的区域会被拉伸。
更厉害的是,Xcode自动找到了圆角,所以我们不需要设置从哪里开始拉伸图片。最后别忘了在Attribtues pane中设置图片是可拉伸的。
有了这个无价之宝,你就不用再在resizableImageWithCapInsets方法中填写那些神奇的数字了,也能帮助你分离view逻辑和app逻辑。
以上资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。在讲可执行文件瘦身之前先介绍Xcode的LinkMap文件。LinkMap文件是Xcode产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。只要设置Project->Build Settings->Write Link Map File为YES,并设置Path to Link Map File,build完后就可以在设置的路径看到LinkMap文件了。这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小。
每个LinkMap由3个部分组成,以58到家为例:
1. Object files:
..........
第一部分列举可执行文件里所有.obj文件,以及每个文件的编号。
第二部分是可执行文件的段表,描述各个段在可执行文件中的偏移位置和大小。第一列是段的偏移量,第二列是段占用大小,Address(n)=Address(n-1)+Size(n-1);第三列是段类型,代码段和数据段数据段(__DATA,保存变量值); 第四列是段名字,如__text是可执行机器码,__cstring是字符串常量 。有关段的概念可参考苹果官方文档《OS X ABI Mach-O File Format Reference》。
3. Symbols:
.........
第三部分详细描述每个obj文件在每个段的分布情况,按第二部分Sections顺序展示。例如序号1的JZScrollBanner.o文件,-[JZScrollBanner initWithFrame:]方法在__TEXT.__text地址是0x100001DD0,占用大小是256字节。根据序号累加每个obj文件在每个段的占用大小,从而计算出每个obj文件在可执行文件的占用大小,进而算出每个静态库、每个功能模块代码占用大小。这里要注意的地方是,由于__DATA.__bbs是代表未初始化的静态变量,Size表示应用运行时占用的堆大小,并不占用可执行文件,所以计算obj占用大小时,要排除这个段的Size。
项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。我们可以从linkmap中统计出这个信息,对此写了个node.js脚本(详见附件:linkmap.js),可以通过linkmap统计每个.o目标文件占用的体积和每个.a静态库占用的体积。
操作步骤:
1.打开终端,cd到存放linkmap.js的文件夹
2.执行命令:node linkmap.js linkmap.txt的文件路径 -hl
得到输出结果resultOfObjectFile.txt:
整理得到第三方库占用详情:
在项目里新建一个类,给它添加几个方法,但不要在任何地方import它,build完项目后观察linkmap,你会发现这个类还是被编译进可执行文件了。按C++的经验,没有被使用到的类和方法编译器都会优化掉,不会编进最终的可执行文件,但object-c不一样,因为object-c的动态特性,它可以通过类和方法名反射获得这个类和方法进行调用,所以就算在代码里某个类没被使用到,编译器也没法保证这个类不会在运行时通过反射去调用,所以只要是在项目里的文件,无论是否又被使用到都会被编译进可执行文件。对此我们可以通过脚本,遍历整个项目的文件,找出所有没有被引用的类文件和没有被调用的方法,在保证没有其他地方动态调用的情况下把它们去掉。由于58到家app整个项目历时很长,历时代码遗留较多,这个清理对可执行文件省出的空间还是挺可观的。根据resultOfObjectFile.txt使用shell脚本:searchUnusedClass.sh搜索出引用次数等于1的类,但在实际中发现有的类虽然也引用了头文件但代码中并未使用该类,或者有引用代码但是被注销了,所以配合实际情况,在脚本中我们把引用次数设置成小于6。58到家中我们删除了41个无用的类,大小512KB,
删除无用的类后可执行代码体积减小400KB!
删除前:
删除后:
结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。另外第三方库的无用selector也可以这样扫出来的。
Strip Link Product设成YES
Make Strings Read-Only设为YES
去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions。可以对某些文件单独支持异常,编译选项加上-fexceptions即可。但有个问题,假如ABC三个文件,AC文件支持了异常,B不支持,如果C抛了异常,在模拟器下A还是能捕获异常不至于Crash,但真机下捕获不了。个人认为关键路径支持异常处理就好,像启动时NSCoder读取setting配置文件得要支持捕获异常,等等
标签:
原文地址:http://blog.csdn.net/zhangyangsk8/article/details/51579889