函数插桩技术是可以提高开发者开发效能的有力工具。常用的组合是TransformApi+ ASM,在打包apk的过程中,对特定的类最修改,偷梁换柱,以满足我们的一些特殊需要,如全局监控网络、计算方法耗时、组件化中的路由收集,自动加埋点等。

但是使用中对Transform API 的理解一直不是很到位,如Transform是在apk打包apk的哪个环节生效,Transfrom的边界是哪里?

本文就上面这些问题 做一些梳理和总结。

一、Transform API 的常规用法

Transform API 是gradle 1.5.0 开始引入的,它允许第三方插件在编译后的类文件转换为dex文件之前做处理操作. Transform API 可以让我们聚焦在如何对输入的类文件进行处理,而不用关系AppPlugin的编译流程。

1.1、注册Transform

使用Transform 只需要注册一个Plugin,在Apply方法中,在AppExtension对象上调用registerTransform() 将自定义Transform添加进去就可以了。

class TrPlugin : Plugin {

override fun apply(project: Project) {

var isApp =

project.plugins.hasPlugin(AppPlugin::class.java) //是否引入了com.android.application 插件

TrLogger.setLogger(project.logger)

if (isApp) {

//注册Transform

val android = target.extensions.findByType(AppExtension::class.java)

android?.registerTransform(CostTransform())

}

}

}

AppExtension 是实际上对应build.gradle中的android{}标签

AppExtension 集成自BaseExtension,可见注册Transform仅是将Transform对象 加入到了AppExtension的transforms容器中.

1.2、TransForm 的主要API

Transform的常用API 如下

public abstract class Transform {

public abstract String getName();

public abstract Set getInputTypes();

public abstract Set super Scope> getScopes();

public abstract boolean isIncremental();

fun transform(transformInvocation:TransformInvocation)

}

1.2.1、name :Transform的唯一名称。

Transform 最终会被封装成一个TransformTask,TransformTask的名称并不与Transform完全一致。

TransformTask的名称格式如下:

transform+"InputType"+With+"TransformName"+For+"BuildType"

static String getTaskNamePrefix(Transform transform) {

StringBuilder sb = new StringBuilder(100);

sb.append("transform");

sb.append((String)transform.getInputTypes().stream().map((inputType) -> {

return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, inputType.name());

}).sorted().collect(Collectors.joining("And"))).append("With").append(StringHelper.capitalize(transform.getName())).append("For");

return sb.toString();

}

如自定义的Transform 名称是Cost,对应的TransformTask的名称可能为:transformClassesWithCostForDebug

1.2.2、getInputTypes:transform 要处理的数据类型

我们可以用的只有两个:

CLASSES 表示要处理编译后的字节码,CLASSES已经包含了class文件和jar文件

RESOURCES 表示的是啥???? 没搞清楚,此处待定。

1.2.3、getScopes 表示transform 的作用域

type

Des

PROJECT

只处理当前项目

SUB_PROJECTS

只处理子项目

PROJECT_LOCAL_DEPS

只处理当前项目的本地依赖,例如jar, aar

EXTERNAL_LIBRARIES

只处理外部的依赖库

PROVIDED_ONLY

只处理本地或远程以provided形式引入的依赖库

TESTED_CODE

测试代码

1.2.4、tranform方法

transform() 是Tranform进行数据处理的地方。

image

TransForm是链式调用的,如上图所示,TransformB的输入 是TransformA的输出,TransformB的输出同时也是TransformC的输入。

所以Transform.transform()方法 即使任何功能不实现,也需要完成一个将文件从input目录拷贝到output目录的动作,否则下一个Transform将会丢失待处理的文件(class或jar)。

加入Transform的名字为Cost,项目编译之后,在build/intermediates/transform目录下就会出现Cost目录,该目录就是Cost Transform的输出目录,同时也是下一级Tranform的输入目录。

image

Cost目录目录之下还会生成一个content.json ,类似一个文件清单的样子。

