iOS - ipa包大小之LinkMap文件分析

96
MichaelLedger
1.8 2018.04.23 18:07* 字数 3448

近来我们一直在做 ipa 包大小的缩减,在删除了无用图片,缩减项目中图片的体积,取得了较大的效果。但是成果背后的问题也接踵而至,删除完了图片,压缩完了图片大小之后,我们应该怎么来减少 ipa 包大小呢?从哪里去减小、减小什么、怎么减小呢?

Xcode编译后,除了一些资源文件,剩下的就是一个可执行文件,有时候项目大了,引入的库多了,可执行文件很大,如何知道这个可执行文件的构成是怎样,里面的内容都是些什么,哪些库占用空间较高?

带着这样的问题,我们先来了解一下 ipa 包的构成:

ipa包构成

ipa 包解压之后主要由三部门构成:
1、同名的可执行文件;
2、Asset.car.nib.bundleLocalizable.strings等资源文件;
3、其他:_CodeSignature文件夹,签名信息。

什么是可执行文件?

作为iOS客户端开发者,我们比较熟悉的一种文件是ipa包(iPhone Application)。但实际上这只是一个变相的zip压缩包,我们可以把一个ipa文件直接通过unzip命令解压。

解压之后,会有一个Payload目录,而Payload里则是一个.app文件,而这个实际上又是一个目录,或者说是一个完整的App Bundle。

在这个目录中,里面体积最大的文件通常就是和ipa包同名的一个二进制文件。找到它,使用 file 命令 探测可执行文件XXX的类型:

$ file XXX
// 支持arm64处理器架构的Mach-O格式通用程序包
XXX: Mach-O 64-bit executable arm64

$ file XXX
// 支持armv7和armv7s两种处理器架构的通用程序包,里面包含的两部分都是Mach-O格式
XXX: Mach-O universal binary with 2 architectures
XXX (for architecture armv7): Mach-O executable arm
XXX (for architecture armv7s): Mach-O executable arm

在Windows上.exe是可直接执行的文件扩展名,而在Linux(以及很多版本的Unix)系统上ELF是可直接执行的文件格式,在iOS(和Mac OS X)上,主要的可执行文件格式是Mach-O格式。

Mach-O格式是iOS系统上应用程序运行的基础,了解Mach-O的格式,对于调试、自动化测试、安全都有意义。在了解二进制文件的数据结构以后,一切就都显得没有秘密。

这里先提醒大家一下,Mach不是Mac,Mac是苹果电脑Macintosh的简称,而Mach则是一种操作系统内核。Mach内核被NeXT公司的NeXTSTEP操作系统使用。在Mach上,一种可执行的文件格是就是Mach-O(Mach Object file format)。1996年,乔布斯将NeXTSTEP带回苹果,成为了OS X的内核基础。所以虽然Mac OS X是Unix的“后代”,但所主要支持的可执行文件格式是Mach-O。

iOS是从OS X演变而来,所以同样是支持Mach-O格式的可执行文件。

A Mach-O file has the following regions of data (the complete format is described in OS X ABI Mach-O File Format Reference):

Header: Specifies the target architecture of the file, such as PPC, PPC64, IA-32, or x86-64.
Load commands: Specify the logical structure of the file and the layout of the file in virtual memory.
Raw segment data: Contains raw data for the segments defined in the load commands.

Mach-O通常有三部分组成
头部 (Header): Mach-O文件的架构 比如Mac的 PPC, PPC64, IA-32, x86-64,ios的arm系列。
加载命令(Load commands): 在虚拟内存中指定文件的逻辑结构和布局。
原始段数据(Raw segment data):可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。

Mach-O_ Construction

可执行文件的组成

  1. XCode开启编译选项Write Link Map File
    XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为YES,并指定好linkMap的存储位置
    特别提醒:打包发布前记得还原为NO
  2. 编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File位于
~/Library/Developer/Xcode/DerivedData/XXX-XXXXXXXXXXXX/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

//example
/Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/4dBookCity-LinkMap-normal-arm64.txt

这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。

LinkMap结构

1.首先列出来的是目标文件列表(中括号内为文件编号):

# Path: /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Products/Debug-iphoneos/4dBookCity.app/4dBookCity
# Arch: arm64
# Object files:
[  0] linker synthesized
[  1] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/Bulk_Arrays_12.o
[  2] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRSnapLearnInviteView.o
[  3] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRPKHomeCellViewModel.o
[  4] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/Bulk_Arrays_5.o
[  5] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRBookStoreItemScrollTemplateCell.o
[  6] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRAutoReadViewController.o
[  7] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRQAExerciseQestionTitleView.o
[  8] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/MXRMyTaskController.o
[  9] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/UnityView.o
[ 10] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Intermediates.noindex/huashida_home.build/Debug-iphoneos/4dBookCity.build/Objects-normal/arm64/main.o
...
[5229] /Users/mxr/Library/Developer/Xcode/DerivedData/huashida_home.xcodeproj-fvbzvmahuzlfgqbzehannctanrbl/Build/Products/Debug-iphoneos/libPods-4dBookCity.a(Pods-4dBookCity-dummy.o)
[5230] /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a(arclite.o)
[5231] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/usr/lib/libobjc.tbd
[5232] /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/lib/darwin/libclang_rt.ios.a(os_version_check.c.o)
[5233] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/System/Library/Frameworks//AudioToolbox.framework/AudioToolbox.tbd
[5234] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.2.sdk/System/Library/Frameworks//CoreVideo.framework/CoreVideo.tbd

2.接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)

# Sections:
# Address   Size        Segment Section
0x100005B00 0x0304A29C  __TEXT  __text
0x10304FD9C 0x00004BC0  __TEXT  __stubs
0x10305495C 0x000044E8  __TEXT  __stub_helper
0x103058E50 0x0021563C  __TEXT  __cstring
0x10326E48C 0x000AD400  __TEXT  __objc_methname
0x10331B88C 0x0000E6BA  __TEXT  __objc_classname
0x103329F46 0x000166E3  __TEXT  __objc_methtype
0x103340640 0x002A0B60  __TEXT  __const
0x1035E11A0 0x001346D4  __TEXT  __gcc_except_tab
0x103715874 0x00008C78  __TEXT  __ustring
0x10371E4EC 0x0004D80C  __TEXT  __unwind_info
0x10376BCF8 0x00000300  __TEXT  __eh_frame
0x10376C000 0x000015D8  __DATA  __got
0x10376D5D8 0x00003280  __DATA  __la_symbol_ptr
0x103770858 0x00001838  __DATA  __mod_init_func
0x103772090 0x000FF7F8  __DATA  __const
0x103871888 0x0006F9C0  __DATA  __cfstring
0x1038E1248 0x00004778  __DATA  __objc_classlist
0x1038E59C0 0x00000290  __DATA  __objc_nlclslist
0x1038E5C50 0x00000708  __DATA  __objc_catlist
0x1038E6358 0x00000038  __DATA  __objc_nlcatlist
0x1038E6390 0x00000910  __DATA  __objc_protolist
0x1038E6CA0 0x00000008  __DATA  __objc_imageinfo
0x1038E6CA8 0x00206C58  __DATA  __objc_const
0x103AED900 0x00027F28  __DATA  __objc_selrefs
0x103B15828 0x000000C0  __DATA  __objc_protorefs
0x103B158E8 0x000041B8  __DATA  __objc_classrefs
0x103B19AA0 0x000030C0  __DATA  __objc_superrefs
0x103B1CB60 0x0000BB54  __DATA  __objc_ivar
0x103B286B8 0x0002CB00  __DATA  __objc_data
0x103B551C0 0x01D52748  __DATA  __data
0x1058A7920 0x00714878  __DATA  __bss
0x105FBD000 0x0012B978  __DATA  __common

首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。
每一行的数据都紧跟在上一行后面,如第二行__stubs的地址0x10304FD9C就是第一行__text的地址0x100005B00加上大小0x0304A29C,整个可执行文件大致数据分布就是这样。
这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。

3.接着就是按上表顺序,列出具体的按每个文件列出每个对应字段的位置和占用空间

