热修复改进版 - 自己的热修复方法

1. 概述


前边我们分析并写了阿里的热修复方法,可以知道阿里的热修复是不能增加成员变量、成员方法和资源的,所以基于这个原因,然后我们上节课又通过对类的加载流程的源码做了一个分析,那么我们这节课就来看下我们自己的一个修复的方法,其实很简单,就是钻了一个空子,说白了,就是根据这几个弊端以及类的加载流程然后得出自己的一个热修复的方法,如果没有看的可以先去看下我的这两篇文章:

Android热修复打补丁技术 - (阿里热修复生成补丁包)
类的加载流程源码分析

2. 回顾阿里热修复流程和类的加载流程


2.1>:首先先来回顾下阿里的热修复流程,流程图如下:
阿里打补丁流程.png
分析如下:

1>:把我们之前有bug的版本打一个包,我们叫做 old.apk,然后我们修复该bug,现在是没有bug的,叫做 new.apk;
2>:在客户端去阿里官网下载 AndFix,然后生成一个 fix.apatch差分包,然后把这个差分包放在我们自己的服务器,让用户去下载;
3>:当用户把app只要一打开,检测到有bug的话,就会去服务器下载这个差分包,然后我们就会在 BaseApplication中调用 addPatch()方法去修复bug;
生成差分包的方法:

1.命令是:
apkpatch.bat -f <new> -t <old> -o <output> -k <keystore> -p <*> -a <alias> -e <*>
-f : 没有Bug的新版本apk.
-t : 有bug的旧版本apk
-o : 生成的补丁文件所放的文件夹
-k : 签名打包密钥
-p : 签名打包密钥密码
-a : 签名密钥别名
-e : 签名别名密码(这样一般和密钥密码一致)

我的:apkpatch.bat -f new.apk -t old.apk -o out -k joke.jks -p 123456 -a test -e 123456
然后点击回车,会在out中生成一个 xxx.apatch文件,将 xxx.apatch文件重名为 fix.apatch;

以上就是阿里热修复的一个流程;
2.2>:回顾类的加载流程,分析流程图如下:
类的加载机制.png
分析如下:场景:从MainActivity启动一个 TestActivity

首先先来看下继承关系:
PathClassLoader --> BaseDexClassLoader --> ClassLoader
1>:首先会去找 PathClassLoader,然后会去找BaseDexClassLoader,然后会去找 ClassLoader;
2>:然后调用 ClassLoader中的findClass()方法,但是由于 子类 BaseDexClassLoader覆盖了父类的该方法,所以这里就调用的是 子类BaseDexClassLoader的findClass()方法,调用方法如下;


图片.png

3>:由上边方法可以看出,其实是调用的 pathList.findClass()方法,而pathList它就是 DexPathList类,可以发现它里边的 findClass()方法如下:


图片.png

4>:可以看出, DexPathList中的 findClass()方法其实就是 通过for循环遍历 app中所有的 dexElements的数组,只要找到了 class,这里就是说只要找到了 TestActivity的这个class,那么就直接返回 一个 class给 PathClassLoader,然后 通过 (Acitivty)cl.loadClass(className).newInstance()方法,其实就是通过反射去创建对象;

以上就是类的加载流程分析
那么基于上边的分析,下边我们就来看下我们这节课所要讲解的一个我们自己的热修复方法。

3. 自己的热修复方法,流程图如下:


热修复原理.png

分析上图可知:
我们其实所采用的方式就是:
1>:首先先去修复好这个bug,然后打一个apk包,然后把后缀名改为 .zip并且解压,解压后会有一个 class.dex文件,这里需要手动去把这个文件重命名为 fix.dex,然后把这个 fix.dex文件放到服务器中;
2>:用户手中的apk是有bug的,上图的左边就是我们 app中所有的 dex文件,比如说我们把有bug的类暂且就叫做 bug.class,比如说它就在 所有 dex文件的最前边;
3>:然后我们只需要把这个 已经修复的 fix.class文件插入到 有bug.class的最前边就可以,由 类的加载流程可以知道,在 DexPathList中的findClass()方法中,会for循环遍历class,这里就是需要去找到 TestActivity,只要找到后就直接 return class,就不会去往后边去找,所以也就不会去找 后边有bug的class了,所以就达到了修复的一个目的;
这样的话,只要修复bug了,那么每次就不会再去找后边有bug的类了。

