作为一名Android码农,相信大家也有跟我一样的感觉,很多框架或者第三方的SDK我们是只会用,但是很少去了解如何实现和它的原理是什么。主要也不是我比较懒,而是工作环境的影响很少有时间去研究。不过想成为一名技术资深的码农,了解原理并且学会自己造轮子是必走的路,我也开始反思了。
好了言归正传, 相信很多同僚都知道build.gradle的作用,负责构建我们的App,语言用的是Groovy,但是其实里面真正起作用的是Plugin插件,如下图
导入了一个id为xxx的插件,其中android{}都是这个插件的Task,gradle作用就是把一个一个Plugin一次执行。
我这次要演示的批量给每个Activity的进入和退出插入Toast(第三方统计的方案有类似实现)。
一般我们如果需要App每个页面的进入和退出加入埋点,我们初级版本会每个类的onCreate和onDestroy加入自己的埋点代码,高级版本可以自己封装一个BaseActivity,所有Activity继承它。而我今天要使用的是一个便于维护在各个App的方案,后续还可以看自己需要放到maven库。
我们需要在java文件生成Class到Dex文件之间去修改类的Class文件,这样就能够不修改源码的情况下无感知的加入了我们的代码。
删除掉所有文件,只保留main目录(目录下文件清空)和build.gradle
main\groovy目录下创建自己的包路径,我这里是cn.berfy.gradle.plugin。
在main目录下创建resources文件夹,新建META-INF\gradle-plugins目录,这里是存放最终给其他build.gradle调用apply的路径名。
新建cn.berfy.gradle.plugin.properties文件,编写
implementation-class=cn.berfy.gradle.plugin.TestPlugi
创建自己的包路径,我这里是cn.berfy.gradle.plugin
编写build.gradle文件
开头导入groovy,如果想用kotlin开发插件也可以导入。
apply plugin: 'maven-publish'
加入maven发布功能
dependencies 下引入gradleApi(gradleApi)和localGroovy(groovy Api)
引入com.android.tools.build:gradle支持
引入org.ow2.asm:asm:7.1(这个是真正修改字节码的Api)
groupId 你的maven库要显示的包名
artifactId
你的库:后面的名字
version
版本
repositories
是maven生成的repo文件路径,可以自定义,这里稍加修改可以自动上传到maven仓库
apply plugin: 'groovy'
apply plugin: 'kotlin'
//apply plugin: 'java-gradle-plugin'
//apply plugin: 'cn.berfy.gradle.plugin.TestPlugin'
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.4.1'
implementation 'org.ow2.asm:asm:7.1'
}
apply plugin: 'maven-publish'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
publishing {
publications {
mavenJava(MavenPublication) {
groupId 'cn.berfy.gradle.plugin'
artifactId 'berfy'
version '1.0.0'
from components.java
}
}
}
publishing {
repositories {
maven {
// change to point to your repo, e.g. http://my.org/repo
url uri('src/main/repos')
}
}
}
----------------------------------------------------------------
groovy目录下你的包名根路径新建一个TestPlugin.groovy文件,内容为
AppExtension就是apply plugin: 'com.android.application'这个插件的扩展。
我们检测到这个Plugin就传递给MyClassTransform负责拦截App编译后的操作。
package cn.berfy.gradle.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.gradle.AppExtension
class TestPlugin implements Plugin {
void apply(Project project) {
def android = project.extensions.getByType(AppExtension)
//注册一个Transform
def logTransform = new MyClassTransform(project)
android.registerTransform(logTransform)
}
}
新建MyClassTransform.groovy,负责class文件和jar文件的拦截,便于我们队class字节码修改。
getInputType这里用的是Class的枚举,拦截编译到class这步。
package cn.berfy.gradle.plugin;
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.IOException
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Set;
/**
* Created by 刘镓旗 on 2017/8/30.
*/
public class MyClassTransform extends Transform {
private Project mProject;
public MyClassTransform(Project p) {
this.mProject = p;
}
//transform的名称
//transformClassesWithMyClassTransformForDebug 运行时的名字
//transformClassesWith + getName() + For + Debug或Release
@Override
public String getName() {
return "MyClassTransform";
}
//需要处理的数据类型,有两种枚举类型
//CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源
@Override
public Set getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
// 指Transform要操作内容的范围,官方文档Scope有7种类型:
//
// EXTERNAL_LIBRARIES 只有外部库
// PROJECT 只有项目内容
// PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
// PROVIDED_ONLY 只提供本地或远程依赖项
// SUB_PROJECTS 只有子项目。
// SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
// TESTED_CODE 由当前变量(包括依赖项)测试的代码
@Override
public Set getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
//指明当前Transform是否支持增量编译
@Override
public boolean isIncremental() {
return false;
}
// Transform中的核心方法,
// inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
// outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
// @Override
// public void transform(Context context,
// Collection inputs,
// Collection referencedInputs,
// TransformOutputProvider outputProvider,
// boolean isIncremental) throws IOException, TransformException, InterruptedException {
// super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
// println("字节码插桩的地方 哈哈哈哈")
// }
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println("==============================TracePlugin visit start========================================")
def isIncremental = transformInvocation.isIncremental()
//OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
def outputProvider = transformInvocation.outputProvider
if (!isIncremental) {
//不需要增量编译,先清除全部
outputProvider.deleteAll()
}
transformInvocation.inputs.forEach { input ->
input.jarInputs.forEach { jarInput ->
//处理Jar
println("fix Jar name=" + jarInput.name)
processJarInput(jarInput, outputProvider, isIncremental)
}
input.directoryInputs.forEach { directoryInput ->
//处理文件
println("fix file name=" + directoryInput.name)
processDirectoryInput(directoryInput, outputProvider, isIncremental)
}
}
println("==============================TracePlugin visit end========================================")
}
//============================================jar文件修改总入口=======================================================================
//jar输入文件 修改
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
def dest = outputProvider.getContentLocation(
jarInput.file.absolutePath,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR)
if (isIncremental) {
//处理增量编译
processJarInputIncremental(jarInput, dest)
} else {
//不处理增量编译
processJarInputNoIncremental(jarInput, dest)
}
}
//jar 没有增量的修改
void processJarInputNoIncremental(JarInput jarInput, File dest) {
transformJarInput(jarInput, dest)
}
//jar 增量的修改
void processJarInputIncremental(JarInput jarInput, File dest) {
switch (jarInput.status) {
case Status.NOTCHANGED:
break;
case Status.ADDED:
//真正transform的地方
transformJarInput(jarInput, dest)
break;
case Status.CHANGED:
//Changed的状态需要先删除之前的
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
//真正transform的地方
transformJarInput(jarInput, dest)
break;
case Status.REMOVED:
//移除Removed
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
break;
}
}
//真正执行jar修改的函数
void transformJarInput(JarInput jarInput, File dest) {
FileUtils.copyFile(jarInput.file, dest)
}
//============================================================文件及文件夹修改总入口======================================================================
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
def dest = outputProvider.getContentLocation(
directoryInput.file.absolutePath,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)
println("fix processDirectoryInput isIncremental=" + isIncremental)
if (isIncremental) {
//处理增量编译
processDirectoryInputIncremental(directoryInput, dest)
} else {
processDirectoryInputNoIncremental(directoryInput, dest)
}
}
//文件无增量修改
void processDirectoryInputNoIncremental(DirectoryInput directoryInput, File dest) {
println("fix processDirectoryInputNoIncremental ")
transformDirectoryInput(directoryInput, dest)
}
//文件增量修改
void processDirectoryInputIncremental(DirectoryInput directoryInput, File dest) {
println("fix processDirectoryInputIncremental ")
FileUtils.forceMkdir(dest)
def srcDirPath = directoryInput.file.absolutePath
def destDirPath = dest.absolutePath
def fileStatusMap = directoryInput.changedFiles
fileStatusMap.forEach { entry ->
val inputFile = entry.key
val status = entry.value
val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath)
val destFile = File(destFilePath)
switch (status) {
case Status.NOTCHANGED:
break;
case Status.ADDED:
//真正transform的地方
transformDirectoryInput(directoryInput, dest)
break;
case Status.CHANGED:
//处理有变化的
FileUtils.touch(destFile)
//Changed的状态需要先删除之前的
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
//真正transform的地方
transformDirectoryInput(directoryInput, dest)
break;
case Status.REMOVED:
//移除Removed
if (destFile.exists()) {
FileUtils.forceDelete(destFile)
}
break;
}
}
}
//真正执行文件修改的地方
void transformDirectoryInput(DirectoryInput directoryInput, File dest) {
println("fix transformDirectoryInput ")
// directoryInput.forEach { directoryInput: DirectoryInput? ->
//是否是目录
if (directoryInput.file.isDirectory()) {
println("fix transformDirectoryInput isDirectory ")
List files = new ArrayList<>()
findAllFiles(directoryInput.file.listFiles(), files)
for (File file : files) {
def name = file.name
//在这里进行代码处理
if (name.endsWith(".class") && !name.startsWith("R\$")
&& "R.class" != name && "BuildConfig.class" != name) {
ClassReader classReader = new ClassReader(file.readBytes())
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
def className = name.split(".class")[0]
println("class fix chazhuang " + className)
def classVisitor = new TraceVisitor(className, classWriter)
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
def code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(file.parentFile.absoluteFile.toString() + File.separator + name)
fos.write(code)
fos.close()
}
}
} else {
println("fix transformDirectoryInput isFile ")
def name = directoryInput.file.name
//在这里进行代码处理
if (name.endsWith(".class") && !name.startsWith("R\$")
&& "R.class" != name && "BuildConfig.class" != name) {
// ClassReader classReader = ClassReader(file.readBytes())
// ClassWriter classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
// def className = name.split(".class")[0]
println("class fix hahahaha " + name)
}
}
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyDirectory(directoryInput.file, dest)
}
void findAllFiles(File[] files, List outFiles) {
if (null == outFiles) {
return
}
for (File out : files) {
if (out.isDirectory()) {
findAllFiles(out.listFiles(), outFiles)
} else {
outFiles.add(out)
}
}
}
}
transform(TransformInvocation transformInvocation)方法就是拦截到的class文件和jar包。
通过获取到class文件,修改,然后修改后的文件复制到输出路径中完成class的替换。这其中字节码的修改就是ASM的操作了,操作类是TraceVisitor.java。
创建TraceVisitor.java
package cn.berfy.gradle.plugin;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;
/**
* 对继承自AppCompatActivity的Activity进行插桩
*/
public class TraceVisitor extends ClassVisitor {
/**
* 类名
*/
private String className;
/**
* 父类名
*/
private String superName;
/**
* 该类实现的接口
*/
private String[] interfaces;
public TraceVisitor(String className, ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor);
}
/**
* ASM进入到类的方法时进行回调
*
* @param access
* @param name 方法名
* @param desc
* @param signature
* @param exceptions
* @return
*/
@Override
public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature,
String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {
private boolean isInject() {
//如果父类名是AppCompatActivity则拦截这个方法,实际应用中可以换成自己的父类例如BaseActivity
if (superName.contains("AppCompatActivity")) {
return true;
}
return false;
}
@Override
public void visitCode() {
super.visitCode();
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return super.visitAnnotation(desc, visible);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
super.visitFieldInsn(opcode, owner, name, desc);
}
/**
* 方法开始之前回调
*/
@Override
protected void onMethodEnter() {
if (isInject()) {
if ("onCreate".equals(name)) {
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC,
"cn/berfy/demo/gradleplugin/asm/TraceUtil",
"onActivityCreate", "(Landroid/app/Activity;)V",
false);
} else if ("onDestroy".equals(name)) {
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC, "com/xuexuan/androidaop/traceutils/TraceUtil"
, "onActivityDestroy", "(Landroid/app/Activity;)V", false);
}
}
}
/**
* 方法结束时回调
* @param i
*/
@Override
protected void onMethodExit(int i) {
super.onMethodExit(i);
}
};
return methodVisitor;
}
/**
* 当ASM进入类时回调
*
* @param version
* @param access
* @param name 类名
* @param signature
* @param superName 父类名
* @param interfaces 实现的接口名
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
this.interfaces = interfaces;
}
}
ClassVisitor是对类的字节码操作,MethodVisitor是方法。
isInject()拦截类的类型,这里我们只处理AppCompatActivity。
onMethodEnter()方法执行的开始。我们在这里增加自己的代码。
具体的代码意思就是在onCreate方法开始执行的时候,插入cn.berfy.demo.gradleplugin/asm/TraceUtil.class类中的onActivityCreate的方法中的内容。具体可以学习下ASM语法和JVM字节码说明。
TraceUtil.class类可以写在module或者App module中,只要保证运行的class包名正确即可。
这样每个Acitivity就会在onCreate生命周期是调用我们自己类的方法(也不是调用,是插桩,可以叫代码复制)。
我的App module下的TraceUtil.class
package cn.berfy.demo.gradleplugin.asm;
import android.app.Activity;
import android.widget.Toast;
import cn.berfy.demo.gradleplugin.MainActivity;
/**
* Created by will on 2018/3/9.
*/
public class TraceUtil
{
private final String TAG = "TraceUtil";
/**
* 当Activity执行了onCreate时触发
*
* @param activity
*/
public static void onActivityCreate(Activity activity)
{
Toast.makeText(activity, MainActivity.class.getName() + " 我是插桩的检测代码", Toast.LENGTH_LONG).show();
// System.out.println(MainActivity.class);
}
/**
* 当Activity执行了onDestroy时触发
*
* @param activity
*/
public static void onActivityDestroy(Activity activity)
{
Toast.makeText(activity, activity.getClass().getName() + "call onDestroy", Toast.LENGTH_LONG).show();
}
}
插件到这里就编写好了
我们到terminal中执行gradlew publish等待成功。如果失败看一眼log,很好理解。
成功之后main\repo\目录就会生成对应你plugin下的build.gradle中的groupId\artifactId\version\的pom源文件。
关键的关键
项目根目录build.gradle加入maven库和classPath
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
maven {//local maven repo path
url uri('plugin/src/main/repos')
}
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72"
classpath 'cn.berfy.gradle.plugin:berfy:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
App module的build.gradle
plugins {
id 'com.android.application'
}
apply plugin: 'cn.berfy.gradle.plugin.TestPlugin'