Android手寫實現class文件的熱修復(仿Tinker)

前言:

           又有一陣子沒有更新博客了,最近本人在完成一個開源項目。馬上又到年尾了,所以時間比較趕。等完成之後會給大家分享一下,希望小夥伴們能多多支持(star)啊!好了,回到今天的主題,熱修復這個技術想必大家都聽過,或者也試過。這確實是一個非常牛逼的技術。但其實已經不算是新東西了。這些年來國內各大互聯網巨頭也都推出了自己的熱修復方案,比如我要說的微信的tinker,還有像阿里的andfix,等等。之所以會出現這麼一個東西,估計大家也都能猜個七八。無非就是傳統的APP上線流程帶來的各種弊端。小公司的小項目也許可能影響不大。但像大廠的那些DAU海量的項目那就影響非常巨大了。大家也都看到了,我今天分享的是class文件的修復,其實熱修復能做的遠不止這些啊,它還能對資源文件,so庫等等進行修復。限於篇幅和複雜度呢,接下來我將不會涉及到這些。大家如果有興趣可以去看看Tinker的官網,好了,下面就讓我帶大家來看一看熱修復的簡單實現吧!

一,過去與現在:

下面通過兩張圖我們來看看傳統APP開發與熱修復開發的不同之處:

 

 很明顯在第五步開始出現了不同,那我來總結一下熱修復的優勢有哪些:

  1. 無需重新發布新版本省時省力
  2. 用戶無感修復,無需下載新版本,體驗很好
  3. 修復效率高,降低損失

二 ,熱修復原理:

關於原理我也直接畫了張圖,帶大家看看

總結起來就是利用反射,把修復好的dexclassloader的pathlist的dexElements,和系統原本的dexElements合併,注意合併時把修復好的放前面,最後把這個合併的賦值給系統的pathList即可。說完這些我估計有很多小夥伴會很懵,首先要給大家說的是,這種class文件修復的做法原理和tinker差不多,這個小夥伴也可以去看看tinker的源碼。可能有些小夥伴對這些API或者流程很不理解啊,首先大家要知道,一個apk文件它其實就是打包了dex文件和資源文件,小夥伴可以將apk文件的後綴名改爲zip,再去解壓一下就知道了。那麼dex文件呢就是我們Android工程內所有class文件的合集。因爲APP最終運行的時候就是靠Android系統去執行這些dex文件,整個過程就是從我們寫下Java文件開始(源碼期),經由JVM編譯成class文件(編譯期),然後通過dex工具轉換成dex文件,最後經過Dalvik虛擬機去執行。所以我們的要做到class修復,重點就是要改變這些有bug的dex文件。而這些dex文件就存在於dexclassloader的pathlist的dexElements數組中,之後重點就是各種反射的用法了。關於反射小夥伴們如果不熟悉可以自己去查查資料回顧一下。那麼,以上所有這些流程,下面我將通過代碼實例展示給大家看看。

三,編碼實現: 

下面我直接上代碼,註釋已經很清楚了:

//這裏我直接將已修復的dex文件打包好複製到APP私有路徑中,實際運用中我們先是從服務器拉取
public void goHotFix(View view) {
        //獲取到apk的私有存儲路徑
        File filesDir = getDir("dex", Context.MODE_PRIVATE);
        //獲取到沒有bug的dex文件的名字
        String name = "Test.dex";
        //創建該dex文件的file
        String path = new File(filesDir, name).getAbsolutePath();
        //根據這個路徑去創建一個新的file對象
        File file = new File(path);
        //如果這個文件存在就刪除掉
        if(file.exists()){
            file.delete();
        }
        //創建io流
        InputStream is = null;
        FileOutputStream os = null;
        try {
           is = new FileInputStream(new File(Environment.getExternalStorageDirectory(),name));
           os = new FileOutputStream(path);
           int len = 0;
           byte[] bytes = new byte[1024];
           while ((len = is.read(bytes))!=-1){
                os.write(bytes,0,len);
           }
            File f  = new File(path);
           if(f.exists()){
               Toast.makeText(this, "文件複製成功", Toast.LENGTH_SHORT).show();
           }
           FixManager.loadDex(this);
        }catch (Exception e){
           e.printStackTrace();
        }finally {
            try {
                is.close();
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
/**
 * Author by YX, Date on 2019/12/21.
 * 修復Dex文件的工具類
 */
public class FixManager {

    //創建一個用來存儲加載到的dex文件的集合
    private static HashSet<File> loadedDexSet = new HashSet<>();

    static {
        //保證集合操作前爲空
        loadedDexSet.clear();
    }

    /**
     *這裏就是代替dex文件的操作
     * 根據上下文來加載dex文件,並放入集合中
     * @param context
     */
    public static void loadDex(Context context){
        if(context == null){
            return;
        }
        //獲取當前應用所在私有路徑,也就是dex文件的目錄
        File odexDir = context.getDir("dex", Context.MODE_PRIVATE);
        //通過該目錄獲得目錄下所有文件的數組
        File[] files = odexDir.listFiles();
        for (File file : files) {
            if(file.getName().startsWith("classes")||file.getName().endsWith(".dex")){
                loadedDexSet.add(file);
            }
        }
        //創建一個目錄,用來裝載解壓的文件
        String optimizeDir = odexDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optimizeDir);
        //如果這個目錄不存在就創建
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        //遍歷這個dex集合
        for (File file : loadedDexSet) {
            //獲取當前dex類加載器
            DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
            //實現一個類加載器的對象
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            try {
                //通過反射拿到系統類加載器
                Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
                Field systemPathList = baseDexClassLoaderClass.getDeclaredField("pathList");
                systemPathList.setAccessible(true);
                Object splObj = systemPathList.get(pathClassLoader);
                Class<?> pathListClass = splObj.getClass();
                Field dexElements = pathListClass.getDeclaredField("dexElements");
                dexElements.setAccessible(true);
                Object dexElementsObj = dexElements.get(splObj);

                //創建自己的類加載器
                Class<?> myBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
                Field myPathList = myBaseDexClassLoaderClass.getDeclaredField("pathList");
                myPathList.setAccessible(true);
                Object mysplObj = myPathList.get(dexClassLoader);
                Class<?> myPathListClass = mysplObj.getClass();
                Field myDexElements = myPathListClass.getDeclaredField("dexElements");
                myDexElements.setAccessible(true);
                Object mydexElementsObj = myDexElements.get(mysplObj);

                //進行dex文件的融合
                Class<?> componentType = dexElementsObj.getClass().getComponentType();
                //分別得到兩個dexElements的長度
                int systemDEL = Array.getLength(dexElementsObj);
                int myDEL = Array.getLength(mydexElementsObj);
                //創建一個能放入它們的數組
                int newL = systemDEL + myDEL;
                Object newDEL = Array.newInstance(componentType, newL);
                for (int i = 0; i < newL; i++) {
                    if(i < myDEL){
                        Array.set(newDEL,i,Array.get(mydexElementsObj,i));
                    }else {
                        Array.set(newDEL,i,Array.get(dexElementsObj,i-myDEL));
                    }
                }
                //將融合後的數組賦值給系統
                Field systemDexElements = pathListClass.getDeclaredField("dexElements");
                systemDexElements.setAccessible(true);
                systemDexElements.set(splObj,newDEL);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                Toast.makeText(context, "修復成功", Toast.LENGTH_SHORT).show();
            }
        }
    }
}
public class MyApp extends Application{

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

    @Override
    protected void attachBaseContext(Context base) {
        //注意這裏要到gradle裏面先開啓分包
        MultiDex.install(base);
        FixManager.loadDex(base);
        super.attachBaseContext(base);
    }
}

 上面代碼就是總體實現,之後我們將編譯好的apk解壓,要注意的是,由於做了分包處理,我們要把size更大的那個dex文件提取出來使用,爲什麼要分包,給你一個數字65536。可能有些小夥伴會覺得這樣做,豈不是apk的體積會變得很龐大。其實呢這只是一種簡單通俗的做法。但也確實會存在上述問題。我們也可以單獨把修復的文件打成dex文件,這樣就更好一點。當然我還是要說這只是我自己的做法,小夥伴們可以自己去研究tinker的源碼看看。如果不知道如何單獨把出錯經修復的Java文件打包成dex文件,下面我也分幾步告訴大家 :

  1. 首先rebuild一下工程,在app\build\intermediates\classes\debug下拿到想要代替的class文件
  2. 在AndroidSDK的build-tools下隨便選一個版本進去打開cmd命令,
  3. 輸入dx --dex --no-strict --output空格 生成dex文件的路徑 空格 class文件的路徑  最後回車即可

如果是按我的做法就是把這個dex文件取名爲Test放到sdcard根目錄下即可,隨後就能完成修復工作了。工程源碼我已開源,想要的小夥伴看這裏 https://github.com/OMGyan/XHotFix

以上!!!

發佈了43 篇原創文章 · 獲贊 19 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章