一、目的

通过标识关键代码,利用Lint定期检测关键代码,当关键代码发生变更时,可以在报告中体现,避免引入意外的修改。

二、Lint

Lint介绍 Lint 作为一种工具程序,主要负责静态源码分析,负责代码规范、代码缺陷等检测。(https://zh.wikipedia.org/wiki/Lint) Android 原生已经提供了 Lint 静态分析工具,而 iOS 则有对应的 OCLint 和 SwiftLint。

三、android实现代码监控的原理

Android Lint可以对Java/Kotlin源码使用uast进行语法分析,最后生成一棵AST抽象语法树,这棵树的每个节点都是一个方法,包含方法签名和方法体。 通过自定义的Lint规则,找到应用启动的入口方法。从入口方法开始遍历整棵调用树,即可对每个方法节点的代码段进行分析。

1. 获取关键代码段

open fun shouldMonitor(node: UMethod): Boolean {
    return node.getAnnotations().find { it.qualifiedName == LINT_MONITOR_ANNOTATION } != null
}

android通过注解的方式,标识需要检测的方法实现。通过Android Lint的自定义规则判断注解标识的方法,找到入口方法,然后对代码段进行分析。

iOS对应的OCLint也可以通过自定义规则进行静态分析,但是iOS没有注解,iOS如果需要标识关键代码段,目前想到的只能hardcode检测列表(包括类和方法名)来实现标识关键代码段的效果。但是这样的维护成本比较大。

iOS可以利用类似以下的代码片段进行类和方法的判断:

for(auto it = parent->meth_begin(), end = parent->meth_end(); it != end; ++it) {
    const auto method = *it;
    if(declHasEnforceAttribute(method, *this)) {
        const auto selector = method->getSelector();
        if(!implementation->getMethod(selector, method->isInstanceMethod())) {
            const string className = parent->getNameAsString();
            const string methodName = selector.getAsString();
            addViolation(implementation, this,
                "subclasses of " + className + " must implement " + methodName);
        }
    }
}

注:在生成OCLint自定义规则工程时,Xcode 12会因架构问题出错,暂未找到解决方案,可暂时先切换Xcode 11生成OCLint自定义规则工程

2. 给Report增加代码diff功能

报告中可视化的代码diff功能主要是基于这个开源库diff2html。但原生的Lint Report不支持直接嵌入html的,这里通过重写了HtmlReporter,实现html注入,详细代码可以参考 CodeLink

获取到代码片段后,安卓利用diff2html工具可显示代码修改记录,理论上iOS只要获取到代码片段后,也可以利用这个工具进行diff显示。

四、iOS平台的OCLint初步使用

1. 流程

cd78689fe86fb527954e6d23d23e1f74

2. 安装OCLint

参考官方文档:http://docs.oclint.org/en/stable/

安装方式有三种:

  1. Homebrew安装
  2. 源码编译安装
  3. 安装包安装

这里使用Homebrew安装会比较方便:

Installing OCLint

$ brew tap oclint/formulae
$ brew install oclint

利用oclint --version命令验证OCLint安装成功

9089c13862d19b5e7d143037579c0aa8

4. 安装xcpretty

xcpretty用于对xcodebuild的输出进行格式化(https://github.com/xcpretty/xcpretty

安装xcpretty

$ gem install xcpretty

5. 执行命令

# 生成编译数据
xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json

# 生成报表
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
-rc LONG_LINE=200 \
-disable-rule ShortVariableName \
-disable-rule ObjCAssignIvarOutsideAccessors \
-disable-rule AssignIvarOutsideAccessors \
-max-priority-1=100000 \
-max-priority-2=100000 \
-max-priority-3=100000

利用浏览器打开以上命令生成的oclintReport.html,即可以查看报告

6. 报告

db12d8c7c724d2788da66be29c528704

7. 错误

1、oclint: error: one compiler command contains multiple jobs

将Project和Targets中Building Settings下的COMPILER_INDEX_STORE_ENABLE设置为NO

解决方案:

# Podfile文件添加如下
post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
        end
    end
end
https://github.com/oclint/oclint/issues/462

2、扫描的生成的compile_commands.json文件过大

Traceback (most recent call last):
  File "/usr/local/bin/oclint-json-compilation-database", line 86, in <module>
    exit_code = subprocess.call(oclint_arguments)
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 172, in call
    return Popen(*popenargs, **kwargs).wait()
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 394, in __init__
    errread, errwrite)
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1047, in _execute_child
    raise child_exception
OSError: [Errno 7] Argument list too long

解决方案:

  1. 通过自定义规则过滤不重要的报错,减少分析文件大小
  2. 网上有解决方案,但目前我尝试过并不能解决,具体原因未知
https://github.com/oclint/oclint/issues/233
https://github.com/wuwen1030/oclint_argument_list_too_long_solution/tree/master

五、结论

1、查询较多文档发现,OCLint多用于代码的静态分析,约束代码成员的代码风格。问过平台组的同事,表示OCLint的静态分析不大靠谱; 2、如果要达到代码监控的目的,必须自定义规则,缺少注解功能的OC语言不太好标识关键代码。且自定义规则需要翻阅OCLint开放的接口和学习C++语言,有比较高的入门门槛;

综上所述,方案应该可行,但受限于语言特性,OC的实现难度会更大,且最终的效果可能达不到android的效果。

六、参考

http://docs.oclint.org/en/stable/ http://yulingtianxia.com/blog/2019/01/27/MVVM-Rules-for-OCLint/ https://juejin.im/post/6844903940375445517#heading-1