[{

"name": "org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72_7b6c9b0015ab57b3a6475f5627bb94c0",

"index": 0,

"scopes": ["EXTERNAL_LIBRARIES"],

"types": ["CLASSES"],

"format": "JAR",

"present": true

}, {

"name": "androidx.core:core-ktx:1.3.2_004fa720a5219b591486b877aa0fab1c",

"index": 1,

"scopes": ["EXTERNAL_LIBRARIES"],

"types": ["CLASSES"],

"format": "JAR",

"present": true

}

...

]

在完成class和jar文件从input目录拷贝到output的基础之上,可以完成一些额外的处理操作,如利用ASM 对特定类进行修改。

override fun transform(transformInvocation: TransformInvocation?) {

//TransformInput 包含两个类型的输入:jar文件和文件夹

transformInvocation?.inputs?.forEach { input ->

//jar输入,它代表着以jar包方式参与项目编译的所有本地jar包或远程jar包,

input.jarInputs.forEach { jarInput ->

//输入文件名

val destName = jarInput.name.let {

//jar文件去掉.jar后缀

if (it.endsWith(".jar")) it.substring(0, it.length - 4) else it

}

//确定输出文件名

val finalDestName = "${destName}_${DigestUtils.md5Hex(jarInput.file.absolutePath)}"

//确定输出文件

val destFile = transformInvocation.outputProvider.getContentLocation(

finalDestName,

jarInput.contentTypes,

jarInput.scopes,

Format.JAR

)

//(1) 此处完成对jar文件的额外处理

//通用操作,将jar文件 从输入copy到输出目的地

FileUtils.copyFile(jarInput.file, destFile)

}

//目录输入,它代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件

input.directoryInputs.forEach { directoryInput ->

//(2)此处可以完成对class文件的额外处理操作

//确定输出文件des

val dest: File = transformInvocation.getOutputProvider().getContentLocation(

directoryInput.name,

directoryInput.contentTypes,

directoryInput.scopes,

Format.DIRECTORY

)

//完成从source到dest的拷贝操作

TrLogger.e("DirectoryInput:${directoryInput.file.absolutePath},dest:${dest.absolutePath}")

FileUtils.copyDirectory(directoryInput.file, dest)

}

}

}

二、Tranform 是如何工作的

2.1、AppPlugin

Android项目build.gradle中通常会引入application插件

apply plugin: 'com.android.application'

com.android.application 其实是Gradle内置的一个用于构建apk的gradle插件,对应AppPlugin.class,它负责完成apk整个构建过程。

2.2、Extention

Plugin的入口函数Apply()接收一个参数Project,Project代表运行该插件的项目.

project.extensions.getByType(AppExtension::class.java)

Project中可以注册一些可以供用户个性化配置的信息,称作Extention,通过Extension用户向Plugin插件传递参数。

Extention通过Project.ExtensionContainer进行维护,支持通过名称、类名查找,支持新增Extention

public interface Project extends Comparable, ExtensionAware, PluginAware {

ExtensionContainer getExtensions();

}

AppExtension是AppPlugin会默认创建的一个Extension

//注册AppExtention,取名为android

project.getExtensions()

.create(

"android",//指定extension的名称

AppExtention(),

project,

projectOptions,

globalScope,

sdkHandler,

buildTypeContainer,

productFlavorContainer,

signingConfigContainer,

buildOutputs,

sourceSetManager,

extraModelInfo,

isBaseApplication);

AppExtension 对我们其实并不陌生,它实际上就是build.gradle中的android

android {

compileSdkVersion 29

buildToolsVersion "30.0.2"

defaultConfig {

applicationId "com.sogou.iot.trplugin"

minSdkVersion 16

targetSdkVersion 29

versionCode 1

versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

}

buildTypes {

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

}

}

}

通过AppExtension 我们可以告诉AppPlugin,我们所使用的compileSdkVersion、buildToolsVersion、buildTypes、Flavor 等等。

此外我们在自定义Plugin中,也可以为Project注册Extention,来供用户传递参数。

