做無(wú)埋點(diǎn)的時(shí)候需要 hook 每個(gè) View 的 click 事件,大體辦法有兩種:
- view.setAccessibilityDelegate
- Gradle 插件修改 class 文件
一開始做的時(shí)候選擇的是第一種辦法,遍歷ViewTree來(lái)解決,本文著重記錄第二種辦法。
新建 Gradle 插件
第一步
在app同級(jí)目錄下新建文件夾,文件夾名字必須為 buildSrc,新建 src->main->groovy->com.xxx.xxx 以及一個(gè) build.Gradle 文件,
最終目錄如圖

圖中所示的文件,除了src以及build.gradle之外的,都是系統(tǒng)后期自動(dòng)編譯生成,這步忽略不計(jì)。
第二步
在build.gradle文件中填入如下內(nèi)容:
apply plugin: 'groovy'
dependencies {
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
}
repositories {
jcenter()
}
第三步
在 src 文件夾的包中新建具體的插件,gradle 插件基于 groovy 語(yǔ)言,所以新建的類文件拓展名為 .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中添加如下內(nèi)容:
...
apply plugin: com.netease.plugin.TestPlugin
...
點(diǎn)擊 clean project,然后 make project,當(dāng)插件生效時(shí),會(huì)調(diào)用 TestPlugin 的 apply 方法。
在 onClick 中增加代碼
Gradle提供了一個(gè)Transform api,可以用來(lái)處理編譯之后的class文件,大概原理如下圖:
每個(gè)類文件都會(huì)通過(guò)一個(gè)又一個(gè)Transform
新建自定義 Transform
Transform 為抽象類,我們?cè)?plugin 包中新建一個(gè) 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 {
}
}
如代碼中所示,我們主要關(guān)注 transform 函數(shù),對(duì)類的操作在這個(gè)函數(shù)進(jìn)行。具體代碼如下:
@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 的目錄復(fù)制到 output 指定目錄
FileUtils.copyDirectory(directoryInput.file, dest)
}
input.jarInputs.each { JarInput jarInput ->
//往類中注入代碼
injectClick(jarInput.file.getAbsolutePath(), "com.netease", mProject)
//重命名輸出文件(同目錄 copyFile 會(huì)沖突)
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函數(shù)
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'
}
注冊(cè)自定義Transform
我們寫好了自己的 Transform 之后,需要在插件中注冊(cè)。在自定義 Plugin 的 apply 函數(shù)中增加以下代碼:
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)
//注冊(cè)一個(gè)Transform
def classTransform = new ClickTransform(project);
android.registerTransform(classTransform);
}
}