性能优化二十二之dex热修复原理
来源:互联网 发布:手机登录淘宝达人 编辑:程序博客网 时间:2024/06/16 14:35
前言
热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed属于阿里系,腾讯QQ空间的超级补丁技术和微信的Tinker属于腾讯系。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复,这两大系的主要区别在哪儿呢?阿里系着重从底层C的二进制入手(详细还没有去了解),而腾讯系的着重从Java的类加载机制入手。说到这里可能很多人连热修复是什么都不知道?一般的bug修复,都是等下一个版本解决,然后发布新的apk,而热修复可以直接在客户已经安装的程序当中修复bug。
为什么可以这么做呢?一般我们的bug会出现在某个类的某个方法的某个地方,我们只需要动态地将客户手机里面的apk里面的某个类给替换成我们已经修复好的类就行了。
上面所介绍的就是DEX分包方案,使用了多DEX加载的原理,而腾讯系的超级补丁和微信的Tinker都是基于dex分包。大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。
Dex分包的由来:
相信很多开发者都遇到过65536的问题,当Apk解压后里面会有一个classes.dex文件,这里面就包含了我们项目的所有class,但是随着项目的越来越复杂,由于一个dvm中存储方法id用的是short类型,所以当一个dex包中的方法超过65536的时候,就会导致编译失败,幸好谷歌官方提供了multiDex来解决这种问题,具体怎么解决这里不细说。除此之外还有人提供了其他方式:原理就是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中,并在Application的onCreate回调中被注入到系统的ClassLoader。因此,对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。
Dex分包原理–ClassLoader
在java中,程序执行的时候需要将字节码加载到Jvm之后才会被执行,这个过程就使用到了ClassLoader类加载器,Android中也类似:
由于在AS中看不到DexClassLoader和PathClassLoader源码,所以需要到其他网站去看,这里推荐http://androidxref.com/
从文档中看出,DexClassLoader可以加载指定的某个dex文件,但是必须要在应用程序的目录下面,而PathClassLoader是用来加载应用程序的dex,到了这里我们应该可以猜到热修复用的就是ClassLoader去加载新的dex包。
BaseDexClassLoader源码:
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; /** * Constructs an instance. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; may be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; }}
从源码得知,当我们需要加载一个class时,实际是从pathList中去找的,而pathList则是DexPathList的一个实体。
DexPathList部分源码:
final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; private static final String APK_SUFFIX = ".apk"; /** class definition context */ private final ClassLoader definingContext; /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */ //重点就在于这个dexElements private final Element[] dexElements; /** * Finds the named class in one of the dex files pointed at by * this instance. This will find the one in the earliest listed * path element. If the class is found but has not yet been * defined, then this method will define it in the defining * context that this instance was constructed with. * * @param name of class to find * @param suppressed exceptions encountered whilst finding the class * @return the named class or {@code null} if the class is not * found in any of the dex files */ public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }}
从这段源码可以看出,dexElements是用来保存dex的数组,而每个dex文件其实就是DexFile对象。遍历dexElements,然后通过DexFile去加载class文件,加载成功就返回,否则返回null。
通常情况下,dexElements数组中只会有一个元素,就是apk安装包中的classes.dex
而我们则可以通过反射,强行的将一个外部的dex文件添加到此dexElements中,这就是dex的分包原理了,这也是热补丁修复技术的原理。
疑问:如果两个dex中存在相同的class文件会怎样?
针对上面的问题不需要担心,一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。
实现
首先需要去制造一个错误
public class MyTestClass { public void testFix(Context context){ int i = 10; int a = 0; Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show(); }}
修复类:
public class FixDexUtils { private static HashSet<File> loadedDex = new HashSet<File>(); static{ loadedDex.clear(); } public static void loadFixedDex(Context context){ if(context == null){ return ; } //遍历所有的修复的dex File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE); File[] listFiles = fileDir.listFiles(); for(File file:listFiles){ if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){ loadedDex.add(file);//存入集合 } } //dex合并之前的dex doDexInject(context,fileDir,loadedDex); } private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj,value); } private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) { String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex"; File fopt = new File(optimizeDir); if(!fopt.exists()){ fopt.mkdirs(); } //1.加载应用程序的dex try { PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader(); for (File dex : loadedDex) { //2.加载指定的修复的dex文件。 DexClassLoader classLoader = new DexClassLoader( dex.getAbsolutePath(),//String dexPath, fopt.getAbsolutePath(),//String optimizedDirectory, null,//String libraryPath, pathLoader//ClassLoader parent ); //3.合并 Object dexObj = getPathList(classLoader); Object pathObj = getPathList(pathLoader); Object mDexElementsList = getDexElements(dexObj); Object pathDexElementsList = getDexElements(pathObj); //合并完成 Object dexElements = combineArray(mDexElementsList,pathDexElementsList); //重写给PathList里面的lement[] dexElements;赋值 Object pathList = getPathList(pathLoader); setField(pathList,pathList.getClass(),"dexElements",dexElements); } } catch (Exception e) { e.printStackTrace(); } } private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } private static Object getPathList(Object baseDexClassLoader) throws Exception { return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList"); } private static Object getDexElements(Object obj) throws Exception { return getField(obj,obj.getClass(),"dexElements"); } /** * 两个数组合并 * @param arrayLhs * @param arrayRhs * @return */ private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); int j = i + Array.getLength(arrayRhs); Object result = Array.newInstance(localClass, j); for (int k = 0; k < j; ++k) { if (k < i) { Array.set(result, k, Array.get(arrayLhs, k)); } else { Array.set(result, k, Array.get(arrayRhs, k - i)); } } return result; }}
MainActivity.java中模拟了两个按钮,一个是test,一个是fix修复:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void test(View v){ MyTestClass myTestClass = new MyTestClass(); myTestClass.testFix(this); } public void fix(View v){ fixBug(); } private void fixBug() { //目录:/data/data/packageName/odex File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE); //往该目录下面放置我们修复好的dex文件。 String name = "classes2.dex"; String filePath = fileDir.getAbsolutePath()+File.separator+name; File file= new File(filePath); if(file.exists()){ file.delete(); } //搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath InputStream is = null; FileOutputStream os = null; try { is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+name); os = new FileOutputStream(filePath); int len = 0; byte[] buffer = new byte[1024]; while ((len=is.read(buffer))!=-1){ os.write(buffer,0,len); } File f = new File(filePath); if(f.exists()){ Toast.makeText(this ,"dex 重写成功", Toast.LENGTH_SHORT).show(); } //热修复 FixDexUtils.loadFixedDex(this); } catch (Exception e) { e.printStackTrace(); } }}
MyApplication.java
public class MyApplication extends Application{ @Override public void onCreate() { // TODO Auto-generated method stub super.onCreate(); } @Override protected void attachBaseContext(Context base) { FixDexUtils.loadFixedDex(base); super.attachBaseContext(base); }}
新的修复dex(补丁包)来源:
将上面的错误进行修改,然后重新编译项目,最好这个时候把AS的Instant run关闭:
public class MyTestClass { public void testFix(Context context){ int i = 10; int a = 1; Toast.makeText(context, "shit:"+i/a, Toast.LENGTH_SHORT).show(); }}
去保存项目的的地方,将MyTestClass类的全路径拷贝出来,这里我们需要用到Android中的一个工具:dx.bat,在AndroidSDK的工具的目录(D:\AndroidStudioSDK\build-tools\25.0.1)下打开cmd命令行输入如下命令:
dx --dex --output=D:\Users\kiven\Desktop\dex\classes2.dex D:\Users\kiven\Desktop\dex命令解释: --output=D:\Users\kiven\Desktop\dex\classes2.dex 指定输出路径 D:\Users\kiven\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)
这样就生成了新的dex包。
参考链接:
http://blog.csdn.net/u010386612/article/details/50885320
http://blog.csdn.net/lisdye2/article/details/52119602
- 性能优化二十二之dex热修复原理
- andfix热修复之生成dex文件
- android dex热修复
- Android优化之热修复
- Android热补丁动态修复技术(一)dex分包原理
- Android热修复三部曲之动态加载补丁.dex文件
- Android热修复之dex多分包架构设计
- Android热修复三部曲之动态加载补丁.dex文件
- Android热修复技术(二)------代码修复之底层替换原理
- 替换dex实现热修复
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- Android热补丁动态修复技术(一):从Dex分包原理到热补丁
- 热修复框架研究之Robust原理
- 热修复框架研究之Robust原理
- 关于 Android中的插件化开发,dex分包,热修复(Tinker)的思考(二)
- Dex多分包技术和热修复
- MySQL大数据LIMIT优化
- elasticSearch 分词器踩的坑
- python项目练习四:新闻聚合
- Mysql的Root密码忘记,查看或修改的解决方法(图文介绍)
- KVM/QEMU虚拟机申请和释放内存的步骤
- 性能优化二十二之dex热修复原理
- hdu4675——GCD of Sequence(莫比乌斯反演+组合数取模+乘法逆元+快速幂)
- HDFS Federation在美团点评的应用与改进
- java版微信支付开发
- Windows下阅读linux源码的强大软件Source Insight
- eclipse 快捷键
- python项目练习三:万能的XML
- Android 开发 Tip 8 -- clipToPadding & clipChildren
- 利用 python numpy +matplotlib 绘制股票k线图