注意:自己手中的app一定是有bug的,而服务器上边的是没有bug的,只要用户一打开app,就会去下载整个 dex包,然后进行插入修复。

4. 修复后的最终效果


下图就是我们采用我们自己的修复方法的修复结果,只要用户第一次修复成功后,那么以后每次进入app之后就都会提示修复成功的,就不会去找之前有bug的class类,这个我们在上边也都是说过的;


图片.png

5. 具体代码如下


5.1>:修复的代码如下:
/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/5 9:49
 * Version 1.0
 * Params: 
 * Description:    修复
*/
public class FixDexManager  {


    private Context mContext ;
    private File mDexDir ;
    public FixDexManager(Context context) {
        this.mContext = context ;
        // 获取应用可以访问的 dex目录
        this.mDexDir = context.getDir("odex" , Context.MODE_PRIVATE) ;
    }



    /**
     * 修复dex包
     * fixDexPath:下载补丁的路径
     */
    public void fixDex(String fixDexPath) throws Exception {


        // 2. 然后获取下载好的补丁 dexElement
        // 2.1 把fixDexPath移动到 系统能够访问的 dex目录下,因为我们最终要把它变为 ClassLoader
        File srcFile = new File(fixDexPath) ;
        if (!srcFile.exists()){
            throw new FileNotFoundException(fixDexPath) ;
        }
        File destFile = new File(mDexDir , srcFile.getName()) ;
        if (destFile.exists()){
            Log.d(TAG, "patch [" + fixDexPath + "] has be loaded.");
            return;
        }

        copyFile(srcFile , destFile);

        // 2.2 让该ClassLoader读取 fixDex路径
        //需要修复的文件
        List<File> fixDexFiles = new ArrayList<>() ;
        fixDexFiles.add(destFile) ;
        fixDexFiles(fixDexFiles) ;
    }


    /**
     * 把合并的数组applicationDexElements  注入到 原来的 applicationClassLoader中即可
     */
    private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception {
        // 1. 先获取 pathList(通过反射)
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList") ;
        // private、public、protected都可以反射
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader) ;

        // 2. 获取pathList里边的 dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);

        // 合并
        dexElementsField.set(pathList , 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;
    }



    /**
     * copy file
     *
     * @param src  source file
     * @param dest target file
     * @throws IOException
     */
    public static void copyFile(File src, File dest) throws IOException {
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            if (!dest.exists()) {
                dest.createNewFile();
            }
            inChannel = new FileInputStream(src).getChannel();
            outChannel = new FileOutputStream(dest).getChannel();
            inChannel.transferTo(0, inChannel.size(), outChannel);
        } finally {
            if (inChannel != null) {
                inChannel.close();
            }
            if (outChannel != null) {
                outChannel.close();
            }
        }
    }



    /**
     * 从 ClassLoader中 获取 dexElements
     */
    private Object getDexElementsByClassLoader(ClassLoader classLoader) throws Exception {
        // 1. 先获取 pathList(通过反射)
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList") ;  // pathList是源码中的
        // private、public、protected都可以反射
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader) ;

        // 2. 获取pathList里边的 dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements"); // dexElements是源码中的
        dexElementsField.setAccessible(true);


        return dexElementsField.get(pathList);

    }


    /**
     * 加载全部的修复包
     */
    public void loadFixDex() throws Exception {
        File[] dexFiles = mDexDir.listFiles() ;
        List<File> fixDexFiles = new ArrayList<>() ;
        for (File dexFile : dexFiles) {
            if (dexFile.getName().endsWith(".dex")){
                fixDexFiles.add(dexFile) ;
            }
        }
        fixDexFiles(fixDexFiles) ;
    }


    /**
     * 修复 dex
     */
    private void fixDexFiles(List<File> fixDexFiles) throws Exception {
        // 1. 先获取应用的 已经运行的 dexElements
        ClassLoader applicationClassLoader = mContext.getClassLoader();

        Object applicationDexElements = getDexElementsByClassLoader(applicationClassLoader) ;

        File optimizedDirectory = new File(mDexDir , "odex") ;
        if (!optimizedDirectory.exists()){
            optimizedDirectory.mkdirs() ;
        }
        // 开始修复
        for (File fixDexFile : fixDexFiles) {
            // dexPath:修复dex的路径  -->  fixDexFiles
            // optimizedDirectory:解压路径
            // libraryPath:so文件的路径
            // parent:父的ClassLoader
            ClassLoader fixDexClassLoader = new BaseDexClassLoader(
                    fixDexFile.getAbsolutePath() , // 修复的dex的路径   必须要在应用目录下的odex文件中
                    optimizedDirectory, // 解压路径
                    null , // .so文件的路径
                    applicationClassLoader // 父的 ClassLoader
            ) ;


            // 获取app中的 dexElements数组,然后解析来就需要把 下载的没有bug的 补丁的 dexElement插入到这个数组的最前边
            Object fixDexElements = getDexElementsByClassLoader(fixDexClassLoader) ;

            // 3. 把补丁的 dexElement插到 已经运行的 dexElement的最前面,其实就是合并,修复就ok
            // applicationClassLoader 是一个数组,fixDexElements也是一个数组,就是把两个数组合并

            // 3.1  合并完成
            // 前者是修复的,后者是没有修复的
            applicationDexElements = combineArray(fixDexElements , applicationDexElements) ;
        }


        // 3.2 把合并的数组注入到 原来的 applicationClassLoader中即可
        injectDexElements(applicationClassLoader , applicationDexElements) ;
    }
}
5.2>:在MainActivity中的initData()方法中直接调用:
public class MainActivity extends BaseSkinActivity {


