一、前言

百度APP iOS端包体积优化系列文章的前三篇要点介绍了包体积优化全体计划、图片优化和资源优化,图片优化是从无用图片、Asset Catalog和HEIC格局三个角度做深度优化,资源优化包括大资源优化、无用配置文件和重复资源优化,本文要点介绍代码优化,在百度APP实践中,代码优化包括无用类优化、无用模块减肥、无用办法减肥、精简重复代码、东西类减肥和AB试验固化。在代码优化过程,需求剖析Mach-O和Link Map,在前面的文章咱们现已针对Mach-O文件做过了剖析,本文先介绍Link Map文件,然后再具体介绍代码优化计划。

百度APP iOS端包体积优化实践系列文章回顾:

《百度APP iOS端包体积50M优化实践(一)总览》

《百度APP iOS端包体积50M优化实践(二) 图片优化》

《百度APP iOS端包体积50M优化实践(三) 资源优化》

二、LinkMap文件详解

2.1 简介

Link Map 是 Mach-O 格局的二进制文件的一种辅助文件,它描绘了可执行文件的全貌,包括编译后的每一个方针文件的信息以及它们在可执行文件中的代码段、数据段存储概况。经过Link Map文件,咱们能够知道可执行文件的途径、CPU架构、方针文件、符号等信息,剖析可执行文件中哪个类或库占用比较大,进行装置包减肥,此外,咱们能够清楚地了解可执行文件的内部结构和各个方针文件在其间的方位联系,这关于剖析和调试十分有帮助。

2.2 生成linkMap文件

Xcode -> Project -> Build Settings -> Write Link Map File选项值设为yes,Path to Link Map File设置为指定好的LinkMap文件存储方位。

百度 APP iOS 端包体积 50M 优化实践 (四) 代码优化

2.3LinkMap文件结构解析

2.3.1根底信息

# Path: /Users/richard/Desktop/demo/DerivedData/demo/Build/Products/Debug-iphoneos/demo.app/demo
# Arch: arm64

Path是可执行文件的途径,Arch是架构类型。

2.3.2Object文件列表

百度 APP iOS 端包体积 50M 优化实践 (四) 代码优化

Object文件列表列出了一切编译后的方针文件,包括.o文件和dylib库。每个方针文件都有一个对应的编号,上图榜首列便是,经过该编号能够对应到具体的类,在后边的Symbols部分,还会用到此编号。

2.3.2Section段表

Section段表描绘了各个段在终究编译成的可执行文件中的偏移方位及巨细,包括代码段(TEXT)和数据段(DATA)。段表中榜首列是数据在文件的偏移方位,第二列是Section占用巨细,第三列是Segment类型,第四列是Section类型,关于Segment和Section,在前面文章关于Mach-O详解做过介绍,这儿不再赘述。

百度 APP iOS 端包体积 50M 优化实践 (四) 代码优化

2.3.4Symbols

Symbols模块给出了类里面的办法在内存具体情况。其间

  • 榜首列是办法起始地址,经过这个地址咱们能够查上面的段表;

  • 第二列是巨细,经过这个能够算出办法占用的巨细;

  • 第三列是归属的类(.o),值是具体编号,经过反查方针文件列表能够知道对应的类;

  • 第四列是办法名称。

经过Symbols模块咱们能够剖析出来每个类对应办法的巨细。

百度 APP iOS 端包体积 50M 优化实践 (四) 代码优化

三、代码优化

3.1 无用类减肥

3.1.1静态检测获取无用类

计划介绍

所谓的静态检测,便是剖析linkmap文件和Mach-o文件,Mach-o文件中__DATA __objc_classlist段记录了一切类的地址,__DATA __objc_classrefs段记录了引证类的地址,取差集能够得到未运用的类的地址,然后进行符号化,就能够得到未被引证的类信息。

  • 榜首、获取一切类地址,指令:otool -v -s __DATA __objc_classlist。
otool -v -s __DATA __objc_classlist /Users/ycx/Desktop/demo.app/demoContentsof(__DATA,__objc_classlist)section00000001000082380000998000000001000099d000000001000000010000824800009a480000000100009a9800000001000000010000825800009ac00000000100009b3800000001
  • 第二、获取引证类的地址,指令:otool -v -s __DATA __objc_classrefs。
