做无埋点的时候需要 hook 每个 View 的 click 事件,大体办法有两种:
- view.setAccessibilityDelegate
- Gradle 插件修改 class 文件
一开始做的时候选择的是第一种办法,遍历ViewTree来解决,本文着重记录第二种办法。
新建 Gradle 插件
第一步
在app同级目录下新建文件夹,文件夹名字必须为 buildSrc,新建 src->main->groovy->com.xxx.xxx 以及一个 build.Gradle 文件,
最终目录如图
图中所示的文件,除了src以及build.gradle之外的,都是系统后期自动编译生成,这步忽略不计。
第二步
在build.gradle文件中填入如下内容:
apply plugin: 'groovy'
dependencies {
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
}
repositories {
jcenter()
}
第三步
在 src 文件夹的包中新建具体的插件,gradle 插件基于 groovy 语言,所以新建的类文件拓展名为 .groovy
package com.netease.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
public class TestPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
}
}
第四步
在app模块的build.gradle中添加如下内容:
...
apply plugin: com.netease.plugin.TestPlugin
...
点击 clean project,然后 make project,当插件生效时,会调用 TestPlugin 的 apply 方法。
在 onClick 中增加代码
Gradle提供了一个Transform api,可以用来处理编译之后的class文件,大概原理如下图:
每个类文件都会通过一个又一个Transform
新建自定义 Transform
Transform 为抽象类,我们在 plugin 包中新建一个 ClickTransform,同样是 groovy 文件,代码如下:
package com.netease.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.netease.plugin.util.ClassUtil
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
import org.apache.commons.io.FileUtils
public class ClickTransform extends Transform {
private Project mProject
public ClickTransform(Project p) {
mProject = p
}
/**
* Returns the unique name of the transform.
*
* <p/>
* This is associated with the type of work that the transform does. It does not have to be
* unique per variant.
*/
@Override
String getName() {
return "ClickTransformImpl"
}
/**
* Returns the type(s) of data that is consumed by the Transform. This may be more than
* one type.
* <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
* Transform的输入类型
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
* Transform的作用范围
*/
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
}
}
如代码中所示,我们主要关注 transform 函数,对类的操作在这个函数进行。具体代码如下:
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
inputs.each { TransformInput input->
input.directoryInputs.each { DirectoryInput directoryInput->
//往类中注入代码
injectClick(directoryInput.file.getAbsolutePath(), "com", mProject)
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
//将 input 的目录复制到 output 指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each { JarInput jarInput ->
//往类中注入代码
injectClick(jarInput.file.getAbsolutePath(), "com.netease", mProject)
//重命名输出文件(同目录 copyFile 会冲突)
def jarName = jarInput.name
def md5Name = jarInput.file.hashCode()
if(jarName.endsWith(".jar")){
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}
private void injectClick(String path, String packageName,Project project) {
mPool.appendClassPath(path)
mPool.appendClassPath(project.android.bootClasspath[0].toString())
mPool.importPackage(IMPORT_CLASS_PATH)
File dir = new File(path)
if (dir.isDirectory()) {
dir.eachFileRecurse {
File file ->
String filePath = file.absolutePath
if (filePath.endsWith(".class") && !filePath.contains('R$')
&& !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")) {
int index = filePath.indexOf(packageName);
boolean isMyPackage = index != -1;
if (!isMyPackage) {
return
}
String className = ClassUtil.getClassName(index, filePath)
CtClass ctClass = mPool.getCtClass(className)
if (ctClass.isFrozen())
ctClass.defrost()
//遍历类中的所有方法,找到onClick函数
for (CtMethod method : ctClass.getDeclaredMethods()) {
//找到 onClick(View) 方法
if (checkOnClickMethod(method)) {
injectMethod(method)
ctClass.writeFile(path)
}
}
}
}
}
}
private static void injectMethod(CtMethod method) {
method.insertAfter("YXSConfigManager.getInstance().onInvokeClick(\$1);")
}
private static boolean checkOnClickMethod(CtMethod method) {
return method.getName().endsWith("onClick") && method.getParameterTypes().length == 1 &&
method.getParameterTypes()[0].getName().equals("android.view.View")
}
需要在插件的 build.gradle 中增加以下依赖:
dependencies {
compile 'com.android.tools.build:gradle:2.1.2'
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
compile 'org.javassist:javassist:3.20.0-GA'
compile 'com.android.tools.build:transform-api:1.5.0'
}
注册自定义Transform
我们写好了自己的 Transform 之后,需要在插件中注册。在自定义 Plugin 的 apply 函数中增加以下代码:
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
public class MyPlugin implements Plugin<Project> {
void apply(Project project) {
def android = project.extensions.getByType(AppExtension)
//注册一个Transform
def classTransform = new ClickTransform(project);
android.registerTransform(classTransform);
}
}