一、前言

百度APP包体积通过一期优化,如无用资源清理,无用类下线,Xcode编译相干优化,体积曾经有了显著的缩小。然而优化后APP包体积在iPhone11上仍有350M的空间占用。与此同时百度APP作为百度的旗舰APP,业务迭代十分多且迅速,体积优化和防劣化依然是以后阶段的一个外围工作。因而百度APP开启了粒度更小,修复危险更高的无用办法清理相干工作。冀望通过无用办法清理,无效升高百度APP的包体积,同时删除我的项目中的无用办法,冗余代码,进步代码的整洁度。

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

  • 《百度APP iOS端包体积50M优化实际(一)总览》
  • 《百度APP iOS端包体积50M优化实际(二) 图片优化》
  • 《百度APP iOS端包体积50M优化实际(三) 资源优化》
  • 《百度APP iOS端包体积50M优化实际(四) 代码优化》
  • 《百度APP iOS端包体积50M优化实际(五) HEIC图片和无用类优化实际》

二、计划调研

针对无用办法清理,调研了各家厂商目前已颁布的计划,支流计划基于Mach-O + LinkMap文件的剖析,然而次要存在以下问题:

1.准确度低

2.针对零碎办法须要手动过滤

3.针对load、initilize、attribute 相干调用无奈辨认

4.针对string反射调用无奈辨认,Target-Action 注册,Observer注册办法等无奈辨认

5.简单语法场景下无奈辨认,如继承链中的办法调用,子类实现父类办法等场景

6.零碎告诉等场景

因为目前已颁布计划存在如上有余,同时因为下线代码敏感度十分高,相干业务都很谨慎。因而推动相干无用办法清理,辨认准确度将十分重要,间接关系到相干业务下线无用代码的积极性,因而弃用了上述计划。

三、计划抉择

针对第二局部计划不足之处进行剖析,能够看到其准确度低的外围问题是,针对产物进行剖析,拿不到所有须要的信息,或者说还没有发现无效的伎俩去获取所冀望取得的信息。而想要解决下面提到的问题,最佳路径就是获取到尽可能多的代码信息。既然从产物回溯不到所须要的,那么就能够思考从源头也就是源码层面找到咱们所须要的详细信息。

源码必定蕴含了所有的信息,然而针对源码如何剖析呢,次要有以下三种:

  • 通过脚本间接剖析源码

须要匹配源码的所有语法规定,才可能针对源码进行无效的剖析,相当于写一个源码解析器,所以这个计划放弃

  • 通过脚本间接剖析AST(形象语法树)

编译过程中产生的形象语法树(AST)蕴含了须要的所有信息,并且clang也提供了命令行,应用该命令行可能间接获取到AST数据。然而clang 命令获取AST数据是以单个类为维度的,类与类之间的关系很难获取到,如继承关系,分类和主类的关系是无奈获取的,所以这个计划同样放弃

  • 通过libtooling 和 Swift Compiler自建编译套件剖析AST (Swift相干会在下一篇文章中介绍)

既然通过clang命令生成的AST产物剖析依然不能满足需要,那么间接染指编译过程,从编译外部生成AST过程中获取须要的信息,最终这个计划被采纳。通过libtooling 和 Swift Compiler自建编译套件针对AST进行剖析,获取所须要的所有信息。

四、方案设计

如上所述百度APP最终采纳了libtooling 和 Swift Compiler 动态剖析计划,那么上面就从原理和实现层面别离进行论述。

4.1 编译流程简介

4.1.1 Xcode编译总体构造

本节先简略聊一下编译器的构造,编译流程,和动态剖析是什么?

△图 4-1

如图4-1 所示 LLVM 采纳如上三段构造(Three Phase Design),别离是编译前端(Frontend),编译优化模块,编译器后端(Backend)。那么这三段构造如何对应到Xcode呢,如图4-2所示:

△图 4-2

日常应用Xcode编译时,Xcode调用了两个编译器前端,别离为Clang 和 Swift,通过两个编译器前端构建出通用的编译产物,而后对立通过LLVM后端编译器进行指标文件生成。

通过Xcode的编译log,能够看到针对Objective-C,C, C++ 应用了clang进行编译,针对上述三种不同语言别离用不同编译参数管制:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang

针对swift 文件则采纳了swift编译器进行了编译:

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend

针对这两个可执行文件大家能够自行解包Xcode,进行命令行调用,也能够通过其 --help指令查看其反对哪些编译参数或者性能。Xcode 外部编译器实际上是苹果对LLVM 和 Swift 开源版本的定制化版本, 和开源版本有肯定的差异性。

4.1.2 Clang 和 Swift 编译流程

如下图所示Clang 和 Swift 前端编译流程,能够看到Swift 编译解决流程多了SIL局部,理论外面还有一个SIL Guaranteed Transformations,当然SIL局部不是重点。从图4-3中能够看到Clang 和 Swift compiler 都会生成AST 且发现AST中蕴含了咱们须要的绝大部分信息,并且Clang 和 Swift Compiler 也裸露了相干获取AST信息的接口,那么剩下的工作只有四点:

1.搭建编译套件工程,确保它失常run起来

2.获取AST,并且依据Objective-C 或者 C,C++的语法个性获取所须要的数据

3.针对获取的数据进行业务剖析解决

4.开源版本LLVM和Xcode理论应用版本具备肯定差异性,因而局部编译相干内容须要进行相干适配

△图 4-3

4.2 总体方案设计

针对一门程序语言的应用而言,如图4-4所示,蕴含两个层面,一个层面是申明,另一个层面是调用。申明类,协定,属性,办法,函数等等,同时申明的内容是为了被应用,所以同样申明的内容皆可调用,只不过是外部调用还是公开调用问题。从技术角度而言,申明的所有内容 减去 被调用的申明内容,剩下的就是未被调用的内容,也就是咱们须要的 无用办法。当然技术层面的判断最终还是要进行业务断定,因为有的属于根底能力对外提供,至于是否要删除则须要进一步探讨。本文次要探讨技术层面问题。

△图 4-4

从clang源码中能够晓得申明和调用别离对应LLVM源码中的基类Decl 和 Expr,整体技术计划如下图 4-5所示,针对无用办法分为解决分为四层:

1.Basic 层:组装编译工具所需的编译参数 + 进行语法规定匹配

2.Transformer层:针对语法规定匹配数据进行转换,转换通用型数据格式

3.通用数据层:通过Transformer层产出的数据进行分类存储,所存储数据蕴含了代码的所有数据,如针对属性,办法,协定等数据均进行了分类存储

4.业务应用层:针对通用数据层产出的存储数据进行业务剖析即可

△图 4-5

4.3 具体计划实现

4.3.1 Objective-C 编译工具搭建

编译工具的出现模式是一个相似Xcode自带clang的可执行文件,如图4-6 红框所示内容。

/Users/UserName/Documents/XcodeEdition/Xcode14.2/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang

△图 4-6

简略来说通过源码构建的编译工具具备Xcode clang 的局部性能,利用其编译过程中产生的AST对象进行形象语法树剖析,获取到所须要的编程语言的所有语法信息。

4.3.1.1 LLVM 源码构建

编译工具的搭建须要依赖LLVM提供的动态库或动静库,这些库通过本人构建LLVM源码来取得。能够从github获取LLVM源码门路,进入LLVM github界面后有可能会困惑须要构建哪个分支或者tag的代码呢,哪个版本和Xcode应用的clang是对应的?目前Xcode的版本是 14.2 或者 14.3 ,应用命令 clang --version 能够看到Xcode用到的是clang 14,因而构建了release/14.x(没有找到对应关系,推理得出),构建胜利后执行构建的clang --version 会发现开源版本clang 和 Xcode的小版本号是不一样的,这是因为Xcode 用的clang 苹果会基于开源代码进行定制,这从Xcode中clang 的依赖库或头文件数量。另外从编译log也能够看到,Xcode clang反对的局部参数,开源clang是不反对的。只管苹果有一些定制,然而总体影响无限。因而也不用过于在意小版本号是否统一。(初步验证了一下构建最新的release/16.x clang16 也能够)。

△图 4-7

具体构建命令次要分两种,一个是Ninja 构建形式,一个是Xcode形式,须要Xcode调试源码能够抉择Xcode模式,然而最终集成到编译工具中的动态库,肯定要构建成Release模式,这样工具体积会降到最低,一些正告类异样也会被屏蔽掉。能够参照LLVM 开源库中的start guide 构建过程进行构建,其中波及的组装命令能够自行拼接也能够用上面的命令:

构建过程git clone https://github.com/llvm/llvm-project.gitcd llvm-projectmkdir build (这个build文件夹能够自行命名,不固定。针对不同指标能够创立不同文件夹进行不同构建,如 mkdir ninjaBuild 或 mkdir xcodeBuild)cd build (or cd xcodeBuild)cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release ../llvmcmake --build .

编译Xcode版本,Ninja替换为Xcode即可。

4.3.1.2 工程搭建

LLVM提供了两种工具 libclang 和 libtooling,百度APP采纳的是 libtooling,其异同点如下所示:

  • libclang:(网络材料,未实测)

    1.提供稳固的 C 接口,具备遍历语法树,获取 Token,代码补全等能力。

    2.接口稳固,clang 版本更新对齐影响不大

    3.libclang 不能获取到 AST 的所有信息

  • libtooling:(实测)

    1.提供 C++ 接口,产出的工具不依赖于编译器,可作为独立命令应用

    2.接口不稳固,AST 有降级须要更新相干依赖库

    3.libtooling 能够取得 AST 的所有信息

最终抉择 libtooling 模式,外围起因就是 libtooling 能够获取 AST 的所有信息,同时可能不依赖于Xcode 独立运行。工程的搭建自身并不简单,还是属于API 应用层面,能够间接参照 libtooling的官网文档。