otool -v -s __DATA __objc_classrefs /Users/yangchengxu/Desktop/demo.app/demoContentsof(__DATA,__objc_classrefs)section00000001000099000000000000000000000000000000000000000001000099100000000000000000000099d000000001000000010000992000000000000000000000000000000000
  • 第三、取差集,一切类的地址减去引证类的地址,拿到的便是未运用类的地址信息。

  • 第四、符号化,遍历Linkmap和Mach-O文件可获取地址信息对应的具体类名,建立类和地址的映射联系,经过地址反解析出类名。

优缺陷

长处:检测办法简略易行。

缺陷:

  • 关于存在引证联系但底子不会被调用的类,是无法被判别为无用类的。跟着版别迭代,新老职工工作交代,许多功用的入口现已不存在了,相关的类也底子不会被调用,可是引证联系仍然保存。经过静态检测的办法,无法检测出这种情况。

  • 静态检测无法适用于经过反射调用类及办法的场景。因为静态检测无法感知运转时的环境,无法预测哪些类或办法会被反射调用。因而,在这种情况下,静态检测将无法精确地检测出无用类或无用的办法。

3.1.2动态检测获取无用类

计划介绍

咱们知道OC类结构有个isa指针,指向该类的原类meta-class,经过阅览objc源代码,咱们发现在meta-class的class_rw_t结构体中的一个flag标志位,flags 的1<<29位标识当前类在运转时中是否被初始化过,参阅源码途径:

Values
for
// These are not emitted by the compiler and are never used in class_ro_t. 
// Their presence should be considered in future ABI versions.
// class_t->data is class_rw_t, not class_ro_t
#define RW_REALIZED           (1<<31)
// class is unresolved future class
#define RW_FUTURE             (1<<30)
// class is initialized
#define RW_INITIALIZED        (1<<29)
// class is initializing
#define RW_INITIALIZING       (1<<28)
// class_rw_t->ro is heap copy of class_ro_t
#define RW_COPIED_RO          (1<<27)
// class allocated but not yet registered
#define RW_CONSTRUCTING       (1<<26)
// class allocated and registered
#define RW_CONSTRUCTED        (1<<25)
// available for use; was RW_FINALIZE_ON_MAIN_THREAD
// #define RW_24 (1<<24)
// class +load has been called
#define RW_LOADED             (1<<23)

由此,检测类是否被初始化的办法如下所示:

#define W_INITIALIZED (1<<29)bool isinitialized() {   return getMeta() -›data()-›flags & W_INITIALIZED;}

优缺陷

长处:

  • 关于事务线代码没有侵入性;

  • 没有功能损耗;

  • 能够针对线上实际运转环境做检测;

缺陷:

仅支持OC,无法掩盖Swift和C、C++。

3.1.3手百采用的技能计划

经过动态和静态两种办法结合进步精确度。具体计划咱们后边会有文章要点介绍。

3.2无用模块减肥

没有任何依靠联系即不会被其他库引证的无用模块,比较简略辨认,还有一种类型的无用模块是需求关注的,它们虽然还有代码引证联系,但从逻辑上现已不再被运用。例如一些过期的活动代码(如北京冬奥会等)、事务改版后的老代码以及被废弃运用的开源库等。跟着版别的迭代,这类无用模块的数量会逐渐添加。

在百度APP包体积优化实践中,咱们采用无用类占比这个目标来快速辨认不再被运用的模块。具体来说,这个目标的计算办法是统计模块中一切无用类的数量,然后将其除以模块中总类的数量,再乘以100%。如果这个目标的值比较高,就阐明该模块中有较多的无用代码,需求进行优化和整理。

经过前面的无用类减肥环节,咱们现已知道了百度APP中一切的无用类,从而能够从LinkMap文件中获取每个模块包括的具体类和数量,并计算出每个组件中无用类占比。如果一个库的占比为100%,就阐明这个库现已彻底不再被运用,能够直接下线。关于占比超越90%的库,能够进行适当关停并转,将无用类删除,仅保存有用的几个类并将其迁移到其他库中,从而下降组件数量。

经过遍历LinkMap文件中的Object files字段,能够获取每个组件包括的具体类。你能够参阅以下的脚本代码来完成这个功用:

def find_class(base_link_map_file):    link_map_file = open(base_link_map_file, 'rb')    reach_files = 0    reach_sections = 0    reach_symbols = 0    files_map = {}    while 1:        line = link_map_file.readline()        line = line.decode('utf-8', errors='ignore')        if not line:            break        if line.startswith("#"):            if line.startswith("# Object files:"):                reach_files = 1            if line.startswith("# Sections"):                reach_sections = 1            if line.startswith("# Symbols"):                reach_symbols = 1        else:            if reach_files == 1 and reach_sections == 0 and reach_symbols == 0:                index = line.find("]")                if index != -1:                    tmpfile = line[index + 2:-1]                    file = tmpfile.split("/")[-1]                    frameworkIndex = file.find("(")                    if  frameworkIndex!= -1:                        frameworkName = file[0: frameworkIndex]                        className = file[frameworkIndex + 1:len(file)-1]                        if files_map:                            if frameworkName in files_map:                               files_map[frameworkName] = files_map[frameworkName] + " , " + className                            else:                               files_map[frameworkName] = className                        else:                            files_map[frameworkName] = className    link_map_file.close()    return files_map

3.3无用办法减肥

关于无用办法检查,业内常用的办法是结合Mach-O和LinkMap文件,剖析二者结构来获取无用办法。首要Mach-O中的__objc_selrefs代表一切引证到的办法调集,Mach-O中__objc_classlist代表Objective-C 类列表,然后拆解其结构获取其间BaseMethods、InstanceMethods以及ClassMethods中的数据,作为一切办法的调集,最后和榜首步获取的引证办法做差值从而得到无用办法,为了获取每个无用办法的包体积收益,还需求结合linkmap文件做剖析。这是目前为止看到可行计划,可是该计划存在的问题是精确率不高,实测不超越40%,这也是某些大厂抛弃无用办法减肥的原因。

百度在这方面做了一些技能创新,从编译角度处理这个业界难题,简略来说,首要编写LLVM插件获取静态编译阶段一切办法,然后获取办法调用联系,做diff可开端获取无用办法,然后扫除如下特殊case:分类办法辨认异常、承继链子类调用父类办法、完成系统类协议办法、协议承继链办法辨认问题、硬编码调用问题、反射调用问题、通知调用办法辨认为无用办法问题,具体计划咱们后边会有文章要点介绍。

3.4精简重复代码

在软件开发过程中,尤其是不同部门多人开发项目,存在仿制粘贴的代码,还有一些特殊情况,如项目重构时,为了不影响已有逻辑,程序员会仿制一份老代码然后在此根底上重新开发,此刻重复代码更有可能很多出现,跟着版别迭代上述情况会愈演愈烈导致重复代码越来越多,因而,不论是削减包体积,还是下降历史包袱,精简重复代码十分有必要。

在百度APP包体积优化计划中,咱们采用了开源东西PMD来扫描重复代码,然后再结合实际情况来从逻辑上重构这些代码达到精简的意图,PMD是一个开源东西,官网地址:pmd.github.io/,简略易用,经过静态分… 顺便的CPD东西能够直接检测重复代码,支持Java, C, C++, C#, Groovy, PHP, Ruby, Fortran, JavaScript, PLSQL, Objective C, Matlab, Python, Go, Swift言语,而且检测规则能够自在定制,运用办法参阅:pmd.sourceforge.io/pmd-5.5.1/u…

用brew 指令装置

brew install pmd

用如下指令做重复代码检测

//其间,--files 用于指定文件目录,--minimum-tokens 用于设置最小重复代码阈值,--format 用于指定输出文件格局,支持 xml/csv/txt 等格局,这儿主张运用 xml,方便查看 //生成的 XML 文件内容如下,根据 file 标签信息就能定位到重复代码方位。pmd cpd --files 扫描文件目录 --minimum-tokens 70 --language objectivec --encoding UTF-8 --format xml > repeat.xml
检测成果如下所示,其间duplication标签中的lines表明重复内容的行数,file标签表明从那一行开端重复及具体重复文件途径,codefragment标签表明重复的代码。
<pmd-cpd>   <duplication lines="16" tokens="162">      <file begintoken="16933" column="33" endcolumn="4" endline="28" endtoken="17094" line="13" path="path1">      <file begintoken="23979" column="47" endcolumn="4" endline="26" endtoken="24140" line="11" path="path2" />      <codefragment>       ***************************       </codefragment>   </duplication></pmd-cpd>

3.5东西办法减肥

日常开发工程中咱们都要运用各种东西办法,常用的完成办法有如下两种

  • 完成系统类的Category,如NSDate、UIImage、NSArray、NSDictionary的分类办法完成;

  • 独立封装;

