Gradle-初探代码注入Transform

一文学会Android Gradle Transform基础使用 概述 最近在做一个在 Android 工程编译期间动态插入一些随机代码的需求,我选择的是 Gradle Transform 技术,想起好久没有写过博客了,就记录一下这方面的一些基本使用。 一般来说,在 Android 工程的编译期间可以通过一些技术来动态插入一些代码逻辑甚至生成一些新的 Class 类,具体技术有: APT(Annotation Processing Tool): 编译期注解处理技术,通过自定义注解和注解处理器来实现编译期生成代码的功能,并且将生成的代码和源代码一起编译成 class 文件 阅读详情

简介

本文主要介绍gradle打包过程中transform阶段,这里大概说下AOP(Aspect Oriented Programming),这是一种面向切面的思想,预支对应的是OOP(Object Oriented Programming)面向对象编程,这里不展开说明。可以看下对AOP总结的思维导图

划重点

本篇文章主要介绍下面的几点:

  • Transform可以做什么
  • 简单了解App打包过程,以及介绍Transform
  • 生成自己的MyConfig类文件,有助我们更好理解
  • 介绍ASM

Transform可以做什么

最主要的目的就是:解耦,开发人员专注于需求,其他的边角料,交给Transform来处理。

  1. 权限判断,避免代码中相关的地方都是权限申请和处理的代码
  2. 无痕埋点,简单的场景可以使用,但是场景比较复杂时,就不好处理了,目前网上没有很好的解决方案
  3. 性能监控,trace+字节码插桩,完美监控
  4. 事件防抖,避免短期内多次点击按钮
  5. 热修复,在所有方法前插入一个预留的函数,可以将有bug的方法替换成下发的方法。
  6. 优化代码,例如删除项目中体积很大的R文件中的字段、优化内联函数等等
  7. …
    还有很多功能,都值得我们去尝试。

App打包过程 & Transform

首先我们回顾一下App的打包流程,App打包都需要经理哪些流程,每一个步骤都干了什么?

apk打包过程

谷歌官网的一幅图

从上面的图中大概理一下流程

  1. 编译器将app的源码编译成DEX(Dalvik Executable)文件(其中包括运行在Android设备上的字节码),将所有其他的内容转换为已经编译的资源。
  2. APK打包器将DEX文件和已编译资源合并成单个APK。不过,必须先签署APK,才能将应用安装并部署到Android设备上
  3. APK打包器使用调试或者发布密钥库签署你的APK:
    • 如果你构建的是debug版本应用,打包器会使用debug密钥库签署你的应用,Android Studio自动使用debug密钥库配置新项目
    • 如果你构建的是release版本,打包器会使用release密钥库签署你的应用
  4. 在生成最终APK之前,打包器会使用zipalign工具对应用进行优化,减少其在设备上运行时的内存占用

然后再看一张谷歌之前的打包流程图

这张图相比较第一张图而言就更加详细了,从这张图中,可以看到打包流程可以分为以下七步:

  1. aapt-打包res资源文件,生成R.java、resources.arsc和res文件(二进制&非二进制如res/raw和pic保持原样)
  2. AIDL-Android借口定义语言,Android提供的IPC(Inter Process Communication,进程间通信)的一种独特实现。这个阶段处理.aidl文件,生成对应的Java接口文件。
  3. Java Compiler-通过Java Compiler编译R.java、Java接口文件、Java源文件,生成.class文件。
  4. dex-通过dex命令,将.class文件和第三方库中的.class文件处理生成class.dex。
  5. apkbuilder-将class.dex、resources.arsc、res文件夹(res/raw资源被原封不动的打包进APK之外,其他资源都会被编译或者处理)、OtherResouces(assets文件夹)、AndroidManifest.xml打包进apk文件。
  6. Jarsigner-对上面的apk进行debug或release签名
  7. aipalign-将签名后的pak进行对其处理

最后看一张更加详细的图片

Transform

Transform阶段就是在apk打包图中红圈的位置,第二张图更加详细的表示了Transform的过程,是在.class->.dex的过程。

Gradle Transform是Android官方提供给开发者在项目构建阶段由class到dex转换期间修改class文件的一套api。目前经典的应用就是字节码插桩和代码注入技术。有了这个API,我们就可以根据自己的业务需求做一些定制。

