安装包过大,不利于市场人员做推广,最近做了 iOS 安装包瘦身的技术研究和实践。
iOS APP经过编译,打包文件中除了资源文件,剩下的就是一个可执行文件了。
瘦身,可以从 三个方面入手:
- 资源文件
- 可执行文件
- 编译选项
下面从这三个方面来分析安装包瘦身的方法和一些工具使用。
1. 资源文件
资源文件包括图片、声音、配置文件、文本文件、xib、storyboard、证书等。其中最常用的资源是第一种,优化方式无非删除或压缩处理。
1.1 删除无用的资源文件
推荐使用工具 LSUnusedResources
搜索出来结果后,选中某行,点击Delete按钮即可删除资源。
1.2 压缩资源文件
常用的有两个工具:
- ImageOptiom:无损压缩工具,图片较小时使用
- TinyPNG:有损压缩工具,图片较大尺寸时使用。
1.3 使用.xcassets 导入图片
打包之后会生成 Assets.car ,文件的大小会降低。
2. 可执行文件
Mach-O为Mach Object文件格式的缩写,是mac上可执行文件的格式,类似于windows上的PE格式 (Portable Executable )或 linux上的elf格式。Mach-O文件分为这几类:
- Executable:应用的主要二进制;
- Dylib Library:动态链接库;
- Static Library:静态链接库;
- Bundle:不能被链接的Dylib,只能在运行时使用dlopen( )加载,可当做macOS的插件;
- Relocatable Object File :可重定向文件类型。
对于这几种类型的Mach-O文件,我们可以使用MachOView进行查看。MachOView是一个开源的工具,源码在GitHub上:https://github.com/gdbinit/MachOView。
不过该项目已经很久没有更新了,在 MacOS High Sierra 10.13.3系统上,使用很短的时间后会崩溃。
从上图可以看到,Static Library有很多.o文件,每个.o文件都对应一个类编译后的文件,展开查看“Mach Header”信息,可以看到每个类的CPU架构信息、Load Commands数量 、Load Commands Size 、File Type、Flags等信息。
我们也可以在Xcode中,开启编译选项Write Link Map File,编译之后来查看可执行文件的全貌。
2.2 linkmap文件
LinkMap文件是Xcode产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。
在Xcode中,选择XCode -> Target -> Build Settings -> 搜map -> 把Write Link Map File选项设为YES,并指定好linkMap的存储位置,如图所示:LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。下面来简单分析一下这个文件的结构。
2.2.1目标文件列表
打开LinkMap文件,首先看到的就是编译后的每一个.o目标文件的信息2.2.2 段表
接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)。这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。
2.2.3符号表(Symbols)
Symbols 是对 Sections 进行了再划分,这里会描述所有的 methods、ivar 和字符串,以及它们对应的地址、大小、文件编号信息。首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应2.2.1中的文件编号,最后是名字。
例如第69行代表了文件序号为3(反查上面就是 AppDelegate.o)的window方法占用了44 byte大小。
计算某个.o文件在最终安装包中占用的大小,主要是解析目标文件和符号表两个部分,从目标文件读取出每个.o文件名和对应的序号,然后对Symbols中序号相同的文件的Size字段相加,即可得到每个.o文件在最终包的大小。
2.3 可执行文件瘦身
通过脚本分析前面说的LinkMap文件,我们可以更加清晰的知道具体的某个类在可执行文件中的大小。
var readline = require('readline'),
fs = require('fs');
var LinkMap = function(filePath) {
this.files = []
this.filePath = filePath
}
// 记录总大小
var totalSize = 0
LinkMap.prototype = {
start: function(cb) {
var self = this
var rl = readline.createInterface({
input: fs.createReadStream(self.filePath),
output: process.stdout,
terminal: false
});
var currParser = "";
rl.on('line', function(line) {
if (line[0] == '#') {
if (line.indexOf('Object files') > -1) {
currParser = "_parseFiles";
} else if (line.indexOf('Sections') > -1) {
currParser = "_parseSection";
} else if (line.indexOf('Symbols') > -1) {
currParser = "_parseSymbols";
}
return;
}
if (self[currParser]) {
self[currParser](line)
}
});
rl.on('close', function(line) {
cb(self)
});
},
_parseFiles: function(line) {
var arr =line.split(']')
if (arr.length > 1) {
var idx = Number(arr[0].replace('[',''));
var file = arr[1].split('/').pop().trim()
this.files[idx] = {
name: file,
size: 0
}
}
},
_parseSection: function(line) {
},
_parseSymbols: function(line) {
var arr = line.split('\t')
if (arr.length > 2) {
var size = parseInt(arr[1], 16)
var idx = Number(arr[2].split(']')[0].replace('[', ''))
if (idx && this.files[idx]) {
this.files[idx].size += size;
}
}
},
_formatSize: function(size) {
//totalSize += size;
if (size > 1024 * 1024) return (size/(1024*1024)).toFixed(2) + "MB"
if (size > 1024) return (size/1024).toFixed(2) + "KB"
return size + "B"
},
statLibs: function(h) {
var libs = {}
var files = this.files;
var self = this;
for (var i in files) {
var file = files[I]
var libName
if (file.name.indexOf('.o)') > -1) {
libName = file.name.split('(')[0]
} else {
libName = file.name
}
if (!libs[libName]) {
libs[libName] = 0
}
libs[libName] += file.size
}
var i = 0, sortLibs = []
for (var name in libs) {
sortLibs[i++] = {
name: name,
size: libs[name]
}
}
sortLibs.sort(function(a,b) {
return a.size > b.size ? -1: 1
})
if (h) {
sortLibs.map(function(o) {
o.size = self._formatSize(o.size)
})
}
return sortLibs
},
statFiles: function(h) {
var self = this
self.files.sort(function(a,b) {
return a.size > b.size ? -1: 1
})
if (h) {
self.files.map(function(o) {
o.size = self._formatSize(o.size)
})
}
return this.files
}
}
if (!process.argv[2]) {
console.log('usage: node linkmap.js filepath -hl')
console.log('-h: format size')
console.log('-l: stat libs')
return
}
var isStatLib, isFomatSize
var opts = process.argv[3];
if (opts && opts[0] == '-') {
if (opts.indexOf('h') > -1) isFomatSize = true
if (opts.indexOf('l') > -1) isStatLib = true
}
var linkmap = new LinkMap(process.argv[2])
linkmap.start(function(){
var ret = isStatLib ? linkmap.statLibs(isFomatSize)
: linkmap.statFiles(isFomatSize)
for (var i in ret) {
console.log(ret[i].name + '\t' + linkmap._formatSize(ret[i].size))
totalSize += ret[i].size
}
console.log("totalSize:" + linkmap._formatSize(totalSize))
})
新建一个只引入高德地图的项目,生成alipaylinkmap.txt文件后。
将以上js代码保存为 linkmap.js ,执行脚本(python linkmap.py ./alipaylinkmap.txt)后,输出结果如下:
MAMapKit(MAMapKit-arm64-master.o) 405.51KB
AMapFoundationKit(AMapFoundationKit-arm64-master.o) 314.42KB
AMapFoundationKit(wgs2gcj.o) 13.61KB
AppDelegate.o 8.86KB
libSystem.tbd 2.56KB
CoreGraphics.tbd 2.34KB
libobjc.tbd 1.13KB
CoreFoundation.tbd 544B
ViewController.o 531B
Security.tbd 512B
UIKit.tbd 320B
libPods-TestAMMap.a(Pods-TestAMMap-dummy.o) 257B
MAMapKit(Pods-MAMapKit-dummy.o) 256B
libz.tbd 256B
Foundation.tbd 256B
SystemConfiguration.tbd 256B
libc++.tbd 232B
CFNetwork.tbd 192B
main.o 186B
QuartzCore.tbd 96B
CoreLocation.tbd 64B
linker synthesized 0B
libstdc++.6.0.9.tbd 0B
totalSize:752.30KB
下面是对只引入百度地图的项目link文件的统计
BaiduMapAPI_Map(BMKMapView.o) 133.41KB
BaiduMapAPI_Search(BMKRouteSearch.o) 119.47KB
BaiduMapAPI_Map(BVDEDataCfg.o) 111.16KB
BaiduMapAPI_Map(BVDBBase.o) 96.09KB
BaiduMapAPI_Base(VCMMap.o) 94.91KB
BaiduMapAPI_Map(VMapControl.o) 93.09KB
libcrypto.a(obj_dat.o) 81.05KB
BaiduMapAPI_Search(BMSerail.o) 69.13KB
BaiduMapAPI_Search(RoutePlanJsonPharser.o) 68.54KB
BaiduMapAPI_Map(DrawUnit.o) 61.88KB
BaiduMapAPI_Search(Searcher.o) 61.56KB
BaiduMapAPI_Map(MapView.o) 57.85KB
BaiduMapAPI_Search(PoiJsonPharser.o) 52.56KB
BaiduMapAPI_Base(VHttpClient.o) 43.88KB
BaiduMapAPI_Map(BMKOverlayView.o) 41.80KB
BaiduMapAPI_Search(BMKRouteSearchType.o) 41.74KB
BaiduMapAPI_Map(BVDBUrl.o) 41.37KB
BaiduMapAPI_Base(CommonMemCacheEngine.o) 40.33KB
BaiduMapAPI_Map(Style.o) 37.07KB
BaiduMapAPI_Search(BMKPoiSearch.o) 36.69KB
BaiduMapAPI_Base(SpatialUtil.o) 34.29KB
BaiduMapAPI_Map(BMMapViewManager.o) 33.52KB
BaiduMapAPI_Map(PoiMarkData.o) 29.14KB
BaiduMapAPI_Map(PoiMarkLayer.o) 27.46KB
libssl.a(t1_lib.o) 26.24KB
BaiduMapAPI_Search(RoutePlanSearchUrl.o) 26.15KB
BaiduMapAPI_Map(BVMDDataVMP.o) 24.88KB
BaiduMapAPI_Map(TapDetectingView.o) 24.77KB
BaiduMapAPI_Map(bmanimationfactory.o) 24.68KB
libssl.a(s3_lib.o) 23.41KB
BaiduMapAPI_Map(LocalMap.o) 23.07KB
libcrypto.a(ec_curve.o) 21.93KB
libssl.a(s3_clnt.o) 21.44KB
BaiduMapAPI_Map(GridIndoorLayer.o) 21.29KB
BaiduMapAPI_Cloud(BMKCloudSearch.o) 21.24KB
BaiduMapAPI_Base(BGLLine.o) 21.20KB
BaiduMapAPI_Map(MapController.o) 20.68KB
libssl.a(ssl_ciph.o) 20.32KB
BaiduMapAPI_Map(BMHeatMapService.o) 20.23KB
libssl.a(s3_srvr.o) 19.80KB
BaiduMapAPI_Base(BGLBase.o) 19.70KB
BaiduMapAPI_Base(AppMan.o) 19.63KB
libcrypto.a(wp_block.o) 19.27KB
BaiduMapAPI_Base(gpc.o) 18.86KB
BaiduMapAPI_Map(BVIDDataTMP.o) 18.59KB
BaiduMapAPI_Map(BMKOfflineMap.o) 18.33KB
BaiduMapAPI_Utils(Adapter.o) 18.16KB
libcrypto.a(err.o) 18.08KB
BaiduMapAPI_Map(BaseLayer.o) 17.76KB
...
totalSize:4.83MB
从结果看到,不仅是我们编写的类的大小可以统计出来,第三方的也可以。在实际工程中,我们可以对一些可执行文件中过大的第三方库,思考其存在的必要性,对于不需要存在或者有替换方案的,可以考虑替换或删除。
2.4 清理无用代码神器: AppCode
我们可以用它的inspect code来扫描无用代码,包括无用的类、函数、宏定义、value、属性等,而safe delete功能使得删除一些由于runtime被调用到的代码时更加安全智能。扫描结果示例:3、 编译选项优化
Strip Link Product设成YES
Make Strings Read-Only设为YES
去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions,可执行文件减少了1M,
Build Settings->Strip Debug Symbols During Copy: release版应该设置为YES,可以去除不必要的调试符号。
Build Settings->Optimization Level:release版应该选择Fastest, Smalllest,这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。
其它途径
- iOS9 App Thinning:严格来说App Thinning不会让安装包变小,但用户安装应用时,苹果会根据用户的机型自动选择合适的资源和对应CPU架构的二进制执行文件(也就是说用户本地可执行文件不会同时存在armv7和arm64),安装后空间占用更小
- iOS8 Embed-Framework:该特性需要最低版本iOS8才能用,iOS7设备启动会crash
- ARC->MRC:ARC代码会在某些情况多出一些retain和release的指令,通过实验,结论是ARC大概会使代码段增加10%的size,考虑代码段占可执行文件大约有80%,估计对整个可执行文件的影响会是8%。
- 类/方法命名长度 :从LinkMap可以发现每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的,原因还是Objective-C的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,Objective-C对象模型会把类名,方法名列表都保存下来。实际上这部分占用的长度比较小,较大项目也就几百K,可以忽略。