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
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories