Android 热修复_类替换

设备:MI 5
Android 版本:8.0.0

从上一篇关于 ClassLoader 的介绍,可知修复 类 的一种手段就是:通过修改 DexPathList 中 dexElements 的值,让ClassLoader在加载类的时候使用我们最新的 类,从而达到类替换的效果,完成类的修复。

实践

创建一个新的 Android 工程,应用只有一个 输出 hello world 的 Activity MainActivity

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Utils.test();
    }
}

在 MainActivity 的 onCreate 方法中调用一个 工具类方法 Utils#test

public class Utils {
    public static void test(){
        throw new IllegalArgumentException("参数异常");
    }
}

没错,运行程序会崩溃:

2020-05-10 09:22:29.316 18619-18619/? D/AndroidRuntime: Shutting down VM
    
    
    --------- beginning of crash
2020-05-10 09:22:29.319 18619-18619/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.jxf.androidhotfix, PID: 18619
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.jxf.androidhotfix/com.jxf.androidhotfix.MainActivity}: java.lang.IllegalArgumentException: 参数异常
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2856)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2931)
        at android.app.ActivityThread.-wrap11(Unknown Source:0)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1620)
        at android.os.Handler.dispatchMessage(Handler.java:105)
        at android.os.Looper.loop(Looper.java:173)
        at android.app.ActivityThread.main(ActivityThread.java:6698)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:782)
     Caused by: java.lang.IllegalArgumentException: 参数异常
        at com.jxf.androidhotfix.Utils.test(Utils.java:16)
        at com.jxf.androidhotfix.MainActivity.onCreate(MainActivity.java:16)
        at android.app.Activity.performCreate(Activity.java:7040)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2809)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2931) 
        at android.app.ActivityThread.-wrap11(Unknown Source:0) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1620) 
        at android.os.Handler.dispatchMessage(Handler.java:105) 
        at android.os.Looper.loop(Looper.java:173) 
        at android.app.ActivityThread.main(ActivityThread.java:6698) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:782) 

接下来,我们实践通过热修复 修复出现的崩溃。

程序崩溃的原因在于 Utils#test 方法抛出了异常,而上层调用者没有捕获处理,从而导致程序崩溃。我们修复崩溃的策略:修改Utils 类,把抛出异常的代码删除即可修复崩溃。

修改后的Utils类:

public class Utils {
    private static String TAG = "Utils";
    public static void test(){
        Log.e(TAG, "修复完成");
    }
}

单独编译 Utils 类(或者编译整个项目),找到编译以后的 Utils.class ,根据不同IDE的不同,路径可能不一样,我的在:app/build/intermediates/javac/debug/classes/com/jxf/androidhotfix/Utils.class

然后使用 dx 工具把 Utils.class 打包,目录切换到:app/build/intermediates/javac/debug/classes,执行 /Users/jxf/workspace/Android/sdk/build-tools/26.0.3/dx --dex --output=patch.jar com/jxf/androidhotfix/Utils.class 在当前目录下会生成一个 patch.jar 文件,这就是我们为了热修复打出的patch包。

有了 patch 包,接下来就是让 ClassLoader 在加载 Utils 类的时候,能够抢先一步加载 patch 包中的 Utils 类,这样程序的崩溃就能修复。

一、编写反射工具类 ReflectUtils
public class ReflectUtils {
    public static Field findField(Object obj, String name) throws NoSuchFieldException {
        Class cls = obj.getClass();
        while (cls != Object.class){
            try {
                Field field = cls.getDeclaredField(name);
                if (field != null){
                    field.setAccessible(true);
                    return field;
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
            cls = cls.getSuperclass();
        }
        throw new NoSuchFieldException(obj.getClass().getSimpleName() + " not find " + name);
    }

    public static Method findMethod(Object obj, String name, Class... parameterTypes) throws NoSuchMethodException {
        Class cls = obj.getClass();
        while (cls != Object.class){
            try {
                Method method = cls.getDeclaredMethod(name, parameterTypes);
                if (method != null){
                    method.setAccessible(true);
                    return method;
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            cls = cls.getSuperclass();
        }
        throw new NoSuchMethodException(obj.getClass().getSimpleName() + " not find " + name);
    }
}

getDeclaredField 和 getDeclaredMethod 会寻找当前类定义 field 和 method,不论是 public 还是 private 的,但无法获取在父类中定义的 field 和 method。 getField 和 getMethod 会获取共有的 field 和 method,包括自己定义的,还有父类定义的。

二、编写热修复的功能类 Hotfix
核心功能注释:
     * 1、获取到当前应用的PathClassloader;
     * 2、反射获取到DexPathList属性对象pathList;
     * 3、反射修改pathList的dexElements
     *     3.1、把补丁包patch.dex转化为Element[]  (patch)
     *     3.2、获得pathList的dexElements属性(old)
     *     3.3、patch+old合并,并反射赋值给pathList的dexElements
/**
 * 基于Android8.0   SDK26
 */
public class Hotfix {

    public static void installPatch(Application application, File patch){
        if (!patch.exists()){
            return;
        }
        //获取当前应用的PathClassLoader
        ClassLoader classLoader = application.getClassLoader();

        try {
            //反射获取到DexPathList属性对象pathList;
            Field field = ReflectUtils.findField(classLoader, "pathList");
            Object pathList = field.get(classLoader);


            //3.1、把补丁包patch.dex转化为Element[]  (patch)
            Method method = ReflectUtils.findMethod(pathList, "makeDexElements", List.class, File.class, List.class, ClassLoader.class);
            //构建第一个参数
            List patchs = new ArrayList();
            patchs.add(patch);
            //构建第三个参数
            ArrayList suppressedExceptions = new ArrayList();
            //执行
            Object[] patchElements = (Object[]) method.invoke(null, patchs, null, suppressedExceptions, classLoader);


            //3.2获得pathList的dexElements属性(old)
            Field dexElementsField = ReflectUtils.findField(pathList, "dexElements");
            Object[] dexElements = (Object[]) dexElementsField.get(pathList);

            //3.3、patch+old合并,并反射赋值给pathList的dexElements
            Object[] newElements = (Object[]) Array.newInstance(patchElements.getClass().getComponentType(), patchElements.length + dexElements.length);
            System.arraycopy(patchElements, 0, newElements,0, patchElements.length);
            System.arraycopy(dexElements,0,newElements,patchElements.length, dexElements.length);
            dexElementsField.set(pathList, newElements);


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

    }
}

如何把patch补丁包转换成 DexPathList 中 Element[] , 我们可以参照 DexPathList中的实现:

 // save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
三、在程序创建时,安装补丁包
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Hotfix.installPatch(this, new File("/sdcard/patch.jar"));
    }
}

注意我们把 补丁包 放在了 /sdcard/patch.jar,真实项目里肯定是放在自己的私有目录;此功能需要文件权限,记得加上并且同意。

此时,重新打开App,页面加载出来了,崩溃修复。

运行结果

控制台输出:

2020-05-10 10:21:54.632 20432-20432/? E/Utils: 修复完成

总结:本篇用到的知识点:反射 和 Android ClassLoader。 有些人说这很简单嘛,其实厉害的不是反射,而是上帝,反射只是工具 ,就看你能不能成为上帝。如果没有对于 framwork层很熟悉,是很难有各种各样的奇思妙想的,就像之前的一篇文章一样,启动一个没有在 Manifest 中注册过的 Activity,也是用到了反射,但是如果没有对 framework 层很熟悉,又怎么可能顺手的使用上帝的武器呢。 路漫漫其修远兮,吾将上下而求索。

你可能感兴趣的:(Android 热修复_类替换)