注册Extension

class TrPlugin : Plugin {

override fun apply(project: Project) {

var isApp =

project.plugins.hasPlugin(AppPlugin::class.java) //是否引入了com.android.application 插件

TrLogger.setLogger(project.logger)

if (isApp) {

//注册Extension

project.extensions.create(PluginHolder.componentExt, ComponentExtension::class.java)

project.extensions.getByType(AppExtension::class.java)?.apply {

registerTransform(CostTransform())

//扫描,收集类信息

}

}

}

}

open class ComponentExtension {

//待搜集的接口类

var matchInterfaceType: String = ""

//Container容器类

var matchInjectManagerType: String = ""

//容器类的

var matchInjectManagerInjectMethod: String = ""

var openLog:Boolean = false

var logLevel:Int = TrLogger.LogLevelDebug

}

build.gradle 设置componentExt参数

componentExt{

matchInterfaceType = "com.sogou.iot.trplugin.IComponent"

matchInjectManagerType = "com.sogou.iot.trplugin.ComponentManager"

matchInjectManagerInjectMethod = "initComponet"

openLog = true

logLevel = TrLogger.LogLevelDebug

}

插件中提取参数

(project?.extensions?.getByName(componentExt) as ComponentExtension).openLog

讲了这么多,Extension和Transform有什么关系呢?上面有点扯远了,下面进入正题。

AppExtension继承BaseExtension,BaseExtension中有一个Transform类型的数组

public abstract class BaseExtension implements AndroidConfig {

private final List transforms = Lists.newArrayList();

}

所以registerTransform()注册Transform 实际上是把Tranform对象加入到transforms数组中,以便后面构建TransformTask时使用。

## BaseExtension.class

public void registerTransform(@NonNull Transform transform, Object... dependencies) {

transforms.add(transform);

transformDependencies.add(Arrays.asList(dependencies));

}

2.3、自定义TransformTask的边界在哪里

上面提到一个问题:Transfrom 工作在apk构架的哪一环节?它的边界再哪里?

我们看一下Apk的整体构建流程:

image

我们自定义的Transform 是在javac将java文件编译成.cass文件之后。也就是上图中第4步dex的过程。

dex过程内部又包含了一些列的TrasformTask,完整TransformTask链如下图所示:

dex完整流程

jacoco 是用于统计代码覆盖率的Task,在isTestCoverageEnabled= true时会加入jacoco Task

desuger 脱糖处理,将Java8 的特性语法糖(如lamda表达式) 替换为java 7中的标准语言,以实现对仅支持java7的编译工具的兼容.

android.enableD8.desugaring = false时,会加入desuger TranfromTask

MergeJavaRes:合并资源,处理lib/目录下的aar和so文件

Transform to merge all the Java resources.

自定义Transfrom,假设加入了两个自定义的Transform:Cost和Scan

MergeClass:将class文件合并成jar

A transform that takes the FULL_PROJECT's CLASSES streams and merges the class files into a single jar.

AdvancedProfiling 可选

Proguard:混淆和去除无用代码

PreColdSwap:可选,好像和InstanceRun有关,没弄太明白。

/**

* Task to disable execution of the InstantRun slicer, dexer and packager when they are not needed.

*

*

The next time they run they will pick up all intermediate changes.

*

*

With multi apk (N or above device) resources are packaged in the main split APK. However when

* a warm swap is possible, it is not necessary to produce immediately the new main SPLIT since the

* runtime use directly the resources.ap_ file. However, as soon as an incompatible change forcing a

* cold swap is triggered, the main APK must be rebuilt (even if the resources were changed in a

* previous build).

*/

D8MainDexList 可选,通过D8计算哪些类应该加入到主Dex中

Calculate the main dex list using D8.

Dex: 将class文件生成dex文件

ResourcesShrinker:资源压缩,可选。

DexSplitter:拆分Dex为多个,可选。

/**

* Transform that splits dex files depending on their feature sources

*/

