设备
: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<IOException> suppressedExceptions = new ArrayList<IOException>();
//执行
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,页面加载出来了,崩溃修复。
![](https://img.haomeiwen.com/i18224982/dc0f3644e21beecf.png)
控制台输出:
2020-05-10 10:21:54.632 20432-20432/? E/Utils: 修复完成
总结:本篇用到的知识点:反射 和 Android ClassLoader。 有些人说这很简单嘛,其实厉害的不是反射,而是上帝,反射只是工具 ,就看你能不能成为上帝。如果没有对于 framwork层很熟悉,是很难有各种各样的奇思妙想的,就像之前的一篇文章一样,启动一个没有在 Manifest 中注册过的 Activity,也是用到了反射,但是如果没有对 framework 层很熟悉,又怎么可能顺手的使用上帝的武器呢。 路漫漫其修远兮,吾将上下而求索。
网友评论