天天看點

基于QQ空間熱修複原理實踐基于QQ空間熱修複原理實踐

基于QQ空間熱修複原理實踐

關于熱修複技術,去年真是火的一塌糊塗,俺們沒有及時趕上,好在現在趕上也不算晚,好了廢話不多說,直接進入正題。

  • 原理:

    簡單闡述一下,具體的還是看原文吧。

    說白了這個方案還是在java層的改動,沒有涉及到底層C/C++代碼,還是比較好了解的。說到這裡就不得不提到Android類加載機制。

//DexPathList.java
 /**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses 
* reflection to modify 'dexElements' (http://b/7726934).不得不說Facebook好牛叉
*/
private final Element[] dexElements;



    /**
     * Finds the named class in one of the dex files pointed at by
     * this instance. This will find the one in the earliest listed
     * path element. If the class is found but has not yet been
     * defined, then this method will define it in the defining
     * context that this instance was constructed with.
     *
     * @param name       of class to find
     * @param suppressed exceptions encountered whilst finding the class
     * @return the named class or {@code null} if the class is not
     * found in any of the dex files
     * 這個方法就是這種熱修複的核心所在:
     * 一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個
     * dex檔案排列成一個有序的數組dexElements,當加載類的時候,會按順序周遊
     * dex檔案,然後從目前周遊的dex檔案中找類,如果找類則傳回,如果找不到
     * 從下一個dex檔案繼續查找。
     */

    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;

    }
           

理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex檔案的類,如下圖:

基于QQ空間熱修複原理實踐基于QQ空間熱修複原理實踐

是以,熱更新檔方案就是把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:

基于QQ空間熱修複原理實踐基于QQ空間熱修複原理實踐

這就是更新檔修複的基本原理了,當然實作過程中還存在其他問題:方法中直接引用到的類(第一層級關系,不會進行遞歸搜尋)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFIED:

不過也給出了解決方案:

是以為了實作更新檔方案,是以必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED标志。 最終空間的方案是往所有類的構造函數裡面插入了一段代碼,代碼如下:

if (ClassVerifier.PREVENT_VERIFY) {

System.out.println(AntilazyLoad.class);

}

其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex内的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的标志了,隻要沒被打上這個标志的類都可以進行打更新檔操作。

  • 熱修複實踐

    首先:要來了解2個ClassLoader的子類,

    PathClassLoader 用來記載程式的dex;

    DexClassLoader 用來加載指定的dex檔案(限制:必須要在應用程式的目錄下面)

    public class BaseDexClassLoader extends ClassLoader {
        //待會利用反射要擷取這個屬性
        private final DexPathList pathList;
    }
               
    1. 引用MultiDex分包
dependencies {
    compile 'com.android.support:multidex:1.0.1'
}


defaultConfig {
        multiDexEnabled true
    }


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'
}
}


public class MyApplication extends Application{

    @Override
    protected void attachBaseContext(Context base) {
        // TODO Auto-generated method stub
        MultiDex.install(base);
    }

}
           

然後就是核心的處理工具類:

public class FixDexUtils {
    //用來存放dex檔案
    private static HashSet<File> loadedDex = new HashSet<File>();
    //public static final String DEX_DIR = "odex";
    // /data/data/packageName/odex dex存放路徑

    static{
        loadedDex.clear();
    }

    //在Application中初始化
    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);
    }

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

    //合并dex
    private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
        // /data/data/packageName/odex/opt_dex
        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();

            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.合并
                //擷取更新檔的pathList屬性
                Object dexObj = getPathList(classLoader);
                //擷取程式的pathList屬性
                Object pathObj = getPathList(pathLoader);
                //擷取更新檔的dexElements數組
                Object mDexElementsList = getDexElements(dexObj);
                //擷取程式的dexElements數組
                Object pathDexElementsList = getDexElements(pathObj);
                //合并完成,将更新檔dex插入到第一個
                Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
                //重寫給PathList裡面的Element[] dexElements;指派
                Object pathList = getPathList(pathLoader);
                setField(pathList,pathList.getClass(),"dexElements",dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //利用反射機制,擷取cl屬性
    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);
    }

    //利用反射擷取BaseDexClassLoader中的pathList
    private static Object getPathList(Object baseDexClassLoader) throws Exception {
            return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
    }
    //利用反射擷取DexPathList中的dexElements數組
    private static Object getDexElements(Object obj) throws Exception {
            return getField(obj,obj.getClass(),"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);
        //建立一個length=j的localClass[]數組
        Object result = Array.newInstance(localClass, j);
        for (int k = ; 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;
    }

}
           

頁面中實作:

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 = ;
            byte[] buffer = new byte[];
            while ((len=is.read(buffer))!=-){
                os.write(buffer,,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();
        }
    }
           
  • 手動生成classes2.dex

    看下怎麼手動生成dex檔案

    1,先找到class檔案,javac編譯,或者找IDE編譯好的,MyTestClass.class

    dn_fix_ricky_as\app\build\intermediates\bin\MyTestClass.class

2,dx.bat指令生成dex檔案

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

指令解釋:

–output=D:\Users\ricky\Desktop\dex\classes2.dex 指定輸出路徑

D:\Users\ricky\Desktop\dex 最後指定去打包哪個目錄下面的class位元組檔案(注意要包括全路徑的檔案夾,也可以有多個class)

參考:

1. 安卓App熱更新檔動态修複技術介紹

2. DexPathList源碼

3. BaseDexClassLoader源碼

4. Android4.4.2 DexClassLoader源碼分析

5. 美團Android DEX自動拆包及動态加載簡介