Android Transform API 从原理到实战

2024-06-12 10:08

本文主要是介绍Android Transform API 从原理到实战,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Transform API


从 1.5.0-beta1 开始,Gradle 插件包含一个 Transform API,允许第三方插件在将已编译的类文件转换为 dex 文件之前对其进行操作。(该 API 已存在于 1.4.0-beta2 中,但已在 1.5.0-beta1 中进行了彻底修改)

Transform API 的目标是简化注入自定义类的操作而不必处理任务,并为操作内容提供更大的灵活性。内部代码处理(jacoco,progard,multi-dex)已经在 1.5.0-beta1 中转移到了这一新机制。

Transform 的注册和使用非常简单,在我们自定义的 Gradle 插件中,只需创建一个实现 Transform 接口的类,然后将其注册到 android.registerTransform(theTransform) 或 android.registerTransform(theTransform, dependencies) 中即可。

Transform 是一个链式结构,每个 Transform 都是一个 Gradle 的 Task,Android 编译器通过 TaskManager 将每个 Transform 串联起来。

Gradle Transform 是 Android 官方提供给开发者在项目构建阶段,即由 .class 到 .dex 转换期间修改 .class 文件的一套 API。目前比较经典的应用是字节码插桩、代码注入技术。

主要方法

Transform.java 是一个抽象类,我们在使用是,需要实现它,它的主要方法有哪些呢?

getName()

用于指定 Transform 的名字,对应了该 Transform 的 Task 的名称。

isIncremental()

该方法指明是否支持增量编译,增量编译用于加快编译速度。

getInputTypes()

指定 Transform 要处理的数据类型,可以作为输入过滤的一种手段。

在 TransformManager 中定义了很多类型:

  • CONTENT_CLASS // 代表 javac 编译成的 class 文件,可能是 jar 也可能是目录。
  • CONTENT_JARS
  • CONTENT_RESOURCES // 表示处理标准的 java 资源
  • CONTENT_NATIVE_LIBS
  • CONTENT_DEX
  • CONTENT_DEX_WITH_RESOURCES
  • DATA_BINDING_BASE_CLASS_LOG_ARTIFACT
getScopes()

用于指定 Transform 的作用域。同样在 TransformManager 中定义了很多类型。

常见的作用域有:

PROJECT:只处理当前项目。
SUB_PROJECT:只处理子项目。
PROJECT_LOCAL_DEPS:只处理当前项目的本地依赖,例如:jar、aar。
SUB_PROJECT_LOCAL_DEPS:只处理子项目的本地依赖,例如:jar、aar。
EXTERNAL_LIBRARIES:只处理外部依赖库。
PROVIDED_ONLY:只处理本地或远程以 provided 形式引入的依赖库。
TESTED_CODE:测试代码。
SCOPE_FULL_PROJECT:即代表所有 Project。

确定了 ContentType 和 Scope 后就确定了该自定义 Transform 需要处理的资源流。
例如,上面提到的常用输入类型(CONTENT_CLASS)和常用作用域(SCOPE_FULL_PROJECT)表示的就是 所有项目中 java 编译成的 class 组成的资源流。

transform()

transform 方法来处理中间转换过程,主要逻辑在该方法中实现。

它的定义:

public void transform(@NonNull TransformInvocation transformInvocation)

该方法的参数是 TransformInvocation。

我们可以在 transform 方法中,实现对字节码的修改、处理等操作。

TransformInvocation

我们可以通过 TransformInvocation 来获取输入,同时也获得了输出的功能。

TransformInvocation 接口定义如下:

public interface TransformInvocation {@NonNull Context getContext();//TransformInput 是输入文件的抽象,包括 jar 和目录格式。@NonNull Collection<TransformInput> getInputs();@NonNull Collection<TransformInput> getReferencedInputs();@NonNull Collection<SecondaryInput> getSecondaryInputs();// Transform 的输出,通过它可以获取输出路径。@Nullable TransformOutputProvider getOutputProvider();boolean isIncremental();
}

TransformInvocation 的使用,我们举例说明一下:

    @Overridevoid transform(@NonNull TransformInvocation transformInvocation) {def startTime = System.currentTimeMillis()//如果是非增量编译,则删除之前的输出if (!transformInvocation.isIncremental) {transformInvocation.outputProvider.deleteAll()}//TransformInvocation 来获取输入Collection<TransformInput> inputs = transformInvocation.inputs//TransformInvocation 来获取输出TransformOutputProvider outputProvider = transformInvocation.outputProvider//遍历inputsinputs.each { TransformInput input ->//遍历directoryInputsinput.directoryInputs.each { DirectoryInput directoryInput ->handleDirectoryInput(directoryInput, outputProvider)}//遍历jarInputsinput.jarInputs.each { JarInput jarInput ->handleJarInputs(jarInput, outputProvider)}}def cost = (System.currentTimeMillis() - startTime) / 1000}

TransformInput

TransformInput 是指这些输入文件的抽象。它包括两部分:

1. DirectoryInput 集合

是指以源码方式参与项目编译的所有目录结构及其目录下 的源码文件。

2. JarInput 集合

是指以 jar 包方式参与项目编译的所有本地 jar 包和远程 jar 包。

TransformOutputProvider

是 Transform 的输出的抽象,通过它可以获取输出路径。TransformOutputProvider 通过调用 getContentLocation 来获取输出目录:

    @NonNullFile getContentLocation(@NonNull String name,@NonNull Set<QualifiedContent.ContentType> types,@NonNull Set<? super QualifiedContent.Scope> scopes,@NonNull Format format);

实战

Transform 的注册和使用非常易懂, 在我们自定义的 plugin 内, 我们可以通过 android.registerTransform(theTransform) 或者 android.registerTransform(theTransform, dependencies) 就可以完成注册。

使用 Transform API 主要是写一个类继承 Transform,并把该 Transform 注入到打包过程中。
注入 Transform 很简单,先获取 com.android.build.gradle.AppExtension 对象,然后调用它的registerTransform() 方法。

这个方法实际上是属于 BaseExtension 的,AppExtension 继承自 BaseExtension:

#com.android.build.gradle.BaseExtension
public void registerTransform(@NonNull Transform transform, Object... dependencies) {transforms.add(transform);transformDependencies.add(Arrays.asList(dependencies));
}

注册我们自定义的 Transform:

    void apply(Project project) {AppExtension android = project.extensions.getByType(AppExtension)android.registerTransform(new MethodTimeTransform(project))}

通过获取 module 的 Project 的 AppExtension,通过它的 registerTransform 方法完成 Transform 的注册。

这里注册之后,会在编译过程中的 TransformManager#addTransform 中生成一个 task,然后在执行这个 task 的时候会执行到我们自定义的 Transform 的 transform 方法。这个 task 的执行时机就是 .class 文件转换成 .dex文 件的时候。

我们来看完整的实例代码。

自定义的 Transform 类,AsmTransform:

class AsmTransform extends Transform{private Project mProject;AsmTransform(Project project){mProject = project}@OverrideString getName() {return "budaye_transform"}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return true}@Overridevoid transform(@NonNull TransformInvocation transformInvocation) {def startTime = System.currentTimeMillis()//如果是非增量编译,则删除之前的输出if (!transformInvocation.isIncremental) {transformInvocation.outputProvider.deleteAll()}Collection<TransformInput> inputs = transformInvocation.inputsTransformOutputProvider outputProvider = transformInvocation.outputProvider//遍历inputsinputs.each { TransformInput input ->//遍历directoryInputsinput.directoryInputs.each { DirectoryInput directoryInput ->handleDirectoryInput(directoryInput, outputProvider)}//遍历jarInputsinput.jarInputs.each { JarInput jarInput ->handleJarInputs(jarInput, outputProvider)}}def cost = (System.currentTimeMillis() - startTime) / 1000}/*** 处理文件目录下的class文件*/static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {//是否是目录if (directoryInput.file.isDirectory()) {//列出目录所有文件(包含子文件夹,子文件夹内文件)directoryInput.file.eachFileRecurse { File file ->def name = file.nameif (checkClassFile(name)) {println '----------- deal with "class" file <' + name + '> -----------'ClassReader classReader = new ClassReader(file.bytes)ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)ClassVisitor cv = new LifecycleClassVisitor(classWriter)classReader.accept(cv, EXPAND_FRAMES)byte[] code = classWriter.toByteArray()FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)fos.write(code)fos.close()}}}//处理完输入文件之后,要把输出给下一个任务def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes, directoryInput.scopes,Format.DIRECTORY)FileUtils.copyDirectory(directoryInput.file, dest)}/*** 处理Jar中的class文件*/static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {if (jarInput.file.getAbsolutePath().endsWith(".jar")) {//重名名输出文件,因为可能同名,会覆盖def jarName = jarInput.namedef md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())if (jarName.endsWith(".jar")) {jarName = jarName.substring(0, jarName.length() - 4)}JarFile jarFile = new JarFile(jarInput.file)Enumeration enumeration = jarFile.entries()File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")//避免上次的缓存被重复插入if (tmpFile.exists()) {tmpFile.delete()}JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))//用于保存while (enumeration.hasMoreElements()) {JarEntry jarEntry = (JarEntry) enumeration.nextElement()String entryName = jarEntry.getName()ZipEntry zipEntry = new ZipEntry(entryName)InputStream inputStream = jarFile.getInputStream(jarEntry)//插桩classif (checkClassFile(entryName)) {//class文件处理println '----------- deal with "jar" class file <' + entryName + '> -----------'jarOutputStream.putNextEntry(zipEntry)ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)ClassVisitor cv = new LifecycleClassVisitor(classWriter)classReader.accept(cv, EXPAND_FRAMES)byte[] code = classWriter.toByteArray()jarOutputStream.write(code)} else {jarOutputStream.putNextEntry(zipEntry)jarOutputStream.write(IOUtils.toByteArray(inputStream))}jarOutputStream.closeEntry()}//结束jarOutputStream.close()jarFile.close()def dest = outputProvider.getContentLocation(jarName + md5Name,jarInput.contentTypes, jarInput.scopes, Format.JAR)FileUtils.copyFile(tmpFile, dest)tmpFile.delete()}}/*** 检查class文件是否需要处理* @param fileName* @return*/static boolean checkClassFile(String name) {//只处理需要的class文件return (name.endsWith(".class") && !name.startsWith("R\$")&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)&& "android/support/v4/app/FragmentActivity.class".equals(name))}
}

AsmTransform 的注册:

    @Overridevoid apply(Project project) {if (project.plugins.hasPlugin(AppPlugin)){//registerTransformdef android = project.extensions.getByType(AppExtension)android.registerTransform(new AsmTransform(project))}}

到了这里,就实现了整个 Transform 的定义和注册过程了。


**PS:更多性能优化相关文章,请查看 --> 《Android 性能优化》
**PS:更多性能优化相关文章,请查看 --> 《Android 性能优化》
**PS:更多性能优化相关文章,请查看 --> 《Android 性能优化》

这篇关于Android Transform API 从原理到实战的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1053914

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu