1. 程式人生 > >熱修復(java層dex分包方式實現)

熱修復(java層dex分包方式實現)

 修復原理:
首先要了解 android 載入classes.dex檔案的流程或原理。


       1、android 如何載入classes.dex檔案的?

       // 用來載入apk 的dex檔案:PathClassLoader

        public Class findClass(String name, List<Throwable> suppressed) {
                    for (Element element : dexElements) {
                            DexFile dex = element.dexFile;

                            if (dex != null) {
                                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                                    if (clazz != null) {
                                            return clazz;
                                        }
                                }
                        }
                    if (dexElementsSuppressedExceptions != null) {
                            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
                        }
                    return null;
                }




           通過了解PathClassLoader 載入dex檔案得知,將修復好的dex檔案插入到dexElements集合中,
        根據類的載入機制,一個類只能被載入一次,所以當虛擬機器載入 修復好的dex檔案時,就會載入
        修復好的類,而有問題的類將不被載入。因此達到修復的目的。



實現步驟:
1: 總網路下載補丁包,在app啟動的時候載入補丁包,就到達了修復有問題的類的目的。
   這裡 用本地拷貝的方式來模擬從網路下載。將補丁包放在assets資源目錄下.

2:由於PathClassLoader 和 DexClassLoader 都繼承自BaseClassLoader,所以這裡 通過反射拿到
   BaseClassLoader中的DexPathList pathList 。

3: 拿到DexPathList pathList物件後,再獲取pathList中的存放dex的陣列 Element[] dexElements。

/** list of dex/resource (class path) elements */
    private final Element[] dexElements;

4:通過DexClassLoader 拿到補丁包中dex檔案的 pathList,最終拿到補丁包中的Element[] dexElements。
   
5:用補丁包中的Element[] dexElements 替換 app中的Element[] dexElements。

6:重新啟動app即可

完整實現:

 崩潰程式碼所在類:將本類程式碼修復後 重新打包成dex檔案,放入assets目錄下 來測試。

//生成新的 classes2.dex
//dx --dex --output=D:\classes2.dex class路徑(包含完整包名)

package example.nzh.om.myhandler.hotfix;

public class CommonUtil {

    public static String getStr() {
        String[] array = {"123", "456", "789"};
        return array[3];
    }

}

修復方法:onclick
package example.nzh.om.myhandler.hotfix;

import android.app.Activity;
import android.content.Context;
import android.net.http.HttpResponseCache;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;

import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import example.nzh.om.myhandler.R;

public class HotFixTestActivity extends Activity {

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


    public void test(View view) {
        String s = CommonUtil.getStr();
        if (!TextUtils.isEmpty(s)) {
            Toast.makeText(this, s, Toast.LENGTH_LONG).show();
        }
    }

    // 存放修復好的dex
    HashSet<File> fixDex = new HashSet<>();

    public void fix(View view) {

        //1:將下載好的classes.dex修復補丁包 拷貝到 /data/data/packageName/odex 目錄。
        try {
            String testFile = "classes2.dex";
            String newDexDir = "odex";
            String dir = File.separator + "data" +
                    File.separator + "data" +
                    File.separator + "example.nzh.om.myhandler" +
                    File.separator + newDexDir;

            File dexDir = new File(dir);
            if (!dexDir.exists()) {
                dexDir.mkdir();
            }
            InputStream in = getAssets().open(testFile);
            FileOutputStream fos = new FileOutputStream(dir + File.separator + testFile);
            Toast.makeText(this, dir + File.separator + testFile, Toast.LENGTH_LONG).show();
            byte[] buffer = new byte[1024];

            int len;
            while ((len = in.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            fos.flush();
            fos.close();

            //?2:遍歷該目錄下的所有修復好的dex檔案
            File dir2 = dexDir;//getApplicationContext().getDir(newDexDir, Context.MODE_PRIVATE);
            File[] files = dir2.listFiles();

            for (File file : files) {
                if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
                    fixDex.add(file);
                }
            }
            if (fixDex.size() == 0) {
                Toast.makeText(this, "修復包拷貝失敗!", Toast.LENGTH_SHORT).show();
                return;
            } else {
                Toast.makeText(this, "修復包拷貝成功!", Toast.LENGTH_SHORT).show();
            }
            //2.1 載入應用程式的dex,通過( 用context.get---> PathClassLoader)
            //    得到的classloader就是PathClassLoader
            //2.2 載入修復後的dex檔案  用new DexClassLoader
            //2.3 合併兩個檔案:反射拿到baseClassLoader中的pathList,再從DexPathList中反射獲取Element[]
            //     最終替換Element[] elements變數。

            // 修復:

            PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader();

            String optimizedDir = dir2.getAbsolutePath() + File.separator + "optimized_dir";
            File opt = new File(optimizedDir);
            if (!opt.exists()) {
                opt.mkdir();
            }
            for (File file : fixDex) {

                DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath()
                        , optimizedDir
                        , null
                        , this.getClassLoader());

                //反射第一步:拿到pathList物件(BaseClassLoader-->pathList(DexPathList))
                Class baseClassLoaderCls = Class.forName("dalvik.system.BaseDexClassLoader");
                String feildNamePathList = "pathList";

                Object dexObj = reflectField(dexClassLoader, baseClassLoaderCls, feildNamePathList);//修復後的dex 的DexPathList
                Object pathObj = reflectField(pathClassLoader, baseClassLoaderCls, feildNamePathList);// 源程式的dex的DexPathList

                //反射第二步:得到DexPathList-->Element[]
                String feildNameElement = "dexElements";
                //修復後的ClassLoader裡面的Elements 資料
                Object fixedDexArray = reflectField(dexObj, dexObj.getClass(), feildNameElement);
                //源程式中的ClassLoader裡面的Elements 資料
                Object pathDexArray = reflectField(pathObj, pathObj.getClass(), feildNameElement);

                //合併陣列

                Object fianlDex = combineArray(fixedDexArray, pathDexArray);
                //更換DexPathList中的Element[] elments
                setFeild(pathObj, pathObj.getClass(), "dexElements", fianlDex);
            }

            //生成新的 classes2.dex
            //dx --dex --output=D:\classes2.dex class路徑(包含完整包名)
            Toast.makeText(this, "修復成功", Toast.LENGTH_SHORT).show();

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

    //合併陣列,將修復dex 插入陣列最前面。
    public Object combineArray(Object arrayNew, Object arrayOld) {

        int i = Array.getLength(arrayNew);
        int j = i + Array.getLength(arrayOld);

        //複製陣列,長度為兩個陣列長度之和。型別和新的陣列一樣。

        Object result = Array.newInstance(arrayNew.getClass().getComponentType(), j);


        for (int k = 0; k < j; k++) {
            if (k < i) {
                //新陣列的0-i個元素 插入新的陣列資料
                Array.set(result, k, Array.get(arrayNew, k));
            } else {
                // i-end 插入舊陣列的資料
                Array.set(result, k, Array.get(arrayOld, k - i));
            }

        }

        return result;

    }


    /**
     * 反射成員變數
     *
     * @param obj
     * @param clazz
     * @param fieldName
     */
    public Object reflectField(Object obj, Class clazz, String fieldName) throws Exception {

        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }

    public void setFeild(Object obj, Class clazz, String fieldName, Object value) throws Exception {
        Field f = clazz.getDeclaredField(fieldName);
        f.setAccessible(true);
        f.set(obj, value);
    }


}

最後使用:Manifest.xml 引用MyApplication
public class MyApplication extends Application {

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

    @Override
    protected void attachBaseContext(Context base) {

        MultiDex.install(base);
        super.attachBaseContext(base);
    }
}