# Symbols:
# Address   Size        File  Name
0x100005B00 0x000000EC  [  2] -[MXRSnapLearnInviteView drawRect:]
0x100005BEC 0x0000024C  [  2] -[MXRSnapLearnInviteView generatorlogoImageQRCode]
0x100005E38 0x0000005C  [  2] _CGRectMake
0x100005E94 0x00000034  [  2] -[MXRSnapLearnInviteView inviteCode]
0x100005EC8 0x00000050  [  2] -[MXRSnapLearnInviteView setInviteCode:]
0x100005F18 0x0000003C  [  2] -[MXRSnapLearnInviteView .cxx_destruct]
0x100005F54 0x000001D8  [  3] -[MXRPKHomeCellViewModel initWithModel:]
0x10000612C 0x0000016C  [  3] -[MXRPKHomeCellViewModel encodeWithCoder:]
0x100006298 0x00000268  [  3] -[MXRPKHomeCellViewModel initWithCoder:]
0x100006500 0x00000040  [  3] -[MXRPKHomeCellViewModel desc]
0x100006540 0x00000044  [  3] -[MXRPKHomeCellViewModel setDesc:]
0x100006584 0x00000040  [  3] -[MXRPKHomeCellViewModel name]
0x1000065C4 0x00000044  [  3] -[MXRPKHomeCellViewModel setName:]
0x100006608 0x00000040  [  3] -[MXRPKHomeCellViewModel pic]
0x100006648 0x00000044  [  3] -[MXRPKHomeCellViewModel setPic:]
0x10000668C 0x00000040  [  3] -[MXRPKHomeCellViewModel classifyId]
0x1000066CC 0x00000044  [  3] -[MXRPKHomeCellViewModel setClassifyId:]
0x100006710 0x000000B8  [  3] -[MXRPKHomeCellViewModel .cxx_destruct]
...
0x1060C82D0 0x000000C0  [3391] _jerrenv
0x1060C8390 0x000204E0  [4793] _GC_arrays
0x1060E8870 0x00000100  [4793] _GC_bm_table
0x1060E8970 0x00000008  [4793] _GC_noop_sink

同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。

  1. 已废弃&多余重复的字段
# Dead Stripped Symbols:
#           Size        File  Name
<<dead>>    0x00000001  [  1] literal string: 
<<dead>>    0x00000005  [  3] literal string: desc
<<dead>>    0x00000005  [  3] literal string: name
<<dead>>    0x00000004  [  3] literal string: pic
<<dead>>    0x0000000B  [  3] literal string: classifyId
<<dead>>    0x0000000E  [  3] literal string: .cxx_destruct
<<dead>>    0x0000000B  [  3] literal string: v24@0:8@16
<<dead>>    0x00000008  [  3] literal string: v16@0:8
<<dead>>    0x00000008  [  3] literal string: @16@0:8
<<dead>>    0x00000001  [  4] literal string: 
<<dead>>    0x00000007  [  4] literal string: System
<<dead>>    0x0000000C  [  4] literal string: UnityEngine
<<dead>>    0x0000000A  [  4] literal string: System.IO
<<dead>>    0x00000008  [  5] 8-byte-literal
<<dead>>    0x0000000C  [  5] literal string: PRIMARY KEY
<<dead>>    0x0000000C  [  5] literal string: FOREIGN KEY
<<dead>>    0x00000001  [  5] literal string: 
<<dead>>    0x00000020  [  5] CFString
<<dead>>    0x00000020  [  5] CFString
<<dead>>    0x00000008  [  5] _LKSQL_Type_Text
<<dead>>    0x00000008  [  5] _LKSQL_Type_Int
<<dead>>    0x00000008  [  5] _LKSQL_Type_Double
<<dead>>    0x00000008  [  5] _LKSQL_Type_Blob
<<dead>>    0x00000008  [  5] _LKSQL_Attribute_NotNull
<<dead>>    0x00000008  [  5] _LKSQL_Attribute_PrimaryKey
<<dead>>    0x00000008  [  5] _LKSQL_Attribute_Default
<<dead>>    0x00000008  [  5] _LKSQL_Attribute_Unique
<<dead>>    0x00000008  [  5] _LKSQL_Attribute_Check
<<dead>>    0x00000008  [  5] _LKSQL_Attribute_ForeignKey
<<dead>>    0x00000008  [  5] _LKSQL_Convert_FloatType
<<dead>>    0x00000008  [  5] _LKSQL_Convert_IntType
<<dead>>    0x00000008  [  5] _LKSQL_Convert_BlobType
<<dead>>    0x00000008  [  5] _LKSQL_Mapping_Inherit
<<dead>>    0x00000008  [  5] _LKSQL_Mapping_Binding
<<dead>>    0x00000008  [  5] _LKSQL_Mapping_UserCalculate
<<dead>>    0x00000008  [  5] _LKDB_TypeKey
<<dead>>    0x00000008  [  5] _LKDB_TypeKey_Model
<<dead>>    0x00000008  [  5] _LKDB_TypeKey_JSON
<<dead>>    0x00000008  [  5] _LKDB_TypeKey_Combo
...
<<dead>>    0x00000004  [4311] 4-byte-literal
<<dead>>    0x00000004  [4311] 4-byte-literal
<<dead>>    0x00000004  [4311] 4-byte-literal
<<dead>>    0x00000004  [4311] 4-byte-literal
<<dead>>    0x00000008  [4311] 8-byte-literal
<<dead>>    0x00000008  [4312] 8-byte-literal
<<dead>>    0x00000014  [4320] __ZN15PxcConvexMeshHLC2EP17PxConvexMeshData_
<<dead>>    0x00000004  [4320] 4-byte-literal
<<dead>>    0x00000004  [4320] 4-byte-literal
<<dead>>    0x00000004  [4319] 4-byte-literal
<<dead>>    0x00000076  [4319] literal string: /Applications/buildAgent/work/3d1e9e6e6eefaa7f/SDKs/compiler/iphone/../../../LowLevel/common/include/utils/PxcArray.h

