Android Dex 分包+热修复(QQ空间技术方案)
感谢博主的博客:
博主将主要类打成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} 需要添加进apk
aapt 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,验证后将验证结果贴上来。
|