在我们日常的开发过程中,程序难免会出现BUG,一般有集中处理方式,发布新版本APP让用户来升级,或者打补丁来修复bug
前者本文在这里不错讨论,打补丁升级又分为两种一种是需要重启应用,一种是不需要。不需要的也可以叫他热加载。
首先使用热加载需要了解一些基本常识
1、什么是dex
Dex是Dalvik VM executes的全称,和windows上的exe很像,你项目的源码java文件已被编译成了.dex.
在用ide开发的时候编译发布构建工具(ant,gradle)会调用(aapt)将DEX文件,资源文件以及AndroidManifest.xml文件组合成一个应用程序包(APK)
2、安装apk的过程是怎么样的
复制APK安装包到data/app目录下,解压并扫描安装包,把dex文件(Dalvik字节码)保存到dalvik-cache目录,并data/data目录下创建对应的应用数据目
ODEX是安卓上的应用程序apk中提取出来的可运行文件,即将APK中的classes.dex文件通过dex优化过程将其优化生成一个.dex文件单独存放,原APK中的classes.dex文件会保留
这样做可以加快软件的启动速度,预先提取,减少对RAM的占用,因为没有odex的话,系统要从apk包中提取dex再运行
3、app怎么运行的
简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?
来看看代码
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图
以上就大致清楚了要做到热加载我们该怎么处理了
下面我们处理一个简单逻辑,用Toast 显示一个 除数为零的 模拟bug
接着我们创建一个application
package com.example.andfix;import android.app.Application;public class App extends Application{ private static Application _app; public static Application get() { return _app; } @Override public void onCreate() { _app=this; super.onCreate(); } }
在建立一个Activity
package com.example.andfix;import java.io.File;import java.io.IOException;import android.app.Activity;import android.content.Context;import android.os.Build;import android.os.Bundle;import android.os.Environment;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.Toast;import com.example.andfix.tools.CalcNum;public class MainActivity extends Activity { Button btnfix; Button btntest; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnfix=(Button)findViewById(R.id.btnfix); btntest=(Button)findViewById(R.id.btntest); btntest.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { new CalcNum(getApplicationContext()); } }); btnfix.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { fix(); } }); } private void fix() { inject(); } public void inject() { String sourceFile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex"; String targetFile = this.getDir("odex", Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex"; try { FileUtils.copyFile(sourceFile, targetFile); FixDexUtils.loadFixDex(this.getApplication()); } catch (IOException e) { e.printStackTrace(); } } }
一个工具类
package com.example.andfix;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;public class FileUtils { public static void copyFile(String sourceFile, String targetFile) throws IOException { InputStream is = new FileInputStream(sourceFile); File outFile = new File(targetFile); if(outFile.exists()){ outFile.delete(); } OutputStream os = new FileOutputStream(targetFile); int len = 0; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } os.close(); is.close(); }}
一个热修复逻辑
package com.example.andfix;import java.io.File;import java.lang.reflect.Array;import java.lang.reflect.Field;import java.util.HashSet;import dalvik.system.DexClassLoader;import dalvik.system.PathClassLoader;import android.content.Context;public class FixDexUtils { private static HashSetloadedDex = new HashSet (); static { loadedDex.clear(); } public static void loadFixDex(Context context) { // 获取到系统的odex 目录 File fileDir = context.getDir("odex", Context.MODE_PRIVATE); File[] listFiles = fileDir.listFiles(); for (File file : listFiles) { if (file.getName().endsWith(".dex")) { // 存储该目录下的.dex文件(补丁) loadedDex.add(file); } } doDexInject(context, fileDir); } private static void doDexInject(Context context, File fileDir) { // .dex 的加载需要一个临时目录 String optimizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex"; File fopt = new File(optimizeDir); if (!fopt.exists()) fopt.mkdirs(); // 根据.dex 文件创建对应的DexClassLoader 类 for (File file : loadedDex) { DexClassLoader classLoader = new DexClassLoader(file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader()); //注入 inject(classLoader, context); } } private static void inject(DexClassLoader classLoader, Context context) { // 获取到系统的DexClassLoader 类 PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader(); try { // 分别获取到补丁的dexElements和系统的dexElements Object dexElements = combineArray(getDexElements(getPathList(classLoader)), getDexElements(getPathList(pathLoader))); // 获取到系统的pathList 对象 Object pathList = getPathList(pathLoader); // 设置系统的dexElements 的值 setField(pathList, pathList.getClass(), "dexElements", dexElements); } catch (Exception e) { e.printStackTrace(); } } /** * 通过反射设置字段值 */ private static void setField(Object obj, Class cl, String field, Object value) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj, value); } /** * 通过反射获取 BaseDexClassLoader中的PathList对象 */ private static Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } /** * 通过反射获取指定字段的值 */ 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); } /** * 通过反射获取DexPathList中dexElements */ private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { return getField(paramObject, paramObject.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; }}
这样就可以实现热修复了 此过程是在eclipse 上完成的
通过ant构建
${dexfile} is not handle ${dexfile} is handle APK is released.path:${signed-apk-path}
主dex文件包含的类说明
com/example/andfix/MainActivity.class
com/example/andfix/App.classcom/example/andfix/FileUtils.classcom/example/andfix/FixDexUtils.class文档结构如下
实现过程中也有很多坑
比如:
com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000)
解决方法就是降低你的编译版本(jdk)
如果你在过程中遇到其他问题,不要怕麻烦一点一点采坑。走过来就是一种收获
当然本文只是描述热加载的过程和原理
ps:现在这样的框架也有很多
1.DroidPlugin用途:动态加载使用案例:360手机助手GitHub地址:https://github.com/Qihoo360/DroidPluginppt介绍:https://github.com/Qihoo360/DroidPlugin/tree/master/DOCDemo:https://github.com/SpikeKing/wcl-plugin-test-app详解:http://blog.csdn.net/yzzst/article/details/48093567 http://v2ex.com/t/2164942.AndFix用途:热修复GitHub地址:https://github.com/alibaba/AndFix讲解:http://blog.csdn.net/yzzst/article/details/48465031http://blog.csdn.net/qxs965266509/article/details/49816007http://blog.csdn.net/yaya_soft/article/details/504601023.dexposed用途:热修复GitHub地址:https://github.com/alibaba/dexposed讲解: http://blog.csdn.net/yzzst/article/details/47954479 http://blog.csdn.net/yzzst/article/details/47659987 http://www.jianshu.com/p/14edcb444c514.Small用途:动态加载GitHub地址:https://github.com/wequick/SmallDemo:https://github.com/cayden/MySmall5. DynamicAPK用途:动态加载、热修复案例:携程GitHub地址:https://github.com/CtripMobile/DynamicAPK详解:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading6.ClassPatch用途:热修复GitHub地址:https://github.com/Jarlene/ClassPatch详解:http://blog.csdn.net/xwl198937/article/details/498019757.ACDD用途:动态加载GitHub地址:https://github.com/bunnyblue/ACDD8.HotFix用途:热修复GitHub地址:https://github.com/dodola/HotFix该项目是基于QQ空间终端开发团队的技术文章实现的9.Nuwa用途:热修复GitHub地址:https://github.com/jasonross/Nuwa详解:http://www.jianshu.com/p/72c17fb76f21/comments/128004610.DroidFix用途:热修复GitHub地址:https://github.com/bunnyblue/DroidFix详解:http://bunnyblue.github.io/DroidFix/11.AndroidDynamicLoader用途:动态加载GitHub地址:https://github.com/mmin18/AndroidDynamicLoaderDemo:https://github.com/mmin18/AndroidDynamicLoader/raw/master/host.apk