iOS优化-包大小分析-linkMap

LinkMap解析

导读
IOS在做包大小优化的时候,需要分析包大小组成,然后通过包大小组成来有针对的做优化。其中最主要的工具就是linkmap文件的解析,下面文章讲简单说明如何解析linkmap文件。

如何生成linkMap文件

  1. Xcode开启编译选项Write Link Map File

    XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置

    get_link_map

  2. 编译后,到编译目录里找到该txt文件,文件路径就是上面设定的路径,我的位于:

    ~/Library/Developer/Xcode/DerivedData/FFProject-gdxobffdqcwvyleustpwgfdxslqp/Build/Intermediates/FFProject.build/Debug-iphonesimulator/FFProject.build

linkMap文件结构解析

1. 基础信息

1
2
3
# Path: /Users/bolei/Library/Developer/Xcode/DerivedData/FFProject-gdxobffdqcwvyleustpwgfdxslqp/Build/Products/Debug-iphonesimulator/FFProject.app/FFProject //路径
# Arch: x86_64 //架构

2.类表

1
2
3
4
# Object files: //类文件
[ 0] linker synthesized
[ 1] dtrace
[ 2] /Users/bolei/Library/Developer/Xcode/DerivedData/FFProject-gdxobffdqcwvyleustpwgfdxslqp/Build/Intermediates/FFProject.build/Debug-iphonesimulator/FFProject.build/Objects-normal/x86_64/PAFFConfig.o

这里保存了所有用到的类生成的.o文件,也包括用到的dylib库。前面[num]是序号,类是按照顺序保存的,后续可以通过序号查到具体对应的哪个类。

3.段表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Sections:
# Address Size Segment Section
0x100002460 0x00E382DF __TEXT __text
0x100E3A740 0x000019D4 __TEXT __stubs
0x100E3C114 0x0000273E __TEXT __stub_helper
0x100E3E860 0x0009D78B __TEXT __cstring
0x100EDBFEB 0x00089F7A __TEXT __objc_methname
0x100F65F65 0x0000CFCD __TEXT __objc_classname
0x100F72F32 0x00012A27 __TEXT __objc_methtype
0x100F8595A 0x000122E8 __TEXT __ustring
0x100F97C44 0x00067DA8 __TEXT __gcc_except_tab
0x100FFF9F0 0x000259C8 __TEXT __const
0x1010253B8 0x0000017C __TEXT __entitlements
0x101025534 0x0000037B __TEXT __dof_RACSignal
0x1010258AF 0x000002E8 __TEXT __dof_RACCompou
0x101025B98 0x00016928 __TEXT __unwind_info
0x10103C4C0 0x00013B40 __TEXT __eh_frame
0x101050000 0x00000010 __DATA __nl_symbol_ptr
0x101050010 0x00000D30 __DATA __got
0x101050D40 0x00002270 __DATA __la_symbol_ptr
0x101052FB0 0x00000030 __DATA __mod_init_func
0x101052FE0 0x00036580 __DATA __const
0x101089560 0x0005EB20 __DATA __cfstring
0x1010E8080 0x000040A8 __DATA __objc_classlist
0x1010EC128 0x00000448 __DATA __objc_nlclslist
0x1010EC570 0x00000AA8 __DATA __objc_catlist
0x1010ED018 0x00000048 __DATA __objc_nlcatlist
0x1010ED060 0x00000780 __DATA __objc_protolist
0x1010ED7E0 0x00000008 __DATA __objc_imageinfo
0x1010ED7E8 0x001A2B80 __DATA __objc_const
0x101290368 0x00020CE8 __DATA __objc_selrefs
0x1012B1050 0x00000168 __DATA __objc_protorefs
0x1012B11B8 0x00003B80 __DATA __objc_classrefs
0x1012B4D38 0x00002620 __DATA __objc_superrefs
0x1012B7358 0x00010AF0 __DATA __objc_ivar
0x1012C7E48 0x000286E0 __DATA __objc_data
0x1012F0530 0x0000BB48 __DATA __data
0x1012FC080 0x00011A40 __DATA __bss
0x10130DAC0 0x00000538 __DATA __common

接下来是段表,描述了不同功能的数据保存的地址,通过这个地址就可以查到对应内存里存储的是什么数据。

其中第一列是起始地址,第二列是段占用的大小,第三个是段类型,第四列是段名称,每一行初始地址 = 上一行的初始地址+占用大小

其中:

__TEXT 表示代码段,用于执行,可读不可以写,可以被执行

__DATA 表示数据段,用于存储数据,可以读写,不可以执行

其中:

第一个段是__PAGEZERO 地址从0到0x100000000,程序保留字段。

3.1 段表内容含义

__TEXT段节名含义

1
2
3
4
5
6
7
8
9
10
11
12
13
1. __text: 代码节,存放机器编译后的代码
2. __stubs: 用于辅助做动态链接代码(dyld).
3. __stub_helper:用于辅助做动态链接(dyld).
4. __objc_methname:objc的方法名称
5. __cstring:代码运行中包含的字符串常量,比如代码中定义`#define kGeTuiPushAESKey @"DWE2#@e2!"`,那DWE2#@e2!会存在这个区里。
6. __objc_classname:objc类名
7. __objc_methtype:objc方法类型
8. __ustring:
9. __gcc_except_tab:
10. __const:存储const修饰的常量
11. __dof_RACSignal:
12. __dof_RACCompou:
13. __unwind_info:

__DATA段节名含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. __got:存储引用符号的实际地址,类似于动态符号表
2. __la_symbol_ptr:lazy symbol pointers。懒加载的函数指针地址。和__stubs和stub_helper配合使用。具体原理暂留。
3. __mod_init_func:模块初始化的方法。
4. __const:存储constant常量的数据。比如使用extern导出的const修饰的常量。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函数列表。
10. __objc_protolist:objc协议列表
11. __objc_imageinfo:objc镜像信息
12. __objc_const:objc常量。保存objc_classdata结构体数据。用于映射类相关数据的地址,比如类名,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc协议
15. __objc_classrefs:引用到的objc类
16. __objc_superrefs:objc超类引用
17. __objc_ivar:objc ivar指针,存储属性。
18. __objc_data:objc的数据。用于保存类需要的数据。最主要的内容是映射__objc_const地址,用于找到类的相关数据。
19. __data:暂时没理解,从日志看存放了协议和一些固定了地址(已经初始化)的静态量。
20. __bss:存储未初始化的静态量。比如:`static NSThread *_networkRequestThread = nil;`其中这里面的size表示应用运行占用的内存,不是实际的占用空间。所以计算大小的时候应该去掉这部分数据。
21. __common:存储导出的全局的数据。类似于static,但是没有用static修饰。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存储在__common里面

3.2 后续符号表内容

3.2.1 代码节

1
2
3
4
5
6
# Symbols:
# Address Size File Name
0x100002460 0x00000080 [ 2] +[PAFFConfig instance]
0x1000024E0 0x00000050 [ 2] ___22+[PAFFConfig instance]_block_invoke
0x100002530 0x00000090 [ 2] -[PAFFConfig init]
apiType]

这里面保存里类里面的方法内存情况。其中

  1. 第一列是起始地址位置,通过这个地址我们可以查上面的段表,可以知道,对应的节为__text
  2. 第二列是大小,通过这个可以算出方法占用的大小。
  3. 第三列是归属的类(.o文件),这里序号是2,通过查类表可以知道对应的类是PAFFConfig。

通过这部分我们可以分析出来每个类对应的方法的大小是多少。

3.2.3 方法名节(__objc_methname)

1
2
3
4
5
6
7
8
9
10
11
0x100EDBFEB 0x00000006 [ 2] literal string: alloc
0x100EDBFF1 0x00000005 [ 2] literal string: init
0x100EDBFF6 0x0000000B [ 2] literal string: mainBundle
0x100EDC001 0x0000000F [ 2] literal string: infoDictionary
0x100EDC010 0x0000000E [ 2] literal string: objectForKey:
0x100EDC01E 0x0000000C [ 2] literal string: setAppName:
0x100EDC02A 0x0000000C [ 2] literal string: setVersion:
0x100EDC036 0x0000000C [ 2] literal string: setApiType:
0x100EDC042 0x00000009 [ 2] literal string: instance
0x100EDC04B 0x00000008 [ 2] literal string: isDebug

