当一个App发布以后,忽然发现了一个严重bug须要进行紧急修复,这时候公司各方就会忙得焦头烂额:从新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和从新发布。
这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,再也不须要从新发布App,再也不须要用户从新下载,覆盖安装?
虽然Android系统并无提供这个技术,可是很幸运的告诉你们,答案是:能够,咱们QQ空间提出了热补丁动态修复技术来解决以上这些问题。html
空间Android独立版5.2发布后,收到用户反馈,结合版没法跳转到独立版的访客界面,天天都较大的反馈。在之前只能紧急换包,从新发布。成本很是高,也影响用户的口碑。最终决定使用热补丁动态修复技术,向用户下发Patch,在用户无感知的状况下,修复了外网问题,取得很是好的效果。java
该方案基于的是android dex分包方案的,关于dex分包方案,网上有几篇解释了,因此这里就再也不赘述,具体能够看这里
简单的归纳一下,就是把多个dex文件塞入到app的classloader之中,可是android dex拆包方案中的类是没有重复的,若是classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪一个类进行加载呢?
让咱们来看看类加载的代码:
一个ClassLoader能够包含多个dex文件,每一个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,而后从当前遍历的dex文件中找类,若是找类则返回,若是找不到从下一个dex文件继续查找。
理论上,若是在不一样的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,以下图:
在此基础上,咱们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,而后把这个dex插入到Elements的最前面,以下图:
好,该方案基于第二个拆分dex的方案,方案实现若是懂拆分dex的原理的话,你们应该很快就会实现该方案,若是没有拆分dex的项目的话,能够参考一下谷歌的multidex方案实现。而后在插入数组的时候,把补丁包插入到最前面去。
好,看似问题很简单,轻松的搞定了,让咱们来试验一下,修改某个类,而后打包成dex,插入到classloader,当加载类的时候出现了(本例中是QzoneActivityManager要被替换):
为何会出现以上问题呢?
从log的意思上来说,ModuleManager引用了QzoneActivityManager,可是发现这这两个类所在的dex不在一块儿,其中:android
让咱们搜索一下抛出错误的代码所在,嘿咻嘿咻,找到了一下代码:
从代码上来看,若是两个相关联的类在不一样的dex中就会报错,可是拆分dex没有报错这是为何,原来这个校验的前提是:api
若是引用者(也就是ModuleManager)这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。那么这个标志是何时被打上去的?让咱们在继续搜索一下代码,嘿咻嘿咻~,在DexPrepare.cpp找到了一下代码:
这段代码是dex转化成odex(dexopt)的代码中的一段,咱们知道当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,而后才会拿去执行。
虚拟机在启动的时候,会有许多的启动参数,其中一项就是verify选项,当verify选项被打开的时候,上面doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验,若是dvmVerifyClass校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志,那么具体的校验过程是什么样子的呢?
此代码在DexVerify.cpp中,以下:数组
归纳一下就是若是以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED:
因此为了实现补丁方案,因此必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。
最终空间的方案是往全部类的构造函数里面插入了一段代码,代码以下:if (ClassVerifier.PREVENT_VERIFY) { System.out.println(AntilazyLoad.class); }
缓存
其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类均可以进行打补丁操做。
而后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,否则AntilazyLoad类会被标记为不存在,即便后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。
因此Application做为应用的入口不能插入这段代码。(由于载入hack.dex的代码是在Application中onCreate中执行的,若是在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载以前就使用该类,该类一次找不到,会被永远的打上找不到的标志)
其中:
之因此选择构造函数是由于他不增长方法数,一个类即便没有显式的构造函数,也会有一个隐式的默认构造函数。
空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。
隐患:
虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提升性能的,咱们强制防止类被打上标志是否会影响性能?这里咱们会作一下更加详细的性能测试.可是在大项目中拆分dex的问题已经比较严重,不少类都没有被打上这个标志。
如何打包补丁包:安全
App的上线发布是咱们程序猿开心的事情,证实着一段时间来成果的进步和展示。可是随着App的上线手机App市场,接下来的更新维护工做便成了”屡见不鲜“。尤为是在创业公司,随着业务等不稳定性因素,前期App的更新工做更为频繁,可能两天一小改,三天一大改的状况常常发生。微信
那么应对版本更新的同时,须要咱们不断将新版本上线,并下发到用户,此时两个典型的问题发生了:cookie
(1)发版的周期过长app
(2)用户的App版本更新进度缓慢
因此,在传统App的开发模式下,须要一种手段来改变当前存在的问题。若是存在一种方案能够在不发版的前提下也能够修复线上App的Bug,那么以上两个问题就都得以解决。此时一系列的第三方库扑面而来,阿里的AndFix、腾讯的Qzone修复、以及近期开源的微信Tinker应运而生。
关于各类热更新库的使用,网上已经有不少的博文来介绍。本篇博客着重和你们分享一下关于QQ空间热更新的原理解析。
关于原理的分析,大体分为以下模块:
(1)热修复机制的产生
(2)Android类加载机制
一、热修复机制的产生
随着App业务不断叠加,以及第三方库的多种依赖,相信不少人某天运行程序忽然出现以下异常:
1 java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536 2 at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501) 3 at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:282) 4 at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490) 5 at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167) 6 at com.android.dx.merge.DexMerger.merge(DexMerger.java:188) 7 at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439) 8 at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287) 9 at com.android.dx.command.dexer.Main.run(Main.java:230) 10 at com.android.dx.command.dexer.Main.main(Main.java:199) 11 at com.android.dx.command.Main.main(Main.java:103)
从异常信息中,咱们不难发现:method ID not in 65536。而且大体能够看出是在dex层跑出的异常。什么意思呢?
咱们编写的Java业务代码为.java类型文件,当咱们编译运行一个完整的App项目时,系统会执行以下流程:
.Java --> .class --> dex --> (odex ) --> Apk
当class文件被打包成一个dex文件时,因为dex文件的限制,方法的ID为short型,因此一个dex文件存放的方法数最多为65536。超过了该数,系统就会抛出上面所述的异常。为了解决这个问题,Google为咱们提供了multidex解决方案,即把dex文件分为主、副两类。系统只加载第一个dex文件(主dex),剩余的dex文件(副dex)在Application中的onCreate方法中,以资源的方式被加载到系统的ClassLoader。能够理解为:一个APK能够包含多个dex文件。这样就解决了65536问题的同时。也为热修复提供了实现方案:将修复后的dex文件下发到客户端(App),客户端在启动后就会加载最新的dex文件。
关于如何实现加载最新dex文件,咱们还须要了解下Android Davilk虚拟机的类加载流程。
二、Android类加载机制
Android虚拟机对于类的加载机制为:同一个类只会加载一次。因此要实现热修复的前提就是:让下发到客户端的补丁包类要比以前存在bug的类优先加载。相似于一种“替换”的解决。如何实现优先加载呢?咱们先来了解下Davilk虚拟机的类加载方式。
Java虚拟机JVM的类加载是:ClassLoader。一样Android系统提供了两种类加载方式:
(1)DexClassLoader
(2)PathClassLoader
首先从源码中深刻:
libcore/dalvik/src/main/java/dalvik/system/
(1)DexClassLoader源码:
package dalvik.system; 19import java.io.File; 36public class DexClassLoader extends BaseDexClassLoader { 37 /** 38 * Creates a {@code DexClassLoader} that finds interpreted and native 39 * code. Interpreted classes are found in a set of DEX files contained 40 * in Jar or APK files. 41 * 42 * <p>The path lists are separated using the character specified by the 43 * {@code path.separator} system property, which defaults to {@code :}. 44 * 45 * @param dexPath the list of jar/apk files containing classes and 46 * resources, delimited by {@code File.pathSeparator}, which 47 * defaults to {@code ":"} on Android 48 * @param optimizedDirectory directory where optimized dex files 49 * should be written; must not be {@code null} 50 * @param libraryPath the list of directories containing native 51 * libraries, delimited by {@code File.pathSeparator}; may be 52 * {@code null} 53 * @param parent the parent class loader 54 */ 55 public DexClassLoader(String dexPath, String optimizedDirectory, 56 String libraryPath, ClassLoader parent) { 57 super(dexPath, new File(optimizedDirectory), libraryPath, parent); 58 } 59}
(2)PathClassLoader:
25 public class PathClassLoader extends BaseDexClassLoader { 26 /** 27 * Creates a {@code PathClassLoader} that operates on a given list of files 28 * and directories. This method is equivalent to calling 29 * {@link #PathClassLoader(String, String, ClassLoader)} with a 30 * {@code null} value for the second argument (see description there). 31 * 32 * @param dexPath the list of jar/apk files containing classes and 33 * resources, delimited by {@code File.pathSeparator}, which 34 * defaults to {@code ":"} on Android 35 * @param parent the parent class loader 36 */ 37 public PathClassLoader(String dexPath, ClassLoader parent) { 38 super(dexPath, null, null, parent); 39 } 40 41 /** 42 * Creates a {@code PathClassLoader} that operates on two given 43 * lists of files and directories. The entries of the first list 44 * should be one of the following: 45 * 46 * <ul> 47 * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as 48 * well as arbitrary resources. 49 * <li>Raw ".dex" files (not inside a zip file). 50 * </ul> 51 * 52 * The entries of the second list should be directories containing 53 * native library files. 54 * 55 * @param dexPath the list of jar/apk files containing classes and 56 * resources, delimited by {@code File.pathSeparator}, which 57 * defaults to {@code ":"} on Android 58 * @param libraryPath the list of directories containing native 59 * libraries, delimited by {@code File.pathSeparator}; may be 60 * {@code null} 61 * @param parent the parent class loader 62 */ 63 public PathClassLoader(String dexPath, String libraryPath, 64 ClassLoader parent) { 65 super(dexPath, null, libraryPath, parent); 66 } 67}
从源码能够看出,DexClassLoader和PathClassLoaderr继承自BaseDexClassLoader。
(1)PathClassLoader能够操做本地文件系统的文件列表或目录中的classes。PathClassLoader负责加载系统类和主Dex中的类。
(2)DexClassLoader是一个能够从包含classes.dex实体的.jar或.apk文件中加载classes的类加载器。DexClassLoader负责加载其余dex文件(副dex)中的类。
既然是类加载器,必然存在类加载方法,继续查看源码,能够发现BaseDexClassLoader提供了findClass方法用于加载类:
(1)BaseDexClassLoader源码:
17 package dalvik.system; 18 19 import java.io.File; 20 import java.net.URL; 21 import java.util.ArrayList; 22 import java.util.Enumeration; 23 import java.util.List; 29 public class BaseDexClassLoader extends ClassLoader { 30 private final DexPathList pathList; 45 public BaseDexClassLoader(String dexPath, File optimizedDirectory, 46 String libraryPath, ClassLoader parent) { 47 super(parent); 48 this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); 49 } 50 51 @Override 52 protected Class<?> findClass(String name) throws ClassNotFoundException { 53 List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); 54 Class c = pathList.findClass(name, suppressedExceptions); 55 if (c == null) { 56 ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); 57 for (Throwable t : suppressedExceptions) { 58 cnfe.addSuppressed(t); 59 } 60 throw cnfe; 61 } 62 return c; 63 }
在findClass方法中,又调用了DexPathList对象的findClass方法,DexPathList源码以下:
17 package dalvik.system; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.net.MalformedURLException; 22 import java.net.URL; 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.Collections; 26 import java.util.Enumeration; 27 import java.util.List; 28 import java.util.zip.ZipFile; 29 import libcore.io.ErrnoException; 30 import libcore.io.IoUtils; 31 import libcore.io.Libcore; 32 import libcore.io.StructStat; 33 import static libcore.io.OsConstants.*; 48/*package*/ final class DexPathList { 49 private static final String DEX_SUFFIX = ".dex"; 50 private static final String JAR_SUFFIX = ".jar"; 51 private static final String ZIP_SUFFIX = ".zip"; 52 private static final String APK_SUFFIX = ".apk"; 53 54 /** class definition context */ 55 private final ClassLoader definingContext; 56 57 /** 58 * List of dex/resource (class path) elements. 59 * Should be called pathElements, but the Facebook app uses reflection 60 * to modify 'dexElements' (http://b/7726934). 61 */ 62 private final Element[] dexElements; 63 64 /** List of native library directories. */ 65 private final File[] nativeLibraryDirectories; 305 /** 306 * Finds the named class in one of the dex files pointed at by 307 * this instance. This will find the one in the earliest listed 308 * path element. If the class is found but has not yet been 309 * defined, then this method will define it in the defining 310 * context that this instance was constructed with. 311 * 312 * @param name of class to find 313 * @param suppressed exceptions encountered whilst finding the class 314 * @return the named class or {@code null} if the class is not 315 * found in any of the dex files 316 */ 317 public Class findClass(String name, List<Throwable> suppressed) { 318 for (Element element : dexElements) { 319 DexFile dex = element.dexFile; 320 321 if (dex != null) { 322 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); 323 if (clazz != null) { 324 return clazz; 325 } 326 } 327 } 328 if (dexElementsSuppressedExceptions != null) { 329 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); 330 } 331 return null; 332 }
package dalvik.system; 19 import java.io.File; 20 import java.io.FileNotFoundException; 21 import java.io.IOException; 22 import java.util.ArrayList; 23 import java.util.Enumeration; 24 import java.util.List; 25 import libcore.io.ErrnoException; 26 import libcore.io.Libcore; 27 import libcore.io.StructStat; 28 29/** 30 * Manipulates DEX files. The class is similar in principle to 31 * {@link java.util.zip.ZipFile}. It is used primarily by class loaders. 32 * <p> 33 * Note we don't directly open and read the DEX file here. They're memory-mapped 34 * read-only by the VM. 35 */ 36 public final class DexFile { 37 private int mCookie; 38 private final String mFileName; 39 private final CloseGuard guard = CloseGuard.get(); 207 /** 208 * See {@link #loadClass(String, ClassLoader)}. 209 * 210 * This takes a "binary" class name to better match ClassLoader semantics. 211 * 212 * @hide 213 */ 214 public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { 215 return defineClass(name, loader, mCookie, suppressed); 216 } 218 private static Class defineClass(String name, ClassLoader loader, int cookie, 219 List<Throwable> suppressed) { 220 Class result = null; 221 try { 222 result = defineClassNative(name, loader, cookie); 223 } catch (NoClassDefFoundError e) { 224 if (suppressed != null) { 225 suppressed.add(e); 226 } 227 } catch (ClassNotFoundException e) { 228 if (suppressed != null) { 229 suppressed.add(e); 230 } 231 } 232 return result;
DexFile类中,loadClassBinaryName方法中调用了defineClass方法,该方法直接经过defineClassNative执行Android原生层代码...到此为止,整个加载流程就走完了。
因此,要实现热修复的就必需要在DexPathList中遍历Element元素时,让补丁dex在Element数组中的为止优先于原有已存在的dex。这样,当系统遍历dexElement时,就能够加载最新补丁dex,实现dex的 “替换”。1 /** 2 * Created by Song on 2017/5/15. 3 */ 4 public class Cal { 5 public float calculate() { 6 // 很明显,会有算数异常抛出 7 return 1 / 0; 8 } 9 }
1 public class MainActivity extends AppCompatActivity { 2 3 private Cal cl; 4 @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 setContentView(R.layout.activity_main); 8 cl = new Cal(); 9 } 10 11 /** 12 * 点击按钮测试 13 * @param view 14 */ 15 public void cal(View view) { 16 cl.calculate(); 17 }
1 /** 2 * Created by Song on 2017/5/15. 3 */ 4 public class Cal { 5 public float calculate() { 6 // 修改后的,不存在任何问题 7 return 1 / 1; 8 } 9 }
补丁包实际上是一个dex文件。dex文件的造成过程为:.class --> jar --> dex。因此,先要将class文件打包为jar。
从新编译后的class文件在app / build / intermediates / classes / debug / 包名 / ...1 /** 2 * Created by Song on 2017/5/15. 3 */ 4 public class MainApplication extends Application { 5 6 @Override 7 public void onCreate() { 8 super.onCreate(); 9 // 获取补丁 执行注入 10 11 String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar"); 12 File file = new File(dexPath); 13 if (file.exists()) { 14 Log.e("-----","开始................."); 15 inject(dexPath); 16 } 17 } 18 19 /** 20 * 要注入的dex的路径 21 */ 22 private void inject(String path) { 23 try { 24 // 获取classes的dexElements 25 Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader"); 26 Object pathList = getField(cl, "pathList", getClassLoader()); 27 Object baseElements = getField(pathList.getClass(), "dexElements", pathList); 28 // 获取patch_dex的dexElements(须要先加载dex) 29 String dexopt = getDir("dexopt", 0).getAbsolutePath(); 30 DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader()); 31 Object obj = getField(cl, "pathList", dexClassLoader); 32 Object dexElements = getField(obj.getClass(), "dexElements", obj); 33 // 合并两个Elements 34 Object combineElements = combineArray(dexElements, baseElements); 35 // 将合并后的Element数组从新赋值给app的classLoader 36 setField(pathList.getClass(), "dexElements", pathList, combineElements); 37 } catch (ClassNotFoundException e) { 38 e.printStackTrace(); 39 } catch (IllegalAccessException e) { 40 e.printStackTrace(); 41 } catch (NoSuchFieldException e) { 42 e.printStackTrace(); 43 } 44 } 45 46 /** 47 * 经过反射获取对象的属性值 48 */ 49 private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException { 50 Field field = cl.getDeclaredField(fieldName); 51 field.setAccessible(true); 52 return field.get(object); 53 } 54 55 /** 56 * 经过反射设置对象的属性值 57 */ 58 private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException { 59 Field field = cl.getDeclaredField(fieldName); 60 field.setAccessible(true); 61 field.set(object, value); 62 } 63 64 /** 65 * 经过反射合并两个数组 66 */ 67 private Object combineArray(Object firstArr, Object secondArr) { 68 int firstLength = Array.getLength(firstArr); 69 int secondLength = Array.getLength(secondArr); 70 int length = firstLength + secondLength; 71 72 Class<?> componentType = firstArr.getClass().getComponentType(); 73 Object newArr = Array.newInstance(componentType, length); 74 for (int i = 0; i < length; i++) { 75 if (i < firstLength) { 76 Array.set(newArr, i, Array.get(firstArr, i)); 77 } else { 78 Array.set(newArr, i, Array.get(secondArr, i - firstLength)); 79 } 80 } 81 return newArr; 82 } 83 }
分析:
3、更详细介绍:
Qzone 超级补丁技术基于dex分包方案,使用了多dex加载(multidex)的原理,大体的过程就是:把BUG方法修复之后,放到一个单独的dex文件,而后插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。
该方案的灵感来源?
没错就是类加载机制,相信大部分同窗都对它有所了解吧。
Android应用程序本质上使用的是java开发,使用标准的java编译器编译出Class文件,和普通的java开发不一样的地方是把class文件再从新打包成dex类型的文件,这种从新打包会对Class文件内部的各类函数表、变量表等进行优化,最终产生了odex文件。odex文件是一种通过android打包工具优化后的Class文件,所以加载这样特殊的Class文件就须要特殊的类装载器,因此android中提供了DexClassLoader类。
类图:
Android使用的是Dalvik虚拟机装载class文件,因此classloader不一样于java默认类库rt.jar包中java.lang.ClassLoader, 能够看到android中的classloader作了些修改,可是原理仍是差很少的。
学过java的同窗都知道, 类加载器是采用双亲委派机制来进行类加载的。
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成此加载任务时,才本身去加载。
双亲委派机制 从ClassLoader.java 源代码能够清晰的看出来:
ClassLoader.java
流程大概以下:
1.判断类是否已经加载过;
2.父类加载器优先加载;
3.parent为null,则调用BootstrapClassLoader进行加载 ;
4.若是class依旧没有找到,则调用当前类加载器的findClass方法进行加载;
BaseDexClassLoader.java
DexPathList.java
DexFile.java(\dalvik\dx\src\com\android\dx\dex\file\DexFile.java)
defineClassNative(android4.4版本,区分ART 和Dalvik两种状况)
1.ART 环境 [art\runtime\native\dalvik_system_DexFile.cc]
2.Dalvik 环境 [\dalvik\vm\native\dalvik_system_DexFile.cpp ]
(注:dvmDefineClass函数则是类加载机制中最为核心的逻辑,因为和本文深刻探索的方向关联性不强,就不做深究了。源码在 dalvik2/vm/oo/Class.cpp中,有兴趣可自行研究。)
从以上类加载机制的源码中咱们能够分析出,当找类的时候,会按顺序遍历dex文件,而后从当前遍历的dex文件中找类,若是找类则返回,若是找不到继续从下一个dex文件查找。理论上,若是在不一样的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,Qzone方案的灵感就是从上述的DexPathList类中的for循环体而来。
在此基础上,Qzone 团队构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,而后把这个dex插入到Elements的最前面,以下图:
若是懂拆分dex的原理的话,你们应该很快就会实现该方案。若是没有拆分dex的项目的话,能够参考一下谷歌的multidex方案实现,而后在插入数组的时候,把补丁包插入到最前面去。
当patch.dex中包含Main.class时就会优先加载,在后续的DEX中遇到Main.class的话就会直接返回而不去加载,这样就达到了修复的目的。看似问题很简单,轻松的搞定了,Qzone一开始按照以上思路进行了实践,但在实际操做中,出现了 unexpected DEX 的异常。这个问题是由于在Dalvik环境中,类被打上CLASS_ISPREVERIFIED的标志,主动抛出异常报错。
为何系统要给类打上CLASS_ISPREVERIFIED的标志?
咱们知道,在APK安装时,虚拟机须要将classes.dex优化成odex文件,而后才会执行。在这个过程当中,会进行类的verify操做,为了提升运行性能,若是调用关系的类都在同一个DEX中的话,就会被打上CLASS_ISPREVERIFIED的标志,而后写入odex文件,代表它没有被其余Dex的类引用。
规避 Dalvik 下 “unexpected DEX” 的异常。
以下是 dalvik 的一段源码,当补丁安装后,首次使用到补丁里的类时会调用到这里, 源代码以下:
[dalvik/vm/oo/Resolve.cpp]
从代码逻辑咱们能够看出,须要同时知足代码中标出来的三个条件,才会出现异常,这三个条件的含义以下:
所以,想要避免补丁类加载时发生 “unexpected DEX ” 的异常,则须要从以上三个地方来入手。
Qzone 的超级补丁方案采用的是经过绕过这里的第二个判断来避免报错的。若是一个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。而要避免报错,首先得弄清楚它是什么条件下才会被打上。继续搜索源代码,发如今DexPrepare.cpp找到了以下代码:
[dalvik\vm\analysis\DexPrepare.cpp]
这段代码是dex转化成odex(dexopt)的代码中的一段,咱们知道当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,而后才会拿去执行。
虚拟机在启动的时候,会有许多的启动参数,其中一项就是verify选项,当verify选项被打开的时候,上面doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验,若是dvmVerifyClass函数校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志。
DexClassDef 结构体代码:
struct DexClassDef {
u4 classIdx; //类的类型, DexTypeId中的索引下标
u4 accessFlags; //类的访问标志
u4 superclassIdx; //父类类型, DexTypeId中的索引下标
u4 interfacesOff; //接口偏移, 指向DexTypeList的结构
u4 sourceFileIdx; //源文件名, DexStringId中的索引下标
u4 annotationsOff; //注解偏移, 指向DexAnnotationsDirectoryItem的结构
u4 classDataOff; //类数据偏移, 指向DexClassData的结构
u4 staticValuesOff; //类静态数据偏移, 指向DexEncodedArray的结构
};
而具体的校验过程,即dvmVerifyClass函数是什么样子的呢?咱们继续往下探索。
代码在DexVerify.cpp中,以下:
[dalvik\vm\analysis\DexVerify.cpp]
该方法作了三件事情:
1. 是否已被校验过?
2. 验证clazz->directMethods方法,directMethods包含了如下方法:
● static方法
● private方法
● 构造方法
3. clazz->virtualMethods。虚函数=override方法?
归纳一下就是,只要在static方法,private方法,构造方法,override方法中直接引用了其余dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。也就是说若是以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED。
搞清了前因后果,因此就能够从这些地方入手。最终Qzone的方案是往全部补丁类的构造函数里面插入了一段代码,来引用另一个dex的类,防止类被打上CLASS_ISPREVERIFIED标志。代码以下:
if (ClassVerifier.PREVENT_VERIFY) { System.out.println(AntilazyLoad.class); }
打补丁包:
1.在正式版本发布的时候,会生成一份缓存文件,里面记录了全部class文件的md5,还有一份mapping混淆文件。
2. 在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,而后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。
Hook及加载patch操做:
1. 打包过程当中,会往全部补丁类的构造函数里面插一段代码。
2. 其中AntilazyLoad类会被打包成单独的hack.dex,当安装apk的时候,patch.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志,只要没被打上这个标志的类均可以进行打补丁操做。
3. 先加载进来AntilazyLoad类,否则AntilazyLoad类会被标记为不存在,即便后续加载了hack.dex包也于事无补。
4. 获取到当前应用的Classloader,即为BaseDexClassloader。
5. 经过反射获取到它的DexPathList属性对象pathList。
6. 经过反射调用pathList的dexElements方法把补丁包patch.dex转化为Element[]。
7. 两个Element[]进行合并,把patch.dex放到最前面去。
8. 加载Element[],达到修复目的。
该方案之因此选择构造函数进行插入代码,是由于它不增长方法数,一个类即便没有显式的构造函数,也会有一个隐式的默认构造函数。
细节:Qzone使用的是在字节码插入代码,而不是源代码插入,使用的是java assist库来进行字节码插入的。
互动问题:思考一下,除了经过防止补丁类被打上CLASS_ISPREVERIFIED标志,咱们还能够想到有哪些方式来解决Dalvik下的 “unexpected DEX” 异常问题?
优点:
1.没有合成整包(和微信Tinker比起来),输出产物比较小,比较灵活。
2.能够实现类替换,兼容性较高。(某些三星手机不起做用)
不足:
1.不支持即时生效,必须经过重启才能生效。
2.为了实现修复这个过程,必须在应用中加入两个dex, dalvikhack.dex中只有一个类,对性能影响不大,可是对于patch.dex来讲,修复的类到了必定数量,就须要花很多的时间加载。对大型应用来讲,启动耗时增长2s以上是很难接受的事。
3.在ART模式下,若是类修改告终构,就会出现内存错乱的问题。为了解决这个问题,就必须把全部相关的调用类、父类子类等等所有加载到patch.dex中,这会致使ART下的补丁包异常的大,进一步增长应用启动加载的时候,耗时更加严重。
其中第二点不足,即性能没法提高的缘由:
插桩的解决方案会影响到运行时性能的缘由在于:app 内的全部补丁类都预埋引用一个独立 dex 的空类,致使安装 dexopt 阶段的 preverify 失败,运行时将再次 verify+optimize。
另外即便后期发布版本实际上无需发布补丁,咱们也须要预埋插桩的逻辑,这自己也是不合理的一点,因此确实有必要去探索新的方向,既保留补丁的能力,同时去掉插桩带来的负面影响。
第三点不足,即ART模式下补丁包异常大的缘由:
ART(Android Runtime)是Android在4.4版本中引入的新虚拟机环境,在5.0版本正式取代了Dalvik VM。ART环境下,App安装时其包含的Dex文件将被dex2oat预编译成目标平台的机器码,从而提升了App的运行效率。在这个预编译过程当中,dex2oat对目标代码的优化过程与Dalvik VM下的dexopt有较大区别,尤为是在5.0版本之后ART环境下新增的方法内联优化,因为方法内联改变了本来的方法分布和调用流程。
方法内联之因此会致使优先加载补丁Dex的方案出现问题,本质上是由于补丁Dex只覆盖了旧Dex里的一部分类,一旦被覆盖的类的方法被内联到了调用者里,则加载类的过程仍是正常的,即从补丁Dex里加载了新版本的类。但因为内联,执行流程并未跳转到新的方法里,因而全部关于新版本的类的方法、成员、字符串的查找用的就都是旧方法里的索引了。所以,在ART模式下,若是类修改告终构,就会出现内存错乱的问题。为了解决这个问题,就必须把全部相关的调用类、父类子类等等所有加载到patch.dex中,这会致使ART下的补丁包异常的大,进一步增长应用启动加载的时候,耗时更加严重。
参考文章:
《安卓App热补丁动态修复技术介绍》 - QQ空间开发团队
《QFix探索之路》 - 手Q热补丁轻量级方案