背景知识
ClassLoader:类的"图书管理员"
Android 中,类加载器负责把 .class 文件加载到内存。 常见的有两种:
View、TextView | ||
如果把类比作书,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.mClassLoaderLayoutInflater 和 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普通 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 == null) return;try { Resources resources = pluginContext.getResources(); Class<?> clazz = Class.forName("android.graphics.drawable.DrawableInflater"); Constructor<?> c = clazz.getDeclaredConstructor(Resources.class, ClassLoader.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);}总结
LayoutInflater 和 DrawableInflater 用不同的 ClassLoader,需要对两个加载器都要修改。 drawable 引用可能藏在 style → parent → AAR 的继承链里,排查时要一层层跟。 插件框架传错 Resources 的 ClassLoader 是根因,业务层反射修复是 workaround,不是治本。
夜雨聆风