这部分保存了类里方法的字符串信息(所以原则上方法名起短一些,是可以减少占用的 - -!)

分析步骤:

  1. 查看第一列起始地址,然后在上面的段表中查看这个地址在那个节里,可以看到在__objc_methname中。
  2. 通过第二列对比大小
  3. 通过第三列解析对应的类和对应方法名称

类列表节(__objc_classlist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x1010E8080 0x00000008 [ 2] anon
0x1010E8088 0x00000008 [ 3] anon
0x1010E8090 0x00000008 [ 4] anon
0x1010E8098 0x00000008 [ 5] anon
0x1010E80A0 0x00000008 [ 7] anon
0x1010E80A8 0x00000008 [ 9] anon
0x1010E80B0 0x00000008 [ 10] anon
0x1010E80B8 0x00000008 [ 11] anon
0x1010E80C0 0x00000008 [ 12] anon
0x1010E80C8 0x00000008 [ 13] anon
0x1010E80D0 0x00000008 [ 14] anon
0x1010E80D8 0x00000008 [ 15] anon
0x1010E80E0 0x00000008 [ 16] anon
0x1010E80E8 0x00000008 [ 17] anon
0x1010E80F0 0x00000008 [ 18] anon
0x1010E80F8 0x00000008 [ 19] anon
0x1010E8100 0x00000038 [ 20] anon
0x1010E8138 0x00000030 [ 21] anon

__objc_classlist存储了所有类的虚拟地址。即__objc_data地址。这里都是二进制数据,具体保存了什么,看下对应的数据结构

__objc_data的数据结构为:

1
2
3
4
5
6
7
8
9
10
11
typedef struct objc_class{
unsigned long long isa;
unsigned long long wuperclass;
unsigned long long cache;
unsigned long long vtable;
unsigned long long data;
unsigned long long reserved1;
unsigned long long reserved2;
unsigned long long reserved3;
}objc_class;

其中最主要的是data字段,保存了_objc_const节对应的数据地址。数据结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_classdata{
long long flags;
long long instanceStart;
long long instanceSize;
long long reserved;
unsigned long long ivarlayout;
unsigned long long name;
unsigned long long baseMethod;
unsigned long long baseProtocol;
unsigned long long ivars;
unsigned long long weakIvarLayout;
unsigned long long baseProperties;
}

这里面保存了类名,方法名,协议名,ivar指针和属性对应的地址。最后对应到相应的TEXT段里就能找到。比如类名在__objc_classname可以找到,方法名可以在__objc_methname。应用程序就是通过这个结构来寻找哪个类对应的那个方法,从而执行相关逻辑

如何找到没有用到的类和方法?

通过__objc_classrefs_objc_classname对比就可以知道哪些类没用。其中__objc_classrefs的解析需要通过otool命令才能解析

同理:
通过__objc_selrefs_objc_methname对比可以知道哪些方法没有使用到。其中__objc_selrefs需要用otool命令才能解析。

otool -v -s __DATA __objc_selrefs <path>

otool使用

这个用来做反汇编的,比如分析哪些类被使用了,需要用这个工具。

比如获取使用到的方法可以用这个命令:

otool -V -s __DATA __objc_selrefs <path> -arch arm64 | open -f

其中path是你的应用编译后生成的可执行文件。通常在项目的DerivedData目录下的Build/Products//.app文件,然后显示包内容,有个和工程同名的可执行文件。比如我的目录:

/Users/bolei/Library/Developer/Xcode/DerivedData/FFProject-gqpkbetfhlofkxcmyfwpmkfqubun/Build/Products/Release-iphoneos/FFProject.app/FFProject

打印使用到的类: _objc_classrefs

otool -V -o FFProject -arch arm64 | open -f

可以打印出来objc Section中的所有数据

otool说明

代码参考

相关代码欢迎fork:https://github.com/dishibolei/iSee.git

主要功能包含了

  1. 代码占用分析
  2. 未使用类分析
  3. 未使用方法分析

参考

  1. IOS Section Document
  2. IOS 官方Section介绍
  3. mach-o文件结构分析类名方法名
  4. load 方法全程跟踪
  5. iOS APP可执行文件的组成
坚持原创技术分享,您的支持将鼓励我继续创作!