ipa文件“减肥”初探

96
Andy__M
0.1 2016.03.30 13:22* 字数 4135

前言

如今,日子是越来越好了,大多数的同学有时还是管不住自己的嘴,一不留神把自己吃成了“小胖子”;软件也如此,随着科技的发展,手机硬件配置的提高,手机App的功能也越来越多,经过1年左右的开发叠代,“百度高考iOS”从最初1.0版的10MB左右,不知不觉,安装程序已43MB+了,唉,“减肥”迫在眉睫啊!!!!

现实问题

说回到我们的百度高考iOS,我们早先是3周左右一版,差不多是2周开发,1周测试;随着项目的成长,团队的逐步扩大,需求也是一波波的袭来,我们也逐渐适应了2周一版的叠代周期,到现在为止,已发布16个版本。回想这一年的开发,功能是加加减减,界面是修修改改,但引入的资源文件几乎是只加不减,而这些资源差不多分为这么几类:图片素材、配置文件、每日美文、H5模板、第三方类库和音频文件,不用想,图片素材当然是占用空间最大的一类,“减肥”也自然先那它“下手”了,但具体又有哪些方面可以下手呢?

我的猜想

结合现实的问题,给ipa“减肥”,我觉得大体可以从如下方面着手:

  1. 删除项目中无用的文件,如被弃用的类、图片等,猜测差不多能减掉个2~3MB

  2. 其次当然就是图片了,可以使用图片压缩工具,通过无损或有损压缩的方式减小图片大小,假设我们的项目中有30M的图片,然后将它们有损压缩到80%的质量,那么就可以减掉6MB左右。(PS:根据大炜哥的经验,这是最有效的给ipa“减肥”方法了)

    说到这里,我想到了另一个问题:都知道,图片的导入方式有如下几种:

  • 加入到Assets.xcassets中

    • 只支持png格式的图片
    • 图片只支持[UIImage imageNamed]的方式实例化,但是不能从Bundle中加载
    • 在编译时,Images.xcassets中的所有文件会被打包为Assets.car的文件
  • CreateGroup

    • 黄色文件夹图标;Xcode中分文件夹,Bundle中所有所在都在同一个文件夹下,因此,不能出现文件重名的情况
    • 可以直接使用[NSBundle mainBundle]作为资源路径,效率高!
    • 可以使用[UIImage imageNamed:]加载图像
  • CreateFolderRefences

    • 蓝色文件夹;Xcode中分文件夹,Bundle中同样分文件夹,因此,可以出现文件重名的情况
    • 需要在[NSBundle mainBundle]的基础上拼接实际的路径,效率较差
    • 不能使用[UIImage imageNamed:]加载图
  • PDFs矢量图(Xcode6+)

  • Bundle(包)中的图片素材

    那这不同的导入方式,会对打出的包的大小有影响么?

  1. 以上2个角度是我能想到的(也是最好想到的)“减肥”方法了,除此之外,梦凯还给我提过一个技术名词——BitCode——刚听到这个词的时候我还懵逼了一会儿。

    大概的意思是说:之前打包,可以运行在各个不同型号的iOS设备上,是因为在打包的时候,苹果帮我们把app在各型号设备上运行所需要的“东西”一并全部打到包里了。假设我们在打包的时候,只把要运行的设备所需的“东西”打到包里,而不需要其他型号运行所需要的“东西”,这样不就达到减小ipa大小的目的了么?BitCode就是来完成这个任务的中间件。

    那么问题来了,BitCode真的有这么厉害么?它到底是怎样做到的呢?接入它麻烦么?。。。

给ipa减个肥

如何优雅的清理掉那些弃用的文件

一个项目开发得越久,添加的功能模块也就越多,相应地,也会慢慢引入大量图片等资源。但是,在移除一些不再使用的模块的时候,开发者往往会忘记把该模块所对应的图片资源一起删除,因为源码和资源是分离的。长久以来,项目中就会存在大量没被使用的资源文件。

具体方法无非是,一个一个地复制资源文件名,然后在 XCode 中全局查找该字符串,如果结果为 0,那么这个资源很可能就没有被使用。为什么说很可能?因为在代码中,有可能通过字符串拼接的方式使用了这个资源,而这种情况是没办法通过字符串匹配查找出来的。

于是,我们需要这么一款工具:能够迅速找出工程中所有没被使用的资源文件。

脚本

详见链接