先看下transform主要有哪些方法

  1. getName():Transform的名称,但是这里并不是真正的名称,真正的名称还需要进行拼接
  2. getInputTypes():Transform处理文件的类型
    • CLASSES 表示要处理编译后的字节码,可能是jar包也可能是目录
    • RESOURCES表示处理标准的java资源
  3. getScopes():Transform的作用域
    typeDes
    PROJECT只处理当前的文件
    SUB_PROJECTS只处理子项目
    EXTERNAL_LIBRARIES只处理外部的依赖库
    TESTED_CODE测试代码
    PROVIDED_ONLY只处理本地或远程以provided形式引入的依赖库
    PROJECT_LOCAL_DEPS (Deprecated,使用EXTERNAL_LIBRARIES)只处理当前项目的本地依赖,例如jar、aar
    SUB_PROJECTS_LOCAL_DEPS (Deprecated,使用EXTERNAL_LIBRARIES)只处理子项目的本地依赖。
  4. isIncremental():是否支持增量编译,增量编译就是如果第二次编译相应的task没有改变,那么就直接跳过,节省时间,更详细的解释可以看这里
  5. transform():这是最主要的方法,这里对文件或jar进行处理,进行代码的插入。
    • TransformInput:对输入的class文件转变成目标字节码文件,TransformInput就是这些输入文件的抽象。目前它包含DirectoryInput集合与JarInput集合。
    • DirectoryInput:源码方式参与项目编译的所有目录结构及其目录下的源文件。
    • JarInput:Jar包方式参与项目编译的所有本地jar或远程jar包
    • TransformOutProvider:通过这个类来获取输出路径。

通过自定义Plugin创建一个类

我们都知道通过Gradle编译后,会生成一个BuildConfig的类,其中有一些项目的信息,例如APPLICATION_ID、DEBUG等信息,我们依照BuildConfig生成规则,也生成一个自己的MyConfig类,通过这个例子我们可以了解一些gradle的语法和api。

在自定义Plugin类的apply()函数中添加下面的代码

class ConfigPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        //只在'applicatoin'中使用,否则抛出异常
        if (!project.plugins.hasPlugin(AppPlugin::class.java)) {
            throw GradleException("this plugin is not application")
        }
        //获取build.gradle中的"android"闭包
        val android = project.extensions.getByType(AppExtension::class.java)
        //创建自己的闭包
        val config = project.extensions.create("config", ConfigExtension::class.java)
        //遍历"android"闭包中的"buildTypes"闭包,一般有release和debug两种
        android.applicationVariants.all {
            it as ApplicationVariantImpl
            println("variant name: ${it.name}")

            //创建自己的config task
            val buildConfigTask = project.tasks.create("DemoBuildConfig${it.name.capitalize()}")
            //在task最后去创建java文件
            buildConfigTask.doLast { task ->
                createJavaConfig(it, config)
            }
            // 找到系统的buildConfig Task
            val generateBuildConfigTask = project.tasks.getByName(it.variantData.scope.taskContainer.generateBuildConfigTask?.name)
            //自己的Config Task 依赖于系统的Config Task
            generateBuildConfigTask.let {
                buildConfigTask.dependsOn(it)
                it.finalizedBy(buildConfigTask)
            }
        }
    }

    fun createJavaConfig(variant: ApplicationVariantImpl, config: ConfigExtension) {
        val FileName = "MyConfig"
        val constantStr = StringBuilder()
        constantStr.append("\n").append("package ")
                .append(config.packageName).append("; \n\n")
                .append("public class $FileName {").append("\n")
        config.constantMap.forEach {
            constantStr.append("public static final String ${it.key} = \"${it.value}\";\n")
        }
        constantStr.append("} \n")
        println("content: ${constantStr}")

        val outputDir = variant.variantData.scope.buildConfigSourceOutputDir
        val javaFile = File(outputDir, config.packageName.replace(".", "/") + "/$FileName.java")
        println("javaFilePath: ${javaFile.absolutePath}")
        javaFile.writeText(constantStr.toString(), Charsets.UTF_8)
    }

}

就可以生成MyConfig类,这个类简单定义了一些我们可以在build.gradle中定义的变量,可以分为下面的几个步骤

  1. 判断是否为application的module(仅在application中进行操作)
  2. 遍历buildTypes也就是release和debug
  3. 在对应的buildTypes中创建task
  4. 设置自定义的task依赖于BUildConfig的Task
  5. 新建自定义的MyConfig.java文件

