Android 热修复一

文章介绍了Android应用的热修复原理和实现过程,包括生成补丁包,通过PathClassLoader动态加载修复bug的.dex文件,以及如何处理混淆后的代码。提供了Demo源码供参考,详细解释了不同Android系统版本下的实现差异。
摘要由CSDN通过智能技术生成

一、什么是热修复?

在我们应用上线后出现bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户无感知下修复掉bug。

实现效果:

Demo源码:

https://gitee.com/sziitjim/hotfix

 二、怎么进行热修复?

1.开发端:生成补丁包,开发者把Bug修复后,将修复后的代码打包成.jar或者.dex文件,上传到服务端。

2.服务端:补丁包管理,制作好的补丁包放到服务器进行管理;

3.用户端:执行热修复,APP启动后,通过api查询如果服务端有补丁包,则下载并执行补丁包,进行bug修复,补丁包要在调用bug代码前执行才能修复达到bug效果;

三、制作补丁包流程:

1、把Bug修复掉后,先生成类的class文件。

1.1首先我在代码里面制造了一个bug,程序运行后抛出异常,

1.2修复bug,成类的class文件;

 2、执行命令:dx --dex --output=patch.jar com/hotfix/Utils.class  ,生成补丁包.jar或者.dex文件,我这里生成.jar文件;

命令目录地址不要输错:

注意:如果提示错误:'dx' 不是内部或外部命令,也不是可运行的程序 或批处理文件。 

要配置dex环境变量:

 如果提示错误:-Djava.ext.dirs=D:\Android\Sdk\build-tools\30.0.2\lib is not supported. Use -classpath instead.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

则修改:D:\Android\Sdk\build-tools\30.0.2目录下的dx.bat 最后一行

把   call "%java_exe%" %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
改成 call "%java_exe%" %javaOpts% --class-path="%frameworkdir%" -jar "%jarpath%" %params%

四、使用补丁包:

1.做测试我这边没有使用服务器管理,直接将生成的补丁包复制到手机SDcard目录下,由APP端读取SDcard目录下补丁包patch.jar使用;

2.使用类替换的方式实现热修复,所以不能立即生效,需要重启APP才能生效;

2.1类加载:

由于补丁包的类是我们自己写的,所以是使用当前程序的PathClassLoader去获取补丁包的类数据。

2.2使用类加载器 ClassLoader获取补丁包的Utils类,代替APP原来的Utils,到达修复bug的效果;

核心思想:当程序要使用Utils类的时候,会通过类加载器 ClassLoader去Element数组里面的各个dex文件中查找Utils类,如果查找到了,就不会再查找后面的dex文件,所以我们要想办法,把补丁的dex文件放在Element数组的最前面,ClassLoader找到修复后的Utils类后就会直接返回,达到修复的效果。

热修复流程:

1、获取当前程序的PathClassLoader对象;
2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象;
3、反射获取pathList的dexElements对象 (oldElement)
4、把补丁包patch.jar转化为Element数组:patchElement(反射执行makePathElements)
5、合并patchElement(放在前面)+oldElement = newElement (Array.newInstance)
6、反射把oldElement赋值成newElement 

Android6.0及以下系统代码实现:

// 1、获取程序的PathClassLoader对象
ClassLoader classLoader = application.getClassLoader();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
	try {
		ClassLoaderInjector.inject(application, classLoader, patchFile);
	} catch (Throwable throwable) {
	}
	return;
}
try {
	// 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
	Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
	Object pathList = pathListField.get(classLoader);
	// 3、反射获取pathList的dexElements对象 (oldElement)
	Field dexElementsField = ShareReflectUtil.findField(pathList, "dexElements");
	Object[] oldElements = (Object[]) dexElementsField.get(pathList);
	// 4、把补丁包变成Element数组:patchElements(反射执行makePathElements)
	Object[] patchElements = null;
	ArrayList<IOException> ioExceptions = new ArrayList<>();
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
		Method makePathElementsMethod = ShareReflectUtil.findMethod(pathList, "makePathElements",
				List.class, File.class, List.class);
		patchElements = (Object[]) makePathElementsMethod.invoke(pathList, patchFile,
				application.getCacheDir(), ioExceptions);
	} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
		Method makePathElements = ShareReflectUtil.findMethod(pathList, "makeDexElements",
				ArrayList.class, File.class, ArrayList.class);
		patchElements = (Object[])
				makePathElements.invoke(pathList, patchFile, application.getCacheDir(), ioExceptions);
	}
	// 5、合并patchElement+oldElement = newElement (Array.newInstance)
	Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
			oldElements.length + patchElements.length);
	Log.i("FixUtil", "installPatch patchElements " + patchElements.length + ",oldElements " + oldElements.length);
	// copy patchElements
	System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
	// copy oldElements
	System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
	// 6、反射把oldElement赋值成newElement
	dexElementsField.set(pathList, newElements);
} catch (Exception e) {
	Log.i("FixUtil", "err:" + e.getMessage());
}

Android6.0以上系统代码实现:

