Android开发AsmClassVisitorFactory使用详解
作者:究极逮虾户 发布时间:2023-07-22 05:34:53
前言
之前就和大家介绍过AGP(Android Gradle Plugin) 7.0.0
版本之后Transform
已经过期即将废弃的事情。而且也简单的介绍了替换的方式是Transform Action
,经过我这一阵子的学习和调研,发现只能说答对了一半吧。下面介绍个新东西AsmClassVisitorFactory
。
com.android.build.api.instrumentation.AsmClassVisitorFactory
A factory to create class visitor objects to instrument classes.
The implementation of this interface must be an abstract class where the parameters and instrumentationContext are left unimplemented. The class must have an empty constructor which will be used to construct the factory object.
当前官方推荐使用的应该是这个类,这个类的底层实现就是基于gradle
原生的Transform Action
,这次的学习过程其实走了一点点弯路,一开始尝试的是Transform Action
,但是貌似弯弯绕绕的,最后也没有成功,而且Transform Action
的输入产物都是单一文件,修改也是针对单一文件的,所以貌似也不完全是一个很好的替换方案,之前文章介绍的那种复杂的asm操作则无法负荷了。
AsmClassVisitorFactory
根据官方说法,编译速度会有提升,大概18%左右,这个下面我们会在使用阶段对其进行介绍的。
我们先从AsmClassVisitorFactory
这个抽象接口开始介绍起吧。
AsmClassVisitorFactory
@Incubating
interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> : Serializable {
/**
* The parameters that will be instantiated, configured using the given config when registering
* the visitor, and injected on instantiation.
*
* This field must be left unimplemented.
*/
@get:Nested
val parameters: Property<ParametersT>
/**
* Contains parameters to help instantiate the visitor objects.
*
* This field must be left unimplemented.
*/
@get:Nested
val instrumentationContext: InstrumentationContext
/**
* Creates a class visitor object that will visit a class with the given [classContext]. The
* returned class visitor must delegate its calls to [nextClassVisitor].
*
* The given [classContext] contains static information about the classes before starting the
* instrumentation process. Any changes in interfaces or superclasses for the class with the
* given [classContext] or for any other class in its classpath by a previous visitor will
* not be reflected in the [classContext] object.
*
* [classContext] can also be used to get the data for classes that are in the runtime classpath
* of the class being visited.
*
* This method must handle asynchronous calls.
*
* @param classContext contains information about the class that will be instrumented by the
* returned class visitor.
* @param nextClassVisitor the [ClassVisitor] to which the created [ClassVisitor] must delegate
* method calls.
*/
fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor
/**
* Whether or not the factory wants to instrument the class with the given [classData].
*
* If returned true, [createClassVisitor] will be called and the returned class visitor will
* visit the class.
*
* This method must handle asynchronous calls.
*/
fun isInstrumentable(classData: ClassData): Boolean
}
简单的分析下这个接口,我们要做的就是在createClassVisitor
这个方法中返回一个ClassVisitor
,正常我们在构造ClassVisitor
实例的时候是需要传入下一个ClassVisitor
实例的,所以我们之后在new的时候传入nextClassVisitor就行了。
另外就是isInstrumentable
,这个方法是判断当前类是否要进行扫描,因为如果所有类都要通过ClassVisitor进行扫描还是太耗时了,我们可以通过这个方法过滤掉很多我们不需要扫描的类。
@Incubating
interface ClassData {
/**
* Fully qualified name of the class.
*/
val className: String
/**
* List of the annotations the class has.
*/
val classAnnotations: List<String>
/**
* List of all the interfaces that this class or a superclass of this class implements.
*/
val interfaces: List<String>
/**
* List of all the super classes that this class or a super class of this class extends.
*/
val superClasses: List<String>
}
ClassData
并不是asm的api,所以其中包含的内容相对来说比较少,但是应该也勉强够用了。这部分大家简单看看就行了,就不多做介绍了呢。
新的Extension
AGP版本升级之后,应该是为了区分新旧版的Extension
,所以在AppExtension
的基础上,新增了一个AndroidComponentsExtension
出来。
我们的transformClassesWith
就需要注册在这个上面。这个需要考虑到变种,和之前的Transform
还是有比较大的区别的,这样我们就可以基于不同的变种增加对应的适配工作了。
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
variant.transformClassesWith(PrivacyClassVisitorFactory::class.java,
InstrumentationScope.ALL) {}
variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}
实战
这次还是在之前的敏感权限api替换的字节码替换工具的基础上进行测试开发。
ClassVisitor
看看我们正常是如何写一个简单的ClassVisitor的。
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor methodFilterCV = new ClassFilterVisitor(classWriter);
ClassReader cr = new ClassReader(srcClass);
cr.accept(methodFilterCV, ClassReader.SKIP_DEBUG);
return classWriter.toByteArray();
首先我们会构造好一个空的ClassWriter
,接着会构造一个ClassVisitor
实例,然后传入这个ClassWriter
。然后我们构造一个ClassReader
实例,然后将byte数组传入,之后调用classReader.accept方法,之后我们就能在visitor中逐个访问数据了。
那么其实我们的类信息,方法啥的都是通过ClassReader读入的,然后由当前的ClassVisitor
访问完之后交给我们最后一个ClassWriter
。
其中ClassWriter
也是一个ClassVisitor
对象,他复杂重新将修改过的类转化成byte数据。可以看得出来ClassVisitor
就有一个非常简单的链表结构,之后逐层向下访问。
介绍完了这个哦,我们做个大胆的假设,如果我们这个ClassVisitor
链表前插入几个不同的ClassVisitor
,那么我们是不是就可以让asm修改逐个生效,然后也不需要多余的io操作了呢。这就是新的asm api 的设计思路了,也是我们这边大佬的字节码框架大佬的设计。另外bytex内的设计思路也是如此。
tips ClassNode 因为是先生成的语法树,所以和一般的ClassVisitor有点小区别,需要在visitEnd方法内调用accept(next)
实际代码分析
接下来我们上实战咯。我将之前的代码套用到这次的逻辑上来。
demo地址
abstract class PrivacyClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
return PrivacyClassNode(nextClassVisitor)
}
override fun isInstrumentable(classData: ClassData): Boolean {
return true
}
}
我在isInstrumentable都返回的是true,其实我可以将扫描规则限定在特定包名内,这样就可以加快构建速度了。
class PrivacyClassNode(private val nextVisitor: ClassVisitor) : ClassNode(Opcodes.ASM5) {
override fun visitEnd() {
super.visitEnd()
PrivacyHelper.whiteList.let {
val result = it.firstOrNull { whiteName ->
name.contains(whiteName, true)
}
result
}.apply {
if (this == null) {
// println("filter: $name")
}
}
PrivacyHelper.whiteList.firstOrNull {
name.contains(it, true)
}?.apply {
val iterator: Iterator<MethodNode> = methods.iterator()
while (iterator.hasNext()) {
val method = iterator.next()
method.instructions?.iterator()?.forEach {
if (it is MethodInsnNode) {
it.isPrivacy()?.apply {
println("privacy transform classNodeName: ${name@this}")
it.opcode = code
it.owner = owner
it.name = name
it.desc = desc
}
}
}
}
}
accept(nextVisitor)
}
}
private fun MethodInsnNode.isPrivacy(): PrivacyAsmEntity? {
val pair = PrivacyHelper.privacyList.firstOrNull {
val first = it.first
first.owner == owner && first.code == opcode && first.name == name && first.desc == desc
}
return pair?.second
}
这部分比较简单,把逻辑抽象定义在类ClassNode
内,然后在visitEnd
方法的时候调用我之前说的accept(nextVisitor)
方法。
另外就是注册逻辑了,和我前面介绍的内容基本都是一样的。
个人观点
AsmClassVisitorFactory
相比较于之前的Transform
确实简化了非常非常多,我们不需要关心之前的增量更新等等逻辑,只要专注于asm api的操作就行了。
其次就是因为减少了io操作,所以其速度自然也就比之前有所提升。同时因为基于的是Transform Action
,所以整体性能还是非常ok的,那部分增量可以说是更简单了。
另外我也和我的同事大佬交流过哦,复杂的这种类似上篇文章介绍的,最好还是使用Gradle Task
的形式进行修改。
来源:https://juejin.cn/post/7016147287889936397
猜你喜欢
- Mybatis 逆向工程 逆向工程通常包括由数据库的表生成 Java 代码 和 通过 Java 代码生成数据库表。而Mybatis 逆向工
- C# 从枚举值获取对应的文本描述详解有时枚举值在显示时,需要显示枚举值对应的文本串。一种方案是在调用的地方使用switch或者if来判断枚举
- 什么是Etcd?etcd是一个强大的一致性的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器群访问的数据。它优雅地处理网络
- 前言上文讲的MyBatis部署运行且根据官网运行了一个demo:一步到位部署运行MyBatis3源码<保姆级>jdbc再贴一个J
- 如果不熟悉Java8新特性的小伙伴,初次看到函数式接口写出的代码可能会是一种懵逼的状态,我是谁,我在哪,我可能学了假的Java,(・∀・(・
- 开发中经常遇到从集合类List、Map中取出数据转换为String的问题,这里如果处理不好,经常会遇到空指针异常java.lang.Null
- 关于UIToolbarToolBar工具栏是视图View的属性,可以在工具栏上添加工具栏按钮Bar Button Item(可以是自定义的C
- C#实现的鼠标钩子,可以获取鼠标在屏幕中的坐标,记得要以管理员权限运行才行using System;using System.Collect
- 一. break1. 作用break关键字可以用于for、while、do-while及switch语句中,用来跳出整个语句块,结束当前循环
- 简单工厂模式(Simple Factory),说他简单是因为我们可以将此模式比作一个简单的民间作坊,他们只有固定的生产线生产固定的产品。也可
- 本文实例为大家分享了Java判断对象是否为空的具体代码,供大家参考,具体内容如下package com.gj5u.publics.util;
- Java 实现网络爬虫框架最近在做一个搜索相关的项目,需要爬取网络上的一些链接存储到索引库中,虽然有很多开源的强大的爬虫框架,但本着学习的态
- 合成聚合复用原则合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP
- 本文实例讲述了如何计算(或者说,估算)一个Java对象占用的内存数量的方法。分享给大家供大家参考。具体分析如下:通常,我们谈论的堆内存使用的
- Spring是一个非常流行的Java Web开发框架,它提供了强大的依赖注入、面向切面编程、声明式事务管理等功能,为开发者提供了高效、快速地
- 概述本文的编写初衷,是想了解一下Spring Boot2中,具体是怎么序列化和反序列化JSR 310日期时间体系的,Spring MVC应用
- Android 开发的程序员开发程序的时候,一定为log而苦恼过吧。Eclipse老是Log找不到,是不是很让人不爽,虽然Android S
- 1.引入依赖 <!--mybatisplus依赖--> <dependency> &nbs
- Android仿360悬浮小球自定义view实现示例效果图如下:实现当前这种类似的效果 和360小球 悬浮桌面差不错类似。这种效果是如何实现
- 适合人群学完Java基础想通过Java快速构建web应用程序想学习或了解SpringBoot背景本节给大家讲讲 Java的Spri