Transform的优化:增量与并发

增量

我们想一个问题,遍历一遍项目中所有源文件和jar,时间都是很长的,如果我们每次改一行编译都需要经过这个过程,是很浪费时间。这个时候需要实现增量编译,什么意思呢?增量,顾名思义,就是在已有的基础上,对增加的进行编译,这样在编译过一次的基础上,以后就会大大的缩短时间。

想要开启增量编译,我们需要重写Transform的这个接口,返回true,上面代码中的注释也说明了。

 @Override
    boolean isIncremental() {
        return true
    }

这里需要注意一点:不是每次的编译都是可以怎量编译的,毕竟一次clean build完全没有增量的基础,所以,我们需要检查当前的编译是否增量编译。
需要做区分:

  • 不是增量编译,则清空output目录,然后按照前面的方式,逐个class/jar处理
  • 增量编译,则要检查每个文件的Status,Status分为四种,并且对四种文件的操作不尽相同
    • NOTCHANGED:当前文件不需要处理,甚至复制操作都不用
    • ADDED、CHANGED:正常处理,输出给下一个任务
    • REMOVED:移除outputProvider获取路径对应的文件
@Override
public void transform(TransformInvocation transformInvocation){
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    boolean isIncremental = transformInvocation.isIncremental();
    //如果非增量,则清空旧的输出内容
    if(!isIncremental) {
        outputProvider.deleteAll();
    }	
    for(TransformInput input : inputs) {
        for(JarInput jarInput : input.getJarInputs()) {
            Status status = jarInput.getStatus();
            File dest = outputProvider.getContentLocation(
                    jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            if(isIncremental && !emptyRun) {
                switch(status) {
                    case NOTCHANGED:
                        break;
                    case ADDED:
                    case CHANGED:
                        transformJar(jarInput.getFile(), dest, status);
                        break;
                    case REMOVED:
                        if (dest.exists()) {
                            FileUtils.forceDelete(dest);
                        }
                        break;
                }
            } else {
                transformJar(jarInput.getFile(), dest, status);
            }
        }
        for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(), directoryInput.getScopes(),
                    Format.DIRECTORY);
            FileUtils.forceMkdir(dest);
            if(isIncremental && !emptyRun) {
                String srcDirPath = directoryInput.getFile().getAbsolutePath();
                String destDirPath = dest.getAbsolutePath();
                Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
                for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
                    Status status = changedFile.getValue();
                    File inputFile = changedFile.getKey();
                    String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
                    File destFile = new File(destFilePath);
                    switch (status) {
                        case NOTCHANGED:
                            break;
                        case REMOVED:
                            if(destFile.exists()) {
                                FileUtils.forceDelete(destFile);
                            }
                            break;
                        case ADDED:
                        case CHANGED:
                            FileUtils.touch(destFile);
                            transformSingleFile(inputFile, destFile, srcDirPath);
                            break;
                    }
                }
            } else {
                transformDir(directoryInput.getFile(), dest);
            }
        }
    }
}

这样做真的有用吗?让我们用数据说话,首先准备好测试数据,一个demo,对所有的源文件和第三方依赖库进行扫描,使用增量和非增量的模式进行三次编译,然后取平均值。
两种方式计算自定义transform:

  1. ./gradlew assembleDebug --profile命令,在根目录的build/reports目录下会生成一个文件,可以查找每一个transform所花费的时间。
  2. 在自定义的transformTask之前记录时间,在执行完后记录所花费的时间,贴一下简单的代码
 Task doubleCheckTask = project.tasks["transformClassesWithDoubleCheckTransformFor${variant.name.capitalize()}"]
        doubleCheckTask.configure {
            def startTime
            doFirst {
                startTime = System.nanoTime()
            }
            doLast {
                println()
                println " --> COST: ${TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)} ms"
                println()
            }
        }

这里我们使用的是第二种方式,我们在每次只改动一行代码的情况下,然后让我们看结论

试验次数非增量增量
11309ms151ms
21093ms183ms
31153ms130ms

可以发现,增量的速度比全量的速度快了将近10倍多,这是增量的文件比较少的情况,但是总的来说增量编译还是会大幅度增加编译的速度。

并发编译

并发编译并不复杂,只需要将上面处理单个jar/class的逻辑,并发处理,最后阻塞等待所有任务结束即可。看下伪代码:

 WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//异步并发处理jar/class
waitableExecutor.execute(() -> {
   //jar织入字节码
    return null;
});
waitableExecutor.execute(() -> {
   //file织入字节码
    return null;
});  
//等待所有任务结束
waitableExecutor.waitForTasksWithQuickFail(true);

与增量编译一样,做一些实验对比,用数据说话。

试验次数正常编译并发编译
11309ms856ms
21093ms702ms
31153ms790ms

使用ASM织入代码

从前面几个章节中,了解了自定义Plugin、Transform、Transform的优化,最后一步,就是对目标类进行改造,也就是对class文件进行代码织入

ASM简介

ASM官网中这样介绍ASM

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

ASM是用来对Java字节码进行修改和分析的框架。ASM可以用来修改已经存在的类或者动态生成类,它是直接对二进制文件进行操作的。ASM提供了一些常见的字节码转换和分析算法,从这些转换和分析算法中构建定制复杂的转换和代码分析工具。因为它被设计的和实现的非常的小和尽可能得快,所以它非常和用来动态系统(但是也可以用在静态的方式,例如在编译时)

常用字节码框架

常用的字节码框架就三个Aspectj、Javassist、ASM,三个都有什么区别呢?

织入代码的时期

一图胜千言,直接看图

效率和学习成本

  • AspectJ:是一个代码生成工具,使用它定义的语法生成规则来编写,基本上要扫描所有的文件,当然AspectJx已经实现的非常的好。最主要的是有个坑,在抖音目前的多module的工程上,是有很多坑的,例如,和后面的Transform过程有一些冲突,导致代码一直织入失败,而且编译的时长也大大增加。
  • Javasist:直接操作修改编译后的字节码,而且可以自定义Transform,编译时长可以做很大空间的优化,就是织入代码的效率不如ASM。
    有关javassits的使用可以看这篇文章

根据网上的信息,大神得出的数据结果,这里盗用一下,基本上有3倍的差别,文件越多,ASM和Javasist的效率相差就越大。

ASM的用法

ASM框架中的核心类有以下几个:

  • ClassReader:用来解析编译过的class字节码文件
  • ClassWriter:用来重新构建编译后的类,比如修改类名、属性以及方法,甚至可以生成新的类的字节码文件
  • ClassVisitor:主要负责“拜访”类成员信息。其中包括标记在类上的注解、类的构造方法、类的字段、类的方法、静态代码块。
  • AdviceAdapter:实现了MethodVisitor接口,主要负责“拜访”方法的信息,用来具体的方法字节码操作。

ClassVisitor的全部方法如下,按一定的次序来遍历类中的成员

让我们简单写个demo,这段代码很简单,通过Visitor API读取一个class的内容,保存到另一个文件中去

static void copy(File inputFile, File outputFile) {
        def weavedBytes = inputFile.bytes
        ClassReader classReader = new ClassReader(bytes)
        ClassWriter classWriter = new ClassWriter(classReader,
                ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)

        DoubleClickCheckModifyClassAdapter classAdapter = new DoubleClickCheckModifyClassAdapter(classWriter)
        try {
            classReader.accept(classAdapter, ClassReader.EXPAND_FRAMES)
            weavedBytes = classWriter.toByteArray()
        } catch (Exception e) {
            println "Exception occurred when visit code \n " + e.printStackTrace()
        }

        outputFile.withOutputStream{
            it.write(weavedBytes)
        }
    }

首先,我们通过ClassReader读取某个class文件,然后定义一个ClassWriter,这个ClassWriter其实就是一个ClassVisitor的实现,负责将ClassReader传递过来的数据写到一个字节流中,而真正触发这个逻辑就是通过ClassWriter的accept方式。

上面代码DoubleClickCheckModifyClassAdapter类,也是一个Visitor,也就是我们自定义需要实现的功能。

最后,我们通过ClassWriter的toByteArray(),将从ClassReader传递到ClassWriter的字节码导出,写入新的文件即可。这样我们就完成了字节码的操作,是不是感觉也不难。

ASM code

从上面的例子中,可以看出来,只有DoubleClickCheckModifyClassAdapter需要自己定义,其他的都由上面的模板来写就行,来看下这个类是怎么实现的。


public class DoubleClickCheckModifyClassAdapter extends ClassVisitor implements Opcodes {

