一、背景经过多年的发展,高德App已达到百万行代码,支持高德地图复杂的业务功能。但同时,随着团队的扩大和业务的复杂化,代码越来越碎片化,代码之间的依赖关系复杂,带来了很多维护问题。比较突出的问题包括:不敢轻易修改或下载对外暴露的接口或组件,因为不知道它们依赖在哪里,会受到什么影响,所以代码变得臃肿,包体积变大更大;模块发布到新版本的客户端使用时,需要对整个功能进行全面的回归测试,因为不知道依赖的模块是否发生了变化;Native从业务实现向底层支撑的转变趋势是否合理,治理是否有效,难以判断;这些问题已经到了我们必须要治理的层面了,解决这类问题的关键是需要理解代码之间的依赖关系。2.高德APP平台架构为了排除一些疑惑,在讨论依赖分析的实现之前,先简单说明一下高德APP的平台架构,以便对一些术语和场景有一些背景了解。高德APP从语言平台上可以分为4个部分。JS层主要负责业务逻辑和UI框架;中间是高性能渲染(主要是地图渲染)的C++层,同时实现了一些方面的API。端只维护一套逻辑;Android和iOS层主要作为适配层做一些操作系统界面对接和双端分化(尽可能)平滑。这里的切面是指JS层和Native/C++层的分界线。这里会实现一些方面的API,即JS层和Native/C++层交互的一系列接口,比如蓝牙接口,系统信息接口等。C++层实现接口,然后暴露给JS层,由JS层调用。3.基本实现原理整个项目最基本也是最重要的数据就是依赖关系。所谓依赖关系,最简单的例子就是文件A依赖于文件B的一个方法,要找出这种关系,一般来说需要两步。第一步:编译源码,获取AST。遍历所有源代码,通过语法分析生成抽象语法树(AbstractSyntaxTree,AST)。以JS扫描器为例,我使用typeScript模块作为编译器,它同时支持JS(X)和TS(X),通过ts.createSourceFile生成AST。除了JS,iOS用的是CLang,Android用的是字节码分析,C++用的是符号表分析。Step2:路径提取,依赖寻路从AST中,我们可以找到所有的引用和暴露表达式,以JS为例,import/require和export/module.exports。查找表达式的方法是递归遍历所有语法节点。在JS中,我使用TypeScript编译器提供的ts.forEachChild进行遍历,使用ts.SyntaxKind来识别语法节点类型。找到表达式后,通过依赖路径找到具体的依赖文件。以JS为例,我们可以使用const{identifierName}=require('@bundleName/fileName')在其他模块(bundleName)中引用一个文件(fileName)的某些标识符(identifierName),我们需要使用这个表达式用于定位特定的标识符。跨方面依赖将需要一个额外的步骤。切面API需要分为调用端和声明端。调用端的数据在JS层通过AST解析,声明端的数据在Native/C++层解析(对应切面API的具体实现.identifier),链接调用端的数据端和声明端通过版本号实现全依赖链接穿透。我们保存这个关系和一些元数据,可以作为数据分析的源数据。4.项目架构项目整体架构如下:我们使用Node.js和集团的egg.js框架搭建了这个依赖分析工程服务,考虑到数据使用场景的多变性和多样性,我选择了GraphQL作为查询接口.输出我们定义的数据类型,由上层应用自己封装。如果有多个上层应用同时需要类似的数据,我们也会整合复用。其中,数据处理模块是一个独立的模块,由Node.js编写,支持其他项目复用,未来计划在IDE等项目中复用。左边是我们的数据消费者,这里只是一些;右边是我们的数据库,用来存放分析结果;下方是四端扫描器和触发器,四个端分别获取各自平台的源代码。对于数据生产,触发器支持发布流程触发事件触发、定时触发、前端触发(应用端前端,非Web前端)和手动触发。五、应用场景及实现原理全链接依赖的使用场景具有无限的想象空间。这里有一些例子。影响范围判断(反向依赖分析)我们能想到的第一个应用场景就是影响范围判断,这也是我们项目的第一个出发点。大家可以想一想,如果我们维护一个接口(或者组件),会发现当用到的地方越来越多的时候,迭代它的风险也会相应增加。我们需要明确知道哪些地方调用了这个接口,用于确定有多少功能要回归测试,如何发布,如何做兼容等等,这就需要进行反向依赖分析。反向依赖关系是相对于在扫描器中分析的依赖关系而言的。扫描仪分析的内容称为前向依赖性。主要表示“这个模块依赖于哪些其他模块”;而反向依赖指的是“这个模块依赖于哪些模块”。那么很自然地,我们的反向依赖就是在正向依赖的基础上进行数据处理。(反向依赖查询页面)基于反向依赖数据,结合多版本数据,我们还可以计算“连续未引用版本数”来衡量离线接口的安全性。(某些切面API的连续未使用版本数)组件库、框架、切面API的维护者是该能力的重度用户,这为他们带来了数据支持,并阐明了他们的修改对其他模块的更改、发布决策和发布有多大影响回归测试。版本间变化分析在测试版本时,我们可以比较两个版本的依赖链,分析文件变化和整个影响环节,为QA提供一些数据支持,更准确的知道需要实现哪些功能。回归测试,哪些是不需要的。分析版本间变化的场景有很多。除了正常的版本迭代场景外,还有一个常见的场景:模块原封不动地集成到新版本的高德APP中,然后“Releasecode保持不变,但依赖的其他模块发生了变化”,尤其是Native/C++和公共模块,测试环境需要知道的是当前模块所依赖的其他模块发生了哪些变化,这些变化对本模块有什么影响,需要回归哪些功能点等主要消费者这份数据的大部分是QA同学,他们可以利用这份数据提高测试效率,找到没有考虑到的回归点趋势变化的判断前面说到,由于高德APP的时间跨度大,之前没有限制,我们的一些业务逻辑代码还是通过Native实现的,希望能逐步迁移到JS或者C++层,Native只是为了适配,判断这个gov的进展和效果需要从两个方面的数据来支持。一是每个平台上的代码行数。对此我们有专门的服务,暂时不提;另一个是接口趋势。接口趋势也分为调用端和声明端两种。按照我们治理的方向,我们预期的效果应该是:一条Native业务切面API的调用量随着版本/时间不断减少的曲线。当部分API的调用量为0后,API可以下线,随之而来的是另一条曲线——Native业务切面API的声明量也在下降。(从某个版本开始不断减少的切面API)(某个版本没有使用的切面API)进行架构治理和切面API治理的同学是这些数据的主要消费者。有了这些数据,他们就可以判断架构治理的趋势是否合理,某个方面的API是否可以下线等等。BundleSizeOptimization-Useless,DuplicateFileFinder我们也为BundleSizeOptimization做出了贡献。根据依赖数据,我们可以找到一些没有被引用或者内容完全相同(相同的md5值)的文件,这些文件也占用了大量的体积。我们使用依赖分析工程找到了数千张这样的图片,@1x@2x@3x文件是重灾区,我们发现很多图片伪装成另一种分辨率(我们甚至推动设计者提出了Graphnormalization和inspectiontoolsadded).6、写在最后以上是高德全链路依赖分析项目的基本概况。在具体实现中,会有无数的细节需要处理,比如各种历史问题、多级版本处理生成指数代码快照、Change分析产生指数分析结果等等,其中也涉及到很多编译原理、数据结构和算法(尤其是图结构)的知识,这是对编程能力和权衡能力的考验,最重要的是——弹性。欢迎大家一起讨论,一起爆出新点子、新场景!
