框架手写系列---javassist修改字节码方式,实现美团Robust热修复框架

本文用javassist方式,模拟美团Robust插件的前置处理:用插入代码的方式,针对apk中的每个方法都插入一段静态代码判断语句,用于控制是否启用热修复fix(也就是动态加载patch包到原apk中)。

一、插件的生成与依赖导入

1、用该文章中的方法:Android中Plugin插件工程的自动生成 自动生成一个android的插件模板;

2、加入javassist依赖,加入依赖后的插件build的格式如下:

dependencies{
    compile gradleApi()
    compile localGroovy()
    //transform与javassist依赖增加
    compile 'com.android.tools.build:transform-api:1.5.0'
    compile 'javassist:javassist:3.12.1.GA'
    compile 'commons-io:commons-io:2.5'
    implementation 'com.android.tools.build:gradle:3.6.3'
 }

二、javassist编译时处理代码

javassist的作用期是编译打包时从.class--转换-->.dex过程中。本例中,将通过插件的方式,用字节码手术刀javassist,修改class字节码文件,为各方法添加代码。

注:javassist、aspectJ、apt三种编译时技术的起效时期如下:

1、插件注册

在生成的插件模板中,做编辑。新增一个transForm到编译过程。

public class SunnyEasyUse implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        println 'A Plugin'
        //该插件配置到对应工程的build中,此处做注册
        def android = project.extensions.getByType(AppExtension)
        //将SunnyTransForm加入到编译过程
        android.registerTransform(new SunnyTransForm(project))
        println 'register A Plugin'
    }
}

2、TransFrom编写

此处主要做两个操作:

(1)将第三方依赖(jar包),从上一个transForm中取出,并传递到下一个transForm;

(2)对源文件(也就是自己编写的代码)做处理,在代码的方法中插入预期代码;

1、热修复控制类新建

首先,在主工程中,新建类如下:

public class FixUtil {
    //是否开启使用热修复
    public static boolean isFixUse(){
        return false;
    }
}

2、TransFrom针对所有方法,将上述判断插入

具体步骤,参照注释

public class SunnyTransForm extends Transform {

    //字节码池
    def pool = ClassPool.default
    //工程
    def project

    SunnyTransForm(project) {
        this.project = project
    }

    @Override
    public String getName() {
        return "SunnyTransForm"
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    public Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)

        //class路径,加载到字节码池。
        //把project下的每个class路径,加载到pool字节码池,也就是加载到内存中
        project.android.bootClasspath.each {
            pool.appendClassPath(it.absolutePath)
        }
        //输出
        def outProvider = transformInvocation.getOutputProvider()
        //输入的文件处理
        transformInvocation.inputs.each { input ->
            //jar包处理
            input.jarInputs.each { it ->
                processJarInput(it, outProvider)
            }
            //源文件---自己写的源文件,都放置在文件夹中
            input.directoryInputs.each { it ->
                processDirectoryInput(it, outProvider)
            }
        }
    }

    private void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        def destFile = outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
        pool.insertClassPath(jarInput.file.absolutePath)
        FileUtils.copyFile(jarInput.file, destFile)
    }

    private void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {

        def preFileName = directoryInput.file.absolutePath
        pool.insertClassPath(preFileName)
        //开始修改class代码
        findTargetClassFile(directoryInput.file, preFileName)


        def destFile = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, destFile)
    }

    private void findTargetClassFile(File file, String fileName) {
        //递归查找.class结尾的文件
        if (file.isDirectory()) {
            file.listFiles().each {
                findTargetClassFile(it, fileName)
            }
        } else {
            modify(file, fileName)
        }
    }
    //修改代码
    private void modify(File file, String fileName) {
        def filePath = file.absolutePath
        if (!filePath.endsWith(SdkConstants.DOT_CLASS)) {
            return
        }

        if (filePath.contains('R$') || filePath.contains('R.class') || filePath.contains('BuildConfig.class')) {
            return
        }

        println("文件路径------------>:" + fileName)

        //根据路径得到包名与类名,用于加载

        //fileName是文件夹
        // 正反斜杠是兼容window与mac
        // 获取到 类似com.sunny.file.MainActivity.class全类名
        def className = filePath.replace(fileName, "")
                .replace("\\", ".")
                .replace("/", ".")
        //获取 类似com.sunny.file.MainActivity
        def name = className.replace(SdkConstants.DOT_CLASS, "").substring(1)

        println("name------------>:" + name)

        //取到字节码操作对象。类似 json --> 转javabean
        CtClass ctClass = pool.get(name)

        if (name.contains("com.sunny.aplugin")) {
            def modifyBody = "if(com.sunny.aplugin.FixUtil.isFixUse()){}"
            addCode(ctClass, modifyBody,fileName)
        }
    }
    //添加代码到方法中
    private void addCode(CtClass ctClass, String body,String fileName) {
        println("addCode------------>:" + body)
        CtMethod[] methods = ctClass.getDeclaredMethods()
        ctClass.defrost()
        methods.each {
            if(!it.getName().contains("isFixUse")){
                println("it.getName()------------>:" + it.getName())
                //插入到方法最前
                it.insertBefore(body)
            }
        }
        ctClass.writeFile(fileName)
        ctClass.detach()
    }
}

3、编译后效果

(1)在编译的build目录下,可以看到新的transForm

(2)被修改后的方法,如类MainActivity中的onCreate中,在方法头部位置,插入了判断代码

三、热修复的后续操作说明

至此,热修复Robust前期的核心工作已经做完。

后续需要做的是将下载到本地的patch文件,通过动态加载的方式,加入到apk中。

此外,javessist除以上可以插入到方法头部,也可以如aspectJ一样,在方法前、方法后、环绕等方式处理方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值