    public DoubleClickCheckModifyClassAdapter(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        if ((ASMUtils.isPublic(access) && !ASMUtils.isStatic(access)) && 
                name.equals("onClick") && 
                desc.equals("(Landroid/view/View;)V")) {
            methodVisitor = new View$OnClickMethodVisitor(methodVisitor);
        }

        return methodVisitor;
    }
}

可以从上面的visitMethod方法中看到,我们只对View的onClick函数进行代码织入,然后再看下View$OnClickMethodVisitor的实现

public class View$OnClickMethodVisitor extends MethodVisitor {
    private boolean weaved;

    public View$OnClickMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        if (weaved) return;

        AnnotationVisitor annotationVisitor =
                mv.visitAnnotation("L" + DoubleCheckConfig.checkClassAnnotation + ";", false);
        annotationVisitor.visitEnd();

        mv.visitMethodInsn(Opcodes.INVOKESTATIC, DoubleCheckConfig.checkClassPath, "isClickable", "()Z", false);
        Label l1 = new Label();
        mv.visitJumpInsn(Opcodes.IFNE, l1);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitLabel(l1);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        /*Lcom/smartdengg/clickdebounce/Debounced;*/
        weaved = desc.equals("L" + DoubleCheckConfig.checkClassAnnotation + ";");
        return super.visitAnnotation(desc, visible);
    }

}

最主要的实现就是visitCode函数,这里面,我们实现了代码的织入。

我们设想一下,如果要对某个class进行修改,那需要对字节码具体做什么修改呢?最直观的方法就是,先编译生成目标class,然后看它的字节码和原来class的字节码有什么区别,但是这样还不够,其实我们最终并不是读取字节码,而是使用ASM来修改。

问题来了,我不懂ASM,对字节码更是很陌生,怎么办?难道我要从新学习一下字节码,才能进行开发吗?答案当然不是,如果我们只是对字节码做一些简单的操作,完全可以使用工具来帮我们完成

这里安利一个非常好用的工具,Intellij IDEA有个插件Asm Bytecode Outline,可以查看一个class文件的bytecode和ASM code,同样,Android Studio同样也有一个类似的插件ASM Bytecode Viewer实现了同样的功能。

如何编写ASM代码

这是我们源代码

public class A {
    static void toast(Context context) {
        Toast.makeText(context,"test",Toast.LENGTH_LONG).show();
    }
}

我们想通过字节码插装后变为

public class A {
    static void toast(Context context) {
        Log.i("tag","test");
        Toast.makeText(context,"test",Toast.LENGTH_LONG).show();
    }
}

也就是在toast函数的第一行插入Log代码。

  1. 我们将要插入的代码先写入源代码中,在文件中右击鼠标.
  2. 点击ASM Bytecode Viewer
  3. 打开右侧的ASM预览界面,就能看到对应的ASM代码


到此为止,貌似使用对比ASM code的方式,来实现字节码修改也不难,但是,这种方式只是可以实现一些修改字节码的基础场景,还有很多场景是需要对字节码有一些基础只是才能做到,而且,要阅读懂ASM code,也是需要一定字节码的知识。所以,如果要开发字节码工程,还是需要学习一番字节码的。

实际应用

Theory without practice is empty,practice without theory is blind

我们既然已经了解了ASM的原理,那么我们应用于实践,我们不能为了学技术而学技术,技术最终是要服务于业务的,我们更加应该从业务的角度出发,来思考问题和提升自己(题外话)。

有一个场景,在可点击的地方,经常出现连击,但是结果并不是我们想要的,例如,跳转到个人页面,我们快速点击两次,就会发现出现了两个个人页面,需要back两次才能回到之前的页面,这个肯定是不符合预期的,几乎在所有的点击地方,都应该做防抖动的操作(抖动,就是快速或者不小心在短时间内多次点击,我也不知道为什么叫抖动,网上都这样写),现在有几个思路:

  • Kotlin中实现view的扩展函数,这样就可以统一的地方做防抖动操作,但是如果大家不是理解这个点,有可能还会调用之前的点击事件,那么还有可能出现这个问题。
  • Java中封装一个工具类,要求每个开发人员在在OnClick中添加这个函数,但是这个很容易被遗忘,可操作性不大。
  • 使用AOP(AspectJ和ASM),在编译期间,将所有的Onclick函数中做判断,而且ASM兼容Java和Kotlin。这里选择ASM,上一章节已经说明原因

从以上几点中,可以得出使用ASM是目前最好的方案。

参考文章

