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

Posted 战国剑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了框架手写系列---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一样,在方法前、方法后、环绕等方式处理方法。

以上是关于框架手写系列---javassist修改字节码方式,实现美团Robust热修复框架的主要内容,如果未能解决你的问题,请参考以下文章

Javassist/ASM 框架比较

Javassist/ASM 框架比较

动态字节码技术 javassist 初探

字节码基于javassist的第一个案例helloworld

JAVAssist字节码操作

使用Javassist对字节码操作为JBoss实现动态"AOP"框架