Has your iOS app been “stripped”?

size

各位iOS开发同学们,你们打出来的iOS的ipa包真的已经不能再小了么?如果你也像题图一样,.ipa文件比AppStore上显示的大小相差太多,那你应该对我说的有兴趣。
以下是从一个QA的角度作出的从发现问题到解决问题的整个详细历程,对ObjC的编译连接方式可能理解是错误的,还请各位ObjC大牛指正。文章较长,我尽量讲得更清楚些。为了防止大家对这种长文直接放弃,我先上一个优化前后对比结果图:

对比结果

以阿里旅行5.2.0发布到Appstore的版本为例,对比结果如下:

对比 优化前 优化后 减少了
二进制文件大小 46.6M 31.7M 14.9M
.ipa文件大小(本身就是zip格式) 25.2M 22.3M 2.9M
手机“用量”中显示的大小 57.2M 43.0M 14.2M

注:前两项大小都是Mac OS X 10.10中,通过命令行ls命令查询得到
如果你还是没兴趣看长篇大论,直接看最后“我找到的优化方法”那段就行。

为什么苹果商店包与我们产出的ipa大小有差别

为了方便大家理解,先对Xcode生成的各种包进行说明:

iOS的各种结果文件:

1) .app

iOS编译以后生成的原始文件,实际是一个文件夹,里面包含各种资源文件(图片,第三方bundle,plist等文件),程序的可执行文件(二进制格式)以及对所有文件的签名记录(_CodeSignature
不能上传AppStore

2) .dSYM

生成.app时的附属产物。本质是一个文件夹,其中只有一个最大的文件,作用是对iOS程序闪退后产生的log文件进行符号化(desymbolicate);通俗的说,就是把无意义的内存地址变成可读的程序中的类和方法以及代码行数
不能上传AppStore

3) .ipa

实际上就是把.app放到Payload文件夹后,对Payload就行了zip操作,最后改了下扩展名。
可通过Application Loader上传AppStore

4) .xcarchive

实际上也是一个文件夹,包含.ipa.dSYM文件
可通过Xcode上传AppStore

原因

由于上传AppStore速度很慢,我们一般选择上传.ipa文件到AppStore。那为什么从苹果商店看到的包大小和我们的.ipa大小不一致呢。下面是苹果官方的说法(注意黑体字,后面会用到):

When your application is approved by Apple to sell on the App Store, it is encrypted for DRM purposes and re-compressed. When the encryption is added, the size of the compressed file will increase. The exact size of the increase will vary from app to app, however, the size increase can be large when the binary contains a lot of contiguous zeros. We are unable to guarantee the size of your file after the encryption has been added.

也就是说,苹果获取上传的ipa文件后,进行解压缩成.app,然后对其中二进制文件进行Apple FairPlay DRM加密,最后重新压缩成.ipa,此时生成的.ipa作为AppStore上的显示程序的大小。

分析

回到我们阿里旅行的包:我们v5.1.1生成的.ipa文件是23.9M左右,而苹果商店上显示的是36.9M
为什么会有这么大差距呢,我们来看看我们的二进制文件AliTrip的前后大小对比:
adding: Payload/Alitrip.app/AliTrip …. (in=42806208) (out=14899495) (deflated 65%)
上面是打包机使用PackageApplication.app进行压缩成zip(即ipa)时的日志。可以看到我们的二进制文件高达42.8M,经过zip压缩后变为14.8M左右,缩小了65%的体积。
为什么我们的文件压缩比率可以这么高呢。我们使用Sublime Text打开此文件就会发现,果不其然!连续的0数都数不过来。

大胆的猜想

  • 因为是重复的0,在被zip压缩时可以直接写作“n个0”来保存,所以压缩比率很高;
  • 而在进行DRM加密时,这些重复的0生成了非0字段,再进行压缩时,压缩比上不去了

验证

以上猜想可以通过iTunes下载ipa文件来看:
通过iTunes下载阿里旅行,找到下载到的ipa文件,解压缩后,对其中的Payload文件夹中进行zip压缩,发现二进制的压缩比与之前相比,已经下降了30%:
adding: Payload/Alitrip.app/AliTrip (deflated 35%)
从42806064压缩到27654347
所以DRM操作是导致.ipa文件变大的原因

解决思路

我们现在要做的就是如果减少和消除我们二进制文件中多出的哪些连续的0:

  • 从Xcode编译阶段着手,研究连续的0产生的原因
    • 从生成的二进制文件着手,删除多余的0
    • 删除是可行的,但是上面提到,.app里面会对生成的文件进行签名,如果我们修改了二进制文件,签名就失效了。修改过得ipa文件不能通过苹果审核