ASM 操作字节码初探
一起玩转Android项目中的字节码
一文读懂 AOP | 你想要的最全面 AOP 方法探讨

Gradle Transform 详解 我们先来看下Android应用程序打包流程: 通过上图可知,我们只要在图中红色箭头处拦截(生成class文件之后,dex文件之前),就可以拿到当前应用程序中所有的.class文件,再去借助ASM之类的库,就可以遍历这些.class文件中所有方法,再根据一定的条件找到需要的目标方法,最后进行修改并保存,就可以插入我们的埋点代码。 Google从 Android Gradle 1.5.0 开始,提供... 阅读详情

相关推荐

Transform+Javassist实现一个方法耗时打印

背景 某天晚上睡不着在思考一个问题:组件化app module的Application的生命周期如何让lib module感知到,即lib module在应用启动时在自己的Application里做初始化操作而不用写到app module的Application里,实现完全解耦。 查阅资料后发现好像可以用Transform+class代码注入(Javassist)的方式实现,因为以前没接触过,方法...

pxq的博客 1394

Gradle学习系列(五):Gradle Transform

概述 又开始了一个新的系列,这个系列学习Gradle,目标就是彻底理解Gradle,主要还是做下自己理解的笔记,防止忘记 Gradle系列(一):Groovy学习 Gradle学习系列(二):Gradle核心解密 Gradle学习系列(三):Gradle插件 Gradle学习系列(四):Gradle依赖 Gradle学习系列(五):Gradle Transform 简介 Google从 Android Gradle 1.5.0开始提供了Transform API,Gradle Transform是Andro

R.先森的博客 945

Android动态编译技术:Plugin Transform Javassist操作Class文件

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 动态编译技术在开源框架中的应用非常的广泛,现在市面上的插件化框架,热修复框架几乎都使用了动态编译技术,原理几乎都是在编译期间动态的在class文件中注入代码或者或修改。那就让我们来了解一下这高大上的技术吧。 本章会以完成一个demo的形式来揭开动态编译的神秘面纱,将会分成三步 一、自定义Gradle插件 二

yulong0809的专栏 1万+

【Android 字节码插桩】Gradle插件基础 & Transform API的使用

啪~我给大家开个会(手机扔桌子上)什么叫做 客户无感的数据脱敏!?师爷给翻译翻译什么叫做客户无感的数据脱敏?什么特么的叫做客户无感数据脱敏?举个栗子~客户端Sdk新升级了一个版本,增加针对客户的数据的脱敏,但是客户不需要重新调用新的api,且旧的api执行的性能还是不变的,那么大家可能就会问, 切入点在哪呢?你不调用新的api或者改动旧的api,如何获取用户数据呢?字节码的插桩就是做这个用的, 一句话描述~

无方少年游 2001

“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform/ASM—动态代码注入

本篇内容较多,前半部分都是在介绍铺垫知识,只有完全理解了才能掌握ARouter框架的核心。其中Gradle、AGP是Android开发中必定使用到的技术点,希望通过对ARouter的学习能更加深刻的理解Android编译构建的过程,以及使用Gradle插件能完成哪些事情。Transform是官方Gradle 插件中提供的API,它能获取和处理被打进dex文件之前的class文件。不过Transform在最新的Gradle插件版本中被废弃了,不过这不影响我们我们对它的学习。

胡飞洋的技术积累 1287

Gradle Transform API

概述 动态编译技术在开源框架中的应用非常的广泛,现在市面上的插件化框架,热修复框架几乎都使用了动态编译技术,原理几乎都是在编译期间动态的在class文件中注入代码或者或修改。 Java的AOP 框架是一个比较出名的面向切面编程框架,这是一种通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的技术,可以动态的修改Java的Class字节码的内容。而比较出名的AOP 框架是...

xiangzhihong8的专栏 2717

Gradle8.0中Transform的替代方案

在Gradle插件中对HuiRouterApi进行插桩,添加插入路由信息的字节码指令,在程序运行时,插桩的字节码指令执行,路由信息被注册到HuiRouterApi内部的路由表中,然后外部就可以通过HuiRouterApi进行跳转操作了。扫描过程中,收集的路由信息记录到annotationPathMap中。5、在taskAction方法中,扫描遍历所有的jar和class文件,收集路由信息,查找到HuiRouterApi类,并将收集到的路由信息通过插桩的方式添加到HuiRouterApi中。

zhaonhui的博客 2257

