首页 > IT基础架构 > 服务器/存储 > 289M→259M得物包体积治理实践
来源:得物技术
目录
一、前言
二、原理介绍
1. Macho产物测试
2. LinkMap分析
3. Mach-o代码内容分析
三、落地实践
1. Cocoapods原理与实践
2. Native代码整改
3. 组件发版流程重构
四、总结与收益
五、展望
一
前言
二
原理介绍
Macho产物测试
Linkmap文件中记录MachO文件中每个符号所占用的体积大小,因此通过分析Linkmap可以分析MachO具体符号占用变化,由于Linmap介绍不是本文重点,不多做赘述,更多详情可参考网上文章https://juejin.cn/post/6844904168096792583。


LinkMap分析
Linkmap.txt文件第一列展示的是符号的起始地址,第二列展示的是大小,16进制,将16进制转换为10进制,即是大小。相减得出变化大小。
__text段存储的机器编译后的代码。
en_frame存储了函数调用入口帧信息。具体查看https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html。

Linkmap符号变化


左图为组建变化,右图为链接符号变化
通过对Linkmap的分析,确实存储代码段和函数入口帧信息减小使得编译后的.o文件变小了,那么.o文件编码整合为DemoModule.a文件也随之变小了,那么到底是哪块代码变小了呢?我们继续往下分析。
Mach-o代码内容分析
Mach-o代码内容分析
objdump --macho -d --start-address=0x10025FDD0 --stop-address=0x100257668 ~/Desktop/IPATestProj > ~/Desktop/result.txtobjdump --macho -d --start-address=0x10025FDD0 --stop-address=0x100257668 ~/Desktop/IPATestProj-after > ~/Desktop/result-after.txt
对比发现
针对s13DemoModule0A29TSearchHotRecommendDemoModuleCACycfC优化了28Byte。
allocWithZone以及objc的init方法。调用了DemoModule0A21FollowBrandDemoModuleCACycfC, DemoModule0A21FollowBrandDemoModuleCACycfC 调用了s13DemoModule0A17PaySendDemoModuleCACycfC,s13DemoModule0A17PaySendDemoModuleCACycfC里实现了alloc with zone和init方法。也就是说编译器优化了经过编码的alloc with zone方法,只会有一个alloc with zone 的实现。
针对s13DemoModule0A29TSearchHotRecommendDemoModuleCMr
DemoModule.ExampleModule.__deallocating_deinit优化了32Byte
优化了meta的deinit与寻找metaclass的过程。s13DemoModule0A29DemoModuleCMa调用了s13DemoModule0A31tDemoModuleCMaTm - ,而 和SearchDemoModule同时继承了CustomRequestDemoModule。
_$s13DemoModule0A29TSearchHotRecommendDemoModuleCACycfC:1002fe8c8: fd 7b bf a9 stp x29, x30, [sp, #-16]!1002fe8cc: fd 03 00 91 mov x29, sp1002fe8d0: e0 03 14 aa mov x0, x201002fe8d4: 3d 2a 20 94 bl 0x100b091c8 ; symbol stub for: _objc_allocWithZone1002fe8d8: 28 64 00 b0 adrp x8, 3205 ; 0x100f830001002fe8dc: 01 79 44 f9 ldr x1, [x8, #2288] ; Objc selector ref: init1002fe8e0: fd 7b c1 a8 ldp x29, x30, [sp], #161002fe8e4: 7b 2a 20 14 b 0x100b092d0 ; Objc message: -[x0 init]_$s13DemoModule0A29TSearchHotRecommendDemoModuleCACycfc
1002577a0: b2 ff ff 97 bl _$s13DemoModule0A29TSearchHotRecommendDemoModuleCACycfc1002577a4: fd 7b 41 a9 ldp x29, x30, [sp, #16]1002577a8: f4 4f c2 a8 ldp x20, x19, [sp], #321002577ac: c0 03 5f d6 ret_$s13DemoModule0A29TSearchHotRecommendDemoModuleCfD:1002577b0: 60 fe ff 10 adr x0, #-521002577b4: 1f 20 03 d5 nop1002577b8: f2 fd ff 17 b _$s13DemoModule0A31IdentComTrendDelLightDemoModuleCfDTm_$s13DemoModule0A26TMeasureRecordAiUpdateSkinCACycfc:
_$s13DemoModule0A29TSearchHotRecommendDemoModuleCMa:1002fe9fc: fd 7b bf a9 stp x29, x30, [sp, #-16]!1002fea00: e8 03 00 aa mov x8, x01002fea04: 89 71 00 b0 adrp x9, 3633 ; 0x10112f0001002fea08: 20 7d 40 f9 ldr x0, [x9, #248]1002fea0c: 80 00 00 b4 cbz x0, 0x1002fea1c1002fea10: 01 00 80 d2 mov x1, #01002fea14: fd 7b c1 a8 ldp x29, x30, [sp], #161002fea18: c0 03 5f d6 ret1002fea1c: 41 4c 00 90 adrp x1, 2440 ; 0x100c860001002fea20: 21 80 24 91 add x1, x1, #23361002fea24: e0 03 08 aa mov x0, x81002fea28: f7 2c 20 94 bl 0x100b09e04 ; symbol stub for: _swift_getSingletonMetadata1002fea2c: fd 7b c1 a8 ldp x29, x30, [sp], #161002fea30: c0 03 5f d6 ret
0x10025777C 0x00000014 [583] _$s13DemoModule0A29TSearchHotRecommendDemoModuleCMa
由此可以得出结论。
编译器针对不同的Class,经过编码整合后,编译时会触发编译优化,alloc with zone、deinit寻找metaclass方法。将文件编码后整合会优化为一个。
同时相关的寻址和寄存器的addr,以及mov、内存地址的存储已随之删除,具体对比结果可以看上面的产物对比。
三
落地实践
Cocoapods原理与实践


由于笔者的pod是使用homebrew装的,因此pod可执行文件在homebrew的安装目录: /opt/homebrew/Cellar,这个pod文件本质是个bash sh文件,咱们将文件已编辑器打开有如下的内容。
GEM_HOME="/opt/homebrew/Cellar/ruby环境根目录" exec "/opt/homebrew/Cellar/真实的cocoapods目录/bin/pod" "$@"
继续打开后面的执行文件,发现这个pod文件就是cocoapods安装文件下的pod,pod是一个ruby文件,也就是cocoapods最终的命令入口,内容如下:
#!/usr/bin/env rubyrequire 'rubygems'if Gem.respond_to?(:activate_bin_path)load Gem.activate_bin_path('cocoapods', 'pod', version)elsegem "cocoapods", versionload Gem.bin_path("cocoapods", "pod", version)endXcodeproj::PlainInformative.send(:include, CLAide::InformativeError) autoload :Command, 'cocoapods/command' # 命令行入口 autoload :ExternalSources, 'cocoapods/external_sources' # git 依赖,本地依赖处理类 autoload :Installer, 'cocoapods/installer' # pod install核心 类
cocoapods/command是一个基类,每个命令pod install、pod update、pod repo add都会有相应command重写。可查看下面的截图:

def initialize(argv)super@deployment = argv.flag?('deployment', false)@clean_install = argv.flag?('clean-install', false)enddef runverify_podfile_exists! # 校验 工程目录Podfile 文件是否存在installer = installer_for_config # 根据config 生成 installerinstaller.repo_update = repo_update?(:default => false) # 配置是否需要更新索引库installer.update = false # 由于是install 因此 update 是falseinstaller.deployment = @deploymentinstaller.clean_install = @clean_installinstaller.install! # 进入真正的install 流程end

# install 源代码def install! prepare resolve_dependencies # 依赖链分析 download_dependencies # validate_targets clean_sandbox if installation_options.skip_pods_project_generation? show_skip_pods_project_generation_message run_podfile_post_install_hooks else integrate end write_lockfiles perform_post_install_actionsend
1. 为了能将:git、:branch、:commit依赖与本地:path依赖做代码编码并整合,我们需要再resolve_dependencies中加载git依赖与本地依赖阶段时做hook,满足本地依赖与git依赖组件集成时做代码编码整合。
2. 为了能对正常pod"Example"组件做整合,我们需要再download_dependencies中对组件做整合。具体实现整合与定制参数传入,我们继续往下看。
# hook_file用于统一管理du_hook文件# 判断有没有hook_fileif File.exist?(File.join(__dir__, 'cocoapods/du_hook/hook_file.rb')) require 'cocoapods/hook/hook_file'end
热更机制简单理解就是每次执行pod命令时会执行git操作,将魔改部分的仓库代码保持到最新,Cat这一巧妙的设计让得物iOSer都能实时享受到cocoapods改造带来的新功能。
require 'cocoapods/hook/cocoapods-hook/cocoapods_concurrent_hook' # 魔改的高并发下载require 'cocoapods/hook/cocoapods-hook/cocoapods_option' # 为cocoapod 加入 命令参数的入口
module Pod class Command module Options module Demo module Options def initialize(argv) # 每个电脑都有一个全局的环境变量,在执行命令的生命周期内是一直存在的,给环境变量传入配置,不改动cocoapods的config源配置文件,不入侵cocoapods的源代码。 ENV['transform_FILE'] = '1' if @transform_file ENV['ransform_LOCAL'] = '1' if @transform_local super end end end endend
Ruby是一个运行时的动态语言,在required cocoapods_transform_file文件中,将指定的cocoapods函数进行重写,就能实现HOOK的功能。
因此在cocoapods_transform_file.rb文件中覆盖cocoapods/external_sources/path_source.rb 下class PathSource的fetch方法就能定制为本地依赖的组件、git依赖的组件的组件执行定制的整合能力
覆盖cocoapods/downlod.rb下的Module Downloader self.download module类方法就能定制为pod"Example"的组件执行定制的整合。
具体的整合思路我们继续往下看。


Native代码整改
SwiftLint中依赖了Swiftsytax,本质都是借助Swiftsyntax进行词法分析,扫描出工程的所有extension同名函数,并进行改造。
override func visitPost(_ node: ExtensionDeclSyntax) { let functionList = _isFunctionDecl(node) guard !functionList.isEmpty else { return } for funcItem in functionList { // 如果是private function 那么不纳入考虑范围 guard !_isPrivateFunction(funcItem) else { continue } // 如果不是public的extension,并且函数也不是public 那么这个函数就不是公开函数,也可以忽略 if !isPublicExtension && !_isPublicFunction(node: funcItem) { continue } violations.insert(ReasonedRuleViolation(position: funcItem.position, reason: funcItem.resolvedName(), severity: .warning), at: violations.count) } }Indexstore-db是一个用于存储和管理源代码索引数据的开源工具。indexstore-db工具可以收集和存储源代码的元数据信息,包括符号、模块依赖关系、引用关系等,以便在开发工具(如Xcode)中进行快速的代码导航和搜索。它在构建大型代码库时尤其有用,可以提高代码编辑、查找引用、代码重构等操作的效率。
func testExtensionSymbol() throws { // indexstore-db 的动态加载库 let libIndexStore = try! IndexStoreLibrary(dylibPath: "/Applications/Xcode.app/xxx/libIndexStore.dylib") // 生成indexstore 实例 let indexWait = try IndexStoreDB(storePath: "/Users/xxx/Library/Developer/Xcode/DerivedData/.../DataStore", databasePath: "/Users/xxx/Downloads/aaa", library: libIndexStore, waitUntilDoneInitializing: true) indexWait.pollForUnitChangesAndWait() // 假设我们需要扫描如下的文件 let symbols = indexWait.symbols(inFilePath: "/Users/xxx/Project/String+Demo.swift") for symbol in symbols { // 假设我们需要扫描searchAtRange函数。 guard symbol.name == "searchAtRange()" else { continue} let res = indexWait.occurrences(ofUSR: symbol.usr, roles: .reference) for x in res { debugPrint(x.relations.compactMap({ symbol in return symbol.symbol.usr })) } } }组件发版流程重构
未整合的组件二进制符号是确定的,调用下游的符号签名也是确定的,Swift有 fileprivate的函数定义,当函数由A文件经过编码迁移到整合后的文件时,函数的签名也会变化。因此会出现函数签名符号不对齐。
开发者开发时使用正常的二进制制作任务。发版与出包的打包机会使用整合二进制索引库。这样设计使得对日常开发无感知,而且能保证对外提测的任务都是整合后的包。


四
总结与收益



五
展望
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70027824/viewspace-3018420/ ,如需转载,请注明出处,否则将追究法律责任。