插件目的
public class HookTest {
public void hookTest(boolean show){
Log.i("HookTest","start");
if(show)
printStr("1");
Log.i("HookTest","end");
Log.i("HookTest","normal start");
printStr("normal1");
Log.i("HookTest","normal end");
}
public void printStr(String s) {
Log.i("HookTest",s);
}
public void printStr1(String s) {
Log.i("HookTest","printStr1:"+s);
}
}
外部调用hooTest方法时,将第三行的printStr方法更改为printStr1方法,不影响后续printStr或者其他地方printStr的调用,并且考虑show传值(不管传啥第三行都改成printStr1)
问题扩展自Android-Daily-Interview/issues/227#issuecomment-751621763
在没有if包裹的情况下可以使用aspectJ等方法切面很方便的解决,
但是在加入if分支后,似乎只能选中ASM的方式了。
以下代码仅做测试使用。
步骤
新建android library module
修改library的build.gradle
apply plugin: 'groovy'
apply plugin: 'kotlin'
apply plugin: 'maven'
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:4.0.1'
implementation 'commons-io:commons-io:2.6'
implementation 'org.javassist:javassist:3.27.0-GA'
}
uploadArchives {
repositories {
mavenDeployer {
//设置插件的GAV参数
pom.groupId = 'com.example.testpro'
pom.artifactId = 'hook'// 这个如果不设置,发布之后应该是com.lenny.plugin.customPlugin
pom.version = '1.0.0'
//文件发布到下面目录
repository(url: uri('../repo'))
}
}
}
建立plugin
public class EditMethodPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
appExtension.registerTransform(new EditMethodTransform());
}
}
建立Transform
class EditMethodTransform : Transform() {
override fun getName(): String {
return "EditMethodTransform"
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
*
* @return
*/
override fun getInputTypes(): Set<QualifiedContent.ContentType> {
return TransformManager.CONTENT_CLASS
}
override fun getScopes(): MutableSet<in QualifiedContent.Scope>? {
return TransformManager.SCOPE_FULL_PROJECT
}
override fun isIncremental(): Boolean {
return false
}
@Throws(TransformException::class, IOException::class)
override fun transform(transformInvocation: TransformInvocation) {
transformInvocation.outputProvider.deleteAll()
transformInvocation.inputs.forEach {
it.directoryInputs.forEach {
val dest=transformInvocation.outputProvider.getContentLocation(
it.name,
it.contentTypes,
it.scopes,
Format.DIRECTORY
)
val dir =it.file
FileUtils.copyDirectory(dir, dest)
for (file in FileUtils.getAllFiles(dir)) {
if (file.name == "HookTest.class") {
println("dirFile: ${file.name}")
val target = File(dest.absolutePath, file.name)
target.delete()
val bytes = IOUtils.toByteArray(FileInputStream(file))
/**
* 修改bytes start
*/
val classNode = ClassNode(Opcodes.ASM5)
val classReader = ClassReader(bytes)
//1 将读入的字节转为classNode
classReader.accept(classNode, 0)
//2 对classNode的处理逻辑
val iterator: Iterator<MethodNode> =
classNode.methods.iterator()
var needEdit=true
while (iterator.hasNext()) {
val method = iterator.next()
method.instructions?.iterator()?.forEach {
if (it.opcode == Opcodes.INVOKEVIRTUAL) {
if (needEdit && it is MethodInsnNode && it.name == "printStr" && it.owner == "com/example/testpro/hook/HookTest") {
it.name="printStr1"
needEdit=false
return@forEach
}
}
}
}
val classWriter = ClassWriter(0)
//3 将classNode转为字节数组
classNode.accept(classWriter)
val newBytes= classWriter.toByteArray()
/**
* 修改bytes end
*/
target.createNewFile()
FileOutputStream(target).write(newBytes)
println(target.path+"文件已生成")
}
}
}
/**
* 因为我这里的情况是确定的,没在jar包里,所以不处理jar包,实际项目中按实际情况具体处理
*/
it.jarInputs.forEach {
/**
* 能否用it.name 替代it.file.name
* 这里没直接用name,加了些花里胡哨的功能应该是为了防止同名jar包放到一个文件夹中发生覆盖的情况
*/
val dest=transformInvocation.outputProvider.getContentLocation(
it.file.name.replace(
".jar",
""
) + "_" + DigestUtils.md5Hex(it.file.absolutePath),
it.contentTypes,
it.scopes,
Format.JAR
)
val dir =it.file
FileUtils.copyFile(dir, dest)
}
}
}
}
向外注册插件
在library的src/main下面新建resources/META-INF/gradle-plugins
然后在下面新建文件,插件名.properties,并输入如下(值为Plugin文件的包名路径)
implementation-class=com.example.editmethod.EditMethodPlugin
打包
点击gradle->插件module名->upload->uploadArchives
打包成功后会在根目录下生成repo文件夹(步骤“修改library的build.gradle”配置的,也可以自己上传到远程仓库)
引入classpath
根目录下的build.gradle中添加“classpath '包名:artifactId:version'”(步骤“修改library的build.gradle”配置的)
如下:
classpath 'com.example.testpro:hook:1.0.0'
应用插件
在app目录下的build.gradle中添加apply plugin: 'properties的文件名'(步骤“向外注册插件”配置的)
apply plugin: 'editMethod'
rebuild
rebuild后可以在“app\build\intermediates\transforms\Plugin文件名”目录下看到相关class文件