#!/bin/sh
PROJ=`find . -name '*.xib' -o -name '*.[mh]'`
for png in `find . -name '*.png'`
do
name=`basename $png`
if ! grep -qhs "$name" "$PROJ"; then
    echo "$png is not referenced"
fi
done

缺点:不够智能,不够通用,速度太慢,结果不正确。

Unused

Unused:
A Mac app for checking Xcode projects for unused resources

图片

Unused 对脚本的调用做了封装,通过界面可以配置一定的信息,然后比较清晰的输入结果。

缺点:实际上,Unused 的内部还是调用了方案 1 的脚本,所以方案 1 的缺点也就是方案 2 的缺点。

LSUnusedResources与改进

图片
图片
提高匹配速度

LSUnusedResources 很大程度上受 Unused 的影响,比如界面、交互,以及部分代码。但是,本工具在核心代码上做了优化,使其在搜索的速度、结果的正确上都有了很大的提高。

核心步骤,简述如下:
查找:选定的目录下的所有资源文件。这一步与上述方案1区别不大,都是调用 find 命令查找指定后缀名的文件。
匹配:与上述方案不同,我不是对每个资源文件名都做一次全文搜索匹配,因为加入项目的资源太多,这里会导致性能快速下降。而我只是针对源码、Xib、Storyboard 和 plist 等文件,先全文搜索其中可能是引用了资源的字符串,然后用资源名和字符串做匹配。
3.2 优化匹配结果

Unused 会把大量实际上有使用的资源,当做未使用的资源输出。LSUnusedResources 则不会出现这样的问题,并且使得结果更加优化。

举例说明:
你在工程中添加了下面资源:

icon_tag_0.png
icon_tag_1.png
icon_tag_2.png
icon_tag_3.png

然后用字符串拼接的方式在代码中引用:

NSInteger index = random() % 4;
UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:@"icon_tag_%d", index]];

icon_tag_x.png 是不应该被当做未使用的资源的,只是以一种比较隐晦的方式间接引用了,所以不应该出现在结果列表中。

使用方法

点击 Browse.. 选择一个文件夹;
点击 Search 开始搜索;
等待片刻即可看到结果。

下载安装

下载: LSUnusedResources.app.zip或者使用 XCode 编译运行项目代码

正确导入图片的姿势

经过测试得知:CreateGroup、CreateFolderRefences两种方式打出来的包,图片都会直接放在.app文件中,所以打包前后,图片的大小不会改变

而加入到Assets.xcassets中的方法则不同,打包后,在.app中会生成Assets.car文件来存储Assets.xcassets中的图片,并且文件大小方面也大大降低

打包前Assets.xcassets文件夹 打包后的Assets.car文件夹
测试1 32.7MB 16.3MB
测试1 33.5MB 26.1MB

所以,使用Assets.xcassets来管理图片也可以达到ipa瘦身的效果~~

压压更健康 - imageoptim

图片
图片

imageoptim是一款基于Mac的图像“瘦身”软件,内置有6种压缩算法,通过删除图片部分无用的EXIF等信息来减小PNG、JPEG和GIF图片的大小。ImageOptim合并了OptiPNG、PNGCrush、AdvanceComp、PNGOUT、Jpegoptim+Jpegtran和Gifsicle等几个工具,旨在为设计师提供最好的优化效果。在最新发布的1.4.4版本中,ImageOptim改进了文件在文件列表中的拖拽、复制、粘贴功能。 下载地址

------(分割线)-------

压缩结果.png
压缩前后对比

话说项目中10M的图片,按照默认的80%压缩,也小了4M(见上图),可是实际打包出来,为什么ipa没有小呢??

BitCode

Ta是谁

我们先来看下官方文档:在App Distribution Guide – App Thinning (iOS, watchOS)一节中,找到了下面这样一个定义:

Bitcode is an intermediate representation of a compiled program. Apps
you upload to iTunes Connect that contain bitcode will be compiled and
linked on the App Store. Including bitcode will allow Apple to
re-optimize your app binary in the future without the need to submit a
new version of your app to the store.

说的是bitcode是被编译程序的一种中间形式的代码。包含bitcode配置的程序将会在App store上被编译和链接。bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到App store上。

亮个剑

真是不明觉厉啊,继续看,在What’s New in Xcode-New Features in Xcode 7中,还有一段如下的描述

