我们的SDK项目随着各种功能的加入,SDK文件也越来越大。Objective-C的库最终会把用到的,没有用到类和方法都连接进App里,所以精简SDK大小很有必要,有助于减少最终App的size。

iOS平台上库文件格式

库文件主要分动态库和静态库两种。
动态库:

文件后缀名有.dylib和.framework。 链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

静态库:

文件后缀名有.a和.framework。.framework是一个文件包,包含二进制文件、头文件及相关的资源文件。 链接时完整地拷贝至可执行文件中。

游戏SDK文件格式

现在游戏SDK是framework形式的静态库。framework文件夹中二进制文件是占比最大的部分,其格式为“Mach-O universal binary”,是一种Fat binary文件。

  1. Fat binary文件由多个cpu平台上的archive文件合并生成。
  2. archive文件是由多个.o文件及调试用的符号文件合并生成。
  3. .o文件即代码源文件编译后生成。

大致关系是 Fat binary包含多个.a文件,.a文件包含多个.o文件。

相关命令行工具使用

查看SDK的架构信息

查看SDK文件的架构信息可以使用 file命令 和 lipo命令

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命令的使用:

lipo -info TestSDK.framework/TestSDK
Architectures in the fat file: TestSDK.framework/TestSDK are: armv7 arm64

导出.a文件

在SDK文件中包含多种cpu架构的二进制文件,我们可以使用lipo命令导出。

#导出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文件中抽取文件的命令。

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文件中代码段、数据段等详细的大小信息

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大小信息

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文件的大小增量。 思路大致如下:

  1. 使用“lipo -info”命令获取SDK中包含的cpu架构

    lipo -info SDKFilePath | sed -En -e "s/^(Non-|Architectures in the )fat file: .+( is architecture| are): (.*)$/\\3/p"
    
  2. 使用“lipo -thin”命令抽取出各个cpu架构的.a文件

    lipo -thin arm64 SDKFilePath -output SDKFilePath_arm64.a  
    
  3. 使用“ar -t -v”命令获取SDK中.o文件的大小

    ar -t -v SDKFilePath_arm64.a | awk '{printf \"%s:%s\\n\", $8, $3}'  
    
  4. 比较SDK新旧版本中同一个.o文件大小

按照上述步骤能比对两个SDK中.o文件的变化。 这里有一个脚本sdkdiff.py
使用方法

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”) 方法使用该类。

下面是查找无用类的脚本

#!/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等第三方库的功能,这样对于没有接入微信登录功能的接入方,就没必要引入微信等第三方库。

+ (void)enableWeChat:(NSString *)appid class:(Class)cls {
  [NSClassFromString(@"WXApi") registerApp:appid];
}

接入方如果需要引入微信登录功能,需要引入微信SDK,然后再添加这样的代码

[TestSDK enableWeChat:@"1234567890"  class:WXApi.class];