LinkMap的使用

这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小。

nodejs版统计程序

// linkmap.js
var readline = require('readline'),
    fs = require('fs');

var LinkMap = function(filePath) {
    this.files = []
    this.filePath = filePath
}

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) {
        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' + ret[i].size)
    }
})

终端操作:

$ node linkmap.js 4dBookCity-LinkMap-normal-arm64.txt -hl

libmml.a    15.42MB
libiPhone-lib.a 11.45MB
libmtl.a    6.88MB
Il2CppGenericMethodDefinitions.o    2.55MB
libReact.a  2.21MB
Bulk_Assembly-CSharp_6.o    1.63MB
Il2CppTypeDefinitions.o 1014.05KB
libmcl.a    915.16KB
Bulk_Assembly-CSharp_8.o    887.68KB
Bulk_mscorlib_5.o   851.41KB
Bulk_Assembly-CSharp_10.o   836.57KB
Il2CppMetadataCacheData.o   776.86KB
Bulk_Assembly-CSharp_11.o   746.68KB
libjcore-ios-1.1.6.a    657.94KB
Bulk_UnityEngine_2.o    646.65KB
Bulk_mscorlib_0.o   644.23KB
Bulk_Assembly-CSharp_2.o    630.32KB
GeneratedInvokers.o 617.64KB
Bulk_mscorlib_1.o   592.24KB
Bulk_Mono.Security_0.o  586.58KB
libAliyunOSSiOS.a   575.32KB
Bulk_mscorlib_6.o   572.95KB
libMAREXT.a 554.18KB
lame    547.98KB
Bulk_mscorlib_2.o   532.28KB
libmil.a    519.21KB
Bulk_Assembly-CSharp_1.o    512.61KB
Bulk_Assembly-CSharp_9.o    491.22KB
Bulk_Assembly-CSharp_3.o    479.75KB
GenericMethods15.o  476.69KB
Bulk_Assembly-CSharp_13.o   474.51KB
Bulk_UnityEngine.UI_0.o 470.58KB
MOBFoundation   428.76KB
UTMini  425.42KB
libWeChatSDK.a  418.78KB
Bulk_System_0.o 408.20KB
Bulk_Assembly-CSharp_12.o   396.19KB
Bulk_DOTween_0.o    377.52KB
Bulk_Assembly-CSharp_4.o    373.51KB
Bulk_Assembly-CSharp_0.o    365.18KB
Bulk_Assembly-CSharp_14.o   357.25KB
Bulk_System_1.o 349.00KB
ShareSDK    347.48KB
Bulk_mscorlib_7.o   346.76KB
Bulk_Assembly-CSharp_7.o    344.18KB
UMMobClick  333.30KB
libAFNetworking.a   327.87KB
libM13ProgressSuite.a   326.06KB
...

关于Xcode的Other Linker Flags

  • 背景

