Shadow icon indicating copy to clipboard operation
Shadow copied to clipboard

将 PluginContainerActivity 改为继承 FragmentActivity 时遇到的类加载问题

Open hangox opened this issue 1 month ago • 4 comments

背景

我们有一个需求:宿主会传递一个 WebView 给插件使用,WebView 有时需要启动 Dialog。Dialog 需要 FragmentActivityAppCompatActivity 作为 Context,而当前 PluginContainerActivity 继承自普通 Activity,导致 Dialog 无法正常弹出。

因此我尝试将 PluginContainerActivity 改为继承 FragmentActivity

已完成的修改

  1. 修改 ActivityCodeGenerator.kt,将 GeneratedPluginContainerActivity 的父类从 Activity 改为 FragmentActivity
  2. 添加 FragmentActivity stub 类用于编译
  3. 处理 ComponentActivity 的 final 方法(如 onRetainNonConfigurationInstance
  4. 解决 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 类存在版本冲突或类加载器隔离问题。

问题

  1. 我的修改方向是否有问题?作者能给一个大概要修改的类的路线吗?
  2. 如果要让 PluginContainerActivity 继承 FragmentActivity,在 Shadow 框架中还有需要注意哪些地方?

环境信息

  • Shadow 版本:基于 2024-12-25 的 master 分支(commit 09e7e0a6)
  • androidx.fragment 版本:1.4.1

hangox avatar Dec 12 '25 04:12 hangox

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。

shifujun avatar Dec 12 '25 06:12 shifujun

作者不好意思,我没讲好我们遇到的问题,你说的事情我们是清楚的,我们要用 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 能力

hangox avatar Dec 12 '25 08:12 hangox

因为太长了,我分开讲。 我们在调研这个方案,因为目前的问题在于,根上是 Activity 的父类问题,所以我们是想从根上解决这个问题,避免以后增加 js 方法还会遇到这个问题。 如果大致思路确认后其实很难改或者不好改,我们也只能接受一个个 js 修改了。

@shifujun

hangox avatar Dec 12 '25 08:12 hangox

androidx里有很多资源文件,所以它不适合在宿主和插件间共享。所以fragmentactivity只能宿主和插件分别打包一个,这样它们就是两个完全不同的类。通过白名单让插件去用宿主的类不适合这种带资源的类。

你这个webview依赖了fragmentactivity这个有资源的类,直接把它交给插件也是不合适的。你这里明明就只是想复用一些JS bridge逻辑。应该只把这些逻辑形成一个类交给插件复用。而且这些代码应该体积也不大吧。懒得改代码的话,你可以考虑直接让插件也打包一份webview代码。

再或者你考虑清楚插件代码到底要动态哪些逻辑。为什么非得要把一个对宿主有这么多依赖的webview塞给插件,而不是让插件注入一些逻辑给宿主?

不管怎样,你改containeractivity的父类都是不明智的。我更推荐用一些AOP编程手段把你的webview重构一下。把对fragmentactivity的依赖改为对接口的依赖,然后让宿主和插件分别注入自己的fragmentactivity实现。

shifujun avatar Dec 12 '25 11:12 shifujun