参考文章:
https://zhuanlan.zhihu.com/p/307382854
https://zhuanlan.zhihu.com/p/43609710
demo工程:https://github.com/TokenYc/LintDemo demo中内置了一些用于检测的Detector。
Lint用于检测静态代码和资源,找到其中不符合预定义规则的地方。
最近由于隐私问题抓的很紧,通过自定义lint,可以实现对三方sdk中涉及到用户隐私的方法进行检测。
自定义lint的最大坑在于:项目中的各种依赖版本不和,可能会导致执行lint失败。
所以,如果自己写的话,版本号还是按照demo中的来吧,测试能正常执行以后再结合项目更改版本。
包括:
- lint-api版本
compileOnly "com.android.tools.lint:lint-api:27.1.3"
- build tool
classpath "com.android.tools.build:gradle:4.1.3"
- appcompat 对,你没看错。
implementation 'androidx.appcompat:appcompat:1.2.0'
自定义Lint
如果是第一次接触lint,建议先按照步骤走,然后再去理解。
1.创建一个java module
在java module的build.gradle中添加
dependencies {
compileOnly "com.android.tools.lint:lint-api:27.1.3"
}
2.创建Detector和Issue
首先是Detector,代表一个检测器。继承Detector
,然后实现ClassScanner
。
文件放在创建的java module中即可。
为什么是ClassScanner?其实有多个Scanner可以使用,但是目前只测试了这个Scanner是可以扫描三方sdk的。
直接上代码,这里以检测调用获取IMEI方法为例(已省略部分方法内部代码):
package com.qianfanyun.lintlib.detector
import com.android.tools.lint.detector.api.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
class StealImeiDetector : Detector(), ClassScanner {
/**
* 返回这个 Detector 适用的 ASM 指令
*/
override fun getApplicableAsmNodeTypes(): IntArray? {
//这里关心的是与方法调用相关的指令,其实就是以 INVOKE 开头的指令集
return intArrayOf(AbstractInsnNode.METHOD_INSN)
}
/**
* 扫描到 Detector 适用的指令时,回调此接口
*/
override fun checkInstruction(context: ClassContext, classNode: ClassNode, method: MethodNode, instruction: AbstractInsnNode) {
}
}
重写了两个方法:
- getApplicableAsmNodeTypes 这个方法表示,扫描器要扫描哪些指令。我们这边返回的是Method,就是方法相关的指令。
- checkInstruction 这个方法提供了一些方法的描述,以便于进行筛选。
在理解了整体结构后,添加逻辑相关内容:
package com.qianfanyun.lintlib.detector
import com.android.tools.lint.detector.api.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
/**
* @date on 2019-12-23 14:31
* @author ArcherYc
* @mail [email protected]
*/
class StealImeiDetector : Detector(), ClassScanner {
/**
* 返回这个 Detector 适用的 ASM 指令
*/
override fun getApplicableAsmNodeTypes(): IntArray? {
//这里关心的是与方法调用相关的指令,其实就是以 INVOKE 开头的指令集
return intArrayOf(AbstractInsnNode.METHOD_INSN)
}
/**
* 扫描到 Detector 适用的指令时,回调此接口
*/
override fun checkInstruction(context: ClassContext, classNode: ClassNode, method: MethodNode, instruction: AbstractInsnNode) {
if (instruction.opcode != Opcodes.INVOKEVIRTUAL) {
return
}
val callerMethodSig = classNode.name + "." + method.name + method.desc
val methodInsn = instruction as MethodInsnNode
// 这里逻辑是:调用 NfcAdapter 中的任何方法都会报告异常
if (methodInsn.name == "getDeviceId"&&methodInsn.owner=="android/telephony/TelephonyManager") {
val message = "SDK 中 $callerMethodSig 调用了 " +
"${methodInsn.owner.substringAfterLast('/')}.${methodInsn.name} 的方法,需要注意!"
context.report(ISSUE, method, methodInsn, context.getLocation(methodInsn), message)
}
}
}
逻辑代码还是比较好理解的,就是通过比较方法的名称和方法所在类来找到我们要检测的方法。
然后通过report方法,添加到lint的检测报告中。
然后是Issue。Detector表示检测器,Issure则表示一个问题,可以理解为是对Detector的描述。一般直接作为静态成员,放在Detector中。创建的方法是比较固定的,具体参数表示什么,跑一遍就知道了。
package com.qianfanyun.lintlib.detector
import com.android.tools.lint.detector.api.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
/**
* @date on 2019-12-23 14:31
* @author ArcherYc
* @mail [email protected]
*/
class StealImeiDetector : Detector(), ClassScanner {
companion object {
val ISSUE = Issue.create(
"StealImei",//问题 Id
"",//问题的简单描述,会被 report 接口传入的描述覆盖
"",//问题的详细描述
Category.CORRECTNESS,//问题类型
6,//问题严重程度,0~10,越大严重
Severity.ERROR,//问题严重程度
//Detector 和 Scope 的对应关系
Implementation(
StealImeiDetector::class.java,
Scope.ALL_CLASSES_AND_LIBRARIES
)
)
}
/**
* 返回这个 Detector 适用的 ASM 指令
*/
override fun getApplicableAsmNodeTypes(): IntArray? {
//这里关心的是与方法调用相关的指令,其实就是以 INVOKE 开头的指令集
return intArrayOf(AbstractInsnNode.METHOD_INSN)
}
/**
* 扫描到 Detector 适用的指令时,回调此接口
*/
override fun checkInstruction(context: ClassContext, classNode: ClassNode, method: MethodNode, instruction: AbstractInsnNode) {
if (instruction.opcode != Opcodes.INVOKEVIRTUAL) {
return
}
val callerMethodSig = classNode.name + "." + method.name + method.desc
val methodInsn = instruction as MethodInsnNode
// 这里逻辑是:调用 NfcAdapter 中的任何方法都会报告异常
if (methodInsn.name == "getDeviceId"&&methodInsn.owner=="android/telephony/TelephonyManager") {
val message = "SDK 中 $callerMethodSig 调用了 " +
"${methodInsn.owner.substringAfterLast('/')}.${methodInsn.name} 的方法,需要注意!"
context.report(ISSUE, method, methodInsn, context.getLocation(methodInsn), message)
}
}
}
到这里,最核心的部分就完成了。
3.创建Registry
(1)需要通过一个Registry来讲Issue注册进lint。
代码很简单,需要注册的Issue直接加载list中即可。
api返回固定CURRENT_API即可。
package com.qianfanyun.lintlib
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
import com.qianfanyun.lintlib.detector.*
import java.util.*
class DangerIssueRegistry : IssueRegistry() {
override val issues: List
get() {
return Arrays.asList(
StealImeiDetector.ISSUE,
)
}
override val api: Int
get() = CURRENT_API
}
(2)在java module的build.gradle中添加
jar {
manifest {
attributes("Lint-Registry-v2": "com.qianfanyun.lintlib.DangerIssueRegistry")
}
}
4.在app的build.gradle中引入
在app的build.gradle中引入
dependencies {
lintChecks project(':lintlib')
}
5.执行
在命令行中执行
gradle app:lintDebug
如果你的项目中添加了Varint,则需要在lintDebug之间加上对应的名称。对应的命令变为
gradle app:lintxxxxDebug
执行后,如果一切顺利,会出现两个链接,一个是html格式的,一个是xml格式的。
点击就可以查看了。html格式的可以在浏览器中查看。
大致就像这样
这样,就可以找到第三方sdk中涉及到隐私政策的代码了。