在ios开发过程中,有时候会用到第三方的静态库(.a文件),然后导入后发现编译正常但运行时会出现selector not recognized的错误,从而导致app闪退。接着仔细阅读库文件的说明文档,你可能会在文档中发现诸如在Other Linker Flags中加入-ObjC或者-all_load这样的解决方法。

那么,Other Linker Flags到底是用来干什么的呢?还有-ObjC-all_load到底发挥了什么作用呢?

  • 链接器

首先,要说明一下Other Linker Flags到底是用来干嘛的。说白了,就是ld命令除了默认参数外的其他参数。ld命令实现的是链接器的工作,详细说明可以在终端man ld查看。

如果有人不清楚链接器是什么东西的话,我可以作个简单的说明。

一个程序从简单易读的代码到可执行文件往往要经历以下步骤:

源代码 > 预处理器 > 编译器 > 汇编器 > 机器码 > 链接器 > 可执行文件

源文件经过一系列处理以后,会生成对应的.obj文件,然后一个项目必然会有许多.obj文件,并且这些文件之间会有各种各样的联系,例如函数调用。链接器做的事就是把这些目标文件和所用的一些库链接在一起形成一个完整的可执行文件。

可能我描述的比较肤浅,因为我自己了解的也不是很深,建议大家读一下这篇文章,可以对链接器做的事情有个大概的了解:链接器做了什么

  • 为什么会闪退

苹果官方Q&A上有这么一段话:

The "selector not recognized" runtime exception occurs due to an issue between the implementation of standard UNIX static libraries, the linker and the dynamic nature of Objective-C. Objective-C does not define linker symbols for each function (or method, in Objective-C) - instead, linker symbols are only generated for each class. If you extend a pre-existing class with categories, the linker does not know to associate the object code of the core class implementation and the category implementation. This prevents objects created in the resulting application from responding to a selector that is defined in the category.

翻译过来,大概意思就是Objective-C的链接器并不会为每个方法建立符号表,而是仅仅为类建立了符号表。这样的话,如果静态库中定义了已存在的一个类的分类,链接器就会以为这个类已经存在,不会把分类和核心类的代码合起来。这样的话,在最后的可执行文件中,就会缺少分类里的代码,这样函数调用就失败了。

  • 解决方法

解决方法在背景那块我就提到了,就是在Other Linker Flags里加上所需的参数,用到的参数一般有以下3个:

`-ObjC`       : 链接器会把静态库中所有的类和分类都加载到最后的可执行文件中
`-force_load` : 需要指定要进行全部加载的库文件的路径,避免引用多个第三方库时会出现类名重叠的冲突
`-all_load`   : 让链接器把所有找到的目标文件都加载到可执行文件中,不建议使用
`-dead_strip` : 删除多余的库符号,不建议使用

下面来说说每个参数存在的意义和具体做的事情。

首先是-ObjC,一般这个参数足够解决前面提到的问题,苹果官方说明如下:

This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.

简单说来,加了这个参数后,链接器就会把静态库中所有的Objective-C类和分类都加载到最后的可执行文件中,虽然这样可能会因为加载了很多不必要的文件而导致可执行文件变大,但是这个参数很好地解决了我们所遇到的问题。但是事实真的是这样的吗?

如果-ObjC参数真的这么有效,那么事情就会简单多了。

Important: For 64-bit and iPhone OS applications, there is a linker bug that prevents -ObjC from loading objects files from static libraries that contain only categories and no classes. The workaround is to use the -allload or -forceload flags.

当静态库中只有分类而没有类的时候,-ObjC参数就会失效了。这时候,就需要使用-all_load或者-force_load了。

-all_load会让链接器把所有找到的目标文件都加载到可执行文件中,但是千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件,所以建议在遇到-ObjC失效的情况下使用-force_load参数。

-force_load所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载。

在能拿到静态库源码情况下,建议对.a库重新打包,删除部分重复的symbol。

在拿不到静态库源码情况下 ,只能采用-force_load+库文件路径方法设置Other Linker Flags,逐个加静态库,最终完美解决两个静态库存在同名文件冲突,发现那个静态库无法调用,就采用以下语句添加进去。
-force_load EightPartyCall/standaloneclass/BaiduSocialShare/WX/libWeChatSDK
(-force_load后面为静态库文件路径,根据自己项目对应路径)

也可以拆分静态库

