上一篇文章(ASTMatcher分析函数调用链(上))讲到ASTMatcher的原理以及创建,本文将详细介绍ASTMatcher获取函数调用链在iOS app中的应用。
上篇中的ASTMatcher只能获取有消息调用的函数定义,那没有消息调用的函数定义就无法匹配到,所以无消息调用的函数定义也需要获取
DeclarationMatcher AllFuncMatcher = objcMethodDecl(
hasAncestor(objcImplementationDecl().bind("allClass"))
).bind("allSelector");
同理也需要获取匹配节点
objcImplementationDecl()中并不包括category类,所以category类需要单独写Matcher匹配
DeclarationMatcher CategoryFuncLinkMatcher = objcMethodDecl(
hasAncestor(objcCategoryImplDecl().bind("myCategoryClass"))
,forEachDescendant(objcMessageExpr().bind("categoryFuncCaller"))
).bind("myCategorySelector");
同理也需要获取匹配节点
本文将分析到的消息调用、函数定义和类声明写到缓存文件中,这里需要适配多业务,缓存目录不能写死在同一个文件夹,所以缓存目录通过参数传递:
rootPath = argv[1];
std::vector<std::string> sv;
split(rootPath, sv, '/');
for (const auto& s : sv) {
fileName = s;
}
Matcher命令行中必须通过-extra-arg参数传递:
~/clang-llvm/build/bin/func-call ~/www/CYHTest/get_func_link/demoB.m -extra-arg=~/www/AST_Matcher_Result/func-category-new -- -I ~/www/CYHTest/get_func_link/
分析整个app的函数调用关系,需要遍历每个.m和.mm文件,并且每个.m和.mm文件引用的头文件目录必须 -I 给Matcher,所以需要拿到每个.m和.mm以及其对应的.h文件的引用文件路径集合。
# 获取头文件所在目录,为后续参数-I做准备
def get_head_file_relation(workspace):
head_dir_relation = {}
for root, dirs, files in os.walk(workspace):
for file_one in files:
if file_one.endswith(".h"):
head_dir_relation[file_one] = root
return head_dir_relation
# 获取每个.m或者.mm文件import的头文件所在目录,输出为字典
def get_import_file_dict(file_list, head_dir_relation):
...
ASTMatcher分析不到条件语句中的代码,所以条件语句全部屏蔽
def do_work(ROOT):
line_records = {}
for root, dirs, files in os.walk(ROOT):
for file in files:
file_name, file_extension = os.path.splitext(file)
if file_extension in ['.m', '.mm']:
path = os.path.join(root, file)
line_records[path] = pre_edit(path)
with open("line_record.txt", 'wb') as fp:
json.dump(line_records, fp)
def pre_edit(path):
line_record = []
new_lines = []
with open(path, 'rt') as fp:
lines = fp.readlines()
reg = re.compile(r'^\s*\#if|^\s*\#else|^\s*\#endif|^\s*\#error')
for index, line in enumerate(lines):
for m in reg.finditer(line):
line = line[:m.start()] + '//' + line[m.start():]
line_record.append(index)
break
new_lines.append(line)
with open(path, 'wt') as fp:
fp.writelines(new_lines)
return line_record
获取所有.m和.mm文件list,循环通过func-call分析(这里使用func-call和func-call-category-only两个ASTMatcher,3.5章节有解释)
for file_one in file_list:
try:
header_one_file_str = " -I ".join(file_head_relation[file_one])
terminal = "~/clang-llvm-6.0/build/bin/func-call {} -extra-arg={} -- -I {}".format(file_one, ast_matcher_cache_dir, header_one_file_str)
status, output = commands.getstatusoutput(terminal)
if status == 0:
print 'success:', number
number = number + 1
else:
print 'fail:', fail, ":", file_one
fail = fail + 1
fail_file_list.append(file_one)
except Exception as e:
except_number = except_number + 1
except_file_list.append(file_one)
print "except file:", e, ":", file_one
continue
for file_one_category in file_list:
try:
header_one_file_str = " -I ".join(file_head_relation[file_one_category])
terminal = "~/clang-llvm/build/bin/func-call-category-only {} -extra-arg={} -- -I {}".format(file_one_category, ast_matcher_cache_dir, header_one_file_str)
status, output = commands.getstatusoutput(terminal)
if status == 0:
print 'success category:', number
number = number + 1
else:
print 'fail category:', fail, ":", file_one_category
fail = fail + 1
fail_file_list.append(file_one_category)
except Exception as e:
except_number = except_number + 1
except_file_list.append(file_one_category)
print "except file category:", e, ":", file_one_category
continue
解决:原来clang6.0不支持获取objcCategoryImplDecl(),使用的clang版本比较旧,更新至最新clang8.0即可。
解决:ObjCCategoryImplDecl()->getIdentifier()->getName()方法只能获取category类的别名,通过ObjCCategoryImplDecl()->getCategoryDecl()->getClassInterface()->getNameAsString()获取类名,曲线救国~
const ObjCCategoryImplDecl *categoryImplDecl = Result.Nodes.getNodeAs<ObjCCategoryImplDecl>("myCategoryClass");
std::string categoryName = categoryImplDecl->getIdentifier()->getName();
ObjCCategoryDecl *categoryDecl = categoryImplDecl->getCategoryDecl();
if(!categoryDecl->IsClassExtension())
{
//这里有两种情况,一种是Extension,一种是Category。这里是处理Category
ObjCInterfaceDecl *itfcdecl = categoryDecl->getClassInterface();
outCategoryMessage << itfcdecl->getNameAsString() << "::";
}
在clang更新到clang8.0后,sudo ninja构建时报错CMake Error at tools/clang/tools/extra/func-category/CMakeLists.txt:6 (target_link_libraries):
解决:CMakeLists.txt target_link_libraries中加入PRIVATE,见链接:https://stackoverflow.com/questions/47737558/build-llvm-clangtool
clang8.0生成的func-call可执行文件获取调用链不全,具体原因没有去研究
解决:func-call使用clang6.0生成的可执行文件,func-call-category-only使用clang8.0生成的可执行文件。这里还要注意func-call-category-only依赖clang8.0的环境,所以机器上clang环境还得是clang8.0。
这就导致了普通函数的获取与category类函数的获取不能在同一个ASTMatcher中,所以需要两个ASTMatcher来分析整个工程,这里后续再优化~
app category类函数的获取有两种方法,一是遍历整个工程目录,拿到文件名中有“+”号的category文件list,然后循环分析;二是分析整个app所有.m和.mm文件。方式一速度快,但不是以“+”号命名的category类无法获取;方式二会增加分析时间,但可以分析到所有category类,数据比较完整。
解决:本文为了数据完整性,牺牲分析时间,采用方式二。
由于系统方法在我们调用链中没有用处,所以这里就放弃了对系统方法的获取,包括系统类的category类以及方法。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。