Android Dex 分包+熱修復(QQ空間技術方案)

Android Dex 分包+熱修復(QQ空間技術方案)
    感謝博主的博客:
  主要代碼:上面博主的github:  MultiDex Demo
  
  如果用的是AS,可以參考 multidex-sampleAndroid的multidex帶來的性能問題-減慢app啓動速度;裏面有個檢測APP引用了哪些其他dex文件中的類的工具類,便於main-dex-rule.xml的收集,避免出現 NoClassDefFoundError以及優化啓動速度。
  
  博主將主要類打成classes.dex,其他業務類打成classes2.dex,將jar包打成classes3.dex,這樣分工明細,出錯也只會出在業務類(一般jar不會出錯,主類也一般是不能有問題的),線上修復的時候只需替換classes2.dex即可。但美中不足之處在於,沒有將本項目和依賴項目中的so文件進行拷貝,那麼完善的build.xml如下(如有誤,還請各位指教):

1 初始化,刪除bin和gen目錄
2.1 生成工程的R.java 文件,輸出到 gen目錄,此時需要把依賴工程的res資源一起生成R.java

aapt package -m -J ${project-dir}/gen -M ${manifest} -S ${project-dir}/res -S ${baidu-dir}/res -I ${android-jar}
2.2 生成依賴工程的R.java 文件,輸出到 gen目錄
aapt package -m -J ${project-dir}/gen -M ${baidu.manifest} -S ${project-dir}/res -S ${baidu-dir}/res -I ${android-jar}
  
-->  

3.1 編譯依賴工程的Java文件,輸出到 bin/classes目錄3.2 編譯項目工程的Java文件,輸出到 bin/classes目錄
javac -bootclasspath ${android-jar} -d ${project-dir}/bin/classes ${project-dir}/src gen/R.java

4.1 構建dex主包和次包;分爲三個部分 主包dex包含定義的文件,剩下的在classes2.dex 所有的jar都在classes3.dex

dx --dex --multi-dex --set-max-idx-number=20000 --main-dex-list ${project-dir}/main-dex-rule.txt --minimal-main-dex --output=${project-dir}/bin
4.2 構建項目和依賴項目所包含的jar
de --dex --output=bin/classes3.dex ${project-dir}/libs ${baidu-dir}/libs

5 將res和assets,AndroidManifest.xml 打包爲resources.arsc

如果依賴項目中使用了自己項目下的aeests目錄下資源,需要在 生成 R 文件,以及 打包時一併帶上,這裏沒寫

aapt package -f -M ${manifest} -S res -S ${baidu-dir}/res -A assets -I ${android-jar} -F ${project-dir}/bin/resources.arsc --auto-add-overlay
6 將 classes.dex文件和resources.arsc打包成臨時APK
注: 1,如果需要將so文件打包進apk,一定要加上-nf參數 2,如果第三方jar包裏含有圖片資源,一定要加上-rj參數,不然jar包裏資源文件解不出來,程序會因爲無法引用資源而報錯!

java ${sdk-folder}/tools/lib/sdklib.jar/com.android.sdklib.build.ApkBuilderMain ${project-dir}/bin/unsign.apk -u -z ${project-dir}/bin/resources.arsc -f bin/classes.dex -rf ${project-dir}/src -rf ${baidu-dir}/src -rj ${project-dir}/libs -rj ${baidu-dir}/libs -nf ${project-dir}/libs -nf ${baidu-dir}/libs

7 複製所有bin/classes*.dex文件到項目根目錄,因爲我們的腳本是在根目錄下面,這樣在運行aapt的時候,可以直接操作dex文件了
8 循環將bin/classes*.dex文件 aapt 添加到apk中"${dexfile} 已經打包進了apk,這裏不在添加${dexfile} 需要添加進apkaapt add bin/unsign.apk ${dexfile}添加完成,將項目根目錄下的 ${dexfile} 刪除9 生成簽名的apk
jarsigner -keystore ${project-dir}/my.keystore -storepass 123456 -keypass 123456 -signedjar ${project-dir}/bin/sign.apk ${project-dir}/bin/unsign.apk ant_test
10 刪除bin/resources.arsc和bin/unsign.apk; 對APK進行對齊優化
zipalign 4 ${project-dir}/bin/sign.apk ${project-dir}/bin/${ant.project.name}_signed_zipaligned.apk
com/alex_mahao/multidex/MainActivity.class
com/alex_mahao/multidex/MyApp.class
com/alex_mahao/multidex/FixDexUtils.class
com/alex_mahao/multidex/FileUtils.class

  **分包注意:
  1.Android 5.0 以上的系統,默認會加載多個dex,5.0以下的版本需要手動加載dex(在Application中重寫attachBaseContent(Context base) 方法,調用 SecondaryDexUtils.loadSecondaryDex(base));
  2.先配置Ant編譯環境(注意下載 ant-contrib-1.0b3.jar 放到ant的lib目錄下面
  3.將SDK plant-tools 更新到20.0.0以上(20.0.0以下的dx.bat 不支持--multidex);
  4.必須將Application中引用到的直接類寫在main-dex-rule.xml裏面,如果引用的類種有內部類,那麼也要寫上,如:
com/huyu/MainActivity.class  
com/huyu/MainActivity$Loaddex.class  (Loaddex 是 MainActivity 中的內部類)
  
  **動態加載 dex :
  1.網上很多原理分析以及demo,但是在5.0以下的手機上還是會有問題。
  我將網上的 SecondaryDexUtils 改良後如下:

  主要更改的部分邏輯:
  1.在APP每次啓動時,判斷dex存放目錄: data/data/<packageName>/app_odex/  下是否存在從APK解壓出來的dex文件,或者是從服務器上下載下來的需要修復的dex文件(這裏纔是熱修復的地方);
  2.如果不存在,就是APP安裝後初次啓動,需要從APK裏解壓出來(只解壓除了classes.dex之外的dex)。
  3.如果存在,就直接執行注入;
  4.熱修復的時候,只需從服務器上下載dex文件,先刪除data/data/<packageName>/app_odex/ 目錄下的要替換的dex文件,再將dex文件拷貝到data/data/<packageName>/app_odex/ 目錄下即可
  dex存放目錄:可以通過 context.getDir("odex",Context.MODE_PRIVATE).getAbsolutePath();獲得;如果該目錄不存在,系統會自動創建,並在 “odex”前加上“app_”的標識即 “app_odex”。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import android.content.Context;
import android.os.Build;
import android.util.Log;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class SecondaryDexUtils {

    public static final boolean ON = true;

    //use classes2.dex.lzma in apk from android 4.x+
    //use classes2.dex in apk for android 5.x+ 4.x-

    private static final String TAG = "TAG_注入Dex";

    /***************************************/
    private static int SUB_DEX_NUM = 10;
    private static final String CLASSES_PREFIX      = "classes";
    private static final String DEX_POSTFIX         = ".dex";
    private static final HashSet msLoadedDexList = new HashSet();
    /***************************************/

    private static final int BUF_SIZE = 1024 * 32;
    private static String mSubdexExt = DEX_POSTFIX;

    private static class LoadedDex{
        private File   dexFile;
        private ZipEntry zipEntry;
        private LoadedDex(File dir,String name){
            dexFile = new File(dir,name);
        }
        private LoadedDex(File dir,String name,ZipEntry zipE){
            dexFile = new File(dir,name);
            zipEntry = zipE;
        }
    }
    static{
        msLoadedDexList.clear();
    }

/*
    public static final File getCodeCacheDir(Context context) {
    	ApplicationInfo appInfo = context.getApplicationInfo();
    	return createFilesDir(new File(appInfo.dataDir, "dex_cache"));
    }
*/
/*
    private synchronized static File createFilesDir(File file) {
        if (!file.exists()) {
            if (!file.mkdirs()) {
                if (file.exists()) {
                    return file;
                }
                Log.e(TAG, "創建文件夾失敗:" + file.getPath());
                return null;
            }
        }
        return file;
    }
*/

    /**
     * 複製子dex
     * @param inputStream
     * @param outputFile
     * @return
     */
    public static boolean copydexFile(InputStream inputStream,File outputFile) {

        BufferedInputStream bis = null;
        OutputStream dexWriter = null;

        try {
            bis = new BufferedInputStream(inputStream);
            assert bis != null;

            dexWriter = new BufferedOutputStream(new FileOutputStream(outputFile));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                dexWriter.write(buf, 0, len);
            }

        } catch (IOException e) {
            return false;
        } finally {
            if (null != dexWriter)
                try {
                    dexWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            if (bis != null)
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return true;
    }

    /**
     * 加載子dex
     * @param appContext
     */
	public static void loadSecondaryDex(Context appContext) {
    	
        if(appContext == null){
            return;
        }

        ZipFile apkFile = null;
        try {
            apkFile = new ZipFile(appContext.getApplicationInfo().sourceDir);
        } catch (Exception e) {
        	Log.i(TAG, "create zipfile error:"+Log.getStackTraceString(e));
            return;
        }
        
        Log.i(TAG, "APK-zipfile:"+apkFile.getName());
        File filesDir = appContext.getDir("odex", Context.MODE_PRIVATE);
        Log.i(TAG, "APK-複製子dex的目標路徑:"+filesDir.getAbsolutePath());
        
        for(int i = 0 ; i < SUB_DEX_NUM; i ++){
            String possibleDexName = buildDexFullName(i);
            ZipEntry zipEntry = apkFile.getEntry(possibleDexName);
            Log.i(TAG, "APK下的entry:"+zipEntry);
            if(zipEntry == null) {
                break;
            }
            msLoadedDexList.add(new LoadedDex(filesDir,possibleDexName,zipEntry));
        }
        Log.i(TAG, "子dex總數:"+msLoadedDexList.size());
        
        //判斷  目標目錄下是否已經有dex文件
        boolean isOpted = false;
        File[] listFiles = filesDir.listFiles();
        for (int i = 0; i < listFiles.length; i++) {
        	File file = listFiles[i];
        	if(file.isFile() && file.getName().endsWith(".dex")){
        		isOpted = true;
        		break;
        	}
		}
        
        //  data/data//app_odex 目錄下存在.dex文件 就不再從APK解壓,否則從APK解壓
        if(!isOpted){
        	for (LoadedDex loadedDex : msLoadedDexList) {
        		File dexFile = loadedDex.dexFile;
        		try {
        			boolean result = copydexFile(apkFile.getInputStream(loadedDex.zipEntry), dexFile);
        			Log.i(TAG, "複製子dex結果:"+result);
        		} catch (Exception e) {
        			Log.i(TAG, "複製子dex錯誤:"+Log.getStackTraceString(e));
        		}
        	}

        	if (apkFile != null) {
        		try {
        			apkFile.close();
        		} catch (Exception e) {
        		}
        	}
        }
        doDexInject(appContext, filesDir, msLoadedDexList);
    }

    private static String buildDexFullName(int index){
        return CLASSES_PREFIX + (index + 2) + mSubdexExt;
    }
    
    private static void doDexInject(final Context appContext, File filesDir,HashSet loadedDex) {
        if(Build.VERSION.SDK_INT >= 23){
            Log.w(TAG,"無法注入dex,SDK版本太高;版本=" + Build.VERSION.SDK_INT);
        }
        String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optimizeDir);
        if (fopt.exists())
        	fopt.delete();
        fopt.mkdirs();

        try {
            ArrayList dexFiles = new ArrayList();
            for(LoadedDex dex : loadedDex){
                dexFiles.add(dex.dexFile);
                DexClassLoader classLoader = new DexClassLoader(
                		dex.dexFile.getAbsolutePath(), 
                		fopt.getAbsolutePath(),null, 
                		appContext.getClassLoader());
                inject(classLoader, appContext);
            }
        } catch (Exception e) {
            Log.i(TAG, "install dex error:"+Log.getStackTraceString(e));
        }
    }

    /**
     * @param loader
     */
    private static void inject(DexClassLoader loader, Context ctx){
        PathClassLoader pathLoader = (PathClassLoader) ctx.getClassLoader();
        try {
            Object dexElements = combineArray(
                    getDexElements(getPathList(pathLoader)),
                    getDexElements(getPathList(loader)));
            Object pathList = getPathList(pathLoader);
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            Log.i(TAG, "inject dexclassloader error:" + Log.getStackTraceString(e));
        }
    }

    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

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

    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }
    
    private static void setField(Object obj, Class<?> cl, String field,
                                 Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    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;
    }
    
    /**刪除文件*/
    public static boolean deleteFile(String path){
    	File file = new File(path);
    	if(file.exists())
    		return file.delete();
    	return true;
    }
}
/**這裏省略了從服務器下載dex的代碼,直接將dex放在了SD卡根目錄,直接替換dex;如果文件比較大最好開啓線程去複製*/
public void inject(View view) {
	// 無bug的classes2.dex 存放 到SD卡 根目錄
	String sourceFile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex";
	// 系統的私有目錄
	String targetFile = getDir("odex", Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex";
	try {
		//先把	 data/data//app_odex/classes2.dex   刪除
		SecondaryDexUtils.deleteFile(targetFile);
		// 複製文件到私有目錄
		SecondaryDexUtils.copydexFile(new FileInputStream(sourceFile), new File(targetFile));
		// 刪除 SD卡上的 classes2.dex
		SecondaryDexUtils.deleteFile(sourceFile);
	} catch (Exception e) {
		e.printStackTrace();
	}
}



  最後關於性能方面:

在冷啓動時因爲需要加載多個DEX文件,如果DEX文件過大時,處理時間過長,很容易引發ANR(Application Not Responding);採用MultiDex方案的應用可能不能在低於Android 4.0 (API level 14) 機器上啓動,這個主要是因爲Dalvik linearAlloc的一個bug (Issue 22586);採用MultiDex方案的應用因爲需要申請一個很大的內存,在運行時可能導致程序的崩潰,這個主要是因爲Dalvik linearAlloc 的一個限制(Issue 78035). 這個限制在 Android 4.0 (API level 14)已經增加了, 應用也有可能在低於 Android 5.0 (API level 21)版本的機器上觸發這個限制。

Dex分包後,如果是啓動時同步加載,對應用的啓動速度會有一定的影響(主要表現爲白屏或黑屏,這個好像與Theme有關),但是主要影響的是安裝後首次啓動。這是因爲安裝後首次啓動時,Android系統會對加載的從dex做Dexopt並生成ODEX,而 Dexopt 是比較耗時的操作,所以對安裝後首次啓動速度影響較大。在非安裝後首次啓動時,應用只需加載 ODEX,這個過程速度很快,對啓動速度影響不大。同時,從dex 的大小也直接影響啓動速度,即從dex越小則啓動越快。

後面將繼續優化,可以考慮APP初次啓動時打開啓動圖,在這段時間內加載dex,驗證後將驗證結果貼上來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章