我找到的优化方法

对与iOS的编译和连接等操作我完全是门外汉,经过几天的搜索整理,我找到的突破口就是 Stripstrip从字面意思上其实就是“脱光”的意思(嗯,点题了…),也就是把生成的 对象文件 (.o文件)中不必要的 符号(symbols)去掉的意思。所以从Xcode的Strip相关配置下手进行优化。以上是我对strip的理解,可能不完全对,望大家指正。
在Xcode项目的Build Settings中,搜索Strip:我们可以看到,程序默认做了一些配置,相关strip选项解释如下(BuildSettings所有选项的官方点这里)。

选项 意义
Deployment Postprocessing strip所有选项的总开关,如果选NO,以下选项均无效
Strip Debug Symbols During Copy 文件拷贝编译阶段时是否进行strip,你的工程中有CopyFilesBuildPhase才有意义
Strip Linked Product 这个选项才对最后生成的二进制文件进行strip
Strip Style allnon-globaldebugging strip程度依次降低:all一般用于最后生成.app的工程;non-global用于bundle和framework,debugging一般都可以。虽然all是strip最多的选项,但是选择错误会导致strip失败
Dead Code Stripping 用于删除对象文件中不需要加载的符号,减小二进制文件大小

这是阿里旅行其中两个工程的strip设置截图(其中 粗体 是优化过的选项):

  • Portal工程是生成.app的工程:
    portal
  • CommonUI是其中一个底层framework工程:
    commonui

可以看到阿里旅行的strip设置基本正确,但是最关键的strip总开关Deployment Postprocessing没有打开。然后就是Strip Style对线上包的strip程度不够(framework工程只选择了debugging级别,改为non-global
这个strip总开关在Xcode的Release配置中本身也是默认关闭的,需要我们手动打开。另外其实strip除了降低app大小外,一定程度上提高了从app获得更多信息的难度,安全性更佳
以上strip优化只针对内测包和商店包。

结论与思考

由于iOS工程的线上配置中使用了Xcode的默认设置,导致没有开启strip开关,最后生成的二进制文件偏大。
由于我对ObjectiveC编译、连接的实现完全不理解,我这种方式不一定是最优化的选择;也可能除了strip外,BuildSetting还有其他的优化方式。在此抛砖引玉,欢迎大家讨论。
最后,这种处理后会不会对我们crash日志解析产生影响,还需要开发同学来确认下。优化过的app可以正常运行,我已经试验过了。​

xUnique – Xcode project file merge with no conflicts

Introduction


If you are an Objective C developer using Xcode, and push your code to Git/SVN like other guys did in the team, I think you most probably have encountered the merge conflicts of project.pbxproj file.

It’s such a pain to merge this file by searching the file with <<<,>>> and ===, and then deleting and keeping lines by your judge. It would leave some unused lines or even make Xcode build fail due to wrong decision.

Since UUID generated by Xcode in project.pbxproj file is not unique for all machines, different Xcode got different UUID for the same file or filegroup. That’s why it created conflicts.

xUnique


I just found that Xcode does not care the UUID in the file, it just needs to be unique in the file. So I made xUnique to fix the merge conflicts issue.

How it works

  • All elements in project file are actually connected as a tree
  • We give a path to every node of the tree using its unique attribute; this path is the absolute path to the root node connected by these attributes
  • Apply MD5 hex digest to the path for the node
  • these digests are the new UUIDs in the project file
  • Sort project file using my pure Python implementation of my modified sort-Xcode-project-file, supports following new features:
    • sort PBXFileReference and PBXBuildFile sections
    • avoid modified changes in Git/SVN if no change made in the project file

How to use

  1. Put xUnique.py file in your project repository somewhere and add it as track file via git add path/to/xUnique.py, so all members could use the same script
  2. create a git hook: ln -s path/to/xUnique.py .git/hooks/pre-push
  3. Add permission chmod 555 .git/hooks/pre-push
    • use hook pre-push instead of pre-commit is a safe consideration: you decide to commit the newly generated project file or not
  4. In all your branches, uniquify project.pbxproj file in either way:
    • make some changes and commit. Try to push, git hook would be triggered
    • manually run script: python path/to/xUnique.py path/to/MyProject.xcodeproj and then committing changes.
  5. All Done;)

Notice