Android热修复技术——QQ空间补丁方案解析(2)

接下来的几篇博客我会用一个真实的demo来介绍如何实现热修复。具体的内容包括:

  • 如何打包补丁包
  • 如何将通过ClassLoader加载补丁包

1. 创建Demo

demo很简单,创建一个只有一个Activity的demo:

package com.biyan.demo
public class MainActivity extends Activity {

    private Calculator mCal;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCal = new Calculator();
    }
    public void click(View view) {
        Toast.makeText(this, String.valueOf(mCal.calculate()),Toast.LENGTH_SHORT).show();
    }
}
Public class Caculoator {
    public float calculate() {
        return 1 / 0;
    }
}

demo的代码很简单,运行会出什么bug也很清楚了,在此就不演示了。

2.创建补丁包

首先修复Calculator的bug。

package com.biyan.demo
Public class Caculoator {
    public float calculate() {
        return 1 / 1;
    }
}

重新编译项目,在build目录下找到Calculator.class文件,将其拷出来,准备打包。放置在于Calculator包名相同的路径下。

将其打成jar包:

jar -cvf patch.jar com

然后再将对应的jar包打成dex包:

dx --dex --output=patch_dex.jar patch.jar

dx是讲jar包打成dex包的工具,安装在path-android-sdk/build-tools/version(如24.0.0)/dx。
patch_dex.jar就是补丁包,接下来将其安装在sdCard中,接下来应用从sdCard上加载该补丁包。

3. 加载补丁

根据上一篇博客的介绍,加载补丁的思路如下:

  • 在Application的onCreate()方法中获取应用本身的BaseDexClassLoader,然后通过反射得到对应的dexElements
  • 创建一个新的DexClassLoader实例,然后加载sdCard上的补丁包,然后通过同样的方法得到对应的dexElements
  • 将两个dexElements合并,然后再利用反射将合并后的dexElements赋值给应用本身的BaseDexClassLoader

接下来看下具体代码:

package com.hotpatch.demo;

import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;

/**
 * Created by hp on 2016/4/6.
 */
public class HotPatchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 获取补丁,如果存在就执行注入操作
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
        File file = new File(dexPath);
        if (file.exists()) {
            inject(dexPath);
        } else {
            Log.e("BugFixApplication", dexPath + "不存在");
        }
    }

    /**
     * 要注入的dex的路径
     *
     * @param path
     */
    private void inject(String path) {
        try {
            // 获取classes的dexElements
            Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
            Object pathList = getField(cl, "pathList", getClassLoader());
            Object baseElements = getField(pathList.getClass(), "dexElements", pathList);

            // 获取patch_dex的dexElements(需要先加载dex)
            String dexopt = getDir("dexopt", 0).getAbsolutePath();
            DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
            Object obj = getField(cl, "pathList", dexClassLoader);
            Object dexElements = getField(obj.getClass(), "dexElements", obj);

            // 合并两个Elements
            Object combineElements = combineArray(dexElements, baseElements);

            // 将合并后的Element数组重新赋值给app的classLoader
            setField(pathList.getClass(), "dexElements", pathList, combineElements);

            //======== 以下是测试是否成功注入 =================
            Object object = getField(pathList.getClass(), "dexElements", pathList);
            int length = Array.getLength(object);
            Log.e("BugFixApplication", "length = " + length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过反射获取对象的属性值
     */
    private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }

    /**
     * 通过反射设置对象的属性值
     */
    private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    /**
     * 通过反射合并两个数组
     */
    private Object combineArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class<?> componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }

}

核心代码就这么多,接下来运行一下程序。程序还是Crash了。。。

原因是类预校验问题引起的:

  • 在apk安装的时候系统会将dex文件优化成odex文件,在优化的过程中会涉及一个预校验的过程
  • 如果一个类的static方法,private方法,override方法以及构造函数中引用了其他类,而且这些类都属于同一个dex文件,此时该类就会被打上CLASS_ISPREVERIFIED
  • 如果在运行时被打上CLASS_ISPREVERIFIED的类引用了其他dex的类,就会报错
  • 所以MainActivityonCreate()方法中引用另一个dex的类就会出现上文中的问题
  • 正常的分包方案会保证相关类被打入同一个dex文件
  • 想要使得patch可以被正常加载,就必须保证类不会被打上CLASS_ISPREVERIFIED标记。而要实现这个目的就必须要在分完包后的class中植入对其他dex文件中类的引用
  • 要在已经编译完成后的类中植入对其他类的引用,就需要操作字节码,惯用的方案是插桩。常见的工具有javaassist,asm等。

其实QQ空间补丁方案的关键就在于字节码的注入而不是dex的注入。下一篇博客将会介绍字节码注入的相关细节。

时间: 2024-11-02 12:50:13

Android热修复技术——QQ空间补丁方案解析(2)的相关文章

Android热修复技术——QQ空间补丁方案解析(1)

传统的app开发模式下,线上出现bug,必须通过发布新版本,用户手动更新后才能修复线上bug.随着app的业务越来越复杂,代码量爆发式增长,出现bug的机率也随之上升.如果单纯靠发版修复线上bug,其较长的新版覆盖期无疑会对业务造成巨大的伤害,更不要说大型app开发通常涉及多个团队协作,发版排期必须多方协调. 那么是否存在一种方案可以在不发版的前提下修复线上bug?有!而且不只一种,业界各家大厂都针对这一问题拿出了自家的解决方案,较为著名的有腾讯的Tinker和阿里的Andfix以及QQ空间补丁

Android热修复技术——QQ空间补丁方案解析(3)

如前文所述,要想实现热更新的目的,就必须在dex分包完成之后操作字节码文件.比较常用的字节码操作工具有ASM和javaassist.相比之下ASM提供一系列字节码指令,效率更高但是要求使用者对字节码操作有一定了解.而javaassist虽然效率差一些但是使用门槛较低,本文选择使用javaassist.关于javaassist可以参考Java 编程的动态性, 第四部分: 用 Javassist 进行类转换 正常App开发过程中,编译,打包过程都是Android Studio自动完成.如无特殊需求无

Android热修复技术选型——三大流派解析

文章作者:所为 淘宝无线开发专家 2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker,它们在原理各有不同,适用场景各异,到底采用哪种方案,是开发者比较头疼的问题.本文希望通过介绍QQ空间补丁.Tinker以及基于AndFix的阿里百川HotFix技术的原理分析和横向比较,帮助开发者更深入了解热修复方案. 技术背景 一.正常开发流程 从流程来看,传统的开发流程存在很多弊端: 重新发布版本

Android热修复技术原理详解与升级探索

在2017云栖大会-上海峰会上手机淘宝资深无线开发工程师甘晓霖(万壑)作了题为<Android热修复技术原理详解与升级探索>的分享,如何实现客户端与开发节奏最快同步,阿里云为此开发了移动热修复框架Sophix.它在代码修复.资源修复.SO库修复中都展示了极高的能力,在于其他竞品的对比中,Sophix展示出来极大的优势,并且非常容易上手.

Android热修复技术总结

插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化.本篇重点讲解热修复,并对当前流行的热修复技术做一个简单的总结. 热修复 什么是热修复? 简单来讲,为了修复线上问题而提出的修补方案,程序修补过程无需重新发版! 技术背景 在正常软件开发流程中,线下开发->上线->发现bug->紧急修复上线.不过对于这种方式代价太大. 而热修复的开发流程显得更加灵活,无需重新发版,实时高效热修复,无需

阿里巴巴朱中明--Android热修复技术分析和阿里的技术实践

[51CTO.com原创稿件]在WOT2016移动互联网技术峰会上,阿里朱中明老师为我们讲解热修复里面问题.第一讲解热修复的技术,第二讲解HotFix. 热更新和热修复的区别 通常所说的热更新和热部署都是对这个已经发布的客户端代码做一个更新,这里面有一个不同点,热更新强调它是一种实时更新和微小改动,而在热部署里面讲的是在工具链和工程上的完整的更新周期. 拦截技术 因为在热更新里面其实只讲到了两个比较重要的点,第一个就是拦截.这个拦截在业界里面,现在只有三种方面,第一种是类替换,第二种是AOP,第

干货满满,Android热修复方案介绍

摘要:在技术直播中,阿里云客户端工程师李亚洲(毕言)从技术原理层面解析和比较了业界几大热修复方案,揭开了Qxxx方案.Instant Run以及阿里Sophix等热修复方案的神秘面纱,帮助大家更加深刻地理解了代码插桩.全量dex替换.资源修复等常见场景解决方案,本文干货满满,精彩不容错过. 以下内容根据演讲视频以及PPT整理而成. 视频分享链接,点击这里! 在传统的修复模式下,如果线上的App出现Bug之后进行修复所需要的时间成本非常高,这是因为往往需要发布一个新的版本,然后将其发布到对应的应用

热修复技术对比及阿里百川HtFix 2.0深入剖析

近两年来,热修复技术在安卓开发圈儿成为焦点.随之而来的是,相关的解决方案也不断涌现.为此,本文将热修复的几大流派分别做较深入的阐述,以使关注这一技术的开发同学有更深的了解. 在正式切入话题之前,我们先来看看传统的开发流程究竟有哪些痛点.概括之,可以用三个"太"来描述:1.重新发布版本的代价太大:2.用户下载安装的成本太高:3.BUG修复不及时造成用户体验太差. 正因为如此,热修复技术才得以施展,并被广大开发者追捧.那么,热修复开发流程具有怎样的优势?总结起来,也有三点. 第一, 无需重

Android热修复

我们部门有很多Android的能力SDK,被很多App(约1000个)集成.每次SDK有微调发布新版本后,App集成需要花上1-2个月时间,很多时候SDK团队和App团队双方都很痛苦.16年10月份,Boss叫搞一个Android的热修复功能.神奇的是,居然让我一个从未搞过Android的人来负责(看来我在老板心中 只能充当救火队员).我在16年12月完成了第一个版本的实现,后面详细针对200多种机型的调试,就交给其他同事去了. 最近看见已在部门几个产品推广该功能了,想想还是记录下当时实现的思路