乐于分享
好东西不私藏

插件弹窗crash:一个 ClassLoader 引发的血案

插件弹窗crash:一个 ClassLoader 引发的血案

背景知识

ClassLoader:类的"图书管理员"

Android 中,类加载器负责把 .class 文件加载到内存。  常见的有两种:

ClassLoader
作用
能加载的类
BootClassLoader
系统类加载器
Android 框架自带类,如 ViewTextView
PathClassLoader
应用类加载器
App 自己 APK 里的类,以及 AAR 库里的类

如果把类比作书,ClassLoader 就是图书管理员,告诉它书名,它去对应的书架(APK)上找。  让一个只管理系统书架的管理员,去找 App 书架上的书,就会报 ClassNotFoundException

Resources:资源读取入口

context.getResources().getString(R.string.app_name);context.getResources().getDrawable(R.drawable.bg);

Resources 负责读取布局、图片、字符串等资源。关键的是Resources

 内部有一个 mClassLoader 字段,  它决定了加载某些资源时用哪个 ClassLoader 去加载类。

LayoutInflater:布局 XML → View

<com.demo.ui.widget.DemoSwitchButton... />

LayoutInflater把布局文件变成真正的 View 对象。LayoutInflater 看到这行,会用 context.getClassLoader() 去加载 DemoSwitchButton 类。

DrawableInflater:Drawable XML → Drawable

<com.demo.ui.drawable.DemoLayerDrawable... />

DrawableInflater把 Drawable XML 变成 Drawable 对象。DrawableInflater 用 Resources.mClassLoader 去加载 DemoLayerDrawable 类。

两条路径不一样

View.inflate(context, layoutId, parent)├── LayoutInflater 加载 DemoSwitchButton│   └── ClassLoader = context.getClassLoader()└── DrawableInflater 加载 DemoLayerDrawable    └── ClassLoader = Resources.mClassLoader

LayoutInflater 和 DrawableInflater 用的 ClassLoader 来源不同。 在普通 App 里它们恰好相同,但在插件类里可能是不同的。当插件跑在宿主进程里时,插件框架可能这样创建 ClassLoader

pluginContext.getClassLoader()          → 插件 ClassLoader ✓pluginContext.getResources().mClassLoader → 宿主 ClassLoader ✗

问题描述与分析

测试反馈,点击状态栏敏感权限图标,弹窗没出来,系统状态栏进程直接崩溃。关键日志:

android.view.InflateException: Error inflating class com.demo.ui.widget.DemoSwitchButtonCaused by: Class not found com.demo.ui.drawable.DemoLayerDrawableCaused by: java.lang.ClassNotFoundException:  Didn't find class "com.demo.ui.drawable.DemoLayerDrawable"  on path: DexPathList[[directory "."], ...]

日志显示无法查找到类,注意 DexPathList[[directory "."]]——这是系统类加载器的典型路径,说明找类时没用插件自己的加载器。

Android 加载 UI 时,其实走了两条独立的路径:

View.inflate(context, layoutId, parent)├── 路径 A:LayoutInflater 加载 View 类│   └── 用 context.getClassLoader()└── 路径 B:DrawableInflater 加载 Drawable 类    └── 用 Resources.mClassLoader
加载器
负责内容
ClassLoader 来源
LayoutInflater
布局 XML → View
Context.getClassLoader()
DrawableInflater
Drawable XML → Drawable
Resources.mClassLoader

普通 App 里这两个 ClassLoader 一样,不会出问题。  但插件跑在宿主进程里时,插件框架可能给 Context 配了插件加载器,  却给 Resources 配了宿主加载器——两条路径就不一样了。

问题修复

我们把 View.inflate 的 Context 改成了插件 Context:

// 修改前View.inflate(parent.getContext(), R.layout.item, null);// 修改后View.inflate(mContext, R.layout.item, null);

这只能修复路径 A(LayoutInflater),让 DemoSwitchButton 能找到;但路径 B(DrawableInflater)还是走 Resources.mClassLoader,  DemoLayerDrawable 依然找不到,继续崩溃。

最终修复采用反射方式修正 Resources 的加载器来workround。核心思路是用反射把 Resources 里的 mDrawableInflater 换成使用插件 ClassLoader 的实例。

publicstaticvoidfixPluginResourcesClassLoader(Context pluginContext){if (pluginContext == nullreturn;try {        Resources resources = pluginContext.getResources();        Class<?> clazz = Class.forName("android.graphics.drawable.DrawableInflater");        Constructor<?> c = clazz.getDeclaredConstructor(Resources.classClassLoader.class);        c.setAccessible(true);        Object newInflater = c.newInstance(resources, pluginContext.getClassLoader());        Field field = Resources.class.getDeclaredField("mDrawableInflater");        field.setAccessible(true);        field.set(resources, newInflater);    } catch (Exception e) {        Log.w("PluginFix""fix failed", e);    }}

在插件 onCreate 中、setTheme 之前调用:

publicvoidonCreate(Context hostContext, Context pluginContext){super.onCreate(hostContext, pluginContext);    mContext = pluginContext;    fixPluginResourcesClassLoader(mContext);  // ← 关键    mContext.setTheme(R.style.Theme_Demo_DayNight);}

总结

那么这个问题是否是Android框架的bug呢?答案并不是。Android Resources 构造函数本来就要调用方传入正确的 ClassLoader,  普通 App 通过 LoadedApk 传的是 App 自己的 ClassLoader,完全正常。真正的问题在插件框架,它创建插件 Resources 时,错误地传了宿主 ClassLoader。 我们的反射修复只是业务层的 workaround,  最彻底的修复应该在插件框架层,让 Resources 从创建时就带上插件 ClassLoader。
本文我们了解到:
  1. LayoutInflater 和 DrawableInflater 用不同的 ClassLoader,需要对两个加载器都要修改。
  2. drawable 引用可能藏在 style → parent → AAR 的继承链里,排查时要一层层跟。
  3. 插件框架传错 Resources 的 ClassLoader 是根因,业务层反射修复是 workaround,不是治本。