我们的SDK项目随着各种功能的加入,SDK文件也越来越大。Objective-C的库最终会把用到的,没有用到类和方法都连接进App里,所以精简SDK大小很有必要,有助于减少最终App的size。
iOS平台上库文件格式
库文件主要分动态库和静态库两种。
动态库:
文件后缀名有.dylib和.framework。
链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
静态库:
文件后缀名有.a和.framework。.framework是一个文件包,包含二进制文件、头文件及相关的资源文件。
链接时完整地拷贝至可执行文件中。
游戏SDK文件格式
现在游戏SDK是framework形式的静态库。framework文件夹中二进制文件是占比最大的部分,其格式为“Mach-O universal binary”,是一种Fat binary文件。
- Fat binary文件由多个cpu平台上的archive文件合并生成。
- archive文件是由多个.o文件及调试用的符号文件合并生成。
- .o文件即代码源文件编译后生成。
大致关系是 Fat binary包含多个.a文件,.a文件包含多个.o文件。
相关命令行工具使用
查看SDK的架构信息
查看SDK文件的架构信息可以使用 file命令 和 lipo命令
1
2
3
4
|
file TestSDK.framework/TestSDK
TestSDK.framework/TestSDK: Mach-O universal binary with 2 architectures: [arm_v7:current ar archive] [arm64]
TestSDK.framework/TestSDK (for architecture armv7): current ar archive
TestSDK.framework/TestSDK (for architecture arm64): current ar archive
|
可以看到TestSDK.framework包含armv7和arm64两种cpu架构的archive文件。下面是lipo命令的使用:
1
2
|
lipo -info TestSDK.framework/TestSDK
Architectures in the fat file: TestSDK.framework/TestSDK are: armv7 arm64
|
导出.a文件
在SDK文件中包含多种cpu架构的二进制文件,我们可以使用lipo命令导出。
1
2
3
4
|
#导出armv7架构的archive文件
lipo -thin armv7 -output TestSDK_armv7.a TestSDK.framework/TestSDK
#导出arm64架构的archive文件
lipo -thin arm64 -output TestSDK_arm64.a TestSDK.framework/TestSDK
|
查看.a文件中.o文件大小
ar命令是建立或修改archive文件,或是从archive文件中抽取文件的命令。
1
2
3
4
5
|
ar -t -v TestSDK_armv7.a
rw-r--r-- 501/20 416800 Jul 17 15:09 2018 __.SYMDEF
rw-r--r-- 501/20 81488 Jul 17 15:09 2018 ImageViewController.o
rw-r--r-- 501/20 58616 Jul 17 15:09 2018 PasswordViewController.o
......
|
查看.o文件中代码段、数据段等详细的大小信息
1
2
3
4
5
6
7
8
|
size TestSDK.framework/TestSDK
__TEXT __DATA __OBJC others dec hex
15479 3040 0 28683 47202 b862 TestSDK.framework/TestSDK(ImageViewController.o) (for architecture armv7)
12043 2132 0 18722 32897 8081 TestSDK.framework/TestSDK(PasswordViewController.o) (for architecture armv7)
......
16696 5772 0 32674 55142 d766 TestSDK.framework/TestSDK(ImageViewController.o) (for architecture arm64)
13020 4092 0 20627 37739 936b TestSDK.framework/TestSDK(PasswordViewController.o) (for architecture arm64)
......
|
使用size -m 可以显示更多的Mach-O segments 和 sections大小信息
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
|
size -m TestSDK.framework/TestSDK
Segment : 47207
Section (__TEXT, __text): 10072
Section (__TEXT, __gcc_except_tab): 8
Section (__DATA, __objc_data): 40
Section (__DATA, __objc_superrefs): 4
Section (__TEXT, __objc_methname): 2395
Section (__DATA, __objc_selrefs): 424
Section (__TEXT, __cstring): 2274
Section (__DATA, __cfstring): 320
Section (__DATA, __objc_classrefs): 80
Section (__DATA, __objc_ivar): 44
Section (__TEXT, __ustring): 102
Section (__DATA, __const): 180
Section (__TEXT, __objc_classname): 122
Section (__TEXT, __objc_methtype): 506
Section (__DATA, __objc_const): 1648
Section (__DATA, __data): 260
Section (__DATA, __objc_protolist): 20
Section (__DATA, __objc_classlist): 4
Section (__LLVM, __bitcode): 1
Section (__LLVM, __cmdline): 1
Section (__DATA, __objc_imageinfo): 8
Section (__DWARF, __debug_str): 8277
Section (__DWARF, __debug_loc): 2969
Section (__DWARF, __debug_abbrev): 807
Section (__DWARF, __debug_info): 8129
Section (__DWARF, __debug_ranges): 0
Section (__DWARF, __debug_macinfo): 1
Section (__DWARF, __apple_names): 2852
Section (__DWARF, __apple_objc): 252
Section (__DWARF, __apple_namespac): 36
Section (__DWARF, __apple_types): 1351
Section (__DATA, __nl_symbol_ptr): 8
Section (__DWARF, __debug_line): 4007
total 47202
total 47207
|
可以看出size命令是可以直接显示Fat binary文件中各个cpu架构文件信息的,但ar命令不能操作Fat binary文件,只能是某个cpu架构下的archive文件。
如何比较SDK大小变化
结合上面介绍的几个命令行工具,就可以简单比较出SDK文件的大小增量。
思路大致如下:
使用“lipo -info”命令获取SDK中包含的cpu架构
1
|
lipo -info SDKFilePath | sed -En -e "s/^(Non-|Architectures in the )fat file: .+( is architecture| are): (.*)$/\\3/p"
|
使用“lipo -thin”命令抽取出各个cpu架构的.a文件
1
|
lipo -thin arm64 SDKFilePath -output SDKFilePath_arm64.a
|
使用“ar -t -v”命令获取SDK中.o文件的大小
1
|
ar -t -v SDKFilePath_arm64.a | awk '{printf \"%s:%s\\n\", $8, $3}'
|
比较SDK新旧版本中同一个.o文件大小
按照上述步骤能比对两个SDK中.o文件的变化。 这里有一个脚本sdkdiff.py
使用方法
1
2
3
4
5
6
7
8
9
|
sdkdiff.py TestSDK_v1 TestSDK_v2
+1152 361088 Title.o
+824 180032 Notice.o
.....
-4232 260696 Login.o
-5640 243032 Proxy.o
-5856 335680 User.o
----------------------------------
-45288 80801624 Total
|
如何精简SDK包大小
通过上面命令可以看出,SDK二进制文件包含了不同cpu架构下.m .c .cpp等源码文件编译后生成的.o文件及对应的符号文件。
精简SDK二进制文件大小也主要是删除符号文件,删除没有用到的.o文件,删除无用代码。
删除符号文件
设置Xcode Target的Building Settings >> Deployment >> Deployment Postprocessing 为YES
Building Settings >> Deployment >> Strip Style 有三种选项All Symbols、Non-Global Symbols和Debugging Symbols。其中默认是Debugging Symbols。
去掉debug symbols后,不能使用xcode断点调试SDK类的代码,这个对于已发布的release模式的SDK来说,可以去掉。
删除无用类
使用otool -v -s __DATA objc_classlist 可执行文件名, 逆向DATA.__objc_classlist段,提取可执行文件里所有的OC类名,
使用otool -v -s __DATA objc_classrefs 可执行文件名, 逆向DATA.__objc_classrefs段,提取可执行文件里所有的引用到的OC类名,
两者的差集就是代码中没有直接使用到的OC类,注意可以通过反射的方式来使用OC类,还需要搜索代码中有没有通过类的名字来使用该OC类。
这里的方法是针对可执行文件,所以在编译好SDK后,还应为SDK编译一个可执行的Demo,然后使用上面的方法来查找未使用到的类。
注意:OC是动态语言,是可以通过类名来调用类的方法的,所以查找到没有引用到的类后,还需要查看代码中有没有通过 NSClassFromString(@“OCClassName”) 方法使用该类。
下面是查找无用类的脚本
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
|
#!/bin/bash
# 输出未在项目中使用的类名
# 使用说明
# unused_class <hex file>
usage() {
cat <<__EOF
Summary
output names of unused objc class.
Usage
$(basename $0) <hex file>
__EOF
}
error_usage(){
local error=${1:-Undefined error}
echo "$0:$LINE $error"
usage
exit 1
}
if [[ $# -lt 1 ]] ;then error_usage "Error! No project directory was specified."; fi
HEXFILE=$1
CLASSLIST=`otool -s __DATA __objc_classlist $HEXFILE | grep "^[0-9a-f]" | awk '{print $3$2"\n"$5$4}'|sort`
CLASSREFS=`otool -s __DATA __objc_classrefs $HEXFILE | grep "^[0-9a-f]" | awk '{print $3$2"\n"$5$4}'|sort`
nm $HEXFILE >./temp_all_symbols.txt
for classname in $CLASSLIST
do
if [[ $CLASSREFS =~ $classname ]]; then
:
else
grep $classname ./temp_all_symbols.txt
fi
done
rm ./temp_all_symbols.txt
|
删除无用代码
使用otool -v -s __DATA objc_selrefs 可执行文件名,逆向DATA.objc_selrefs段,提取可执行文件里引用到的方法名,
使用LinkMap文件的TEXT.__text 提取当前可执行文件里所有objc类方法和实例方法,
两者的差集就是代码中没有直接使用到的方法,注意可以通过反射的方式来类方法和实例方法,还需要搜索代码中有没有通过方法名字来使用该方法。
精简SDK引用的第三方库
我们的SDK引用了微信,QQ等第三方库来提供第三方登录的功能,为了SDK接入方的方便,我们直接将微信,QQ等公用的第三方库打包进我们的SDK了。虽然这样减少了我们SDK接入方的工作,但是也导致我们SDK文件大小暴涨。另外也带来了一些其他的问题,比如接入方需要更新微信,QQ SDK版本时就麻烦了。
我们SDK去掉微信,QQ SDK,让接入方自己引入,这样可以减小我们SDK的大小。但是最后所有的接入方生成的App还是会包含微信,QQ的SDK,然而,并不是所有的接入方都需要接入微信,QQ登录。这样的话,对于这部分的接入方,App就包含并不需要的微信和QQ的SDK代码。
其实我们可以在我们的SDK中使用类反射的的方式来调用微信,QQ等第三方库的功能,这样对于没有接入微信登录功能的接入方,就没必要引入微信等第三方库。
1
2
3
|
+ (void)enableWeChat:(NSString *)appid class:(Class)cls {
[NSClassFromString(@"WXApi") registerApp:appid];
}
|
接入方如果需要引入微信登录功能,需要引入微信SDK,然后再添加这样的代码
1
|
[TestSDK enableWeChat:@"1234567890" class:WXApi.class];
|