可以看到Dex的过程有非常多的系统Transform非常复杂,这些Transform有些是在特定条件才添加进Transfrom链中的。

假设我们自定义的Transform 处理的InputType 为TransformManager.CONTENT_CLASS (class和jar),去掉哪些可有可无的系统Transform流程就会清晰很多。如下:

dex简化流程

假设我们定义了两个Transform(Cost和Scan),那么

Cost Transform的上一个Task为Javac,Cost的input目录为app/build/intermediates/javac,Cost的输出目录为app/build/intermediates/Cost

Scan Transform的上一个Task为Cost,Scan的输入为app/build/intermediates/Cost,Scan的输出为app/build/intermediates/Scan

2.4 源码

上面Transform的构建流程,可以参考AppPlugin和TaskManager

AppPlugin启动时调用apply()方法,主要做了三件事情:

// 配置项目,设置构建回调

this::configureProject

// 配置Extension

this::configureExtension

// 创建任务

this::createTasks

configureProject 做的事情,主要是进行版本有效性的判断,创建了 AndroidBuilder 对象,并设置了构建流程的回调来处理依赖和dex的加载和缓存清理

configureExtension 方法的作用,主要是创建 AppExtention扩展对象, 创建taskManager。

createTasks 主要是通过taskManager等工具,构建编译任务。

TaskManager是构建任务Task的大管家,它负责组织编译任务,其中就包括Transform的任务链。

##TaskMapager.java

protected void createCompileTask(@NonNull VariantScope variantScope) {

//(1)创建Javac Task

TaskProvider extends JavaCompile> javacTask = createJavacTask(variantScope);

addJavacClassesStream(variantScope);

setJavaCompilerTask(javacTask, variantScope);

//(2)构建Javac之后的TransformTask任务链

createPostCompilationTasks(variantScope);

}

在添加javac Task任务之后,调用了createPostCompilationTasks()方法

了createPostCompilationTasks()中完成了Transform任务链的构建

2.5 小结

TransformTask位于Javac Task之后,主要职责是完成生成dex文件。TransformTask会组成一个"Dex任务链"。自定义的Transform 会插入到任务链的最前面,而DexTransform位于"Dex任务链"的末尾。

所以自定义的Transform 仅能在javac 将java文件编译成class文件之后,在class文件转换成dex之前做一些class处理操作,其输入是class和jar,输出也是class和jar。

其他

相关知识点:

三、Transform的优化

Tranform的增量编译和并发编译可以参照 一起玩转Android项目中的字节码 一文

四、其他

4.1、常用名字解释

D8相关 用于替代dx工具的, 职责是 将class文件转化成dex文件

Proguard 压缩与优化(minification、shrinking、optimization)部分的替代品,依然使用与Proguard一样的keep规则。

4.2、如何查看gradle Task执行时间

./gradlew clean assembleDebug --profile

profile参数 可以查看Gradle 编译各阶段 各任务的耗时,并生成一个Profile网页

See the profiling report at: file:///Users/feifei/Desktop/TM/Demo/TrPlugin/build/reports/profile/profile-2021-01-26-10-10-14.html

image

4.3、Transform+ASM实践

可参照trplugin:

利用函数插桩实现了自定计算方法耗时,自动收集组件信息等功能。

五、参考文章

