Android 动态热修复

    xiaoxiao2021-09-19  45

    第一步构建有bug的APP

    代码如下

    MainActivity代码如下:

    public class MainActivity extends Activity { Button btnOpen, btnModify; NullTest nt = new NullTest(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnOpen = (Button) findViewById(R.id.btn_open); btnModify = (Button) findViewById(R.id.btn_modify); btnOpen.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { nt.printAbcLength(MainActivity.this);// 执行计算 } }); btnModify.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { castielFixMethod();// 调用热修复方法 } }); } }

    出错的NullTest计算工具类:

    public class NullTest { int a = 8; int b = 0;// 故意设置为0 public void printAbcLength(Context context) { // 很明用8除0,一定会导致java.lang.ArithmeticException: / by zero异常 Toast.makeText(context, "count result:" + (a/b), Toast.LENGTH_LONG).show(); } }

    布局文件代码:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dp" android:text="猴子搬来的救兵 http://blog.csdn.net/mynameishuangshuai" /> <ImageView android:id="@+id/imageView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/textView1" android:layout_margin="10dp" android:src="@drawable/old" /> <Button android:id="@+id/btn_open" android:layout_width="200dp" android:layout_height="40dp" android:layout_below="@+id/imageView1" android:layout_margin="10dp" android:text="执行操作" /> <Button android:id="@+id/btn_modify" android:layout_width="200dp" android:layout_height="40dp" android:layout_below="@+id/btn_open" android:layout_margin="10dp" android:text="修复Bug" /> </RelativeLayout>

        OK,项目源码开发到此为止,接下来,我们通过使用ant命令,对该项目进行多分包构建,这次我们一共构建了两个dex包,特地把出错的NullTest类放到classes2.dex中去,为的就是方便后面的热修复。

    <!-- 构建多分包dex文件 --> <target name="multi-dex" depends="compile" > <echo message="Generate multi-dex..." /> <exec executable="${tools.dx}" failonerror="true" > <arg value="--dex" /> <arg value="--multi-dex" /> <arg value="--set-max-idx-number=10000" /> <arg value="--main-dex-list" /> <!-- 主包包含class文件列表 --> <arg value="${main-dex-rule}" /> <arg value="--minimal-main-dex" /> <arg value="--output=${bin}" /> <arg value="${bin}" /> <!-- <arg value="${libs}" /> --> </exec> </target>

        完成以上所有操作后,我们将构建出来的APK安装到手机上,然后测试,点击执行操作按钮,发现项目崩溃并闪退。

    第二部,修复bug的代码如下

    /** * 修复方法 */ private void castielFixMethod() { // 创建一个内部缓存目录,把我们SD卡中的"classes2.dex"文件拷贝到内部缓存目录中cache File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE); String name = "classes2.dex"; String filePath = fileSDir.getAbsolutePath() + File.separator + name; File file = new File(filePath); if (file.exists()) {// 判断是否已经存在dex文件 Log.i("WY", "已经存在dex文件"); file.delete(); } // 通过IO流将dex文件写到我们的缓存目录中去 InputStream is = null; FileOutputStream fos = null; // 版权所有,未经许可请勿转载:猴子搬来的救兵http://blog.csdn.net/mynameishuangshuai try { is = new FileInputStream(Environment.getExternalStorageDirectory()); fos = new FileOutputStream(filePath); int len = 0; byte[] buffer = new byte[1024]; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } File f = new File(filePath); Log.i("WY", "filePath:" + f.getAbsolutePath()); if (f.exists()) { Toast.makeText(this, "新的dex文件已经覆盖", Toast.LENGTH_LONG).show(); } // 动态加载修复dex包 FixDexUtils.loadFixedDex(this); } catch (IOException e) { e.printStackTrace(); } finally { try { fos.close(); is.close(); } catch (IOException e) { e.printStackTrace(); } } }在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。     ClassLoader去加载Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由于apk是一个类似于压缩包的东西,Android其实是使用一个优化的临时缓存目录optimizeDir(dex),专门把Dex文件解压进去,这样以后就从这个临时缓存目录中加载,提高效率。     在1代码中我们提到了loadFixedDex()方法,便是我们的核心热修复工具类,我给大家具体讲一下:     ClassLoader有一个简单的实现类-PathClassLoader。该类作为Android的默认的类加载器,本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。     每个ClassLoader有一个pathList变量,是标识dex文件的路径,我们通过该路径加载dex文件,默认不分包的时候只有一个dex文件,当然谷歌在顶层设计时允许我们有多个dex文件。     ClassLoader去找optimizeDir(dex)目录,然后把目录添加到pathList里面去,接着去找目录下面的所有的dex文件,把这些dex文件当做一个数组放到dexElements中去,这样就可以有多个dex文件。 pathList{ dexElements{ [classes.dex,classes2.dex] } }

        ClassLoader每加载一个类,它会先找classes.dex,如果找不到就去classes2.dex中找,如果里面又一个dex有问题,比如说classes2.dex出问题了,我们就需要弄一个修复的新的classes2.dex文件放到数组中去,替换掉有问题的;但是classes2.dex中可能有多个类,除了有问题的类,也可能有很多正确的类,我们在替换时没必要把所有的类都替换掉,所以我们只要替换有问题的类。     为此,我们可以采用一个策略,把新的替换的dex文件放到数组的最前面,最终数组的形态为:

        这里解释下,ClassLoader类加载器先加载我们修复的正确的dex文件,然后顺序加载数组中其他的dex元素,到了最后加载到旧的classes2.dex元素,由于前面已经加载了更新的classes2.dex(更新的dex文件中只包含修复的class),那么旧的classes2.dex元素中的有Bug的class就不会再加载,而是只加载其余的没有错误的class。     整个流程其实非常简单,但是如果我们要实现这个过程却有个障碍,那就是由于我们的APK程序可能正在运行,谷歌并没有提供相关的接口方法去实现这一步骤,为此,我们需要使用反射的手段去实现。 1.首先需要反射ClassLoader类,找到里面的pathList变量,然后找到dexElements[]数组,该数组在修复之前只有两个元素,分别是classes.dex和classes2.dex(出错的),假设值数组1; 2.接着我们要往dexElements[]数组中添加classes2.dex文件。 Android中要想实现加载dex文件,需要使用DexClassLoader类加载classes2.dex(补丁),加载到dexElements[]数组中去,假设值数组2。 3.最后,我们需要把两个dexElements[]数组合并,作为一个新数组dexElements[],该数组中包含元素为classes2.dex(补丁),classes.dex和classes2.dex(出错的),完成后将数组返回赋值给系统的ClassLoader。

    修复工具类源码

    public class FixDexUtils { private static HashSet<File> loadedDex = new HashSet<File>(); public static void loadFixedDex(Context context) { if (context == null) { return; } // 首先拿到缓存目录 File fileSDir = context.getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE); File[] listFils = fileSDir.listFiles(); // 遍历缓存文件 for (File file : listFils) { // 如果文件是以"classes"开始或者以".dex"结尾,说明这是从SDK中拷贝回来的修复包 if (file.getName().startsWith("classes") || file.getName().endsWith(".dex")) { Log.i("WY", "当前dexName:" + file.getName()); loadedDex.add(file); } } doDexInject(context, fileSDir); } private static void doDexInject(Context context, File fileDir) { if (Build.VERSION.SDK_INT >= 23) { Log.i("WY", "Unable to do dex inject on SDK" + Build.VERSION.SDK_INT); } // .dex 的加载需要一个临时目录 String optimizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex"; File fopt = new File(optimizeDir); if (!fopt.exists()) fopt.mkdirs(); try { // 根据.dex 文件创建对应的DexClassLoader 类 for (File file : loadedDex) {// 循环迭代,用于多个修复包同时注入 DexClassLoader classLoader = new DexClassLoader( file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader()); // 注入 inject(classLoader, context); } } catch (Exception e) { e.printStackTrace(); } } private static void inject(DexClassLoader classLoader, Context context) { // 获取到系统的DexClassLoader 类 PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader(); try { Object dexElements = combineArray( getDexElements(getPathList(classLoader)), getDexElements(getPathList(pathLoader))); Object pathList = getPathList(pathLoader); setField(pathList, pathList.getClass(), "dexElements", dexElements); } catch (Exception e) { e.printStackTrace(); } } /** * 通过反射获取DexPathList中dexElements */ private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { return getField(paramObject, paramObject.getClass(), "dexElements"); } /** * 通过反射获取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); } /** * 通过反射设置字段值 */ 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); } /** * 合并两个数组 */ 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; } }

    转载请注明原文地址: https://ju.6miu.com/read-677714.html

    最新回复(0)