在android apk中如果项目引用的方法数超过64k(包括android框架方法、库方法、自己代码中的方法)的限制,就需要对apk进行dex分包处理。当第一次启动app的时候,系统会执行复杂的计算来确定主dex文件所需要的类,往往这里会花费很多时间。解决的方案就是通过mutildex.keep文件来告诉主dex文件所需要包含的类。这样app首次启动的时候,就不需要进行复杂的计算来确定主dex文件所需要包含的类了。需要做的步骤如下:
1. 在build.gradle同目录中新建multidex.keep文件
2. 在build.gradle文件中指明主dex文件该包含的class文件应该从multidex.keep文件获取
android.applicationVariants.all { variant ->
task "fix${variant.name.capitalize()}MainDexClassList" << {
logger.info "Fixing main dex keep file for $variant.name"
File keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")
keepFile.withWriterAppend { w ->
// Get a reader for the input file
w.append('\n')
new File("${projectDir}/multidex.keep").withReader { r ->
// And write data from the input into the output
w << r << '\n'
}
logger.info "Updated main dex keep file for ${keepFile.getAbsolutePath()}\n$keepFile.text"
}
}
}
tasks.whenTaskAdded { task ->
android.applicationVariants.all { variant ->
if (task.name == "create${variant.name.capitalize()}MainDexClassList") {
task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"
}
}
}
3.在multidex.keep文件中添加主dex文件所需的class文件。主dex文件所需的class文件可以通过以下方法获取。调用MultiDexUtils的getLoadedExternalDexClasses方法即可获取所需的class文件的list。
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import dalvik.system.DexFile;
public class MultiDexUtils {
private static final String EXTRACTED_NAME_EXT = ".classes";
private static final String EXTRACTED_SUFFIX = ".zip";
private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +
"secondary-dexes";
private static final String PREFS_FILE = "multidex.version";
private static final String KEY_DEX_NUMBER = "dex.number";
private SharedPreferences getMultiDexPreferences(Context context) {
return context.getSharedPreferences(PREFS_FILE,
Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
? Context.MODE_PRIVATE
: Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public List
getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException { final ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
final File sourceApk = new File(applicationInfo.sourceDir);
final File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
final List
sourcePaths = new ArrayList<>(); sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//the prefix of extracted file, ie: test.classes
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
//the total dex numbers
final int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
final String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
final File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" +
extractedFile.getPath() + "'");
}
}
return sourcePaths;
}
/**
* get all the external classes name in "classes2.dex", "classes3.dex" ....
*
* @param context the application context
* @return all the classes name in the external dex
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public List
getExternalDexClasses(Context context) throws PackageManager.NameNotFoundException, IOException { final List
paths = getSourcePaths(context); if(paths.size() <= 1) {
// no external dex
return null;
}
// the first element is the main dex, remove it.
paths.remove(0);
final List
classNames = new ArrayList<>(); for (String path : paths) {
try {
DexFile dexfile = null;
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
final Enumeration
dexEntries = dexfile.entries(); while (dexEntries.hasMoreElements()) {
classNames.add(dexEntries.nextElement());
}
} catch (IOException e) {
throw new IOException("Error at loading dex file '" +
path + "'");
}
}
return classNames;
}
/**
* Get all loaded external classes name in "classes2.dex", "classes3.dex" ....
* @param context
* @return get all loaded external classes
*/
public List
getLoadedExternalDexClasses(Context context) { try {
final List
externalDexClasses = getExternalDexClasses(context); if (externalDexClasses != null && !externalDexClasses.isEmpty()) {
final ArrayList
classList = new ArrayList<>(); final java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class});
m.setAccessible(true);
final ClassLoader cl = context.getClassLoader();
for (String clazz : externalDexClasses) {
if (m.invoke(cl, clazz) != null) {
classList.add(clazz.replaceAll("\\.", "/").replaceAll("$", ".class"));
}
}
return classList;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4.再把list数组写到手机sd卡的txt文件中,把txt文件的内容复制到multidex.keep即可。
MultiDexUtils dexUtils = new MultiDexUtils();
List
des = dexUtils.getLoadedExternalDexClasses(this); String sdCardDir = Environment.getExternalStorageDirectory().getAbsolutePath();
File saveFile = new File(sdCardDir, "aaaa.txt");
try {
FileOutputStream outStream = new FileOutputStream(saveFile);
outStream.write(listToString(des).getBytes());
outStream.close();
} catch (IOException e) {
e.printStackTrace();
}
public static String listToString(List
stringList){ if(stringList==null) {
return null;
}
StringBuilder result = new StringBuilder();
boolean flag=false;
for(String string : stringList) {
if(flag) {
result.append("\n");
}else{
flag=true;
}
result.append(string);
}
return result.toString();
}
经过我公司的app测试,可优化app首次启动从4秒左右减到1秒左右。优化还是挺明显的。