App在初始开发阶段都有commonTools模块,用来寄存各种东西办法,可是跟着版别迭代和人员变化,事务也越来越杂乱,新来的同学不知道底层模块现已完成了类似办法,为了开发方便会在自己的模块再集成一套,这样导致的成果是东西办法重复建造,此模块减肥主要意图便是发掘重复东西办法并优化,百度APP实践过程中主要从以下两个角度入手。

  • 遍历LinkMap文件,发掘出重复的Category,参阅以下的脚本代码来完成此功用:
def get_files_map(base_link_map_file):    link_map_file = open(base_link_map_file, 'rb')    reach_files = 0    reach_sections = 0    reach_symbols = 0    files_map = {}    while 1:        line = link_map_file.readline()        line = line.decode('utf-8', errors='ignore')        if not line:            break        if line.startswith("#"):            if line.startswith("# Object files:"):                reach_files = 1            if line.startswith("# Sections"):                reach_sections = 1            if line.startswith("# Symbols"):                reach_symbols = 1        else:            if reach_files == 1 and reach_sections == 0 and reach_symbols == 0:                # files                index = line.find("]")                if index != -1:                    symbol = {"file": line[index + 2:-1]}                    key = int(line[1: index])                    files_map[key] = symbol                pass    link_map_file.close()    return files_map
  • 关于非Category的东西办法,进行排查和合并,终究下沉到一致东西库里面。

3.6AB试验固话

在APP开发过程中,为了愈加有效地验证新开发功用的实际效果,咱们会进行AB试验,一般会将试验组和对照组分隔,并在试验组中进行某种操作,而在对照组中不进行该操作,咱们会调查这个操刁难试验变量的影响,以确定该操作是否对试验成果发生显著影响。

像百度APP这种日活过亿的运用,每个版别会有10个左右AB试验,一年有240个AB试验,跟着长时间的版别迭代,会积累很多AB试验代码,但实际上只有一个分支的代码是线上生效的,另一个分支代码是不会被执行的,所以推进AB试验固化,去除无效分支的代码能够完成削减包体积的意图。

百度APP推进AB试验固化分为三个过程,榜首 、从AB试验渠道获取现已固化的开关;第二、开发东西判别此试验对应的开关是否在代码中存在;第三、分发给负责的开发同学固化AB试验,删除不用的代码。

其间第二步的完成十分关键,便是判别一个开关是否仍有对应的代码逻辑,百度APP采用的计划是获取一切可能运用开关的字符串调集,然后判别榜首步拿到的开关是否在调集中,若在阐明该开关的对应的试验需求做固化操作。

在Objective-C的.h和.m文件中,咱们经常用如下代码来界说一个AB开关,然后再后续代码中引证。

#define kFaceverifyResourceOptimizeABTestKey                  @"face_verify_resource_optimize_enable"

针对Objective-C的.h和.m的文件内容,用正则过滤,匹配表达式为 @”(.*?)”,即可获取一切可能加载开关的字符串调集。

同样道理,在Swift文件咱们一般经过如下代码来界说一个AB开关,然后再后续代码中引证,加载办法彻底一样,针对Swift这种文件,正则表达式应为”(.?)”。

   static let verifyResourceOptimizeABTestKey: String = "face_verify_resource_optimize_enable"

四、总结

代码优化同样也是包体积优化的重头戏,但跟图片和资源优化相比较,代码修正影响规模大,再加上OC言语动态调用办法多种多样,这导致代码的删除操作更简略引起质量问题,所以优化收益落地难度比较大。百度APP在优化实践过程中发掘出20M的收益,经过两个季度仅落地8M左右,剩下部分还需求继续推进。

本文首要对LinkMap文件格局做了具体介绍,然后对百度APP代码优化计划(无用类优化、无用模块减肥、无用办法减肥、精简重复代码、东西类减肥和AB试验固化)做了系统阐释,后续咱们会针对其他优化具体介绍其原理与完成,敬请期待。

——END——

参阅资料:

[1]、PMD介绍:pmd.github.io/

[2]、PMD CPD运用办法:pmd.sourceforge.io/pmd-5.5.1/u…

[3]、XNU源码:github.com/apple/darwi…

[4]、objc源码:github.com/apple-oss-d…

引荐阅览:

百度iOS端长连接组件建造及运用实践

百度App发动功能优化实践篇

扫光动效在移动端运用实践

Android SDK安全加固问题与剖析

查找语义模型的大规模量化实践

怎么规划一个高效的分布式日志服务渠道