public class ClassLoaderInjector {
    public static void inject(Application app, ClassLoader oldClassLoader, List<File> patchs) throws Throwable {
        Log.i("ClassLoaderInjector", "inject");
        //创建我们自己的加载器
        ClassLoader newClassLoader
                = createNewClassLoader(app, oldClassLoader, patchs);
        doInject(app, newClassLoader);
        Log.i("ClassLoaderInjector", "inject end");
    }

    private static ClassLoader createNewClassLoader(Context context, ClassLoader oldClassLoader, List<File> patchs) throws Throwable {

        /**
         * 1、先把补丁包的dex拼起来
         */
        // 获得原始的dexPath用于构造classloader
        StringBuilder dexPathBuilder = new StringBuilder();
        String packageName = context.getPackageName();
        boolean isFirstItem = true;
        for (File patch : patchs) {
            //添加:分隔符  /xx/a.dex:/xx/b.dex
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                dexPathBuilder.append(File.pathSeparator);
            }
            dexPathBuilder.append(patch.getAbsolutePath());
        }

        /**
         * 2、把apk中的dex拼起来
         */
        //得到原本的pathList
        Field pathListField = ShareReflectUtil.findField(oldClassLoader, "pathList");
        Object oldPathList = pathListField.get(oldClassLoader);

        //dexElements
        Field dexElementsField = ShareReflectUtil.findField(oldPathList, "dexElements");
        Object[] oldDexElements = (Object[]) dexElementsField.get(oldPathList);

        //从Element上得到 dexFile
        Field dexFileField = ShareReflectUtil.findField(oldDexElements[0], "dexFile");
        for (Object oldDexElement : oldDexElements) {
            String dexPath = null;
            DexFile dexFile = (DexFile) dexFileField.get(oldDexElement);
            if (dexFile != null) {
                dexPath = dexFile.getName();
            }
            if (dexPath == null || dexPath.isEmpty()) {
                continue;
            }
            if (!dexPath.contains("/" + packageName)) {
                continue;
            }
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                dexPathBuilder.append(File.pathSeparator);
            }
            dexPathBuilder.append(dexPath);
        }
        String combinedDexPath = dexPathBuilder.toString();

        /**
         * 3、获取apk中的so加载路径
         */
        //  app的native库(so) 文件目录 用于构造classloader
        Field nativeLibraryDirectoriesField = ShareReflectUtil.findField(oldPathList, "nativeLibraryDirectories");
        List<File> oldNativeLibraryDirectories = (List<File>) nativeLibraryDirectoriesField.get(oldPathList);
        StringBuilder libraryPathBuilder = new StringBuilder();
        isFirstItem = true;
        for (File libDir : oldNativeLibraryDirectories) {
            if (libDir == null) {
                continue;
            }
            if (isFirstItem) {
                isFirstItem = false;
            } else {
                libraryPathBuilder.append(File.pathSeparator);
            }
            libraryPathBuilder.append(libDir.getAbsolutePath());
        }

        String combinedLibraryPath = libraryPathBuilder.toString();

        //创建自己的类加载器
        ClassLoader result = new EnjoyClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
        return result;
    }


    private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
        Thread.currentThread().setContextClassLoader(classLoader);

        Context baseContext = (Context) ShareReflectUtil.findField(app, "mBase").get(app);
        if (Build.VERSION.SDK_INT >= 26) {
            ShareReflectUtil.findField(baseContext, "mClassLoader").set(baseContext, classLoader);
        }

        Object basePackageInfo = ShareReflectUtil.findField(baseContext, "mPackageInfo").get(baseContext);
        ShareReflectUtil.findField(basePackageInfo, "mClassLoader").set(basePackageInfo, classLoader);

        if (Build.VERSION.SDK_INT < 27) {
            Resources res = app.getResources();
            try {
                ShareReflectUtil.findField(res, "mClassLoader").set(res, classLoader);

                final Object drawableInflater = ShareReflectUtil.findField(res, "mDrawableInflater").get(res);
                if (drawableInflater != null) {
                    ShareReflectUtil.findField(drawableInflater, "mClassLoader").set(drawableInflater, classLoader);
                }
            } catch (Throwable ignored) {
                // Ignored.
            }
        }
    }
}

注意:Android不同系统版本中ClassLoader涉及的源码都可能存在差异,目前demo只适配到了Android9,也就是说安装在Android9.0以上的手机上,该热修复未必能生效,每个版本的适配可以参考:腾讯热修复框架tinker的NewClassLoaderInjector.java类

代码地址:tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader/NewClassLoaderInjector.java · Gitee 极速下载/tinker - Gitee.com

五、如何打包混淆后的补丁包:

由于正式版本的apk我们是经过混淆打包的,所以我们要修复的是混淆后的代码,这边我们可以通过mapping文件, mapping文件存储的是混淆前和混淆后的代码文件对照表。

找到混淆后,Utils 类名 == a;test 方法名 == a;这样我们在编写修复代码时,要改成a类名和a方法再打包成补丁包。

public class a {
    public static void a() {
        throw new IllegalStateException("this is a bug...");
        //Log.i("Utils", "Fix bug.");
    }
}

Demo源码:

https://gitee.com/sziitjim/hotfix

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sziitjin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值