ESP32 DMX512与RDM协议栈开发指南

DMX512-A是专业灯光控制领域广泛采用的串行通信标准,基于RS-485物理层实现确定性单向数据传输;RDM(Remote Device Management)则在其基础上扩展双向、可寻址、可配置的智能管理能力,满足现代舞台设备远程固件升级、自动发现与参数配置需求。二者共用同一总线但时序与帧结构严格隔离,对硬件定时精度、中断响应延迟及电气隔离提出极高要求。在嵌入式实践中,ESP32凭借Wi-Fi/蓝牙连接能力与丰富外设资源,成为构建低成本智能DMX节点的理想平台。esp_dmx协议栈正是面向该场景的高性能

weixin_34945060的博客 316

其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4)

而 Task 的依赖关系是通过 TransformTask 的输入输出关系隐式确定的,TransformManager 通过 TransformStream 链接各个 TransformTask 的输入输出,进而控制 Transform 的依赖关系顺序。是的,就是卷,而且这次直接是降维打击。Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。

chuyouyinghe的专栏 642

爬取国家统计局最近10年年度数据

一、编码流程 1.目标网址:https://data.stats.gov.cn/easyquery.htm?cn=C01 2.编码思路: (1) 解析左侧指标树,实现4级标签定位 (难点:层层嵌套,下一级指标的xpath路径以上一级指标为基础;不是所有二级指标都含有三和四级指标,需要进行判断) (2) 使用selenium webdriver 点选指标,加载出右侧表格数据 (难点:当指标无法在全局页面显示时,需向下拖动滚动条) (3) 解析右侧数据,实现数据存储 3.使用模块: (1) selenium:解

weixin_39645690的博客 4556

HyperLynx实战:手把手教你搞定DDR4信号完整性仿真(附ODT与终端电阻配置避坑)

本文详细介绍了使用HyperLynx进行DDR4信号完整性仿真的实战技巧,重点解析了ODT配置与终端电阻优化的黄金法则。通过模型准备、拓扑构建、ODT扫描等关键步骤,帮助硬件工程师规避常见设计陷阱,提升高速数字电路设计的效率与可靠性。

weixin_30861797的博客 638

Gradle Transform

概述Gradle Transform是Android官方提供给开发者在项目构建阶段即由class到dex转换期间修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。基础 TransformInput:所谓Transform就是对输入的class文件转变成目标字节码文件,TransformInput就是这些输入文件的抽象。目前它包括两部分:DirectoryInput集合与J

Android 1万+

Windows 下 OpenClaw(小龙虾) 安装全记录:从零到一的保姆级图文教程(持续更新中)

本文主要介绍了Windows 下 OpenClaw 安装全记录:从零到一的保姆级图文教程,希望能对学习使用OpenClaw 的同学们有所帮助。 文章目录 1. 背景 2. 实战教程 2.1 步骤一 安装Node.js 2.2 步骤二:安装并配置OpenClaw 2.3 步骤三:配置模型 2.4 步骤四:对话 3. 后续使用步骤

weixin_43178406的博客 16万+

Gradle中Transform是什么?

概述 Google从 Android Gradle 1.5.0 开始,提供了Transform API。通过Transform API,允许第三方以插件的形式,在Android应用程序打包成dex文件之前的编译过程中操作.class文件。我们只要实现一套Transform,去遍历所有.class文件的所有方法,然后进行修改,再对源文件进行替换,即可以达到插入代码的目的。 Transform可以做什么 首先,我们可以先执行一次build操作,命令行会输出如下内容: > Transform core-ru

一个学渣走向android之路 1548

【数据集】全球首个10米分辨率精细分类土地覆盖数据集GLC_FCS10

【数据集】全球首个10米分辨率精细分类土地覆盖数据集GLC_FCS10

WW、forever的博客 1567

【AI智能体】Hermes Agent 从部署到项目实战操作详解

Hermes Agent 桌面版从安装到实战操作详解

congge 5130

Android Gradle Transform 创建自定义插件

Android Gradle Transform 创建自定义插件

风言风语 654

【全网最新OD题库】2026华为 OD 机试C卷 机考真题库清单(全真题库)含考点说明(华为OD+2025年C卷+2025年B卷+2025年A卷+E卷+D卷+C卷)

