关于ios:百度APP-iOS端包体积50M优化实践六无用方法清理

3次阅读

共计 6627 个字符,预计需要花费 17 分钟才能阅读完成。

一、前言

百度 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.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:(网络材料,未实测)

    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 检索引擎,开源了

正文完
 0