记asm的从零到入门的摸索过程

asm是一个操作class字节码文件的框架。通过对底层class文件进行修改,以达到一些全局的目的。
对于asm的初次接触源于自己对于Android中埋点方式的感兴趣,因为有用过一些三方埋点框架,完全不用添加任何额外的代码,却能采集到app中的很多时间,包括但是不限于点击事件输入事件,页面跳转等等。对于他们的实现比较感兴趣,自己想过一些方式实现,本来想通过hook的方式,替换掉View中的OnClickListener,但是View中的OnClickListener变量并不是一个static的,如果需要实现这种埋点需求,需要对每个View调用一次Hook方法,这样最少也需要在BaseActivity中做一些操作。但是用到的三方框架并没有这些操作。经过网上资料查询,了解到一种新的方式,面向切面编程(AOP),通过针对这些事件进行编程,而asm就是这里面用到的一种利器,通过操作底层的class文件,对所有的onClick事件进行代码修改,达到不影响源代码的情况下,无感知埋点,而且由于不使用反射技术,所产生的性能消耗仅限于埋点采集的代码,和手动埋点的性能是一样的。只不过将手动埋点的代码通过asm在编译器注入到代码中,免去手写代码的过程。

对于asm的基础类有不清楚的地方可以看下asm文档,里面有每个类的详细介绍。这里讲下作为一个新手在路上的坎坷。

首先是自己写一个gradle插件,通过插件来接入asm操作,通过gradle的原因主要是因为asm侵入的是一个打包的流程,关于Android打包流程我们可以看一下流程图Android打包流程图

在Android中google提供了在红框处修改相关class的api,就是通过自定义gradle实现Transform的api去侵入到这个流程中,关于自定义gradle就不多说了,可以自行google,不是很复杂。
在学习asm的使用过程中遇到的第一个问题就是导包的问题,找了很久都没有发现网上的文章告诉你怎么导包,这里贴出来我的gradle插件的build.gradle文件依赖配置。

	apply plugin: 'groovy'

	dependencies {
	    compile gradleApi()//gradle sdk
	    compile localGroovy()//groovy sdk
	    compile 'com.android.tools.build:gradle:3.2.1'
	    compile 'commons-io:commons-io:2.4'
	    compile 'org.ow2.asm:asm:6.0'
	}
	
	repositories {
	    jcenter()
	    mavenCentral()
	    google()
	}  

com.android.tools.build:gradle:3.2.1: 里面包含自定义gradle插件的类
commons-io:commons-io:2.4和org.ow2.asm:asm:6.0: 里面是asm的api里面的类

这里贴出来修改字节码部分的代码:

void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
    println "============================"
    println "=========transform=========="
    println "============================"
    inputs.each {
        TransformInput input ->
            input.directoryInputs.each {
                DirectoryInput directoryInput ->
                    operationAllClass(directoryInput.file)
                    def dest = outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, directoryInput.scopes,
                            Format.DIRECTORY)


                    FileUtils.copyDirectory(directoryInput.file, dest)
            }
            input.jarInputs.each {
                JarInput jarInput ->
                    def jarName = jarInput.name
                    def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                    if (jarName.endsWith(".jar")) {
                        jarName = jarName.substring(0, jarName.length() - 4)
                    }

                    def dest = outputProvider.getContentLocation(jarName + md5Name,
                            jarInput.contentTypes, jarInput.scopes, Format.JAR)

                    FileUtils.copyFile(jarInput.file, dest)
            }
    }
}

void operationAllClass(File file){
    if (file.isDirectory()){
        file.eachFile {
            File file1 ->
                operationAllClass(file1)
        }
        return
    }
    String name = file.name
    if (name.endsWith(".class") && !name.startsWith("R\$") &&
                                        "R.class" != name && "BuildConfig.class" != name) {
        println name + "  ############## "
        ClassReader cr = new ClassReader(file.bytes)
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
        ClassVisitor cv = new ChangeVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        byte[] code = cw.toByteArray();
        FileOutputStream fos = new FileOutputStream(
                file.parentFile.absolutePath + File.separator + name);
        fos.write(code);
        fos.close();
    }
}

	class ChangeVisitor extends ClassVisitor{

    ChangeVisitor(int api) {
        super(api)
    }

    ChangeVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }

    @Override
    void visitInnerClass(String s, String s1, String s2, int i) {
        println "*********************************"
        println s + "  ***  " + s1+ "  ***  " + s2+ "  ***  " + i
        println "*********************************"
        super.visitInnerClass(s, s1, s2, i)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        println "*********************************====================="
        println access + "  ***  " + name+ "  ***  " + desc+ "  ***  " + signature
        println "*********************************====================="
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
        if ("onClick" == name){
            return new OnClickAdapter(mv, access, name, desc)
        }
        return mv
    }
}

class OnClickAdapter extends AdviceAdapter{

    /**
     * Creates a new {@link AdviceAdapter}.
     *
     * @param api
     *            the ASM API version implemented by this visitor. Must be one
     *            of {@link Opcodes#ASM4}, {@link Opcodes#ASM5} or {@link Opcodes#ASM6}.
     * @param mv
     *            the method visitor to which this adapter delegates calls.
     * @param access
     *            the method's access flags (see {@link Opcodes}).
     * @param name
     *            the method's name.
     * @param desc
     *            the method's descriptor (see {@link Type Type}).
     */
    protected OnClickAdapter(MethodVisitor mv, int access, String name, String desc) {
        super(Opcodes.ASM5, mv, access, name, desc);
        println "onClickAdapter start=============================="
        println name + "  ******  " + desc
        println "onClickAdapter end================================"
    }

    @Override
    protected void onMethodEnter() {
        println "name start==========================="
        println methodDesc
        println "name end============================="
        super.onMethodEnter()
        mv.visitVarInsn(ALOAD, 1)
        mv.visitMethodInsn(INVOKESTATIC, "com/asmdemo/ToastUtils", "showToast", "(Landroid/view/View;)V", false)
    }
}

这里只做一个简单的Toast注入,在onCLick事件触发的时候弹一个吐司

public class ToastUtils {

public static void showToast(View v){
    Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
}
}

ToastUtils的位置需要注意,因为这里就相当于手动往onClick方法添加一段代码,如果这里ToastUtils找不到汇报异常ClassNotFoundException.

这里看一下效果

	protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    TextView tvHello = findViewById(R.id.tv_hello);
    tvHello.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        }
    });
    findViewById(R.id.tv_1).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        }
    });
    findViewById(R.id.tv_2).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        }
    });
    findViewById(R.id.tv_3).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        }
    });
}

在这里插入图片描述

关于用到的ClassReader和ClassWriter以及ClassVisitor均来自于org.objectweb.asm包下,当你想导包的时候会看到有接近十个包下都有这几个类,但是其他包下的类都用不了,具体原因未知。最终需要选择的还是org.objectweb.asm这个包下的

相关代码已上传github记asm的从零到入门的摸索过程
谨以此文记录自己在入门asm过程中遇到的坑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值