gradle插件化ASM框架实现字节码插桩

kotlin vs java
知识点来源:这个知识点第一次知道是在一次无意之间看到一个高级工程师的售卖课程,花一分钱试听了一堂JVM虚拟机相关的课程才知道还能这样玩儿出花来,接下来进入正题:

DEMO

1. 想要彻底了解这个知识点首先要掌握JVM虚拟机对于内存分配和线程中线程私有区域栈内方法执行的流程。
java虚拟机内存分配

上图中主要理解Threads中JVM Stacks里面虚拟机栈的作用和意义,了解了第一点后可以做到通过字节码在编译时期通过工具asm进行编译时期代码里面插入新代码的需求

2. 了解熟悉gradle插件开发,因为我们最终的目的是在android中使用该功能进行编译时期对特定方法或者类文件进行处理操作,所以插件是必然的,主要目的就是通过gradle的自动编译将我们java文件编译出来的.class文件进行替换成我们处理之后的文件,最终由编译器转换成.dex文件生成apk,自定义插件有多种方式,只要理解了gradle插件的含义和作用以及开发方式,用哪一种我觉得根据需求而定(我这使用buildSrc本地项目使用的方式)
apk编译流程图

上图中我们通过gradle插件化操作的点就是在 .class Files---->dex----->.dex files的过程中替换 .class文件。谷歌为我们提供了完整的API对这一步骤做处理----[Transform]

3. Transform可谷歌百度深入了解,此处给出超链接也不一定是最好的,只是个人阅读的。

根据下图可以看出我们自定义Transform始终是在系统的Transform之前先做编译处理,如果大家有关注过android编译过程,可以发现整个过程中有很多带有Transform文字样式在里面的名字,其实就是相应的系统Transform而已,至于名字都是通过规则拼接的。

Transform
4. 最后一点ASM,整个工具是用于插入字节码操作的工具,这里就贴出目前最新版本, commons依赖下面有很多利于简化开发的工具,具体可自行百度。
    implementation 'org.ow2.asm:asm:7.0'
    implementation 'org.ow2.asm:asm-commons:7.0'

5. 实战:

  • 接下来就是项目实战环节,目的在demo方法代码中插入一个Toast方法,打印一句话。
    1. 创建一个全新的项目:GradleToASMStakeDemo(此过程完全忽略)
    1. 在项目中创建一个lib模块(此过程也完全忽略),创建模块以buildSrc命名,切记名字一定是这个。
    1. 对buildSrc模块进行改造,使它成为我们的gradle插件模块:
  • 删除除了主目录结构的所有文件和build.gradle文件(所谓主目录结构就是src/main/java/包名文件夹)
  • 创建resources目录,该目录与main目录在同一级,然后在resources中穿件META-INF--->gradle-plugins--->项目包名.properties文件,真个文件创建完整结构如下图:
gradle插件demo目录结构
4. 接下来配置配置buildSrc
  • 首先build.gradle文件配置:
apply plugin: 'java-library' //整个插件用Java编写,如果喜欢用groovy可以导入groovy的相关依赖
// 当前开发编译版本
compileJava {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    options.encoding = "UTF-8"
}
//开发文件结构
sourceSets {
    main {
        java {
            srcDir 'src/main/java'
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}
//获取远程库的仓库地址
repositories {
    jcenter()
    mavenCentral()
    maven { url "https://dl.google.com/dl/android/maven2/" }
}
//开发插件需要的库
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:3.6.2'
    implementation 'org.ow2.asm:asm:7.0'
    implementation 'org.ow2.asm:asm-commons:7.0'
}
  • 然后是 .properties文件,此文件可以简单理解成AndroidManifest.xml文件,作用就是注册该插件。
implementation-class=com.kylin.gradle.buildsrc.TestPlugin,
  • 接下来就是实现插件代码逻辑了,在java文件夹下面创建一个TestPlugin的java文件,该类继承于Plugin<Project> ,作为gradle插件开发的入口,实现唯一必须实现方法apply,简单打印一句话就可以在我们编译同步的时候看到我们所打印的东西,当然还需要依赖该插件。
  • 使用gradle插件,使用很简单,只需要在我们项目下的build.gradle文件中依赖即可,就想我们依赖application一样,注意此处依赖使用的是包名,然后编译就可以看到我们上面在apply方法中打印的东西了。
apply plugin: 'com.kylin.gradle.buildsrc' //使用gradle自定义插件
5. 注册我们自己的Transform类文件,注册此文件需要获取到我们AppExtension对象
AppExtension byType = project.getExtensions().getByType(AppExtension.class);
byType.registerTransform(new TestTransform());
  • 接下来创建我们的TestTransform文件,具体方法定义代码中有注释:

/**
 * @Description:Transform在编译过程中.class文件转换成.dex的时候触发(.class -->transform-->.dex)
 * @Auther: wangqi
 * CreateTime: 2020/4/16.
 */
public class TestTransform extends Transform {

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        //当前是否是增量编译(由isIncremental() 方法的返回和当前编译是否有增量基础)
        boolean isIncremental = transformInvocation.isIncremental();
        //消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        for (TransformInput input : inputs) {
            //Failed resolution of: Landroidx/appcompat/R$drawable;(不遍历处理的话会出现这个bug)
            for (JarInput jarInput : input.getJarInputs()) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
                FileUtils.copyFile(jarInput.getFile(), dest);
            }

            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                FileUtils.copyDirectory(
                        directoryInput.getFile(),
                        outputProvider.getContentLocation(directoryInput.getName(), getInputTypes(), getScopes(), Format.DIRECTORY));

                File dest = outputProvider.getContentLocation(directoryInput.getName(),
                        directoryInput.getContentTypes(), directoryInput.getScopes(),
                        Format.DIRECTORY);
                // 插桩
               // replaceFileClass(directoryInput.getFile());
                //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了, 此目的就是把我们修改之后的文件按照android编译要求放置到本来该放置的位置,以助于apk打包。
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
                System.out.println("dest: " + dest);
            }
        }
    }

    @Override
    public String getName() {
        return "kylin0628";
    }

    /**
     * 筛选需要处理的文件
     * 代表了所有jar包,文件夹中,aar包中的.class文件和标准的java源文件,我们都进行筛选。
     *
     * @return
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 插件作用域设置
     * TransformManager.SCOPE_FULL_PROJECT 插件作用域真个项目
     *
     * @return
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY;
    }

    /**
     * 是否支持增量编译
     *
     * @return
     */
    @Override
    public boolean isIncremental() {
        return false;
    }
}
  • 前面步骤主要是实现了gradle插件自动化的帮助我们把改变之后的文件塞到android编译过程中对应的位置,接下来就是要进行我们的字节码处理文件核心------->利用ASM工具实现字节码插桩操作
  • 插桩主要分以下几步:
         1. 读取.class文件(FileInputStream ->ClassReader)
         2. 写入读取的流文件(ClassWriter ->FileOutputStream)
         3. 写入文件后对文件进行加工处理(ASM-->XxxClassVisitor)
         4. 通过自定义Visitor实现相应的方法处理,注解处理,内部类等处理操作
        5. 具体操作就是先通过javap命令把字节码.class文件反编译成字节码,然后按照visitor提供的方法把你要添加的代码写入代码中。具体可以下载demo

字节码反编译技巧:Idea或Android Studio查看字节码当然还有ASM的插件,但是感觉不好用,还不如这个扩展来的简单方便。

gradle插件调试,在开发过程中肯定免不了打断点看数据:

  • IntelliJ(Android Studio) Edit Configurations点击+找到Remote点击创建远程配置
  • 填写信息. Name自定义, 默认远程调试localhost:5005
  • Search sources using module's classpath选择需要调试的插件模块
  • 命令行执行任务调试: ./gradlew tasks -Dorg.gradle.debug=true --no-daemon,等待连接调试
  • 源代码断点, 选择刚创建的调试配置, 点击 Debug Xxx(Shift+F9)
  • 点击同步代码的🐘,调试断点,将在源码断点处停下来。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容