熱修復改進版 - 自己的熱修復方法

1. 概述


前邊我們分析并寫了阿里的熱修復方法,可以知道阿里的熱修復是不能增加成員變量甘耿、成員方法和資源的,所以基于這個原因,然后我們上節(jié)課又通過對類的加載流程的源碼做了一個分析夸赫,那么我們這節(jié)課就來看下我們自己的一個修復的方法,其實很簡單咖城,就是鉆了一個空子茬腿,說白了,就是根據這幾個弊端以及類的加載流程然后得出自己的一個熱修復的方法宜雀,如果沒有看的可以先去看下我的這兩篇文章:

Android熱修復打補丁技術 - (阿里熱修復生成補丁包)
類的加載流程源碼分析

2. 回顧阿里熱修復流程和類的加載流程


2.1>:首先先來回顧下阿里的熱修復流程切平,流程圖如下:
阿里打補丁流程.png
分析如下:

1>:把我們之前有bug的版本打一個包,我們叫做 old.apk辐董,然后我們修復該bug悴品,現(xiàn)在是沒有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類刹缝,可以發(fā)現(xiàn)它里邊的 findClass()方法如下:


圖片.png

4>:可以看出, DexPathList中的 findClass()方法其實就是 通過for循環(huán)遍歷 app中所有的 dexElements的數組颈将,只要找到了 class梢夯,這里就是說只要找到了 TestActivity的這個class,那么就直接返回 一個 class給 PathClassLoader晴圾,然后 通過 (Acitivty)cl.loadClass(className).newInstance()方法颂砸,其實就是通過反射去創(chuàng)建對象;

以上就是類的加載流程分析
那么基于上邊的分析死姚,下邊我們就來看下我們這節(jié)課所要講解的一個我們自己的熱修復方法人乓。

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循環(huán)遍歷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移動到 系統(tǒng)能夠訪問的 dex目錄下,因為我們最終要把它變?yōu)?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. 開發(fā)中的一些細節(jié)


1>:可以把出錯的 class 重新單獨的打成 一個 fix.dex,在這里就指的是 TestActivity壁袄,大小也比較小类早,不過不太可取,除非說代碼不混淆嗜逻;
2>:可以采用分包涩僻,可以把不會出錯的類打成一個 dex(這里的不要混淆),有錯的留在另一個 dex中(這里可以混淆)栈顷,但是如果方法數沒有超過 65536逆日,系統(tǒng)默認不會給你分包,那么你需要去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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市哥纫,隨后出現(xiàn)的幾起案子霉旗,更是在濱河造成了極大的恐慌,老刑警劉巖蛀骇,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厌秒,死亡現(xiàn)場離奇詭異,居然都是意外死亡擅憔,警方通過查閱死者的電腦和手機鸵闪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來暑诸,“玉大人蚌讼,你說我怎么就攤上這事辟灰。” “怎么了篡石?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵芥喇,是天一觀的道長。 經常有香客問我凰萨,道長继控,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任胖眷,我火速辦了婚禮武通,結果婚禮上,老公的妹妹穿的比我還像新娘珊搀。我一直安慰自己冶忱,他們只是感情好,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布食棕。 她就那樣靜靜地躺著朗和,像睡著了一般错沽。 火紅的嫁衣襯著肌膚如雪簿晓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天千埃,我揣著相機與錄音憔儿,去河邊找鬼。 笑死放可,一個胖子當著我的面吹牛谒臼,可吹牛的內容都是我干的。 我是一名探鬼主播耀里,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蜈缤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冯挎?” 一聲冷哼從身側響起底哥,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎房官,沒想到半個月后趾徽,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡翰守,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年孵奶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜡峰。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡了袁,死狀恐怖朗恳,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情载绿,我是刑警寧澤僻肖,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站卢鹦,受9級特大地震影響臀脏,放射性物質發(fā)生泄漏。R本人自食惡果不足惜冀自,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一揉稚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熬粗,春花似錦搀玖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至含末,卻和暖如春猜拾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背佣盒。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工挎袜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肥惭。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓盯仪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜜葱。 傳聞我的和親對象是個殘疾皇子全景,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內容