Android Dex 分包+熱修復(QQ空間技術方案)
感謝博主的博客:
主要代碼:上面博主的github: MultiDex Demo
如果用的是AS,可以參考 multidex-sample,Android的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,驗證後將驗證結果貼上來。