ASM修改SDK中指定位置的函数调用

·  阅读 1350

插件目的

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

image-20201228160132314

修改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
复制代码

image-20201228160807377

打包

点击gradle->插件module名->upload->uploadArchives

image-20201228160934145

打包成功后会在根目录下生成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文件

image-20201228162537375

参考

AndroidAutoTrack

分类:
Android
标签: