新建一个空的Android工程,只有一个MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
使用ASM框架,插入两行日志代码,分别在onCreate方法的第一行和最后一行,目标代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.e("MainActivity", "log from asm enter");
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
Log.e("MainActivity", "log from asm exit");
}
最终效果
2024-03-14 16:02:49.215 32007-32007/com.example.myapp E/MainActivity: log from asm enter
2024-03-14 16:02:49.371 32007-32007/com.example.myapp E/MainActivity: log from asm exit
实现步骤
新建一个Java-library module,asm-plugin,build.gradle如下配置:
apply plugin: 'java-library' // Java-library module必须应用的插件,新建之后默认会有
apply plugin: 'groovy' // 自定义的插件类使用groovy编写
apply plugin: 'java-gradle-plugin' // 自动注册自定义的插件,否则需要手动在main目录下新建resources/META-INF/gradle-plugins/${plugin-id}.properties
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation 'org.ow2.asm:asm:9.0' // ASM依赖
implementation 'org.ow2.asm:asm-commons:9.0'
implementation 'com.android.tools.build:gradle:3.6.3'
}
gradlePlugin {
plugins {
AsmPlugin {
id = 'com.example.asm-plugin' // 在app等模块使用,例如:apply plugin: 'com.example.asm-plugin'
implementationClass = 'com.example.asm.AsmPlugin' //等价于在${plugin-id}.properties里面配置implementation-class = com.example.asm.AsmPlugin
}
}
}
sourceCompatibility = "7"
targetCompatibility = "7"
sourceSets {
main {
groovy {
srcDir 'src/main/groovy' //配置groovy源码路径
}
java {
srcDir 'src/main/java' //配置Java源码路径
}
resources {
srcDir 'src/main/resources' //配置resources路径,如果手动创建
}
}
}
//apply from: file('maven.gradle')
apply from: file('publish.gradle') // 发布到MavenLocal的配置
看下插件module目录结构:
sunshuo@sunshuo-virtual-machine:~/AndroidStudioProjects/myApp/asm-plugin$ tree
.
├── asm-plugin.iml
├── build.gradle
├── maven.gradle
├── publish.gradle
└── src
└── main
├── groovy
│ └── com
│ └── example
│ └── asm
│ └── AsmPlugin.groovy
└── java
└── com
└── example
└── asm
├── AsmTransform.java
├── ClassVisitorAdapter.java
└── MethodAdviceAdapter.java
10 directories, 8 files
AsmPlugin.groovy内容如下:
class AsmPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def extension = project.extensions.getByType(AppExtension)
extension.registerTransform(new AsmTransform()) //注册一个自定义的AsmTransform
}
}
AsmTransform.java内容如下:
public class AsmTransform extends Transform {
private static final List<String> CLASS_FILE_IGNORE = Arrays.asList("R.class", "R$", "Manifest", "BuildConfig.class"); // 忽略这些文件
@Override
public String getName() {
return "AsmPlugin";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS; // 其他类型,可以集成查看
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return true;
}
@Override
public void transform(TransformInvocation transformInvocation) throws IOException {
TransformOutputProvider provider = transformInvocation.getOutputProvider();
for (TransformInput input : transformInvocation.getInputs()) {
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
// directoryInput 的输入目录是:app/build/intermediates/javac/debug/classes
doDirInputTransform(directoryInput);
copyQualifiedContent(provider, directoryInput, null, Format.DIRECTORY);
}
for (JarInput jarInput : input.getJarInputs()) {
copyQualifiedContent(provider, jarInput, getUniqueName(jarInput.getFile()), Format.JAR);
}
}
}
//根据输入的目录,遍历要插桩的class文件
private void doDirInputTransform(DirectoryInput input) {
List<File> files = new ArrayList<>();
listFiles(files, input.getFile());
for (File file : files) {
try {
doAsm(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//核心在这里,读出来二进制文件,访问方法,插入字节码,再写回去
private void doAsm(File file) throws IOException {
InputStream is = null;
FileOutputStream fos = null;
try {
is = new FileInputStream(file);
ClassReader reader = new ClassReader(is);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new ClassVisitorAdapter(Opcodes.ASM5, writer);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
byte[] code = writer.toByteArray();
fos = new FileOutputStream(file.getAbsoluteFile());
fos.write(code);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
}
}
// 列出所有的文件
private void listFiles(List<File> files, File file) {
if (file == null) {
return;
}
if (file.isDirectory()) {
File[] fl = file.listFiles();
if (fl == null || fl.length == 0) {
return;
}
for (File f : fl) {
listFiles(files, f);
}
} else if (needTrack(file.getName())) {
files.add(file);
}
}
private boolean needTrack(String name) {
boolean ret = false;
if (name.endsWith(".class")) {
ret = !CLASS_FILE_IGNORE.contains(name);
}
return ret;
}
private void copyQualifiedContent(TransformOutputProvider provider, QualifiedContent file, String fileName, Format format) throws IOException {
boolean useDefaultName = fileName == null;
File dest = provider.getContentLocation(useDefaultName ? file.getName() : fileName, file.getContentTypes(), file.getScopes(), format);
if (!dest.exists()) {
dest.mkdirs();
dest.createNewFile();
}
if (useDefaultName) {
FileUtils.copyDirectory(file.getFile(), dest);
} else {
FileUtils.copyFile(file.getFile(), dest);
}
}
private String getUniqueName(File jar) {
String name = jar.getName();
String suffix = "";
if (name.lastIndexOf(".") > 0) {
suffix = name.substring(name.lastIndexOf("."));
name = name.substring(0, name.lastIndexOf("."));
}
String hexName = DigestUtils.md5Hex(jar.getAbsolutePath());
return String.format("%s_%s%s", name, hexName, suffix);
}
}
可以看到核心在doAsm函数里,这里使用了访问者设计模式,新建ClassVisitorAdapter如下:
public class ClassVisitorAdapter extends ClassVisitor {
public ClassVisitorAdapter(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodAdviceAdapter(api, methodVisitor, access, name, descriptor);
}
}
里面有对Feild,Annotation,Method的访问,这里由于是对MainActivity的onCreate访问,所以只在visitMethod方法里返回MethodAdviceAdapter,这个类里面可以回调进入方法,离开方法的时机,内容如下:
public class MethodAdviceAdapter extends AdviceAdapter {
private String methodName;
private MethodVisitor methodVisitor;
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param api
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags
* @param name the method's name.
* @param descriptor the method's descriptor
*/
protected MethodAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
this.methodName = name;
this.methodVisitor = methodVisitor;
}
@Override
protected void onMethodEnter() {
enter();
}
private void enter() {
if ("<init>".equals(methodName)) {
return;
}
System.out.println("onMethodEnter");
methodVisitor.visitLdcInsn("MainActivity");
methodVisitor.visitLdcInsn("log from asm enter");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
}
@Override
protected void onMethodExit(int opcode) {
exit();
}
private void exit() {
// 忽略构造方法
if ("<init>".equals(methodName)) {
return;
}
System.out.println("onMethodExit");
methodVisitor.visitLdcInsn("MainActivity");
methodVisitor.visitLdcInsn("log from asm exit");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
}
}
enter和exit方法插入的代码,我们可以利用AS插件ASM ByteCode Viewer帮助我们生成,安装完成之后,在要插入代码所在的文件右键,执行ASM ByteCode Viewer命令,找到要插入的指令,复制过来即可
image.png
都准备好了,就可以打包这个插件,并配置到本地,看下publish.gradle:
apply plugin: 'maven-publish'
publishing {
publications {
mavenJava(MavenPublication) {
groupId 'com.example'
artifactId 'asm'
version '1.0.0'
from components.java
}
}
}
publishing {
repositories {
maven {
// 发布位置
url uri('../plugins')
}
}
}
点击如下任务,发布插件到本地
image.png
DYJ8G(DA(3D5~$P_{~$5)6I.png
这里也可以使用
apply plugin: 'maven'
uploadArchives {
repositories {
mavenDeployer {
pom.groupId = 'com.example'
pom.artifactId = 'asm'
pom.version = '1.0.0'
//提交到远程服务器
// repository(url:"服务器地址"){
// authentication(userName:'admin',password:'123456')
// }
//提交本地maven地址:../plugins
repository(url: uri('../plugins'))
}
}
}
执行如下任务:
W6RU0CWOGYN939LEYKTIRIC.png
然后在工程的build.gradle依赖插件:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
maven {
url uri('./plugins')
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "com.example:asm:1.0.0" // 根据上面的maven url查找对应的jar包
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url uri('./plugins')
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
之后在app module的build.gradle文件使用插件:
apply plugin: 'com.example.asm-plugin' // 这里使用的是plugin-id
运行App,即可看到日志代码插入成功。
当然也可以在处理完成写入的目录查看文件是否插入代码:
.png