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中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过程中遇到的坑。