java 奇门遁甲代码_奇门遁甲之Transform API相关推荐

  1. java 编写代码_如果您在2016年编写过Java代码-这是您不容错过的趋势

    java 编写代码 2016年最有趣的Java相关主题 关于代码,有很多热门话题,而要跟上所有事情,这是一项全职的工作. 如果您想知道如何从谷壳中分离出小麦,我们已经为您完成了工作. 在下面的文章中, ...

  2. java 编写代码_如果您在2015年编写过Java代码-这是您不容错过的趋势

    java 编写代码 去年我们有机会遇到的最有趣趋势的实用概述 在这篇文章中,我们回顾了构成我们2015年对话的5个主题和新发展.与其他许多年终总结保持较高水平的不同,我们将做一个更实际的操作不用流行语 ...

  3. 每行代码都有注释释的java面向对象代码_每行代码都有注释释的java面向对象代码...

    每行代码都有注释释的java面向对象代码 [2021-02-02 01:52:34]  简介: vue如何注释 2020-11-18 vue注释的方法:1.html注释,代码为[]:2.[pug(ht ...

  4. php采集一言代码_用PHP制作api源码简单实现一言 / 随机一句功能

    很多人都喜欢在自己的网站页面中加个一言,不过一般都是调用的第三方api.其实,使用phpfile_get_contents 函数就能通过短短的几行代码就实现该功能! 准备工作 首先准备一个代码编辑器, ...

  5. java恋爱代码_陷入与代码的恋爱中:终生的爱情故事

    java恋爱代码 by Daragh Byrne 达拉·伯恩(Daragh Byrne) 陷入与代码的恋爱中:终生的爱情故事 (Falling in and out of love with code ...

  6. java 奇门遁甲代码_关于奇门遁甲学的一些个人理解

    大家可以参考以下本人这几年研究玄学的经验 #!/bin/bash -xe sleep 20 yum -y install wget memcached systemctl start memcache ...

  7. java 中断代码_你的java代码可中断吗?(2)

    1.确保提交到线程池的任务可中断 原文:www.securecoding.cert.org,TPS02-J. Ensure thattasks submitted to a thread pool a ...

  8. java浪漫代码_程序员表白代码,用过的人都找到了对象...

    在情人节送给自己的女朋友玫瑰花, 对于程序员来说是不是太普通了呢? 为什么不试试让情人节变得更特别一些呢? 作为一名程序员, 可以用自己的技术创造出不一样的浪漫! 让你的女朋友眼前一亮,印象深刻. 这 ...

  9. java时钟代码_一个经典的JAVA APPLET时钟程序(一)

    转眼间一年又要过了,自己又老了一岁,郁闷啊.趁着还有几分钟才新年,赶快再发几篇文章,给过去的一年添点东西. 该程序是从网上发现的,是一个简单的时钟显示程序. 代码特色: 时钟代码提供了各种接口,可以在 ...

最新文章

  1. Linux下安装jdk1.6
  2. java 导出pdf_一次java导出pdf的经历
  3. 51CTO -- 网络自学的瓶颈期
  4. 【maven】改造已有项目
  5. 为什么将0.1f改为0会使性能降低10倍?
  6. 微软Silverlight,你应该知道的10件事
  7. 软工 课堂作业:选出一个整数组中最大子数组
  8. asp.net 异步群发邮件时遭遇到的问题 ddddddddd
  9. 今天在群里面讨论了驱动机制的学习
  10. java openoffic linux_Linux openoffice 安装测试
  11. UU快修-家电维修网点综合查询平台
  12. Hyper-V虚拟机网络配置
  13. 打不开 /dev/vmmon: 无此文件或目录。请确保已载入内核模块 ’vmmon’”。
  14. 豆瓣 API ( 解决104问题 )
  15. wordpress制作微信小程序源码
  16. 数据结构 算法与应用(c++ 描述) 自练答案
  17. 10-113 A1-7在产品表中找出库存量小于订购量的产品信息
  18. PageHelper这种情况下有坑!
  19. Docker整理篇(docker-compose与私服搭建)
  20. 有效前沿,CAMP, CAL, SML

热门文章

  1. C语言:输入三个整数,从小到大排序!
  2. python打印日历_python输出指定月份日历的方法
  3. 2011世界财富500强
  4. RTP协议封装H264/H265/AAC
  5. 我不是来表扬你们的 欢庆更多朋友一起来点赞
  6. GitHub里的灭霸脚本,竟都还不是最奇葩的?
  7. 怎样提高自己的系统架构水平?
  8. 【QQ空间】免费漂浮物代码
  9. Prometheus
  10. Python学习之线性图