华为od机试,华为od机试真题,华为od机试E卷,华为odE卷,华为od机试真题E卷,华为OD,华为OD机试,华为OD机试真题,华为od机考,华为od机试D卷,华为od机试真题D卷,华为od手撕代码,华为od,华为od机试C卷,华为od e卷,华为2024e卷,od机试,2024E卷,华为OD机试E卷,什么是华为od,华为od介绍,全面了解华为od,2024华为od,如何备战华为od,华为od算法,华为od手撕代码,华为OD机试2024年最新题库,一文带你读懂华为OD,华为机试e卷,华为OD2025B卷、华

KJ.JK 3万+

破解Gradle(六) Gradle Plugin技术及玩转transform

如果你想在编译期间搞事情,如常用的有无痕埋点,方法耗时统计和组件通信中自动注入等等,就要来学习字节码插桩的技术。而所谓字节码插桩技术其实就是修改已经编译好的class文件,在里面添加自己的字节码,然后打出的包就是修改后的class文件。

Lebron_xia的博客 2746

YOLOv11涨点改进 | 全网独家创新,注意力改进篇 | TGRS 2025 | 引入APCM自适应像素级协同机制,动态增强目标区域、抑制背景噪声,助力YOLOv11做红外小目标检测有效涨点

本文提出了一种基于APCM模块改进的YOLOv11网络模型,通过像素级局部-全局协同建模增强目标特征并抑制背景噪声。APCM模块采用轻量化设计,无需复杂参数,能有效提升小目标检测性能。文章详细介绍了APCM的原理、优势及其在红外小目标检测中的应用,包括模块结构、核心代码实现和YAML配置文件修改方法。实验结果表明,该方法在NUAA-SIRST和IRSTD-1K数据集上优于现有技术,显著提高了复杂场景下的检测精度与鲁棒性。

Ai缝合怪、助力小伙伴高效发论文 114

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Android Gradle高级用法,动态编译技术:Plugin Transform Javassist操作Class文件

今日快讯2017年9月15日,饿了么母公司拉扎斯集团发内部邮件,宣布饿了么和百度外卖继8月24日合并后的首轮架构调整通知。根据该通知,出任百度外卖新CEO的是魏海,负责百度外卖业务及双品牌战略落地工作。根据公开资料,魏海在饿了么的职位是商业分析副总裁。而百度外卖原CEO巩振兵担任百度外卖董事长,负责集团及百度外卖战略布局及关键业务梳理;百度外卖其他管理层人员任用保持不变。作者简介短暂的休息回到了周

郭霖 5121

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

android aar项目_Android 编译插桩(二): Gradle Transform

Android 编译插桩(二): Gradle TransformAndroid 的打包流程 上图是 apk 打包的流程,可以看启用有一步是 .class 文件到 .dex 发文件的转换。而 Gradle Transform 是 Android 官方提供的在这一阶段用来修改 .class 文件的一套标准 API。 这一应用现在主要集中在字节码查找、代码注入等1. Transform 原理 图片出自...

weixin_39861627的博客 514

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Transform

从gradle 1.5版本开始,Android官方提供了Gradle Transform技术用于在项目构建阶段,即由class到dex转换期间修改class文件的一套api,借用这套api,开发者可以很容易的完成字节码插桩、代码注入技术等注入技术。 Transform用于处理中间构建的工件。对于每个添加的Transform,都会创建一个新任务。 添加Transform的操作负责处理任务之间的依赖...

weixin_34257076的博客 540

一步步实现AddTryCatch插件 —— Gradle Transform和ASM实践

在上一篇AddTryCatch gradle plugin 编译期给代码加try catch的插件中介绍了AddTryCatch插件的使用方法,这一篇来一步步实现一下AddTryCatch插件。 参考这篇文章,可以快速开发一个gradle插件,包含了我很多的探索和踩坑,看完这篇文章直接就可以上手开发插件了。

星辰旋风的博客 3233

Gradle Transform和ASM

本文链接:https://blog.youkuaiyun.com/feather_wch/article/details/131753544。

feather(猎羽) 585

dex工具与transform_初识Android Gradle编译之Transform

什么是Transform从android-build-tool:gradle:1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标就是简化class文件的自定义的操作而不用对Task进行处理。本文主要学习 Transform API 的基本知识,然后借助 javassist 来完成...

weixin_39624332的博客 444
上一篇: 浅析Android插件化
下一篇: Paging Library-初探分页库
Bobby_lyc
博客等级 码龄16年 1164粉丝 89原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值