    @ViewById(R.id.btn_test)
    Button btn_test ;

    @Override
    protected void setContentView() {
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void initTitle() {

    }

    @Override
    protected void initView() {
        btn_test.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(TestActvity.class);
            }
        });
    }

    @Override
    protected void initData() {
        // 用户只要一打开app,就去调用我们自己的修复方式
        fixDexBug() ;
    }

    private void fixDexBug() {
        File fixFile = new File(Environment.getExternalStorageDirectory() , "fix.dex") ;

        if (fixFile.exists()) {
            FixDexManager fixDexManager = new FixDexManager(this);
            try {
                fixDexManager.fixDex(fixFile.getAbsolutePath()) ;
                Toast.makeText(MainActivity.this , "修复成功" , Toast.LENGTH_SHORT).show();
            } catch (Exception e) {
                e.printStackTrace();
                Toast.makeText(MainActivity.this , "修复失败" , Toast.LENGTH_SHORT).show();
            }
        }

    }
}
5.3>:最后需要在BaseApplication中调用之前所有修复的 dex包;
/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/4/1 12:48
 * Version 1.0
 * Params:
 * Description:
*/
public class BaseApplication extends Application {


    public static PatchManager mPatchManager ;
    @Override
    public void onCreate() {
        super.onCreate();

        // 加载所有修复的 dex包
        try {
            FixDexManager fixDexManager = new FixDexManager(this) ;
            fixDexManager.loadFixDex() ;
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

6. 开发中的一些细节


1>:可以把出错的 class 重新单独的打成 一个 fix.dex,在这里就指的是 TestActivity,大小也比较小,不过不太可取,除非说代码不混淆;
2>:可以采用分包,可以把不会出错的类打成一个 dex(这里的不要混淆),有错的留在另一个 dex中(这里可以混淆),但是如果方法数没有超过 65536,系统默认不会给你分包,那么你需要去Android Studio官网找分包,而且运行的时候如果 dex过大会影响启动速度;
3>:直接把整个项目打成apk,然后修改后缀名为 zip并且去解压,然后修改里边的class.dex文件名为 fix.dex,然后把fix.dex文件放在服务器,只要用户一打开app,就会去下载整个 dex包,然后进行插入修复,但也会导致一个问题,就是下载的 补丁的 fix.dex大小可能比较大,2M左右;

一般就用 第3种方法

和阿里的相比较,阿里的不能增加成员变量和成员方法,而我们的可以增加成员变量、成员方法、类,但是不能增加资源(腾讯的可以增加资源)

代码已上传至github:
https://github.com/shuai999/EssayJoke_day_04.git

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

推荐阅读更多精彩内容