1. 程式人生 > >Android應用熱修復

Android應用熱修復

一、修復的工具

當前主要有兩個主流的熱修復工具:
1.阿里系:使用了DeXposed(修改了國外的),一年沒有維護了,現在又搞了一個andfix,是一種黑客技術。自己去實現了底層的zyqote。從底層C的二進位制來入手的。

2.騰訊系:tinker
Java類載入機制來入手的。

這裡我們使用tinker。

二、熱修復的原理(Java類載入機制)

什麼是熱修復?

一般的Bug修復,都是等下一個版本解決,然後釋出新的apk
熱修復:可以直接在客戶已經安裝的程式當中修復bug。

bug一般會出現在某個類的某個方法地方。如果我們能夠動態的將客戶手機裡面的apk裡面的某個類給替換成我們已經修復好的類。

AndroidStudio的Instant run,也是一種熱修復或者增量更新的方式。如果只是改了一個類,那麼它只會把修改的東西打進新的包裡,放到手機上執行。
所以,在用AndroidStudio做熱修復的時候記得把Instant run功能關閉。不然會影響熱修復實現。
機制:dex分包。mutildex。
這裡寫圖片描述

如何實現呢?實現的原理?
從Java的類載入機制來入手的:ClassLoader

這裡寫圖片描述

Android 是如何載入class.dex檔案,啟動程式。
這裡提供了兩個類:
1.PathClassLoader :這個類用來載入應用程式的dex

public class PathClassLoader
extends BaseDexClassLoader {
}

2.DexClassLoader :這個類可以載入指定的某個dex檔案。(限制:必須要在應用程式的目錄下面)

public class DexClassLoader extends BaseDexClassLoader {
}

修復方案:

1.搞多個dex
第一個版本:classes.dex。
修復後的補丁包:classes2.dex(包涵了我們修復xxx.class)
這種實現方式也可以用於外掛開發。

2.把兩個dex合併
將修復的class替換原來出bug的class.
通過BaseDexClassLoader呼叫findClass(className):

Class<?> findClass(String name)

實際上替換的是修復了的dex檔案,這裡面集成了修改了的class檔案。Element[] dexElements;儲存的是dex的集合。

這裡寫圖片描述

在findClass方法中是通過迴圈去找dexElements中的類,如果找到了,就會返回這個Class,停止執行尋找。

這裡寫圖片描述

所以可以採取以下方式:
將修復好的dex插入到dexElements的集合,位置:出現bug的xxx.class所在的dex的前面。
最本質的實現原理:類載入器去載入某個類的時候,是去dexElements裡面從頭往下查詢的。
fixed.dex,classes1.dex,classes2.dex,classes3.dex

三、如何實現

上面已經介紹了其中的原理,接下來在開發中如何具體實現。

步驟

1.先安裝一個帶有bug的版本apk。

2.修復bug,重新打包成dex檔案,放在後臺伺服器。

3.通過主動方式或者推送將修復的dex檔案,放到手機中,進行dex檔案的合併。

用AndroidStudio打包multidex(官方待驗證)

準備工作

1.配置gradle檔案:

1)

dependencies {
    compile 'com.android.support:multidex:1.0.1'
}

2)

defaultConfig {
        multiDexEnabled true
    }

3)

buildTypes {
release {
    multiDexKeepFile file('dex.keep')
    def myFile = file('dex.keep')
    println("isFileExists:"+myFile.exists())
    println "dex keep"
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}

2.在Application中的attachBaseContext方法加入MultiDex.install(base);

public class MyApplication extends Application{
    @Override
    public void onCreate() {
        // TODO Auto-generated method stub
        super.onCreate();
    }
    @Override
    protected void attachBaseContext(Context base) {
        // TODO Auto-generated method stub
        MultiDex.install(base);
        FixDexUtils.loadFixedDex(base);
        super.attachBaseContext(base);
    }
}

這樣配置之後執行打包出來的apk中有兩個dex檔案:

程式碼處理
1.修復的dex檔案必須要在應用的目錄下面,所以第一步要將修復的檔案移動在/data/data/目錄下面

這裡寫圖片描述

程式碼處理

1.修復的dex檔案必須要在應用的目錄下面,所以第一步要將修復的檔案移動在/data/data/目錄下面

public static final String DEX_DIR = "odex";
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();
        }
    }

2.掃描dex檔案目錄下的所有dex,並用HashSet儲存。
記得每次初始化工具類的時候清空HashSet的資料

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);
    }

3.獲取需要修復的dex檔案,並通過反射拿到對應的dex陣列,然後一一合併,再通過反射的方式設定給應用的dex陣列中。這樣就實現了熱修復。

    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();  //原來的dex檔案載入路徑

            for (File dex : loadedDex) {
                //2.載入指定的修復的dex檔案。
                DexClassLoader classLoader = new DexClassLoader(
                        dex.getAbsolutePath(),//String dexPath,
                        fopt.getAbsolutePath(),//String optimizedDirectory,
                        null,//String libraryPath,
                        pathLoader//ClassLoader parent
                );
                //3.合併
                Object dexObj = getPathList(classLoader);
                Object pathObj = getPathList(pathLoader);
                Object mDexElementsList = getDexElements(dexObj);    //需要修復的dex檔案陣列
                Object pathDexElementsList = getDexElements(pathObj);   //原來的dex檔案陣列
                //合併完成 
                Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
                //重寫給PathList裡面的lement[] dexElements;賦值
                Object pathList = getPathList(pathLoader);
                setField(pathList,pathList.getClass(),"dexElements",dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

通過反射設定和獲取值:

//通過反射獲取baseDexClassLoader的pathList
    private static Object getPathList(Object baseDexClassLoader) throws Exception {
            return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
    }



    //通過反射獲取dexElements
    private static Object getDexElements(Object obj) throws Exception {
            return getField(obj,obj.getClass(),"dexElements");
    }

    /**
    * 獲取某個物件中的屬性
    * obj:某個物件
    * cl:物件的類
    * field:屬性值
    **/
    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);
    }


    /**
    * 給某個物件中的新增屬性值
    * obj:某個物件
    * cl:物件的類
    * field:屬性值
    * value:具體值
    **/
    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);
    }

合併陣列:

    /**
     * 兩個數組合並
     * @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;
    }
//  [12345] [9876]
//  [9876  12345]

備註:

BaseDexClassLoader類中的

    DexPathList pathList;

DexPathList類中的

    Element[] dexElements;

Element[] dexElements;原來的dex檔案集合
Element[] dexElements2;合併以後的檔案集合

四、測試

通過上面程式碼的處理,應用就可以實現熱修復,那麼該怎麼生成修復的dex檔案呢?

1.找到MyTestClass.class
fixdix_test\app\build\intermediates\bin\TestClass.class
2.配置dx.bat的環境變數
Android\sdk\build-tools\23.0.3\dx.bat
3.命令

dx --dex --output=D:\Users\song\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex

命令解釋:
–output=D:\Users\song\Desktop\dex\classes2.dex 指定輸出路徑
D:\Users\song\Desktop\dex 最後指定去打包哪個目錄下面的class位元組檔案(注意要包括全路徑的資料夾,也可以有多個class)