iOS 性能iOS日常积累

iOS安装包瘦身方案探索和实践

2018-03-28  本文已影响115人  iLees

安装包过大,不利于市场人员做推广,最近做了 iOS 安装包瘦身的技术研究和实践。
iOS APP经过编译,打包文件中除了资源文件,剩下的就是一个可执行文件了。

瘦身,可以从 ​三个方面入手:
  1. ​资源文件
  2. 可执行文件
  3. ​编译选项

下面从这三个方面来分析安装包瘦身的方法和一些工具使用。

1. 资源文件

资源文件包括图片、声音、配置文件、文本文件、xib、storyboard、证书等。其中最常用的资源是第一种,优化方式无非删除或压缩处理。

1.1 删除无用的资源文件

推荐使用工具 LSUnusedResources


搜索出来结果后,选中某行,点击Delete按钮即可删除资源。
1.2 压缩资源文件

常用的有两个工具:

1.3 使用.xcassets 导入图片

打包之后会生成 Assets.car ,文件的大小会降低。

2. 可执行文件

Mach-O为Mach Object文件格式的缩写,是mac上可执行文件的格式,类似于windows上的PE格式 (Portable Executable )或 linux上的elf格式。Mach-O文件分为这几类:

对于这几种类型的Mach-O文件,我们可以使用MachOView进行查看。MachOView是一个开源的工具,源码在GitHub上:https://github.com/gdbinit/MachOView
不过该项目已经很久没有更新了,在 MacOS High Sierra 10.13.3系统上,使用很短的时间后会崩溃。

查看一个可执行文件(Executable 文件): 查看一个静态库文件(Static Library): 点开一个Static Library

从上图可以看到,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、 编译选项优化
其它途径
上一篇下一篇

猜你喜欢

热点阅读