$ cd /LibSDK 
$ ls
libiot.sdk.a
$ lipo -info libiot.sdk.a
Architectures in the fat file: libiot.sdk.a are: armv7 arm64 
$  lipo libiot.sdk.a -thin armv7 -output tbv7.a
$  lipo libiot.sdk.a -thin arm64 -output tb64.a
$ ls
libiot.sdk.a        tb64.a      tbv7.a
$ ar -d tbv7.a AsyncSocket.o
$ ar -d tb64.a AsyncSocket.o
$ lipo -create tbv7.a tbv64.a -output libSun.a

lipo 命令

lipo源于mac系统要制作兼容powerpc平台和intel平台的程序。
lipo 是一个在 Mac OS X 中处理通用程序(Universal Binaries)的工具。现在发售或者提供下载的许多(几乎所有)程序都打上了“Universal”标志,意味着它们同时具有 PowerPC 和 Intel 芯片能够处理的代码。不过既然你可能不在意其中的一个,你就能够使用 lipo 来给你的程序“瘦身”。

查看静态库支持的CPU架构
lipo -info libname.a / libname.framework / libname

静态库拆分指定CPU架构

# lipo 静态库源文件路径 -thin CPU架构名称 -output 拆分后文件存放路径
# 架构名为armv7/armv7s/arm64等,与lipo -info 输出的架构名一致
lipo  libname.a  -thin  armv7  -output  libname-armv7.a

合并静态库

# lipo -create 静态库存放路径1  静态库存放路径2 ...  -output 整合后存放的路径
lipo  -create  libname-armv7.a   libname-armv7s.a   libname-i386.a  -output  libname.a

让 Mac 命令行说话 - 学外语神器

这个命令是 Mac OS X 独一无二的,比其他任何一个都更有趣。
复制下面的命令,在终端中运行即可
say --voice="Sin-ji" 您好,我叫 Sin-ji 。我講廣東話。

不同的声音

say -v Sin-ji  "Hello, I am Chinese.  What do you want to eat?"
say -v Thomas  "Hello, I am French.  What do you want to eat?"
say -v Carmit  "Hello, I am  Israeli.  What do you want to eat?"

唱歌~~

say -v cellos dum dum dum dum dum dum dum he he he ho ho ho fa lah lah lah lah lah lah fa lah full hoo hoo hoo

say -v Cellos ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee

say --voice="Good News" ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee ni mà bee

n种语言

say --voice="Agnes" Isn't it nice to have a computer that will talk to you? 
say --voice="Albert" I have a frog in my throat. No, I mean a real frog! 
say --voice="Alex" Most people recognize me by my voice. 

say --voice="Alice" Salve, mi chiamo Alice e sono una voce italiana. 

say --voice="Alva" Hej, jag heter Alva. Jag är en svensk röst. 

say --voice="Amelie" Bonjour, je m ’ appelle Amelie. Je suis une voix canadienne. 

say --voice="Anna" Hallo, ich heiße Anna und ich bin eine deutsche Stimme. 

say --voice="Bad News" The light you see at the end of the tunnel is the headlamp of a fast approaching train. 

say --voice="Bahh" Do not pull the wool over my eyes. 

say --voice="Bells" Time flies when you are having fun. 

say --voice="Boing" Spring has sprung, fall has fell, winter's here and it's colder than usual. 

say --voice="Bruce" I sure like being inside this fancy computer 

say --voice="Bubbles" Pull the plug! I'm drowning! 

say --voice="Carmit" שלום. קוראים לי כרמית, ואני קול בשפה העברית. 

say --voice="Cellos" Doo da doo da dum dee dee doodly doo dum dum dum doo da doo da doo da doo da doo da doo da doo 

say --voice="Damayanti" Halo, nama saya Damayanti. Saya berbahasa Indonesia. 

say --voice="Daniel" Hello, my name is Daniel. I am a British-English voice. 

say --voice="Deranged" I need to go on a really long vacation. 

say --voice="Diego" Hola, me llamo Diego y soy una voz española. 

say --voice="Ellen" Hallo, mijn naam is Ellen. Ik ben een Belgische stem. 

say --voice="Fiona" en Hello, my name is Fiona. I am a Scottish-English voice. 

say --voice="Fred" I sure like being inside this fancy computer 

say --voice="Good News" Congratulations you just won the sweepstakes and you don't have to pay income tax again. 

