您的位置:首页 > 移动开发 > Android开发

Android热修复学习(二)

2016-06-23 12:16 357 查看

Multidex

在上一篇(一)中,我们知道应用加载class会用到PathClassLoader,最终会调用DexPathList的
findClass()
方法,通过轮询Element数组用DexFile来加载类。而每个Element又对应一个dex单元(文件),所以我们可以改变这个数组的大小和顺序做一些动态功能。前面我们看到这个数组的初始化是通过调用DexPathList的
makeDexElements()
来生成,如果我们拿到DexPathList的实例,则可以反射调用该方法来新增Element。

当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt。DexOpt 是在第一次加载
Dex 文件的时候执行的。这个过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多。

但是在早期的 Android 系统中,DexOpt 有两个问题。
(一):DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short 类型来保存的,导致了方法
id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。(二):Dexopt 使用 LinearAlloc 来存储应用
的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。
Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃。

尽管在新版本的 Android 系统中,DexOpt 修复了方法数65K的限制问题,并且扩大了 LinearAlloc 限制,但是我们仍然需要对低版本的Android
系统做兼容。


针对上诉的问题,google官方推出了multidex包做兼容。下面我们来看multidex包是怎么做的,它提供了三种接入方式,不过最终都是在Application的
attachBaseContext()
方法中调用MultiDex的
install()
方法。

public static void install(Context context) {
Log.i(TAG, "install");
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}

if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
}

try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
return;
}

synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);

……
……

ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
return;
}
if (loader == null) {
return;
}

……
……

File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);

if (checkValidZipFiles(files)) {
installSecondaryDexes(loader, dexDir, files);
} else {
throw new RuntimeException("Zip files were not valid.");
}
}
}

} catch (Exception e) {
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
}


其中IS_VM_MULTIDEX_CAPABLE是通过虚拟机的版本号来判断的。

private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
private static final int MIN_SDK_VERSION = 4;

static boolean isVMMultidexCapable(String versionString) {
boolean isMultidexCapable = false;
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
|| ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
&& (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
} catch (NumberFormatException e) {
// let isMultidexCapable be false
}
}
}
return isMultidexCapable;
}


可以看出当虚拟机的版本号大于等于2.1的时候,虚拟机本身已经支持多dex加载了,而主版本号2表示art,所以说基本上从5.0开始google已经解决了这个问题。而支持的最小版本MIN_SDK_VERSION为4,所以最小只支持到1.6。

接下来获取应用本身信息ApplicationInfo,通过同步处理和把包路径放入列表中防止多次处理。然后通过方法
MultiDexExtractor.load()
开始加载apk中的其它dex文件,并且每个都以zip存放的形式拷贝到指定的文件夹下。可以看到该文件夹路径为:

private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator +"secondary-dexes";

File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);


而applicationInfo.dataDir为应用本身的内部路径,dataDir为分配给此Application的存放数据的位置,通常是/data/data/packageName/。通过该路径我们可以通过DexClassLoader来加载,前面(一)中说过,DexClassLoader加载类保存优化后的文件路径必须是应用用户自己的。我们来看multidex又是怎么做的。

private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(loader, files, dexDir);
} else {
V4.install(loader, files);
}
}
}

private static final class V19 {

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {

Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(loader, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(loader);

if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions =
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(loader, dexElementsSuppressedExceptions);
}
}

private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);

return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}


从上面可以看出multidex会根据不同的版本来做不同的处理,以V19为例,可以看出是通过获取到当前应用的classloader,即为PathClassLoader,然后通过反射获取到他的DexPathList属性对象pathList,然后继续通过反射调用pathList的
makeDexElements()
方法把之前拷贝的其它dex的zip文件转化为Element[],然后合并到pathList现有的数组中去,这样就完成了其它dex的动态加载。V14和V19类似,而 V4中没有Element类型,则直接采用DexFile的
loadloadDex(String sourcePathName, String outputPathName,int flags)
方法来把zip文件转化为DexFile[],从(一)中我们可以知道DexClassLoader最终也会走到该方法,而且第二个参数为我们上面说的dataDir路径下,为应用本身的路径。

小结:

1.虽然multidex可以很方便的解决64k的问题,不过可以看出它是在应用启动时主线程来加载的,而且第一次加载dex文件,会触发dexopt,首次启动可能会造成应用启动缓慢。所以可能需要考虑异步加载和按需动态加载。

2.multidex拆分dex为自己分析的过程,从参考文章看出可能会出现启动必要的类没有打包进主dex文件的情况,所以可能需要自己干预拆分dex的过程(通过–multi-dex、–main-dex-list=、–minimal-main-dex、–set-max-idx-number等参数)

参考文章:

https://segmentfault.com/a/1190000004053072
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android hotfix