前言
随着各平台对隐私合规愈加严格,APP上架也变得愈加困难。 大厂SDK有反馈都会有解决,但很多第三方库作者已经不维护,想想就让人脑壳疼😿。
ASM
ASM
是一种通用Java
字节码操作和分析框架。它可以用于修改现有的class
文件或动态生成class
文件。
Core API 和 Tree API
ASM
分成两部分,Core API
和Tree API
。
如何描述两者之间的关系呢?
Tree API
是在Core API
的基础上构建而来。直白点就是Core API
非常节约内存,但是编程难度较大,Tree API
消耗内存多,但是编程比较简单。
原理
通过ASM
的Tree API
去查找并替换class
文件中的目标字段或方法。
编码实现
这边尽量使用简单的语言描述,方便没有ASM
基础的读者阅读,如有不正确的地方欢迎指出。
class ScanClassNode(
private val classVisitor: ClassVisitor,
private val scans: List<ScanBean>, //配置的对象(包含目标信息和替换信息)
) : ClassNode(Opcodes.ASM9) { //ASM Tree API 会把 class 文件包装成 ClassNode 方便我们操作
override fun visitEnd() { //顾名思义访问完成的回调,我们在这里可以获取 class 文件的所有字段和方法
//遍历所有方法
methods.forEach { methodNode ->
val instructions = methodNode.instructions
//遍历方法内的每一行代码
val iterator = instructions.iterator()
while (iterator.hasNext()) {
val insnNode = iterator.next()
//ASM Tree API 会把字段包装成 FieldInsnNode ,方法包装成 MethodInsnNode
//查找目标字段或方法
if (insnNode is FieldInsnNode) {
//以Build.BRAND举例,对应的 owner = "android/os/Build",name = "BRAND",desc = "Ljava/lang/String;"
scans.find {
it.owner == insnNode.owner && it.name == insnNode.name && it.desc == insnNode.desc
}?.let {
//通过 instructions.set 替换目标字段
instructions.set(insnNode, newInsnNode(it))
}
}
if (insnNode is MethodInsnNode) {
//以OnClickListener.onClick(View v)举例,对应的 owner = "Landroid/view/View$OnClickListener;",name = "onClick",desc = "(Landroid/view/View;)V"
scans.find {
it.owner == insnNode.owner && it.name == insnNode.name && it.desc == insnNode.desc
}?.let {
//通过 instructions.set 替换目标方法
instructions.set(insnNode, newInsnNode(it))
}
}
}
}
super.visitEnd()
//将 ClassNode 类中字段的值传递给下一个 ClassVisitor 类实例
accept(classVisitor)
}
//构建替换的字段或方法
private fun newInsnNode(bean: ScanBean): AbstractInsnNode {
val opcode = bean.replaceOpcode
val owner = bean.replaceOwner
val name = bean.replaceName
val descriptor = bean.replaceDesc
return if (!bean.replaceDesc.startsWith("(")) { //根据"("判断字段或方法
FieldInsnNode(opcode, owner, name, descriptor)
} else {
MethodInsnNode(opcode, owner, name, descriptor, false)
}
}
}
至此我们的核心代码已经编写完毕,接下来便是介绍如何通过AGP7.0
生成插件及依赖和使用
AGP
并不是本文的重点这边就以贴代码为主配以少量的注释
AGP7.0 编写插件
开发环境
Android Studio Bumblebee (2021.1.1) 🐝
和Android Gradle 7.1.2
。
首先在settings.gradle
添加如下代码:
pluginManagement {
repositories {
maven {
url uri('repo')
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
按照下图新建模块并创建文件
build.gradle
plugins {
id 'kotlin'
id 'kotlin-kapt'
id 'maven-publish'
}
dependencies {
implementation gradleApi() // 需要在 settings.gradle 设置 RepositoriesMode.PREFER_PROJECT
implementation localGroovy()
implementation 'org.ow2.asm:asm:9.3'
implementation 'org.ow2.asm:asm-commons:9.3'
implementation 'org.ow2.asm:asm-analysis:9.3'
implementation 'org.ow2.asm:asm-util:9.3'
implementation 'org.ow2.asm:asm-tree:9.3'
implementation "com.android.tools.build:gradle:$gradle_version", {
exclude group: 'org.ow2.asm'
}
}
publishing {
publications {
mavenJava(MavenPublication) {
groupId = 'com.example.miaow'
artifactId = 'plugin'
version = '1.0.0'
from components.java
}
}
repositories {
maven {
//输出路径
url = parent.layout.projectDirectory.dir('repo') // settings.gradle 记得配置
}
}
}
miaow.properties
implementation-class=com.example.miaow.plugin.MiaowPlugin
ScanBean
class ScanBean(
var owner: String = "",
var name: String = "",
var desc: String = "",
var replaceOpcode: Int = 0,
var replaceOwner: String = "",
var replaceName: String = "",
var replaceDesc: String = "",
) : Cloneable, Serializable {
public override fun clone(): ScanBean {
return try {
super.clone() as ScanBean
} catch (e: CloneNotSupportedException) {
e.printStackTrace()
ScanBean()
}
}
}
ScanClassVisitorFactory
//定义 ScanClassVisitorFactory 需要的参数(AGP的语法不需要纠结)
interface ScanParams : InstrumentationParameters {
@get:Input
val ignoreOwner: Property<String>
@get:Input
val listOfScans: ListProperty<ScanBean>
}
abstract class ScanClassVisitorFactory : AsmClassVisitorFactory<ScanParams> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return ScanClassNode(
nextClassVisitor,
parameters.get().listOfScans.get(),
)
}
override fun isInstrumentable(classData: ClassData): Boolean {
return !classData.className.startsWith(parameters.get().ignoreOwner.get().replace("/", "."))
}
}
MiaowPlugin
class MiaowPlugin : Plugin<Project> {
override fun apply(project: Project) {
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
variant.transformClassesWith(
ScanClassVisitorFactory::class.java,
InstrumentationScope.ALL
) {
//配置忽略路径
it.ignoreOwner.set("com/example/fragment/library/common/utils/BuildUtils")
//配置目标信息和替换信息
it.listOfScans.set(
listOf(
ScanBean(
"android/os/Build",
"BRAND",
"Ljava/lang/String;",
Opcodes.INVOKESTATIC,
"com/example/fragment/library/common/utils/BuildUtils",
"getBrand",
"()Ljava/lang/String;"
),
ScanBean(
"android/os/Build",
"MODEL",
"Ljava/lang/String;",
Opcodes.INVOKESTATIC,
"com/example/fragment/library/common/utils/BuildUtils",
"getModel",
"()Ljava/lang/String;"
),
ScanBean(
"android/os/Build",
"SERIAL",
"Ljava/lang/String;",
Opcodes.INVOKESTATIC,
"com/example/fragment/library/common/utils/BuildUtils",
"getSerial",
"()Ljava/lang/String;"
),
ScanBean( //传感器检测
"android/hardware/SensorManager",
"getSensorList",
"(I)Ljava/util/List;"
),
)
)
}
variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}
}
}
按照下图执行publish
生成插件
在根目录build.gradle
添加插件依赖
buildscript {
dependencies {
classpath 'com.example.miaow:plugin:1.0.0'
}
}
在app目录 build.gradle
apply插件
plugins {
id 'miaow'
}
测试
在MainActivity
编写如下测试代码:
打包并反编译APK
的源码,发现目标代码已经替换成功
再看看coil
的源码,发现目标代码也替换成功
Thanks
以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。 如果喜欢的话希望点个赞吧,您的鼓励是我前进的动力。 谢谢~~
项目地址
- github: fragmject