Android 热修复原理实战

概述

关联文章

JVM 类加载机制

Android 中的ClassLoader

Android APK资源加载流程

Android 热修复原理解析

纸上得来终觉浅,绝知此事要躬行,读了这么多源码是时候实践一下了

代码修复

首先我们定义一个简单的类

public class Text {
    public static String message(){
        return "明天不放假";
    }
}

然后再Activity中调用

    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.code:
                Toast.makeText(MainActivity.this, Text.message(), Toast.LENGTH_LONG).show();
                break;

点击按钮就会弹出明天不放假的消息,我们的目标把明天不放假修复为No,明天放假

第一步 制作补丁包

首先改代码

public class Text {
    public static String message(){
        return "No,明天放假";
    }
}

然后把Text.java 编译成class

在命令行输入 javac Text.java

这样你就拿到了class文件

然后把你的class文件放入和包名一样的文件夹

把你的文件夹打成jar包

在命令行输入 jar -cvf patch.jar com

把你的jar包执行dex命令

在命令行输入 dx --dex --output=patch_dex.jar patch.jar

最后把你制作的补丁包放入sdcard中

写代码

我们已经说过原理,就不在重复说了,不知道的可以看我之前的文章Android 热修复原理解析

  public static void patch_class(Context context) {
        try {
            //获取ClassLoader
            ClassLoader classLoader = MainActivity.class.getClassLoader();
            //获取BaseDexClassLoader的class对象
            Class<?> superclass = classLoader.getClass().getSuperclass();
            //反射获取BaseDexClassLoader的变量pathList
            Field baseDexpathList = superclass.getDeclaredField("pathList");
            //设置可访问
            baseDexpathList.setAccessible(true);
            //获取当前内存中的PathClassLoader的pathList对象
            Object pathlist = baseDexpathList.get(classLoader);
            //获取DexpathList中的dexElements成员变量
            Field dexElementsFiled = pathlist.getClass().getDeclaredField("dexElements");
            //设置可访问
            dexElementsFiled.setAccessible(true);
            //获取当前pathList中的dexElements数组对象
            Object[] dexElements = (Object[]) dexElementsFiled.get(pathlist);
            //获取pathList中的makeDexElements方法
            Method makeDexElements = pathlist.getClass().getDeclaredMethod("makeDexElements",
                    List.class, File.class, List.class, ClassLoader.class);
            makeDexElements.setAccessible(true);
            //构建第一个参数,补丁包
            ArrayList<File> files = new ArrayList<>();
            //获取补丁包
            File file = new File(copyFile("/sdcard/patch_dex.jar", context));
            files.add(file);
            //构建第二个参数,指定解压目录
            File optimizedDirectory = new File(context.getFilesDir().getAbsolutePath() +
                    File.separator + "patch");
            if (!optimizedDirectory.exists()) {
                optimizedDirectory.mkdirs();
            }
            //构建第三个参数
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            //执行makeDexElements方法,获取补丁包的dexElement数组
            Object[] patchdexElements = (Object[]) makeDexElements.invoke(pathlist, files,
                    optimizedDirectory, suppressedExceptions, classLoader);
            //创建一个数组
            Object[] finalArray = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
                    dexElements.length + patchdexElements.length);
            //把补丁包放到刚创建的数组
            System.arraycopy(patchdexElements, 0, finalArray, 0, patchdexElements.length);
            //把原先的数组放入新建的数组
            System.arraycopy(dexElements, 0, finalArray, patchdexElements.length, dexElements.length);
            //把新数组替换掉原先的数组
            dexElementsFiled.set(pathlist, finalArray);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

主要就是把你制作的补丁包解析为数组,然后和原数组合并,并且把你的补丁包放在数组的第一位,这样就会先加载你修复过的文件

效果

sc.png

sc.png

需要注意的地方

不能即时生效:如果你先加载的bug类,再点击代码修复按钮是没有效果的,建议需要卸载apk,在重新安装,因为你的bug类加载后,就不会加载你修复的类,具体可以看这个,JVM 类加载机制

资源修复

原理也是之前讲过了,不懂的可以看

Android APK资源加载流程

Android 热修复原理解析

我们先写一个正常的类加载资源图片

把你的资源放入文件夹中,然后代码加载

 public void onClick(View v) {
        switch (v.getId()) {
            case R.id.resource:
                mImageView.setImageResource(R.mipmap.aaaa);
                break;

准备补丁包

这个没什么好说的,直接把你需要改的图片替换掉就行,比如我们改成了这个

然后build apk,拿到改过的apk,推入sdcard

这样就完成了

写代码

  public static void monkeyPatchExistingResources(@Nullable Context context,
                                                    @Nullable String externalResourceFile,
                                                    @Nullable Collection<Activity> activities) {
        if (externalResourceFile == null) {
            return;
        }
        try {
            //利用反射创建一个新的AssetManager
            AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
            //利用反射获取addAssetPath方法
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            mAddAssetPath.setAccessible(true);
            //利用反射调用addAssetPath方法加载外部的资源(SD卡)
            if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
                throw new IllegalStateException("Could not create new AssetManager");
            }
            // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
            // in L, so we do it unconditionally.
            Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
            mEnsureStringBlocks.setAccessible(true);
            mEnsureStringBlocks.invoke(newAssetManager);
            if (activities != null) {
                //遍历activities
                for (Activity activity : activities) {
                    //拿到Activity的Resources
                    Resources resources = activity.getResources();
                    try {
                        //获取Resources的成员变量mAssets
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        //给成员变量mAssets重新赋值为自己创建的newAssetManager
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    //获取activity的theme
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            //反射得到Resources.Theme的mAssets变量
                            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            //将Resources.Theme的mAssets替换成newAssetManager
                            ma.set(theme, newAssetManager);
                        } catch (NoSuchFieldException ignore) {
                            Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                            themeField.setAccessible(true);
                            Object impl = themeField.get(theme);
                            Field ma = impl.getClass().getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(impl, newAssetManager);
                        }
                        Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
                        mt.setAccessible(true);
                        mt.set(activity, null);
                        Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme");
                        mtm.setAccessible(true);
                        mtm.invoke(activity);
                        Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme");
                        mCreateTheme.setAccessible(true);
                        Object internalTheme = mCreateTheme.invoke(newAssetManager);
                        Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                        mTheme.setAccessible(true);
                        mTheme.set(theme, internalTheme);
                    } catch (Throwable e) {
                        Log.e("mmm", "Failed to update existing theme for activity " + activity,
                                e);
                    }
                }
            }

代码没啥说的,上篇文章已经说过了

效果

sc.png

sc.png

资源修复

首先准备俩个so,一个是有bug的,一个是修复的,怎么制作so我就不展开讲了,复习一下ndk就行了,把你需要修复的libnative-lib_patch.so推入到sdcard

写一个简单的代码,加载so

public void onClick(View v) {
        switch (v.getId()) {
            case R.id.so:
                Toast.makeText(MainActivity.this, SoUtils.getKey(), Toast.LENGTH_LONG).show();
                break;

SoUtils.getKey调用so中的方法,打印出字符串

写代码

 public static void patch_so(Context context){
        try {
            String localPath =  "/sdcard/libnative-lib_patch.so";
            Log.v("mmm", "LazyBandingLib localPath:" + localPath);

            // 开辟一个输入流
            File inFile = new File(localPath);
            // 判断需加载的文件是否存在
            if (!inFile.exists()){
                System.loadLibrary("native-lib");
                return;
            }
            FileInputStream fis = new FileInputStream(inFile);

            File dir = context.getDir("libs", Context.MODE_PRIVATE);
            // 获取驱动文件输出流
            File soFile = new File(dir,"libnative-lib.so");
            if (!soFile.exists()) {
                Log.v("mmm", "### " + soFile.getAbsolutePath() + " is not exists");
                FileOutputStream fos = new FileOutputStream(soFile);
                Log.v("mmm", "FileOutputStream:" + fos.toString());

                // 字节数组输出流,写入到内存中(ram)
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len = -1;
                while ((len = fis.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);
                }
                // 从内存到写入到具体文件
                fos.write(baos.toByteArray());
                // 关闭文件流
                baos.close();
                fos.close();
            }
            fis.close();
            Log.v("mmm", "### System.load start");
            // 加载外设驱动
            System.load(soFile.getAbsolutePath());
            Log.v("mmm", "### System.load End");

        } catch (Exception e) {

            e.printStackTrace();

        }
    }

so修复我选择了加载so方法的替换的形式来写,主要逻辑,如果sdcard中有补丁包,则加载补丁包,如果没有则加载APK中的so

效果

sc.png

sc.png

注意事项

需要先点击一下SO修复按钮,要先把so包加载才行

总结

我的代码运行在华为7.0机器,因为各个的不同的版本,源码细节也会不同,所以其他的版本可能会遇到bug,这就需要自己去看一下源码去解决了,正好也能锻炼一下实际操作能力

Demo已经传到GitHub,如果有帮助希望能给点一个star

Demo地址

我把用到的修复资源,放到了assets文件夹下,直接取出,放入到sdcard中即可

参考:

Android热修复原理探索与实践

从零开始手撸一个热修复框架

Android动态加载so文件

发布了100 篇原创文章 · 获赞 5 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_34760508/article/details/103558926