美文网首页iOS 性能iOS日常积累
iOS安装包瘦身方案探索和实践

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

作者: iLees | 来源:发表于2018-03-28 22:01 被阅读115次

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

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

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

    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系统上,使用很短的时间后会崩溃。

    查看一个可执行文件(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、 编译选项优化
    • 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,可以忽略。

    相关文章

      网友评论

        本文标题:iOS安装包瘦身方案探索和实践

        本文链接:https://www.haomeiwen.com/subject/hcvucftx.html