将 PluginContainerActivity 改为继承 FragmentActivity 时遇到的类加载问题
背景
我们有一个需求:宿主会传递一个 WebView 给插件使用,WebView 有时需要启动 Dialog。Dialog 需要 FragmentActivity 或 AppCompatActivity 作为 Context,而当前 PluginContainerActivity 继承自普通 Activity,导致 Dialog 无法正常弹出。
因此我尝试将 PluginContainerActivity 改为继承 FragmentActivity。
已完成的修改
- 修改
ActivityCodeGenerator.kt,将GeneratedPluginContainerActivity的父类从Activity改为FragmentActivity - 添加
FragmentActivitystub 类用于编译 - 处理
ComponentActivity的 final 方法(如onRetainNonConfigurationInstance) - 解决
KeyEventDispatcher导致的无限递归问题(覆写superDispatchKeyEvent)
遇到的问题
在 sample 项目中测试正常,但在实际项目中遇到以下问题:
问题 1:使用 compileOnly 依赖时类找不到
在 plugin-runtime 中使用 compileOnly 'androidx.fragment:fragment:1.4.1',加载插件时报错:
Failed resolution of: Lcom/tencent/shadow/core/runtime/container/PluginContainerActivity;
问题 2:使用 implementation 依赖时方法不可访问
改为 implementation 'androidx.fragment:fragment:1.4.1' 后,启动时崩溃:
Method 'androidx.lifecycle.LifecycleEventObserver androidx.lifecycle.Lifecycling.lifecycleEventObserver(java.lang.Object)' is inaccessible to class 'androidx.lifecycle.LifecycleRegistry$ObserverWithState' (declaration of 'androidx.lifecycle.LifecycleRegistry$ObserverWithState' appears in /data/app/~~ZsI_dEdXQCfBzCzBCwZ9fg==/com.netease.gl-T_n2e4Q6M7HhQCr2S2jokw==/base.apk)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.<init>(LifecycleRegistry.java:353)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:180)
这个错误看起来是宿主和插件的 androidx.lifecycle 类存在版本冲突或类加载器隔离问题。
问题
- 我的修改方向是否有问题?作者能给一个大概要修改的类的路线吗?
- 如果要让
PluginContainerActivity继承FragmentActivity,在 Shadow 框架中还有需要注意哪些地方?
环境信息
- Shadow 版本:基于 2024-12-25 的 master 分支(commit 09e7e0a6)
- androidx.fragment 版本:1.4.1
PluginContainerActivity是一个面向系统暴露的真正的Activity,它持有动态加载的插件代码,把系统回调转调给插件中假的Activity(ShadowActivity)。
FragmentActivity作为androidx的一部分,它只是一个第三方库的代码,跟你自己写的任何Activity也没什么区别。如果你在插件里用它的话,它最终的父类也会是ShadowActivity,然后被一个PluginContainerActivity持有。
如果你理解上面说的,你应该就不会考虑把PluginContainerActivity的父类改掉了。
然后你原本的需求也就是弹出一个Dialog而已。由WebView发起似乎也没什么特别的,就一个View而已。你这个Dialog可能也是指的androidx中的Dialog,而不是系统原本的吧。但那也没关系,你只要插件的Activity就是androidx的FragmentActivity就应该可以正常用。
奇怪的只是从宿主传给插件一个WebView吧。我也不愿意想太细,你这个webview是怎么创建怎么发起弹出dialog的。你难道不能让这个Webview直接通知它所在的插件Activity,让这个插件Activity弹出dialog吗?从而避免用webview的context。
作者不好意思,我没讲好我们遇到的问题,你说的事情我们是清楚的,我们要用 framgentActivity 有我们的理由,可以看看下面我们的理由是什么
为什么需要 PluginContainerActivity 继承 FragmentActivity
一句话总结
宿主的 HostWebView(封装了 JS Bridge)传递给插件使用时,JS Bridge 弹窗需要 FragmentActivity 作为 Context,但 PluginContainerActivity 继承自 Activity,导致弹窗崩溃。
第一步:理解 HostWebView 是什么
宿主应用封装了一个 HostWebView,它不是普通的 WebView,而是一个内置 JS Bridge 的增强版 WebView。
flowchart LR
subgraph HostWebView["HostWebView (宿主封装)"]
WV["WebView"]
JSB["JS Bridge"]
subgraph "JS Bridge 提供的原生能力"
S1["bridge.share() - 分享"]
S2["bridge.pay() - 支付"]
S3["bridge.scan() - 扫码"]
S4["bridge.chooseImage() - 选图"]
S5["bridge.getLocation() - 定位"]
end
end
WV --> JSB
JSB --> S1 & S2 & S3 & S4 & S5
第二步:理解复用需求
核心需求:同一个 H5 页面,不改代码,既能在宿主中跑,也能在插件中跑。
flowchart TB
subgraph H5["H5 页面 (同一份代码)"]
Code["JS: bridge.share(data)"]
end
subgraph Host["在宿主中运行"]
HA["宿主 Activity"]
HW1["HostWebView"]
end
subgraph Plugin["在插件中运行"]
PA["插件 Activity"]
HW2["HostWebView<br/>(宿主传入,复用)"]
end
Code -->|"加载到"| HW1
Code -->|"加载到"| HW2
为什么要复用 HostWebView?
- 插件不需要重新实现一套 JS Bridge
- 保持 H5 页面调用方式一致
- 减少重复代码和维护成本
第三步:理解 JS Bridge 为什么需要弹窗
JS Bridge 的很多功能实现都需要弹窗:
| JS 调用 | 原生实现 | 需要的弹窗 |
|---|---|---|
bridge.share(data) |
调用系统/第三方分享 | BottomSheetDialog(分享面板) |
bridge.chooseImage() |
选择图片 | BottomSheetDialog(拍照/相册) |
bridge.pickDate() |
选择日期 | DatePickerDialog |
bridge.requestPermission() |
申请权限 | AlertDialog(权限说明) |
bridge.pay() |
支付 | Dialog(支付确认) |
flowchart LR
JS["H5 调用<br/>bridge.share()"] --> Native["HostWebView<br/>JS Bridge"]
Native --> Dialog["弹出<br/>BottomSheetDialog"]
Dialog --> UI["显示分享面板<br/>微信/QQ/微博..."]
第四步:理解弹窗为什么需要 FragmentActivity
HostWebView 中的弹窗代码是历史代码,已经写成了依赖 FragmentActivity 的方式:
// HostWebView 中的分享弹窗实现(历史代码)
fun showShareDialog() {
val activity = context as FragmentActivity // 直接强转
val dialog = ShareBottomSheetDialogFragment()
dialog.show(activity.supportFragmentManager, "share")
}
为什么不改 HostWebView 的代码?
- 现代 Android 开发中,几乎所有弹窗都依赖
FragmentManager,这是标准写法 - 这类代码在项目中非常多,分享、选图、权限等功能都这样写
- 全部改成兼容普通
Activity的方式,工作量巨大 - 宿主中运行正常,没有动力去改
- 改动可能引入新 bug,风险高
第五步:问题发生的完整链路
sequenceDiagram
participant H5 as H5 页面
participant JS as JS Bridge
participant HW as HostWebView
participant PCA as PluginContainerActivity
participant Dialog as BottomSheetDialog
Note over H5,Dialog: 同一个 H5 页面,在插件中运行
H5->>JS: bridge.share({title: "分享"})
JS->>HW: 调用原生分享功能
HW->>PCA: getContext()
PCA-->>HW: 返回 PluginContainerActivity<br/>(继承自 Activity)
HW->>Dialog: new BottomSheetDialog(context)
Dialog->>PCA: getSupportFragmentManager()
Note over Dialog,PCA: Activity 没有这个方法!
Dialog-->>HW: 崩溃!ClassCastException 或<br/>IllegalStateException
第六步:为什么在宿主中正常,在插件中崩溃?
在宿主中运行 - 正常
flowchart LR
HW1["HostWebView"] -->|"getContext()"| HA["宿主 Activity<br/>(继承 FragmentActivity)"]
HA -->|"getSupportFragmentManager()"| D1["BottomSheetDialog"]
D1 -->|"正常显示"| OK_Result["分享面板"]
style OK_Result fill:#ccffcc
在插件中运行 - 崩溃
flowchart LR
HW2["HostWebView<br/>(宿主传入)"] -->|"getContext()"| PCA["PluginContainerActivity<br/>(继承 Activity)"]
PCA -->|"没有这个方法!"| D2["BottomSheetDialog"]
D2 -->|"崩溃"| FAIL_Result["ClassCastException"]
style FAIL_Result fill:#ffcccc
原因:
- 宿主的 Activity 通常继承
AppCompatActivity(它继承自FragmentActivity) - Shadow 的
PluginContainerActivity继承自普通Activity - 同一个 HostWebView,在不同 Context 下表现不同
解决方案
将 PluginContainerActivity 改为继承 FragmentActivity:
flowchart LR
subgraph Before["修改前"]
B1["PluginContainerActivity"]
B2["extends Activity"]
B3["没有 getSupportFragmentManager()"]
end
subgraph After["修改后"]
A1["PluginContainerActivity"]
A2["extends FragmentActivity"]
A3["有 getSupportFragmentManager()"]
end
Before -->|"修改继承关系"| After
style B3 fill:#ffcccc
style A3 fill:#ccffcc
修改后效果:
- HostWebView 在插件中也能正常弹出分享面板、日期选择器等
- H5 页面无需任何修改,在宿主和插件中都能正常运行
- 插件可以完整复用宿主的 HostWebView 能力
因为太长了,我分开讲。 我们在调研这个方案,因为目前的问题在于,根上是 Activity 的父类问题,所以我们是想从根上解决这个问题,避免以后增加 js 方法还会遇到这个问题。 如果大致思路确认后其实很难改或者不好改,我们也只能接受一个个 js 修改了。
@shifujun
androidx里有很多资源文件,所以它不适合在宿主和插件间共享。所以fragmentactivity只能宿主和插件分别打包一个,这样它们就是两个完全不同的类。通过白名单让插件去用宿主的类不适合这种带资源的类。
你这个webview依赖了fragmentactivity这个有资源的类,直接把它交给插件也是不合适的。你这里明明就只是想复用一些JS bridge逻辑。应该只把这些逻辑形成一个类交给插件复用。而且这些代码应该体积也不大吧。懒得改代码的话,你可以考虑直接让插件也打包一份webview代码。
再或者你考虑清楚插件代码到底要动态哪些逻辑。为什么非得要把一个对宿主有这么多依赖的webview塞给插件,而不是让插件注入一些逻辑给宿主?
不管怎样,你改containeractivity的父类都是不明智的。我更推荐用一些AOP编程手段把你的webview重构一下。把对fragmentactivity的依赖改为对接口的依赖,然后让宿主和插件分别注入自己的fragmentactivity实现。