一、背景
当评审别人提过来的一次代码codereview,如何客观的评估此次代码改动对APP有何影响,给测试人员一个明确回归范围。本文就此问题提供一种解决方案。
二、实现思路
1、提取一次git提交中的变更方法列表
- 增加的方法
- 删除的方法
- 更新的方法
2、找到调用变更方法的方法列表
3、找到影响的组件列表
4、通过组件和方法评估影响的功能
三、实践过程
准备工作:需要项目打包出来的macho文件,打包时候生成的linkmap文件。
1、获取一次git提交的具体代码改动,通过git的diff命令提取其中的oc和swift方法名字
git diff | sed -nE 's/.*((+|-).*(.*).*{+|func[[:space:]]+[^{]+).*/\1/p'
2、将获取到的方法名转成oc符号(汇编文件中都是用符号调用)
如下方法转化成对应符号
- (void)requestRecommendDataWithParam:(nullable NSDictionary *)data;
转化后的
requestRecommendDataWithParam:
3、将macho生成汇编文件
otool -t -V -arch arm架构 macho文件 > 生成的汇编文件
4、从汇编文件中获取符号调用的地址
def parseDisassemblyFile(disassembly_file_path,symbol):
disassembly_file = open(disassembly_file_path,'r')
symbol_pattern = re.compile(re.escape(symbol))
address_pattern = re.compile(r'^(\S+)')
address_array = []
for line in disassembly_file:
line = line.strip()
if not symbol_pattern.search(line):
continue
address_match = address_pattern.search(line)
address = address_match.groups()[0]
if(address.startswith("0000")):
address_array.append(int(address,16))
else:
print("类里面直接调用:" + line)
disassembly_file.close()
return address_array
5、通过调用符号的地址从linkmap文件中找到对应调用的方法名字和方法所在的类和所在的组件
linkmap文件的Symbols:结构下目录:
# Symbols:
# Address Size File Name
0x100004900 0x00000008 [ 2] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100004908 0x00000006 [ 2] -[AppDelegate applicationWillResignActive:]
0x10000490E 0x00000006 [ 2] -[AppDelegate applicationDidEnterBackground:]
0x100004914 0x00000006 [ 2] -[AppDelegate applicationWillEnterForeground:]
0x10000491A 0x00000006 [ 2] -[AppDelegate applicationDidBecomeActive:]
0x100004920 0x00000006 [ 2] -[AppDelegate applicationWillTerminate:]
0x100004926 0x00000011 [ 2] -[AppDelegate window]
0x100004937 0x00000014 [ 2] -[AppDelegate setWindow:]
0x10000494B 0x00000013 [ 2] -[AppDelegate .cxx_destruct]
如上列举了每个方法调用的起始地址,方法大小,方法所在的文件目录。例如:-[AppDelegate application:didFinishLaunchingWithOptions:] 这个方法的地址范围在0x100004900 和 0x100004908(0x100004900 + 0x00000008)之间。在第四步中,我们通过符号获取到了调用此符号的地址,然后用此地址在linkmap里面Symbols:下,遍历每一行,要是此地址在这一行的方法地址区间中,说明了这一行就是我们要找的,获取方法名字和方法所在的文件序号。
linkmap文件中Object files: 结构目录:
# Arch: x86_64
# Object files:
[ 0] linker synthesized
[ 1] /Users/duluyang/Library/Developer/Xcode/DerivedData/JDLTAppModule-btfvgfjjtdaoiwdyidelyouzuuut/Build/Intermediates.noindex/JDLTAppModule.build/Debug-iphonesimulator/JDLTAppModule_Example.build/JDMobileLite.app-Simulated.xcent.der
[ 2] /Users/duluyang/Library/Developer/Xcode/DerivedData/JDLTAppModule-btfvgfjjtdaoiwdyidelyouzuuut/Build/Intermediates.noindex/JDLTAppModule.build/Debug-iphonesimulator/JDLTAppModule_Example.build/Objects-normal/x86_64/AppDelegate.o
获取到文件序号后,在limkmap文件下的Object files:目录下,解析类名字和组件名字。
6、swift方法解析
0x100CD7996 0x000001B0 [2678] -[UITableView(COCheckoutLayoutCell) co_heightForCellWithIdentifier:cacheByIndexPath:configuration:]
0x100CD7B46 0x0000003D [2678] -[UITableView(COCheckoutLayoutCell) co_invalidateAllHeightCache]
0x100CD7B83 0x000000CD [2679] -[UITextField(COCheckoutCursorLocation) checkoutSetCursorLocationOfIndex:]
0x100CD7C50 0x00000110 [2680] _$s17JDLTAddressModule22AddressCacheServiceImpCACycfc
0x100CD7D60 0x00000020 [2680] _$s17JDLTAddressModule22AddressCacheServiceImpCACycfcTo
0x100CD7D80 0x00000020 [2680] _$s17JDLTAddressModule22AddressCacheServiceImpC05localdC002pgcdeB009PGAddressD5ModelCSgvgTo
0x100CD7DA0 0x000000B0 [2680] _$s17JDLTAddressModule22AddressCacheServiceImpC05localdC3IdsSSSgvgTo
0x100CD7E50 0x00000010 [2680] _$s17JDLTAddressModule22AddressCacheServiceImpC010clearLocalcD0yyFTo
limkmap中的swift方法是通过转化过的并且都是有_前缀。解析swift方法的时候,先去掉_前缀,在用“xcrun swift-demangle swift方法” 命令进行还原
四、实践结果
一次代码改动使用git diff 查看代码改动点:
diff --git a/pgUniformRecommendModule/Classes/PGUR/JDURMainViewController.m b/pgUniformRecommendModule/Classes/PGUR/JDURMainViewController.m
index f9357d0..9ad9138 100644
--- a/pgUniformRecommendModule/Classes/PGUR/JDURMainViewController.m
+++ b/pgUniformRecommendModule/Classes/PGUR/JDURMainViewController.m
@@ -195,7 +195,7 @@ - (void)requestRecommendDataWithParam:(nullable NSDictionary *)data {
_eventHandler.sku = [data pg_stringValueForKey:@"sku"];
_eventHandler.skus = [data pg_stringValueForKey:@"skus"];
_eventHandler.ptag = [data pg_stringValueForKey:@"ptag"];
- _eventHandler.count = (pageCount > 0) ? pageCount : 25;
+ _eventHandler.count = (pageCount > 0) ? pageCount : 30;
_eventHandler.url = [data pg_stringValueForKey:@"url"];
_eventHandler.old_switch = [data pg_stringValueForKey:@"old_switch"];
_eventHandler.click_eid = [data pg_stringValueForKey:@"click_eid"];
通过脚本提取的方法名字所在的行:- (void)requestRecommendDataWithParam:(nullable NSDictionary *)data {
提取成最终的查找符号:requestRecommendDataWithParam:
按照上面的方法编写脚本解析最终结果如下:
调用方法:requestRecommendDataWithParam:共7处
JDLTMainPageModule(JDLTHomeRecommend.o)-[JDLTHomeRecommend loadData:]
pgProductDetailModule(PGRecommendCell.o)pgProductDetailModule.PGRecommendCell.(refreshRecommendViewData in _67DF5186644D80B71D713F462DA9D1FC)(isPP: Swift.Bool, isNotElderTheme: Swift.Bool) -> ()
pgCartModule(RecommendView.o)pgCartModule.RecommendView.changeOlder(skuIds: Swift.String, refreshSepView: Swift.Bool) -> ()
JDLTRechargeModule(JDLTRechargeRecommend.o)-[JDLTRechargeRecommend loadData:]
JDLTMyJdModule(JDLTMyJdRecommend.o)-[JDLTMyJdRecommend loadData:]
JDLTTaskCenterModule(JDLTTaskCenterRecommend.o)-[JDLTTaskCenterRecommend loadData]
pgUniformRecommendModule(JDURMainViewController.o)-[JDURMainViewController requestRecommendData]
每一行中最左侧的是组件名字,表示影响的组件,一共有7个模块,小括号中是组件中的类名,最右侧的是类中调用- (void)requestRecommendDataWithParam:(nullable NSDictionary *)data的方法名字。
此工具就能分析出这次git提交,总共改动一个方法- (void)requestRecommendDataWithParam:(nullable NSDictionary *)data 。总共在7个组件的7个类的7个方法中调用了此改动方法,影响的功能范围总共有7处。
五、总结:
此方法只适用改动方法较少的情况,能快速定位出一级调用此方法的地方。要是一次提交的代码改动到几十上百个方法,分析起来就非常耗时,适合于封版本后提交的少量代码审核,给测试明确的回归模块和功能范围。
六、参考资料
- macho_analysis · PyPI. pypi.org/project/mac….
- GitHub - datatheorem/strongarm: Mach-O analysis library 💪. github.com/datatheorem….
- GitHub - cxnder/ktool: fully cross-platform toolkit (and library!) for .... github.com/cxnder/ktoo….