Bitcode. When you archive for submission to the App Store, Xcode will
compile your app into an intermediate representation. The App Store
will then compile the bitcode down into the 64 or 32 bit executables
as necessary.

当我们提交程序到App store上时,Xcode会将程序编译为一个中间表现形式(bitcode)。然后App store会再将这个botcode编译为可执行的64位或32位程序。

再看看这两段描述都是放在App Thinning(App瘦身)一节中,可以看出其与包的优化有关了。喵大(@onevcat)在其博客开发者所需要知道的 iOS 9 SDK 新特性中也描述了iOS 9中苹果在App瘦身中所做的一些改进,大家可以转场到那去研读一下。

另外可以参看Xcode 7 Bitcode的工作流程及安全性评估了解更多。

请赐教

实际上,在Xcode 7中,我们新建一个iOS程序时,bitcode选项默认是设置为YES的。我们可以在”Build Settings”->”Enable Bitcode”选项中看到这个设置。

不过,我们现在需要考虑的是三个平台:iOS,Mac OS,watchOS。
对于iOS,bitcode是可选的;对于watchOS,bitcode是必须的;而Mac OS是不支持bitcode。

如果我们开启了bitcode,在提交包时,下面这个界面也会有个bitcode选项:

图片

所以,如果我们的工程需要支持bitcode,则必要要求所有引入的第三方库都支持bitcode。

他山之石以攻玉

除了上面提到的“减肥”方法之外,还有其他的方案或思路么?

编译选项

  1. 编译器优化级别
    Build Settings->Optimization Level有几个编译优化选项,release版应该选择Fastest, Smalllest,这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。

  2. 去除符号信息
    Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release版本应该设为yes,可以去除不必要的调试符号。Symbols Hidden by Default会把所有符号都定义成”private extern”,具体意思和作用我还不清楚,有待研究,但设了后会减小体积。这些选项目前都是XCode默认选项,但旧版XCode生成的项目可能不是,可以检查一下。

第三方库

项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。

ARC->MRC

有人提出用ARC写的代码编译出来的可执行文件是会比用MRC大的,原因大致是ARC代码会在某些情况多出一些retain和release的指令,例如调用一个方法,它返回的对象会被retain,退出作用域后会被release,MRC就不需要,汇编指令变多,机器码变多,可执行文件就变大了。还有其他细节实现的区别,先不纠结了。

那用ARC究竟会增大多少体积?我觉得从汇编指令的增多减少去算是很难算准确的,这东西涉及细节太多,还是得从统计的角度计算。做了几个对比试验,统计了几个同时支持ARC/MRC的开源项目在开启/关闭ARC的情况下__TEXT代码段的大小对比。只对比__TEXT代码段是因为:

ARC对可执行文件大小的影响几乎都是在代码段

可执行文件会进行某种对齐,例如有些段在不足32K的时候填充0直到对齐32K,若用可执行文件大小对比结果可能是对齐后的,不准确。

图片

对比实验数据,结论是ARC大概会使代码段增加10%的size,考虑代码段占可执行文件大约有80%,估计对整个可执行文件的影响会是8%。

可以评估一下8%的体积下降是不是值得把项目里某些模块改成MRC,这样程序的维护成本上升了,一般不到特殊情况不建议这么做。

类/方法名长度

观察linkmap可以发现每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的,原因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类名,方法名列表都保存下来。

可以考虑在编译前把所有类和方法名进行混淆,把长名字替换成短名字,这样做的好处除了缩小体积外,还对安全性有很大提升,别人拿到可执行文件对它class-dump出来的结果都是混淆后的类和方法名,就无法从类和方法名中猜出某个方法是做什么的,就难以挂钩子进行hack。不过这样有个缺点就是crash堆栈反解出来的堆栈方法名会是混淆后的,需要再加一层混淆->原名的转换,实现和使用成本有点高。

实际上这部分占用的长度比较小,中型项目也就几百K,对安全性要求高的情况可以试试。

冗余字符串

代码上定义的所有静态字符串都会记录在在可执行文件的__cstring段,如果项目里Log非常多,这个空间占用也是可观的,也有几百K的大小,可以考虑清理所有冗余的字符串。另外如果有特别长的字符串,建议抽离保存成静态文件,因为AppStore对可执行文件加密导致压缩率低,特别长的字符串抽离成静态资源文件后压缩率会比在可执行文件里高很多。

参考

iOS开发
Web note ad 1