Android热修复三部曲之动态加载补丁.dex文件
Android热修复三部曲之动态加载补丁.dex文件
转载请标明出处:
http://blog.csdn.net/lisdye2/article/details/52119602
本文出自:【Alex_MaHao的博客】
项目中的源码已经共享到github,有需要者请移步【Alex_MaHao的github】
该篇作为Andriod热修复三部曲的最后一篇,本篇基于前两篇
- Android 热修复三部曲之基本的Ant打包脚本
- Android热修复三部曲之MultiDex 分包架构
在之前的博客中,我们将.java
文件打成了三个.dex
文件
- classes.dex
:程序必须启动的类,保证没问题的(Application
,MainActivity
)
- classes2.dex
:业务逻辑的类,如果出问题了可以动态替换。
- classes3.dex
:jar包的类,基本上不会出现问题。
那么我们实现热修复的原理:
- 修改好代码之后打出无bug版的
classes2.dex
。 - 从服务器下载到移动端。
- 动态加载
classes2.dex
,实现覆盖。
如何动态加载.dex文件
在java中,ClassLoader
负责加载.class
文件。那么在Android
中同样存在这样的类即BaseDexClassLoader
.
因为其实存在于系统源码中的,我们可以看他的源码,有一个关键字段
/** * Base class for common functionality between various dex-based * {@link ClassLoader} implementations. */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); } //...}
贴了一部分代码,在BaseDexClassLoader
中有着很关键的字段DexPathList
,从命名上就能看出,其是存放.dex
文件的集合。看一下他的源码
/*package*/ 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). */ private final Element[] dexElements; // 关键字段
在DexPathList
中,存在字段dexElements
,该字段便是存放加载的.dex
文件的字段。
那么从上面我们可以看出,我们实现动态加载的流程:
- 生成补丁包的
BaseDexClassLoader
类 - 获取到
BaseDexClassLoader
中的DexPathList
字段的dexElements
- 将补丁包的
dexElements
和本身的dexElements
合并为一个新的数组(补丁包的放在前面) - 将新合并的
dexElements
设置到系统的dexElements
中
代码实现
在布局文件中,添加两个按钮
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent" > <Button android:text="inject" android:layout_height="wrap_content" android:layout_width="wrap_content" android:onClick="inject"/> <Button android:onClick="test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" />LinearLayout>
两个按钮:分别是热修复,和测试热修复是否成功的。
/** * 测试类 * @author alex_mahao * */public class Test { public static void show(Context context){ Toast.makeText(context, "1", Toast.LENGTH_SHORT).show(); }}
测试类,修改toast的值。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void inject(View view) { // 无bug的classes2.dex文件存放地址 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); // 加载.dex文件 FixDexUtils.loadFixDex(this.getApplication()); } catch (IOException e) { e.printStackTrace(); } } public void test(View view) { Test.show(this); }}
在这里省略了从服务器下载的过程,直接把补丁放在了系统根目录下。
从代码上可以看到,第一步是复制操作。因为android 系统的原因,如果加载.dex
文件,必须放到私有目录odex下。
然后开始加载odex目录下的.dex
文件。FixDexUtils.loadFixDex(this.getApplication());
直接上源码
/** * 动态加载补丁包 * @author alex_mahao * */public class FixDexUtils { private static HashSet loadedDex = 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; }}
这样,热修复就实现了。
完结!!!
更多相关文章
- Android学习之Service(下)
- sqlite数据库默认自增标号RecNo与integer primary key autoincre
- Android(安卓)SDK更新后 ADT R17 E/AndroidRuntime : java.lang.
- 下载android sdk更新包离线安装解决方案
- Android使用Aidl实现跨进程通信
- Android性能测试工具(一)之Emmagee
- 使用Android(安卓)Studio为系统级的app签名
- Android(安卓)setContentView 实现同一个activity下不同view的切
- android 本地数据库sqlite的封装