android 5.0以下系统首次启动黑屏问题
最近在android 机顶盒上发现app首次运行的时候启动黑屏,看日志发现是 MultiDex.install(this)在4.4系统上首次安装时合并dex和进行dexOpt操作导致。整个操作大概耗时15s,下面通过源码分析下原因。
首先在4.4的系统上,安装app后系统做了点什么事呢?
1更新PMS的版本信息,以方便系统对app进行管理。
2 在app对应的目录下面创建目录以及将dex文件和资源文件复制到相应的位置。通过adb shell我们看下。
在data/data/package/目录下面创建了lib,将app中的so导入到这里。
将app放在到/data/app目录下面。
将app的主dex放置到/data/dalvik-cache目录下面。
安装之后在首次点击的时候发生了什么?app启动的时候会首先加载系统的主题界面,之后执行application 的oncreate()方法, 这个时候是没有界面显示,application 的oncreate()方法执行接收后才会显示splash界面。如果application 的oncreate()方法执行时间较长,就会出现黑屏。
首先会执行attachBaseContext,这个方法早于application 的oncreate()方法执行,那么看下这个方法做了点什么。
这里我将MultiDex方法复制后重新命名使用,这样做的好处是可以自由增加日志。
public static void install(Context context) {
Log.i("MultiDex", "Installing application");
//如果是5.0以上的系统,会走这里,也就是只打印日志就返回
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
} else if (Build.VERSION.SDK_INT < 4) {//版本较低,基本不考虑了
throw new RuntimeException("MultiDex installation failed. SDK " + Build.VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
} else {
try {
//获取app的信息
ApplicationInfo applicationInfo = getApplicationInfo(context);
Log.d("MultiDex","applicationInfo = " + applicationInfo.toString());
if (applicationInfo == null) {
Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
return;
}
//开始合并dex,new File(applicationInfo.sourceDir) = /data/app/xxx.apk, new File(applicationInfo.dataDir), "secondary-dexes", "", true) = /data/data/xxx/code_cache/secondary-dexes
doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
} catch (Exception var2) {
Log.e("MultiDex", "MultiDex installation failure", var2);
throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
}
Log.i("MultiDex", "install done");
}
}
private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
synchronized(installedApk) {
if (!installedApk.contains(sourceApk)) {
installedApk.add(sourceApk);
if (Build.VERSION.SDK_INT > 20) {
Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + Build.VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
}
ClassLoader loader;
try {
//获取app的classLoader
loader = mainContext.getClassLoader();
} catch (RuntimeException var25) {
Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
return;
}
if (loader == null) {
Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
} else {
try {
//清除/data/data/xxx/files/secondary-dexes下的dex缓存
clearOldDexDir(mainContext);
} catch (Throwable var24) {
Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
}
///data/data/xxx/code_cache/secondary-dexes
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
Log.d("MultiDex", "dexDir = " + dexDir.toString());
//从/data/app/xxx.apk中提取dex到/data/data/xxx/code_cache/secondary-dexes
MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
IOException closeException = null;
try {
//先看提取结果, loadFiles = [/data/data/xxx/code_cache/secondary-dexes/xxx-1.apk.classes2.zip, /data/data/xxx/code_cache/secondary-dexes/xxx-1.apk.classes3.zip]
List files = extractor.load(mainContext, prefsKeyPrefix, false);
Log.d("MultiDex","loadFiles = " + files);
try {
//装配提取的dex路径
installSecondaryDexes(loader, dexDir, files);
} catch (IOException var26) {
if (!reinstallOnPatchRecoverableException) {
throw var26;
}
Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
files = extractor.load(mainContext, prefsKeyPrefix, true);
installSecondaryDexes(loader, dexDir, files);
}
} finally {
try {
extractor.close();
} catch (IOException var23) {
closeException = var23;
}
}
if (closeException != null) {
throw closeException;
}
}
}
}
}
List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
Log.i("MultiDex", "MultiDexExtractor.load(" + this.sourceApk.getPath() + ", " + forceReload + ", " + prefsKeyPrefix + ")");
if (!this.cacheLock.isValid()) {
throw new IllegalStateException("MultiDexExtractor was closed");
} else {
List files;
//校验dex文件是否有更改,没有更改的话就直接loadExistingExtractions,因此,第二次点击app的时候就直接走的这里,不需要重新提取dex以及dexopt
if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
try {
files = this.loadExistingExtractions(context, prefsKeyPrefix);
} catch (IOException var6) {
Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
files = this.performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
} else {
if (forceReload) {
Log.i("MultiDex", "Forced extraction must be performed.");
} else {
Log.i("MultiDex", "Detected that extraction must be performed.");
}
//从/data/app/xxx.apk中提取dex到/data/data/xxx/code_cache/secondary-dexes
files = this.performExtractions();
putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
}
Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
return files;
}
}
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
//4.4系统走这里
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(loader, files);
} else {
V4.install(loader, files);
}
}
}
static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList();
Log.d("MultiDex","installV9 and optimizedDirectory = " + optimizedDirectory.toString());
//替换classLoader里面的pathList为dex路径,路径是[/data/data/xxx/code_cache/secondary-dexes/xxx-1.apk.classes2.zip, /data/data/xxx/code_cache/secondary-dexes/xxx-1.apk.classes3.zip]
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
Log.d("MultiDex","makeDexElements end");
if (suppressedExceptions.size() > 0) {
Iterator var6 = suppressedExceptions.iterator();
while(var6.hasNext()) {
IOException e = (IOException)var6.next();
Log.w("MultiDex", "Exception in makeDexElement", e);
}
Field suppressedExceptionsField = findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
IOException exception = new IOException("I/O exception during makeDexElement");
exception.initCause((Throwable)suppressedExceptions.get(0));
throw exception;
}
}
private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Log.d("MultiDex","makeDexElements");
if(optimizedDirectory.isDirectory()){
File[] files1= optimizedDirectory.listFiles();
for(File file : files1){
Log.d("MultiDex",file.toString());
}
}
Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
//这个方法调用会会出发dexopt操作,在/data/data/xxx/code_cache/secondary-dexes/下面生成xxx-1.apk.classes2.dex和xxx-1.apk.classes3.dex
Object[] objs = (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
if(optimizedDirectory.isDirectory()){
File[] files2= optimizedDirectory.listFiles();
for(File file : files2){
Log.d("MultiDex",file.toString());
}
}
return objs;
}
总结下结论,在 MultiDex.install调用后,会检测/data/data/xxx/code_cache/secondary-dexes/下面有没有dex文件并且dex文件没有被修改,就直接通过反射将/data/data/xxx/code_cache/secondary-dexes/下面的dex路径添加到classLoader里面的pathList。
如果发现/data/data/xxx/code_cache/secondary-dexes/下面没有dex文件或者dex文件被修改了,则从
/data/app/xxx.apk提取dex文件到/data/data/xxx/code_cache/secondary-dexes,通过反射将/data/data/xxx/code_cache/secondary-dexes/下面的dex路径添加到classLoader里面的pathList。
因此,首次的时候会黑屏,第二次点击的时候就不会。
解决方法:可以参考https://juejin.im/post/5d95f4a4f265da5b8f10714b#heading-10。
这里面遇到一个坑,就是在application oncreate里面执行ARouter.init()的时候会触发dexOpt操作,因此,最好在子线程里面执行。