say --voice="Hysterical" Please stop tickling me! 

say --voice="Ioana" Bună, mă cheamă Ioana . Sunt o voce românească. 

say --voice="Joana" Olá, chamo-me Joana e dou voz ao português falado em Portugal. 

say --voice="Junior" My favorite food is pizza. 

say --voice="Kanya" สวัสดีค่ะ ดิฉันชื่อKanya 

say --voice="Karen" Hello, my name is Karen. I am an Australian-English voice. 

say --voice="Kathy" Isn't it nice to have a computer that will talk to you? 

say --voice="Kyoko" こんにちは、私の名前はKyokoです。日本語の音声をお届けします。 

say --voice="Laura" Ahoj. Volám sa Laura . Som hlas v slovenskom jazyku. 

say --voice="Lekha" नमस्कार, मेरा नाम लेखा है.Lekha मै हिंदी मे बोलने वाली आवाज़ हूँ. 

say --voice="Luciana" Olá, o meu nome é Luciana e a minha voz corresponde ao português que é falado no Brasil 

say --voice="Maged" مرحبًا اسمي Maged. أنا عربي من السعودية. 

say --voice="Mariska" Üdvözlöm! Mariska vagyok. Én vagyok a magyar hang. 

say --voice="Mei-Jia" 您好,我叫美佳。我說國語。 

say --voice="Melina" Γεια σας, ονομάζομαι Melina. Είμαι μια ελληνική φωνή. 

say --voice="Milena" Здравствуйте, меня зовут Milena. Я – русский голос системы. 

say --voice="Moira" Hello, my name is Moira. I am an Irish-English voice. 

say --voice="Monica" Hola, me llamo Monica y soy una voz española. 

say --voice="Nora" Hei, jeg heter Nora. Jeg er en norsk stemme. 

say --voice="Paulina" Hola, me llamo Paulina y soy una voz mexicana. 

say --voice="Pipe Organ" We must rejoice in this morbid voice. 

say --voice="Princess" When I grow up I'm going to be a scientist. 

say --voice="Ralph" The sum of the squares of the legs of a right triangle is equal to the square of the hypotenuse. 

say --voice="Samantha" Hello, my name is Samantha. I am an American-English voice. 

say --voice="Sara" Hej, jeg hedder Sara. Jeg er en dansk stemme. 

say --voice="Satu" Hei, minun nimeni on Satu. Olen suomalainen ääni. 

say --voice="Sin-ji" 您好,我叫 Sin-ji 。我講廣東話。 

say --voice="Tessa" Hello, my name is Tessa. I am a South African-English voice. 

say --voice="Thomas" Bonjour, je m ’ appelle Thomas. Je suis une voix française. 

say --voice="Ting-Ting" 您好,我叫 Ting-Ting 。我讲中文普通话。 

say --voice="Trinoids" We cannot communicate with these carbon units. 

say --voice="Veena" Hello, my name is Veena. I am an Indian-English voice. 

say --voice="Vicki" Isn't it nice to have a computer that will talk to you? 

say --voice="Victoria" Isn't it nice to have a computer that will talk to you? 

say --voice="Whisper" Pssssst, hey you, Yeah you, Who do ya think I'm talking to, the mouse? 

say --voice="Xander" Hallo, mijn naam is Xander. Ik ben een Nederlandse stem. 

say --voice="Yelda" Merhaba, benim adım Yelda. Ben Türkçe bir sesim. 

say --voice="Yuna" 안녕하세요. 제 이름은 Yuna입니다. 저는 한국어 음성입니다. 

say --voice="Zarvox" That looks like a peaceful planet. 

say --voice="Zosia" Witaj. Mam na imię Zosia, jestem głosem kobiecym dla języka polskiego. 

say --voice="Zuzana" Dobrý den, jmenuji se Zuzana. Jsem český hlas.

参考:
iOS APP可执行文件的组成
ipa包大小之linkMap文件分析
了解iOS上的可执行文件和Mach-O格式
Mach-O Programming Topics
Mach-O可执行文件
关于Xcode的Other Linker Flags
iOS项目中引用多个第三方库引发冲突的解决方法
iOS 两个静态库存在同名文件冲突解决方案
Mac OS X上的lipo命令详解
lipo命令
让 Mac 命令行说话

随笔
Web note ad 1