前言
最近有些朋友一直在问ARouter是如何实现的,于是我又把ARouter的源码搂
(lou)了一遍。首先抛开注解处理器和拦截器,还有它内部的一些服务,诸如
AutoWired,build,navigation之类的逻辑。他在编译期间通过gradle-plugin来对class字节码做了一些操作,那么gradle-plugin是什么?
其实每一个Android开发都接触过他,不管是新建一个application module还是library module的时候在头部都有一个apply plugin。其实他是studio帮我们提供的一个gradle插件。
打包apk流程
接下来我们先考虑一下这个插件是什么时候起作用的?他又做了哪些事情?这个就需要我们对打包流程有一定的了解了。
打包流程.png
gradle-plugin中的Transform在字节码打包成dex文件的过程中会被执行。application的插件AppPlugin,它主要就干了检测Gradle版本,初始化插件初始化器和profiler,校验path和module的错误等等。附上一部分源码,感兴趣可以自己去看看。
private void basePluginApply(@NonNull Project project) {
// We run by default in headless mode, so the JVM doesn't steal focus.
System.setProperty("java.awt.headless", "true");
this.project = project;
this.projectOptions = new ProjectOptions(project);
checkGradleVersion(project, getLogger(), projectOptions);
DependencyResolutionChecks.registerDependencyCheck(project, projectOptions);
project.getPluginManager().apply(AndroidBasePlugin.class);
checkPathForErrors();
checkModulesForErrors();
PluginInitializer.initialize(project);
ProfilerInitializer.init(project, projectOptions);
threadRecorder = ThreadRecorder.get();
// initialize our workers using the project's options.
Workers.INSTANCE.initFromProject(
projectOptions,
// possibly, in the future, consider using a pool with a dedicated size
// using the gradle parallelism settings.
ForkJoinPool.commonPool());
ProcessProfileWriter.getProject(project.getPath())
.setAndroidPluginVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION)
.setAndroidPlugin(getAnalyticsPluginType())
.setPluginGeneration(GradleBuildProject.PluginGeneration.FIRST)
.setOptions(AnalyticsUtil.toProto(projectOptions));
if (!projectOptions.get(BooleanOption.ENABLE_NEW_DSL_AND_API)) {
threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,
project.getPath(),
null,
this::configureProject);
threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,
project.getPath(),
null,
this::configureExtension);
threadRecorder.record(
ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,
project.getPath(),
null,
this::createTasks);
} else {
// Apply the Java plugin
project.getPlugins().apply(JavaBasePlugin.class);
// create the delegate
ProjectWrapper projectWrapper = new ProjectWrapper(project);
PluginDelegate<E> delegate =
new PluginDelegate<>(
project.getPath(),
project.getObjects(),
project.getExtensions(),
project.getConfigurations(),
projectWrapper,
projectWrapper,
project.getLogger(),
projectOptions,
getTypedDelegate());
delegate.prepareForEvaluation();
// after evaluate callbacks
project.afterEvaluate(
CrashReporting.afterEvaluate(
p -> {
threadRecorder.record(
ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,
p.getPath(),
null,
delegate::afterEvaluate);
}));
}
}
自定义一个Android Plugin
如果我们可以插手编译的过程,我们是不是可以动态的添加一些代码到源码里面?
接下来我们动态的往Activity的字节码中的onCreate方法中插入一条日志和一行埋点的代码,定义Plugin需要完成以下几步
- 新建一个module
1.将module中除了libs,src,build.gradle文件之外的删除
2.将src中的java改成groovy,并且删除所有的java文件 - 创建一个xxPlugin.groovy文件
- 在main文件夹下创建resources文件夹
- 在resources文件夹下创建META-INF文件夹
- 在META-INF文件夹下创建gradle-plugins文件夹
- 在gradle-plugins文件夹下创建xxx.properties文件,这里的xxx是插件在使用时候的名字
- 在xxx.properties中将上面编写的plugin.groovy的全路径配置进去
# 等号后面的是我的插件的路径
implementation-class=com.domain.android.testplugin.TestPlugin
- build.gradle文件的修改,将文件中的内容全部删除
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation gradleApi() //gradle sdk
implementation localGroovy() //groovy sdk
implementation 'com.android.tools.build:gradle:3.4.1' //最好和自己主项目的版本一致
implementation 'org.javassist:javassist:3.24.0-GA'
}
repositories {
jcenter()
}
//打包
uploadArchives {
repositories.mavenDeployer {
//本地仓库路径,以放到项目根目录下的 repo 的文件夹为例
repository(url: uri('../repo'))
//在依赖的时候会用到groupId,artifactId和version
//groupId ,自行定义,组织名或公司名
pom.groupId = 'com.domain'
//artifactId,自行定义,项目名或模块名
pom.artifactId = 'testplugin.android'
//插件版本号
pom.version = '1.0.0'
}
}
插件的目录结构.png
plugin使用
-
plugin module编译通过后,点击uploadArchives进行打包。
打包plugin.png - 在项目的build.gradle中添加如下代码
buildscript {
repositories {
//由于plugin打包到了本地,所以要加本地的maven地址
maven {
url uri('repo')
}
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:3.4.1"
//com.domain为plugin的groupId,testplugin.android为artifactId,1.0.0为version。
//这些都是在插件的module中的build.gradle中配置的
classpath "com.domain:testplugin.android:1.0.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
- 在需要使用的module中配置apply plugin,例如app module的build.gradle中
apply plugin: 'com.android.application'
//testplugin为插件module中testplugin.properties文件的文件名
apply plugin: 'testplugin'
到此为止整个插件就配置好了。每次使用只需要uploadArchives 插件模块打包,然后Rebuild Project就可以了。
编写Plugin中的逻辑,无痕埋点
transform.png当我们编译的时候会调用到transform,相当于一个Task,如果我们自定义的化会优先执行我们的,然后才会执行默认的。当然我们也可以定义多个transform。
- plugin文件代码
package com.domain.android.testplugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
public class TestPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
def transform = new TestPluginTransform(project)
def extension = project.extensions.findByType(AppExtension.class)
extension.registerTransform(transform)
}
}
- transform代码,最关键的代码就在transform方法中,需要对jar包和class文件分别处理,在下面的代码中我们只对class文件进行插入
package com.domain.android.testplugin
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
class TestPluginTransform extends Transform{
Project project
TestPluginTransform(Project project){
this.project=project
}
@Override
String getName() {
return "TestPluginTransform"
}
//字节码
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
//整个项目的处理
@Override
Set<? super 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 {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
println("transform old")
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println("transform")
def addCode='android.util.Log.i("plugin","plugin info");'
def outputProvider=transformInvocation.outputProvider
transformInvocation.inputs.each { TransformInput it ->
// scan all jars
it.jarInputs.each { JarInput jarInput ->
String destName = jarInput.name
// rename jar files
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)
}
// input file
File src = jarInput.file
// output file
File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(src, dest)
}
println("transform jars end")
// scan class files
it.directoryInputs.each { DirectoryInput directoryInput ->
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
println("transform Directory add before")
// AddCodeUtils.addCode(directoryInput.file.absolutePath, project, addCode)
String root = directoryInput.file.absolutePath
if (!root.endsWith(File.separator))
root += File.separator
directoryInput.file.eachFileRecurse { File f->
def path=f.getAbsolutePath().replace(root,"")
if (f.isFile()&& path.startsWith("com/domain/android/test/")) {
println(path)
def bytes = Util.scanClass(f, path)
def outputStream = new FileOutputStream(f.path)
outputStream.write(bytes)
outputStream.close()
}
}
// copy to dest
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}
- Util代码,主要用于扫描class文件,并且插入代码
package com.domain.android.testplugin
import org.objectweb.asm.*
class Util{
public static byte[] scanClass(File file,String path){
def inputStream = new FileInputStream(file)
ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw,path)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
def byteArray = cw.toByteArray()
inputStream.close()
return byteArray
}
static class ScanClassVisitor extends ClassVisitor{
private String superName
String path
boolean isHasOnCreate=false
ScanClassVisitor(int api, ClassVisitor cv,String path) {
super(api, cv)
this.path=path
}
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.superName=superName
super.visit(version, access, name, signature, superName, interfaces)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv=super.visitMethod(access, name, desc, signature, exceptions)
println("method name"+name+"...desc="+desc)
if (isActivity(superName)&&name=="onCreate"&&desc=="(Landroid/os/Bundle;)V"){
println("superName"+superName)
mv=new InsertCodeClassVisitor(Opcodes.ASM5,mv,path)
isHasOnCreate=true
}
return mv
}
//扫描完之后如果Activity文件中没有onCreate方法需要我们手动添加onCreate
@Override
void visitEnd() {
if (isActivity(superName)&&!isHasOnCreate){
println("activity end")
MethodVisitor methodVisitor = visitMethod(Opcodes.ACC_PUBLIC, "onCreate", "(Landroid/os/Bundle;)V", null, null)
methodVisitor.visitVarInsn(Opcodes.ALOAD,0)
methodVisitor.visitVarInsn(Opcodes.ALOAD,1)
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
superName,"onCreate","(Landroid/os/Bundle;)V",false)
methodVisitor.visitInsn(Opcodes.RETURN)
methodVisitor.visitMaxs(2, 2)
isHasOnCreate=true
methodVisitor.visitEnd()
}
super.visitEnd()
}
private static boolean isActivity(String name){
return name=="androidx/appcompat/app/AppCompatActivity" || name=="com/domain/android/test/MainActivity"
}
}
static class InsertCodeClassVisitor extends MethodVisitor{
String path
InsertCodeClassVisitor(int api, MethodVisitor mv,String path) {
super(api, mv)
this.path=path
}
@Override
void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
//添加Log代码
mv.visitLdcInsn("TAG")
mv.visitLdcInsn(path)
mv.visitMethodInsn(Opcodes.INVOKESTATIC
,"android/util/log"
,"i"
,"(Ljava/lang/String;Ljava/lang/String;)I"
,false)
//添加埋点代码
mv.visitVarInsn(Opcodes.ALOAD,0)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL
,"java/lang/Object","getClass","()Ljava/lang/Class;",false)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL
,"java/lang/Class","getSimpleName","()Ljava/lang/String;",false)
mv.visitMethodInsn(Opcodes.INVOKESTATIC
,"com/domain/android/test/trace/Trace"
,"traceInPage"
,"(Ljava/lang/String;)V"
,false)
}
super.visitInsn(opcode)
}
}
}
动态插入代码就完成了,但是可能很多同学不知道字节码如何去编写,那么可以在android studio中安装一个bytecode插件。
拿出来一段代码,举个例子
MethodVisitor methodVisitor = visitMethod(Opcodes.ACC_PUBLIC, "onCreate", "(Landroid/os/Bundle;)V", null, null)
methodVisitor.visitVarInsn(Opcodes.ALOAD,0)
methodVisitor.visitVarInsn(Opcodes.ALOAD,1)
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
superName,"onCreate","(Landroid/os/Bundle;)V",false)
methodVisitor.visitInsn(Opcodes.RETURN)
methodVisitor.visitMaxs(2, 2)
使用ASM的bytecode插件显示的class代码,大家对比一下其实是一摸一样的
// access flags 0x4
protected onCreate(Landroid/os/Bundle;)V
L0
LINENUMBER 8 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL com/domain/android/test/MainActivity.onCreate (Landroid/os/Bundle;)V
L1
LINENUMBER 9 L1
RETURN
L2
LOCALVARIABLE this Lcom/domain/android/test/AActivity; L0 L2 0
LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
app模块的代码
- Trace,埋点类
package com.domain.android.test.trace;
import android.util.Log;
public class Trace {
public static void traceInPage(String className){
Log.e("a","traceInPage");
}
}
- MainActivity
package com.domain.android.test;
import android.os.Bundle;
import android.os.PersistableBundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
public static void a(){
System.out.println("a");
}
public static int i=10;
@Override
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- AActivity,没有onCreate方法
package com.domain.android.test;
public class AActivity extends MainActivity{
}
编译之后的字节码
-
AActivity.class
AActivity.class.png -
MainActivity.class
MainActivity.class.png
网友评论