(废话)
最近发现了一个问题,一些平时博客写的不少的程序员,反倒在平常的工做中,倒是业务写的很通常,只会摆理论的人,甚至还跑出来教别人如何找工做,如何作架构,其实本身都没搞明白。可是受众的分层致使了输出者的分层,教出清北学生的老师并不必定来自比这更好的学校,所以,对于博客的输出,一个是做为对本身学习的一个记录,很是仔细的梳理能够很是方便的让咱们在须要的时候拿起来,再就是即便这个知识如今不用,没法深刻下去,可能会遗忘的比较快,其细节可能会忘记,可是核心思想咱们仍是有印象的,再就是站在读者的角度上来看,因为读者的差别性,咱们的博客在保证无误的前提下,必定是能够帮助到不少同窗的,本着这些原则,一周一篇的输出,但愿能够坚持下去。git
(正题)
最近在作热修复的相关调研,接着博客的专题,能够好好的发一波文章了,今天要分析的是ClassLoader方案最简单的一个Nvwa,本篇文章将会从class查找过程到Nvwa的实现,以及在实现的时候解决了什么问题,这几个方面展开,逐步讲解。程序员
初始化github
Nuwa.init(this);
装载补丁包数组
Nuwa.loadPatch(this,patchFile)
对于类的加载,在经过DexClassLoader进行加载的时候,经过DexPathList进行加载,其中维护了一个Element的数组,在查找的类的时候,会遍历数组查找类,若是找到则返回。对于数组遍历查找的代码以下所示。安全
public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; }
public static void init(Context context) { File dexDir = new File(context.getFilesDir(), DEX_DIR); dexDir.mkdir(); String dexPath = null; try { //拷贝Asset目录下的Hack.apk到指定路径 dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir); } catch (IOException e) { e.printStackTrace(); } //从拷贝后的指定路径加载apk loadPatch(context, dexPath); }
在nvwaw的init方法中进行的操做是将asset中的一个hack.apk拷贝出来,而后将其做为补丁进行装载。微信
public static void loadPatch(Context context, String dexPath) { if (context == null) { return; } if (!new File(dexPath).exists()) { return; } File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR); dexOptDir.mkdir(); try { DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath()); } catch (Exception e) { e.printStackTrace(); } }
由上面代码,能够看出核心的实如今对DexUtils
的injectDexAtFirst
调用上。架构
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader()); Object baseDexElements = getDexElements(getPathList(getPathClassLoader())); Object newDexElements = getDexElements(getPathList(dexClassLoader)); Object allDexElements = combineArray(newDexElements, baseDexElements); Object pathList = getPathList(getPathClassLoader()); ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements); }
将两个Dex进行合并,将补丁dex塞在数组的前面,而后经过反射的方式设置进去,经过这种方式,根据上面的类加载逻辑,能够知道,对于类的加载是从数组的最开始的位置进行查找加载的,当前面的dex查找到相应的类以后,就会中止后面的查找,这样,咱们经过补丁的替换的类就会生效。框架
private static Object combineArray(Object firstArray, Object secondArray) { Class<?> localClass = firstArray.getClass().getComponentType(); int firstArrayLength = Array.getLength(firstArray); int allLength = firstArrayLength + Array.getLength(secondArray); Object result = Array.newInstance(localClass, allLength); for (int k = 0; k < allLength; ++k) { if (k < firstArrayLength) { Array.set(result, k, Array.get(firstArray, k)); } else { Array.set(result, k, Array.get(secondArray, k - firstArrayLength)); } } return result; }
经过上述的方式,咱们将差别补丁单独打一个包,而后进行下发,从而使得咱们的类获得修复。可是在这样实现的时候,存在一个问题,就是
在Dalvik 虚拟机对于dex的一个优化。Dalvik 虚拟机在启动的时候,会有许多的启动参数,其中有一项就是verify,当verify被打开的时候,doVerify变量为true,则进行类的校验(dvmVerifyClass方法调用)。若校验成功,则这个类会被打上标记:CLASS_ISPREVERIFIED。源码分析
这么作,是防止外部DEX注入的一个安全方案,即保证运行期的Class与其直接引用类之间所在的DEX关系要与安装时候一致,也是为了防止类被篡改校验类的合法性。Dalvik 虚拟机在安装期间,为Class 打上 CLASS_ISPREVERIFIED 是为了提升性能,下次使用时,则会省去校验操做,提升访问效率。dvm在运行期载入Class时候,会对其内存中对应的直接引用类进行校验,若是该类存在与直接引用类所在的dex不是同一个,则直接报“pre-verification” 错误,该类没法加载。性能
因为这一个限制,致使咱们的补丁包没法在被调用到的时候,就会抛出异常,所以咱们须要让咱们的补丁包,如何经过此次校验, 不被打上CLASS_ISPREVERIFIED,这样,咱们的补丁包在被加载的时候,就不会抛出异常了。
nvwa采起的方式就是插桩的方式,在每个类里去引用到另外一个独立dex中的类,也会是在初始化的时候加载的hack.apk中的Hack.class,经过这种方式,可让咱们的类不会被打上这个标签。这样就能够继续装载其它Dex中的类。
插桩存在一个什么问题呢?因为没有打上验证标签,致使每一个类的装载的时候都进行验证。
微信在对插装和不插桩作的测试中。在连续加载700个50行的类,还有统计应用启动耗时获得的数据,700个类:不插桩:84ms,插桩:685ms。启动耗时:4934ms,7240ms。
每周一更,因为最近业务需求较多,更新速度明显慢了不少了,所以本篇分析的也是一很简单的框架。接下来,将会逐步深刻,分析一些更为复杂的热修复方案框架。