Android热更新技术(MutiDex)

学习流程:

一、热更新概念

二、源码分析

      1.代码总结

      2.思考热修复原理

三、原理分析

四、小Demo

一、热更新概念

热更新技术简单点说就是“打补丁”,这样做的好处就是避免了每次出bug,甚至只修改了一行代码就要发布新版本的尴尬,而是通过下载或者推送的方式让用户在不察觉的情况下加载修复好包,这样就实现了热修复。

二、源码分析

重点需要知道的就是,Android的ClassLoader体系,Android中加载类一般使用两个类加载器,PathClassLoader和DexClassLoader,它们都继承自BaseDexClassLoader。

1.PathClassLoader用来加载应用程序的dex,可以理解为加载已经安装的dex文件。

2.DexClassLoader可以加载指定的某个dex文件,可以理解为加载未安装的dex文件。(限制:必须要在应用程序的目录下面,请记住这句话)。

PathClassLoader类源码如下(后面会附上源码地址,请自行扒代码):


image.png

DexClassLoader类源码如下:


image.png

可以看到两个类简单到只有构造方法,但他们有共同的父类BaseDexClassLoader,那么BaseDexClassLoader里有什么好东西,请看圈圈里:


image.png

接着找DexPathList这个类里的findClass方法:


image.png

代码总结:

每个dex文件都是一个Element对象,存放在有序的dexElements数组中,通过类名使用DexFile类的loadClassBinaryName方法查找当前dex中的类,一旦找到马上返回Class实例,找不到就去下一个dex文件找,就是这么简单明了。

思考热修复原理 :

所有我们就可以把修复好的dex文件放在dexElements的最前面,PathClassLoader加载需要的class文件时就会从最开始的补丁文件找,一旦找到直接加载,这样有bug的dex文件就不用再去加载,从而实现了修复补丁的效果。

三、原理分析

直接来张图简单粗暴:


无标题.png

其中蓝色dex是从服务器中下载的修改之后的文件,它和红色dex文件包含有相同名字的目标class文件,比如下面Demo中提到的MyTestClass.class文件,可想而知加载需要的MyTestClass.class时,只用找到蓝色dex中的文件,而不会再去加载红色中的class文件。那么我们只需要拿到原来的dexElements和现有的dexElements然后合并,最后重写给PathList里面的Element[] dexElements。

再啰嗦下具体流程:

1.通过PathClassLoader 来加载我们自身App的dex,其中包含有bug的dex文件

2.通过DexClassLoader来加载我们的补丁dex文件,这是我们修复好的没有bug的dex文件

3.用反射的方式拿到pathList和dexElements

4.合并两个反射到的Element 数组,把我们的补丁dex放在数组的最前面

5.将合并的新的数组,通过Field重新设置到用户App的dexElements数组中,其实就是覆盖原来的数组

6.通过Android build-tools 中的dx命令打包一个修复好bug的dex文件,推送给用户。

四、实例实现

首先要实现的效果是:点击提示按钮吐司10/1的结果,但是bug是我们不小心把1写成了0,点击就会崩掉(为什么会崩掉,不懂的请翻阅小学三年级上册数学课本)。然后我们修复bug后,点击修复按钮,再次点击提示按钮,会顺利的吐司出大大的10!
新建项目MyHotFix_MutiDex,配置gradle和Manifest文件。请按照下面五点自行配置。

image.png

image.png

image.png
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.btn);
        Button buttonFix = (Button) findViewById(R.id.btn_fix);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                MyTestClass myTestClass = new MyTestClass();
                myTestClass.testFix(MainActivity.this);
            }
        });

        buttonFix.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                fixBug();
            }
        });
    }

    private void fixBug() {
        //目录:/data/data/packageName/odex
        File fileDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
        //往该目录下面放置我们修复好的dex文件。
        String name = "classes2.dex";
        String filePath = fileDir.getAbsolutePath() + File.separator + name;
        File file = new File(filePath);
        if (file.exists()) {
            file.delete();
        }
        //搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
        InputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + name);
            os = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }

            File f = new File(filePath);
            if (f.exists()) {
                Toast.makeText(this, "dex 重写成功", Toast.LENGTH_SHORT).show();
            }
            //热修复
            FixDexUtils.loadFixedDex(this);

        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

fixBug()方法中我们热修复之前的一系列操作就是把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录下,原因上面已经说过了,DexClassLoader可以加载指定的某个dex文件,必须要在应用程序的目录下面。其中MyTestClass类就是我们的出bug的类,我们看一下是什么,简单几行代码:

public class MyTestClass {
    public void testFix(Context context) {
        int i = 10;
        int a = 0;
        Toast.makeText(context, "结果:"+i/a, Toast.LENGTH_SHORT).show();
    }
}

可以很明显的看出这样写是错误的!不用管,先让它错着。在FixBug方法里调用了 FixDexUtils.loadFixedDex(this),看一下FieDexUtils类:

public class FixDexUtils {
    private static HashSet<File> loadedDex = new HashSet<File>();

    static{
        loadedDex.clear();
    }

    public static void loadFixedDex(Context context){
        if(context == null){
            return ;
        }
        //遍历所有的修复的dex
        File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();
        for(File file:listFiles){
            if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
                loadedDex.add(file);//存入集合
            }
        }
        //dex合并之前的dex
        doDexInject(context,fileDir,loadedDex);
    }

    private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj,value);
    }

    private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fopt = new File(optimizeDir);
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        //1.加载应用程序的dex
        try {
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

            for (File dex : loadedDex) {
                //2.加载指定的修复的dex文件。
                DexClassLoader classLoader = new DexClassLoader(
                        dex.getAbsolutePath(),//String dexPath,
                        fopt.getAbsolutePath(),//String optimizedDirectory,
                        null,//String libraryPath,
                        pathLoader//ClassLoader parent
                );
                Object dexObj = getPathList(classLoader);
                Object pathObj = getPathList(pathLoader);
                Object mDexElementsList = getDexElements(dexObj);
                Object pathDexElementsList = getDexElements(pathObj);
                //3.合并完成
                Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
                //4.重写给PathList里面的lement[] dexElements;赋值
                Object pathList = getPathList(pathLoader);
                setField(pathList,pathList.getClass(),"dexElements",dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }
    private static Object getPathList(Object baseDexClassLoader) throws Exception {
        return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
    }

    private static Object getDexElements(Object obj) throws Exception {
        return getField(obj,obj.getClass(),"dexElements");
    }

    /**
     * 两个数组合并
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}

友情提示:别忘了AndroidManifest中加权限

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

最终我们要做的事情就是:
1.准备两个手机,手机1刷有bug的apk
2.修改bug之后,手机2刷修复好的apk,然后手机2可以扔掉了,我们只要编译好的正确的class文件
3.由于我们只改动了MyTestClass这个类,只需要找到MyTestClass.class文件,打包成dex文件就可以了。简单粗暴在项目的app目录下直接搜索MyTestClass,拿到MyTestClass.class
4.配置dx.bat的环境变量,找到SDK中build-tools的路径,例如我的:D:\SDK\build-tools\26.0.1,我用的是26.0.1中的dx.bat
5.命令行输入dx --dex --output=dex的输出路径 class文件的路径,例如我的:dx --dex --output=C:\Users\lhx\Desktop\dex\classes2.dex C:\Users\lhx\Desktop\dex
第一个路径是dex输出路径,classes2.dex是自己起的文件名;第二个路径是MyTestClass.class文件所在的文件夹,这个路径可以不写完整,会自动到指向的文件下找class文件(注意要包括全路径的文件夹,例如我的全路径C:\Users\lhx\Desktop\dex\com\example\zt\myhotfix_mutidex,也可以有多个class),回车!然后就会在C:\Users\lhx\Desktop\dex路径下生成一个叫做classes2.dex文件,感觉自己好啰嗦~~


image.png

6.这个dex文件就是我们要的补丁文件啦,把它拷出来放到手机1的根目录下,点击我们刷上的错误的app,点击“按钮”,崩掉(因为还没有修复bug)!再次打开app,点击“修复按钮”,然后点击“按钮”,就不会再崩掉,赫然吐司出一个10!掌声在哪里~~这就是我们要的结果,此时bug就算修复成功了!

PathClassLoader,DexClassLoader,BaseDexClassLoader源码链接

Demo下载地址

特别感谢:动脑学院Ricky老师,讲解的特别详细。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容