百度APP iOS端包体积50M优化实践(六)无用方法清理
The following article is from 百度App技术 Author HYM
一、前言
GEEK TALK
百度APP包体积经过一期优化,如无用资源清理,无用类下线,Xcode编译相关优化,体积已经有了明显的减少。但是优化后APP包体积在iPhone11上仍有350M的空间占用。与此同时百度APP作为百度的旗舰APP,业务迭代非常多且迅速,体积优化和防劣化仍然是当前阶段的一个核心任务。因此百度APP开启了粒度更小,修复风险更高的无用方法清理相关工作。期望通过无用方法清理,有效降低百度APP的包体积,同时删除项目中的无用方法,冗余代码,提高代码的整洁度。
百度APP iOS端包体积优化实践系列文章回顾:
二、方案调研
GEEK TALK
1.准确度低
2.针对系统方法需要手动过滤
3.针对load、initilize、attribute 相关调用无法识别
4.针对string反射调用无法识别,Target-Action 注册,Observer注册方法等无法识别
5.复杂语法场景下无法识别,如继承链中的方法调用,子类实现父类方法等场景
6.系统通知等场景
三、方案选择
GEEK TALK
针对第二部分方案不足之处进行分析,可以看到其准确度低的核心问题是,针对产物进行分析,拿不到所有需要的信息,或者说还没有发现有效的手段去获取所期望获得的信息。而想要解决上面提到的问题,最佳途径就是获取到尽可能多的代码信息。既然从产物回溯不到所需要的,那么就可以考虑从源头也就是源码层面找到我们所需要的详细信息。
源码肯定包含了所有的信息,但是针对源码如何分析呢,主要有以下三种:
通过脚本直接分析源码
通过脚本直接分析AST(抽象语法树)
通过libtooling 和 Swift Compiler自建编译套件分析AST (Swift相关会在下一篇文章中介绍)
既然通过clang命令生成的AST产物分析仍然不能满足需求,那么直接介入编译过程,从编译内部生成AST过程中获取需要的信息,最终这个方案被采用。通过libtooling 和 Swift Compiler自建编译套件针对AST进行分析,获取所需要的所有信息。
四、方案设计
GEEK TALK
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后端编译器进行目标文件生成。
/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
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
△图 4-6
构建过程
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
mkdir build (这个build文件夹可以自行命名,不固定。针对不同目标可以创建不同文件夹进行不同构建,如 mkdir ninjaBuild 或 mkdir xcodeBuild)
cd build (or cd xcodeBuild)
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release ../llvm
cmake --build .
编译Xcode版本,Ninja替换为Xcode即可。
4.3.1.2 工程搭建
LLVM提供了两种工具 libclang 和 libtooling,百度APP采用的是 libtooling,其异同点如下所示:
libclang:(网络资料,未实测)
libtooling:(实测)
1.提供 C++ 接口,产出的工具不依赖于编译器,可作为独立命令使用
2.接口不稳定,AST 有升级需要更新相关依赖库
最终选择 libtooling 形式,核心原因就是 libtooling 可以获取 AST 的所有信息,同时能够不依赖于Xcode 独立运行。工程的搭建本身并不复杂,还是属于API 使用层面,可以直接参照 libtooling的官方文档。
△图 4-6
总体代码流程如图 4-8所示,主要核心点是五个部分:
参数解析
创建 ClangTool 参照LLVM源码 ClangTooling -> Tooling.h Line309
创建 ASTFrontendAction,用于获取 AST 数据,创建 ASTConsumer 和 进行 ASTMatcher 绑定
针对 ASTMatcher 匹配项进行各语法规则匹配
根据匹配数据进行数据过滤及业务处理
4.3.1.3 数据存储结构设计
"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
}
}
五、遇到的问题及解决方案
GEEK TALK
2. 提取方法内容时同样需要对头文件进行提取
方法的实现不一定只在.m 文件中,如C++的头文件是可以进行方法实现的,Objective-C 的.h 文件 通过 inline 实现一些方法,在语法上也是可行的。所以进行方法提取时候关注实现文件,同时也要关注头文件。
4. 过滤系统方法调用
5. 过滤业务类实现系统方法问题
7. 子类实现父类协议问题
8. 正常业务实现协议,应该明确标注当前类遵循了协议 如 interface <conformprotocol>,但是实际场景中有很多代码在实现协议时并没有标注conformprotocol 这样就对协议方法的判断产生影响,如 6.7方案均失效了
如果组件中少量这种问题,当推动相关方修复此问题,需要明确遵循协议。但是如果有的组件这种场景较多,短期不会修复所有,那么就需要进行临时性适配。针对这类组件收集其当前组件所声明的协议的所有协议方法,用收集的协议方法和当前组件提取的所有声明做差集,存在误伤的可能,但结果是置信的(组件只是一个维度,也可以针对其关联组件进行相关处理,因为有时他实现的组件不一定在当前组件内,这就需要当前组件的依赖关系了)。
六、总结
GEEK TALK
这项技术实际上在百度APP早已经应用,因为笔者之前负责百度APP的接口变更审核,组件完整性校验,隐私合规调用链分析等均是依赖于此项技术,无用方法识别只是笔者在做体积优化时想到的其功能的一个延展。当然如上描述的技术问题,细节处理无用方法显然更细腻,case更多。后续文章会针对Swift无用方法分析,接口变更审核,组件完整性校验,隐私合规调用链分析等一一作出介绍。
END
推荐阅读:基于异常上线场景的实时拦截与问题分发策略极致优化 SSD 并行读调度AI文本创作在百度App发文的实践DeeTune:基于 eBPF 的百度网络框架设计与应用百度自研高性能ANN检索引擎,开源了