自定義gradle插件
1.新建Android Studio Project,在項(xiàng)目下新建Module,選擇Library
image.png
2.刪除新建Module下除build.gradle外的所有文件
image.png
3.新建 src
目錄, src/main/groovy
, groovy
目錄下創(chuàng)建插件包名com.example.testplugin
, main
目錄下新建 resource/META-INF/gradle-plugins
目錄
image.png
4.修改 build.gradle
文件
apply plugin: 'maven'
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.0'
}
repositories {
mavenLocal()
}
uploadArchives {
repositories.mavenDeployer {
pom.groupId = 'com.example.testplugin'
pom.artifactId = 'test'
pom.version = '1.0.0'
//發(fā)布到本地倉庫
repository(url: uri('../repo'))
}
}
5.groovy
目錄下新建文件TestPluginImp.groovy
,注意此處新建文件一定要帶上 .groovy 文件后綴
image.png
6.TestPluginImp
文件插入以下代碼
package com.example.testplugin
import org.gradle.api.Plugin
import org.gradle.api.Project
class TestPluginImp implements Plugin<Project> {
void apply(Project project) {
System.out.println '========================'
System.out.println '我是測試gradle TestPlugin'
System.out.println '========================'
}
}
7.META-INF.gradle-plugins
目錄下新建com.example.testplugin.properties
,并在文件中插入以下代碼
implementation-class=com.example.testplugin.TestPluginImp
8. properties文件名稱與文件中內(nèi)容以及我們上面建立的gradle文件TestPluginImp
相互對應(yīng)
image.png
9.到此處自定義gradle插件就完成了,點(diǎn)擊編譯器右側(cè)gradle->upload->uploadArchives將插件發(fā)布到本地倉庫,會發(fā)現(xiàn)項(xiàng)目目錄下會多出repro
目錄,里面是我們打包完的插件資源
image.png
10.話不多說,既然寫完了,我們就驗(yàn)證下剛寫完的gradle插件,在項(xiàng)目跟目錄下增加maven本地倉目錄,同時(shí)導(dǎo)入插件,在app目錄gradle文件下apply插件
image.png
image.png
11. Rebuild Project,查看build output窗口輸出,我們在TestPluginImp.groovy
文件中的apply方法寫入的內(nèi)容被打印了,說明我們自定義的gradle插件生效了,好了自定義gradle插件到此結(jié)束
image.png
ASM字節(jié)碼注入(在Activity生命周期中注入部分代碼)
前言
安卓AOP三劍客:APT
,AspectJ
,Javassist
(http://www.lxweimin.com/p/dca3e2c8608a?from=timeline)
APT代表框架
:DataBinding,Dagger2, ButterKnife, EventBus3 、DBFlow、AndroidAnnotation
AspectJ
代表框架:Hugo(Jake Wharton)
Javassist
代表框架:熱修復(fù)框架HotFix 、Savior(InstantRun)
image.png
直接開始介紹ASM字節(jié)碼注入
1.核心方法為transform
,通過transformInvocation.inputs
、transformInvocation.outputProvider
遍歷和寫入文件,通過ClassVisitor.visitMethod
想方法中寫入想要插入的字節(jié)碼
2.建立java/com.example.testplugin
目錄,并在下面建立LifeTestClassVisitor}$、
LifeTestOnCreateMethodVisitor}
,文件目錄如下:
image.png
3.我們想要在Activity的onCreate以及onDestory里分別注入以下代碼:
Log.e(this.getClass().getName(), this.getClass().getName()+" onCreate");
Log.e(this.getClass().getName(), this.getClass().getName()+" onDestory");
4.由于注入的是字節(jié)碼所以我們要對需要進(jìn)行注入的代碼進(jìn)行轉(zhuǎn)換,這里我們借助一個(gè)插件 ASM Bytecode Outline
,可以直接在Setting->Plugins
中直接安裝:
image.png
5.新建ByteCode.java
文件,寫入測試代碼,通過ASM Bytecode插件查看字節(jié)碼,如下:
image.png
6.繼續(xù)寫入我們需要插入的代碼,并查看字節(jié)碼,獲取差異部分代碼備用
image.png
7.遍歷以及插入部分主要代碼如下:
TestPluginImp.groovy
package com.example.testplugin
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES
class TestPluginImp extends Transform implements Plugin<Project> {
void apply(Project project) {
System.out.println '========================'
System.out.println '我是測試gradle TestPlugin'
System.out.println '========================'
def android = project.extensions.getByType(AppExtension)
android.registerTransform(this)
}
@Override
String getName() {
//該方法表示當(dāng)前Transform在task列表中的名字,返回值最終經(jīng)過一系列的拼接,具體拼接實(shí)現(xiàn)在TransformManager的getTaskNamePrefix()方法中,拼接格式:transform${InputType1}And${InputType2}And${InputTypeN}And${name}For${flavor}${BuildType}
return "TestPluginImp"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
//該方法表示指定輸入類型,這里我們指定CONTENT_CLASS類型
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
//該方法表示指定輸入類型,這里我們指定CONTENT_CLASS類型
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
//該方法表示當(dāng)前Transform是否支持增量編譯
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
//該方法是重點(diǎn),它接收上一個(gè)Transform的輸出,并把處理后的結(jié)果作為下一個(gè)Transform的輸入
System.out.println '--------------- LifeTestPlugin visit start --------------- '
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//刪除之前的輸出
if (outputProvider != null)
outputProvider.deleteAll()
//遍歷inputs
inputs.each { TransformInput input ->
//遍歷directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍歷jarInputs
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
System.out.println '--------------- LifeTestPlugin visit end --------------- '
System.out.println "LifeTestPlugin cost : $cost s"
}
/**
* 處理文件目錄下的class文件
*/
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否是目錄
if (directoryInput.file.isDirectory()) {
//列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (checkClassFile(name)) {
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifeTestClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//處理完輸入文件之后,要把輸出給下一個(gè)任務(wù)
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
/**
* 處理Jar中的class文件
*/
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名輸出文件,因?yàn)榭赡芡?會覆蓋
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的緩存被重復(fù)插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插樁class
if (checkClassFile(entryName)) {
//class文件處理
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifeTestClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//結(jié)束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
/**
* 檢查class文件是否需要處理
* @param fileName
* @return
*/
static boolean checkClassFile(String name) {
//只處理需要的class文件
return (name.endsWith(".class") && !name.startsWith("R\$")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)&&!name.startsWith("android"))
}
}
LifeTestClassVisitor.java
package com.example.testplugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class LifeTestClassVisitor extends ClassVisitor implements Opcodes {
private String mClassName;
public LifeTestClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println("LifeTestClassVisitor : visit -----> started:" + name);
this.mClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("LifeTestClassVisitor : visitMethod " + name);
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
System.out.println("LifeTestClassVisitor : visit -----> started:" + this.mClassName);
//匹配FragmentActivity
if ("onCreate".equals(name)) {
//處理onCreate
System.out.println("LifeTestClassVisitor : change method ----> " + name);
return new LifeTestOnCreateMethodVisitor(mv);
} else if ("onDestroy".equals(name)) {
//處理onDestroy
System.out.println("LifeTestClassVisitor : change method ----> " + name);
return new LifeTestOnDestroyMethodVisitor(mv);
}
return mv;
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
LifeTestOnCreateMethodVisitor.java
package com.example.testplugin;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
public class LifeTestOnCreateMethodVisitor extends MethodVisitor {
public LifeTestOnCreateMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}
@Override
public void visitCode() {
super.visitCode();
//方法執(zhí)行前插入,此處為ASM Bytecode比較出的差異代碼
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" onCreate");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
}
LifeTestOnDestroyMethodVisitor.java
package com.example.testplugin;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
public class LifeTestOnDestroyMethodVisitor extends MethodVisitor {
public LifeTestOnDestroyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM4, mv);
}
@Override
public void visitCode() {
super.visitCode();
//方法執(zhí)行前插入,此處為ASM Bytecode比較出的差異代碼
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" onDestory");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
}
8.重新 uploadArchives插件,看log輸出
image.png
9.運(yùn)行demo,并退出demo查看log輸出
image.png
10.到這里說明我們的代碼生效了,下面我們通過dex2jar以及jd-gui反編譯我們的dex文件看下具體插入的代碼
image.png