△图 4-8

总体代码流程如图 4-8所示,次要外围点是五个局部:

  • 参数解析
  • 创立 ClangTool 参照LLVM源码 ClangTooling -> Tooling.h Line309
  • 创立 ASTFrontendAction,用于获取 AST 数据,创立 ASTConsumer 和 进行 ASTMatcher 绑定
  • 针对 ASTMatcher 匹配项进行各语法规定匹配
  • 依据匹配数据进行数据过滤及业务解决

4.3.1.3 数据存储结构设计

数据存储构造采纳 json 格局,以下为根底数据格式示例,能够依据理论需要拓展:

"objc(协定or类)@类名(类办法or实例办法)@办法名称":{"identifier":"objc(协定or类)@类名(类办法or实例办法)@办法名称","isInstance":true,"kind":16,"location":{"col":36,"filename":"文件名称","line":147    },"name":"办法名称","paramters":"参数","returnType":"返回值类型","sourceCode":"源码"}
{"declaration":{"identifier":"objc(协定or类)@类名(类办法or实例办法)@办法名称","isInstance":true,"kind":16,"location":{            "col":列数,"filename":"申明所在类名",            "line":行数        },"name":"办法名称","paramters":"参数名称","returnType":"返回值类型","sourceCode":"源代码"    },"kind":1,"location":{"col":5,"filename":"以后所在文件名","line":15    }}

五、遇到的问题及解决方案

1. 属性调用辨认问题

针对 Objective-C 的属性,在编译后对应两个办法 get 和 set 一个是 ivar,调用方有可能只调用 get 或者 set 或者 ivar,所以当只产生一种调用时,就算这个属性被调用,以后属性不属于无用办法。须要在后果中把另外两个办法剥离。

2. 提取办法内容时同样须要对头文件进行提取

办法的实现不肯定只在.m 文件中,如C++的头文件是能够进行办法实现的,Objective-C 的.h 文件 通过 inline 实现一些办法,在语法上也是可行的。所以进行办法提取时候关注实现文件,同时也要关注头文件。

3. 针对继承问题

子类实现父类办法等场景,在识别方法时,全副回溯其父类,以其父类名称作为 上文数据结构中 identifier 中类名局部,这样所有的办法都能够和其申明类匹配。

4. 过滤零碎办法调用

LLVM提供了接口判断以后办法是否属于零碎类。

5. 过滤业务类实现零碎办法问题

针对以后类中所有的办法均在以后类 和 回溯其继承链条中的父类, 别离判断其是否属于零碎办法,如果属于零碎办法则间接过滤掉。

6. 针对协定办法的实现,目前还没有无效伎俩辨认,以后计划是间接过滤掉协定办法,所有协定办法均视为曾经调用

在提取办法时,判断以后interface 遵循了哪些协定,遍历协定中的办法,判断其是否为协定办法,是则标记为已调用。

7. 子类实现父类协定问题

回溯以后类的继承链条,在继承链条中判断遍历其所遵循的协定,判断其是否为协定办法。

8. 失常业务实现协定,应该明确标注以后类遵循了协定 如 interface <conformprotocol>,然而理论场景中有很多代码在实现协定时并没有标注conformprotocol 这样就对协定办法的判断产生影响,如 6.7计划均生效了

如果组件中大量这种问题,当推动相干方修复此问题,须要明确遵循协定。然而如果有的组件这种场景较多,短期不会修复所有,那么就须要进行临时性适配。针对这类组件收集其以后组件所申明的协定的所有协定办法,用收集的协定办法和以后组件提取的所有申明做差集,存在误伤的可能,但后果是相信的(组件只是一个维度,也能够针对其关联组件进行相干解决,因为有时他实现的组件不肯定在以后组件内,这就须要以后组件的依赖关系了)。

无用办法case很多,列举局部供大家参考。

六、总结

这项技术实际上在百度APP早曾经利用,因为笔者之前负责百度APP的接口变更审核,组件完整性校验,隐衷合规调用链分析等均是依赖于此项技术,无用办法辨认只是笔者在做体积优化时想到的其性能的一个延展。当然如上形容的技术问题,细节解决无用办法显然更细腻,case更多。后续文章会针对Swift无用办法剖析,接口变更审核,组件完整性校验,隐衷合规调用链分析等一一作出介绍。

——END——

参考资料:

[1]libclang:https://clang.llvm.org/doxygen/group\_\_CINDEX.html

[2]libtooling 官网文档:https://clang.llvm.org/docs/LibTooling.html

[3]LLVM源码:https://github.com/llvm/llvm-project

举荐浏览:

基于异样上线场景的实时拦挡与问题散发策略

极致优化 SSD 并行读调度

AI文本创作在百度App发文的实际

DeeTune:基于 eBPF 的百度网络框架设计与利用

百度自研高性能ANN检索引擎,开源了