使用Python脚本分析iOS工程跨模块引用
一个结构清晰的工程不仅有利于团队新成员快速熟悉,还有利于功能的稳定迭代。但是随着项目的不断迭代,整体结构往往趋向于混乱,这就需要我们在结构上做一些控制,一是编写开发规范限制跨模块引用,二是编写工具脚本不定期检测整个工程引用情况,保障引用的合理性。
查找资料发现 https://github.com/nst/objc_dep,该工具能分析出所有引用关系并且结合 GraphViz 或 OmniGraffle 图像化输出结果。但是该工具无法分析跨模块引用情况,以下是我用Python基于字符串匹配编写的一个小工具,可以扫描出指定目录下的跨模块引用情况,下面具体介绍一下整体思路。
分析流程
image上图列出了完整的分析过程,拆解成以下几个步骤:
-
根据指定的目录,将该目录下的所有第一级目录作为模块名,用Map的结构构建文件关系,结果类似
{"ModuleA":["File1","File2",...]}
{"ModuleB":["File1","File2",...]}
-
从第一步里的每个具体文件中提取出引用列表,结果类似
{"File1":["Import2","Import3","Import4",...]}
,表示文件1引用了2、3、4,此时还不知道2、3、4是否是跨模块引用 -
将第二步中提取出的引用文件和第一步中的文件关系遍历比较,获取出引用文件的归属模块
-
构造数据结构存储引用关系
-
格式化打印出跨模块引用关系
代码实现
构建文件关系
#build module and files relationship
#key:module_name value:file_list
def get_file_relation_map(path):
if path.endswith('/'):
path = path[:-1]
file_map = {}
for dirName, subdirList, fileList in os.walk(path):
for fname in fileList:
if fname.endswith('.h') or fname.endswith('.m') or fname.endswith('.mm'):
module_name = dirName.split(path)[1]
if dirName != path:
module_name = dirName.split(path + '/')[1]
if len(module_name) == 0:
module_name = path.split('/')[-1]
if '/' in module_name:
module_name = module_name.split('/')[0]
list = []
if file_map.has_key(module_name):
list = file_map[module_name]
full_path = '%s/%s' % (dirName,fname)
if full_path not in list:
list.append(full_path)
file_map[module_name] = list
return file_map
这里的思路是将指定路径的第一级目录设置为模块名,并将该模块名下的所有文件组织成文件列表建立键值对关系,最终结构类似{"ModuleA":["File1","File2",...], "ModuleB":["File1","File2",...]}
提取文件引用列表
#get import list from source_file
def get_import_files(source_file):
import_files = []
for line in fileinput.input(source_file):
if '#import' in line and '//' not in line:
file = line.split('#import')[1]
if '<' in file:
file = file.split('<')[1].split('>')[0]
if '/' in file:
file = file.split('/')[1]
elif '"' in file:
file = file.split('"')[1]
import_files.append(file)
return import_files
这里是用关键字匹配的方式提取出指定文件的引用列表,到这一步还不知道哪些文件是跨模块引用的
分析跨模块引用
最关键的是这一步,这里采用的是直接遍历的方式来查找跨模块关系,代码较长这里就不贴出了,感兴趣的可以查看Demo工程,最终生成如下格式的数据
[{"A":[ {"B":[{"B1":"A1"},{"B2":"A1"}, ...] },{"C":[ {"C1":"A1"},{"C2":"A1"},...]}, ...] },
{"B":[ {"C":[{"C1":"B1"}, {"C2":"B1"}, ...] },{"A":[ {"A1":"B1"},{"A2":"B1"}, ...]}, ...]}, ...]
格式化打印
这一步就是将前面生成的数据格式化输出以便直观查看引用关系
用法说明
以下例子基于Demo工程 AnalyseDependencyDemo 进行演示,该工程有4个模块,如下图所示:
image使用该脚本分析跨模块引用关系的结果如下:
image再结合使用 https://github.com/nst/objc_dep 图像化显示引用关系如下图所示:
image可见该工程的引用关系十分混乱,不利于长期维护,因此有必要做一些结构分层。还是以上面的工程为例,我们抽象出一个中间层,所有业务层都向下引用该层,但不允许反向依赖,整理后的结构如下图所示:
image使用该脚本分析跨模块引用关系的结果如下:
image再使用 https://github.com/nst/objc_dep 图像化结果如下:
image整理后的工程引用关系清晰,数据流向单一,有利于工程的可维护性和扩展性,因此十分有必要不定期在工程里检查模块间的引用情况,避免到了后期维护成本过高。
该脚本还支持指定分析两个模块间的引用情况,参数1 跨模块引用 参数2 的结果如下:
image总结
这个脚本的原理比较简单,关键是要有这个意识,在版本迭代过程中时刻注意控制工程结构的合理性,不定期使用这个脚本检测跨模块引用情况,等到了后期无法维护的时候再回头重构的成本就会很高。
欢迎大家关注我们团队的公众号,不定期分享各类技术干货
image