框架手写系列---Asm方式实现日志插入

Posted 战国剑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了框架手写系列---Asm方式实现日志插入相关的知识,希望对你有一定的参考价值。

asm与javassist作为修改字节码文件的两种常用方式,在编译过程中,可以动态修改class字节码文件。前文已用javassist方式实现热修复的前置步骤,本文用asm实现动态的给每个activity插入Log日志。

一、插件的生成与依赖导入

1、用该文章中的方法:Android中Plugin插件工程的自动生成 自动生成一个android的插件模板;

2、加入asm依赖,加入依赖后的插件build的格式如下:

dependencies
    compile gradleApi()
    compile localGroovy()
    implementation 'com.android.tools.build:gradle:3.6.3'
    //asm依赖
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'

在最外层的build中,添加依赖如下:

dependencies 
        classpath 'com.android.tools.build:gradle:3.6.3'
        //asm路径
        classpath 'com.sunny.asmplugin:asmplugin:1.0.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    

二、插件的注册与编写

修改AsmPlugin文件如下,该文件将SunnyAsmTransForm注册到工程中。

public class AsmPlugin implements Plugin<Project> 
    @Override
    public void apply(Project project) 
        def android = project.extensions.getByType(AppExtension)
        println '----------- registering SunnyAsmTransForm  -----------'
        SunnyAsmTransForm transform = new SunnyAsmTransForm()
        android.registerTransform(transform)
        println '----------- registered SunnyAsmTransForm  -----------'
    

SunnyAsmTransForm负责实现具体逻辑:

package com.sunny.asmplugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter

/**
 * @author shenxiao* @version V1.0* @package com.sunny.easyuse* @date 2020/6/22 4:34 PM
 */
public class SunnyAsmTransForm extends Transform 

    SunnyAsmTransForm() 

    

    @Override
    public String getName() 
        return "SunnyAsmTransForm"
    

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() 
        return TransformManager.CONTENT_CLASS
    

    @Override
    public Set<QualifiedContent.Scope> getScopes() 
        return TransformManager.PROJECT_ONLY
    

    @Override
    public boolean isIncremental() 
        return false;
    

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException 
        super.transform(transformInvocation)

        //拿到所有的class文件
        Collection<TransformInput> transformInputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) 
            outputProvider.deleteAll()
        

        transformInputs.each  TransformInput transformInput ->
            // 遍历directoryInputs(文件夹中的class文件) directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
            // 比如我们手写的类以及R.class、BuildConfig.class以及MainActivity.class等
            transformInput.directoryInputs.each  DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) 
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\\.class/)  File file ->
                        System.out.println("find class: " + file.name)
                        //对class文件进行读取与解析
                        ClassReader classReader = new ClassReader(file.bytes)
                        //对class文件的写入
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        //访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
                        ClassVisitor classVisitor = new SunnyClassVisitor(classWriter)
                        //依次调用 ClassVisitor接口的各个方法
                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                        //toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
                        byte[] bytes = classWriter.toByteArray()

                        //通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
                        //FileOutputStream outputStream = new FileOutputStream( file.parentFile.absolutePath + File.separator + fileName)
                        FileOutputStream outputStream = new FileOutputStream(file.path)
                        outputStream.write(bytes)
                        outputStream.close()
                    
                

                //处理完输入文件后把输出传给下一个文件
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
                        directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            
        
    

上文中涉及到的SunnyClassVisitor与SunnyMethodVisitor,是实现修改字节码的具体文件:

SunnyClassVisitor中指定了需要访问的方法:

package com.sunny.asmplugin;

/**
 * @author shenxiao
 * @version V1.0
 * @package com.sunny.asmplugin
 * @date 2020/9/8 11:41 AM
 */
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class SunnyClassVisitor extends ClassVisitor 
    private String className;
    private String superName;

    public SunnyClassVisitor(ClassVisitor cv) 
        super(Opcodes.ASM5, cv);
    

    @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;
    

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 
        System.out.println("ClassVisitor visitMethod name-------" + name + ", superName is " + superName);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);

        if (superName.equals("android/support/v7/app/AppCompatActivity")
            || superName.equals("androidx/appcompat/app/AppCompatActivity") ) 
            if (name.startsWith("onCreate")) 
                //处理onCreate()方法
                return new SunnyMethodVisitor(mv, className, name);
            
        
        return mv;
    

    @Override
    public void visitEnd() 
        super.visitEnd();
    

SunnyMethodVisitor实现方法中修改字节码逻辑:

package com.sunny.asmplugin;

/**
 * @author shenxiao
 * @version V1.0
 * @package com.sunny.asmplugin
 * @date 2020/9/8 11:42 AM
 */

import org.objectweb.asm.MethodVisitor;
        import org.objectweb.asm.Opcodes;

public class SunnyMethodVisitor extends MethodVisitor 
    private String className;
    private String methodName;

    public SunnyMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) 
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    

    //方法执行前插入
    @Override
    public void visitCode() 
        super.visitCode();
        System.out.println("MethodVisitor visitCode------");

        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn(className + "---->" + methodName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    

到此完成了插件编写,将插件上传到本地后,可应用插件。

三、插件的使用

新建工程,工程中含MainActivity文件。在工程的build中,加上:

apply plugin: 'com.sunny.asmplugin'

编译工程后,可在app/build/intermediates/transforms/SunnyAsmTransForm/debug/0/com/sunny/asm/MainActivity.class中查看插入的日志代码:

public class MainActivity extends AppCompatActivity 
    public MainActivity() 
    

    protected void onCreate(Bundle savedInstanceState) 
        //此处为asm新增的日志代码
        Log.i("TAG", "com/sunny/asm/MainActivity---->onCreate");
        super.onCreate(savedInstanceState);
        this.setContentView(2131296284);
    

至此,完成asm动态修改字节码逻辑。

以上是关于框架手写系列---Asm方式实现日志插入的主要内容,如果未能解决你的问题,请参考以下文章

框架手写系列---javassist修改字节码方式,实现美团Robust热修复框架

框架手写系列---apt方式实现ARouter框架

框架手写系列---AspectJ方式实现埋点上传框架

框架手写系列---通过反射手写EventBus框架

手写Spring MVC框架 实现简易版mvc框架

java SPI 06-自己从零手写实现 SPI 框架