docs/content/zh-Hans/blog/DevLog-2026.03.29/index.md
欢迎回来,这里是 @LemonNekoGH,AIRI 维护者之一,距离柠猫的上一篇 DevLog 已经过去一个月了,这段时间里又整了一点点小活,今天就来和大家分享一下。
我们在两个月前引入了 Capacitor 来构建基于 WebView 的原生移动端应用,以便利用移动端设备的一些特有功能,比如后台任务驻留、闹钟、日历、计步器什么的。
但是我们发现 Live2D 和 VRM 在 WebGL 上的性能表现不是特别好,而且内存占用偏高,加载一个 VRM 模型就会占用 700+ MB 的内存,这导致在部分设备上会直接崩溃,体验过于糟糕了。
于是我们开始寻找能渲染复杂 3D 场景的替代方案,柠猫负责的部分是调研 Godot 引擎,然而 Godot 的 UI 开发体验过于差了,很难做到现在 Web 页面这样的复杂度,而且几乎所有的 UI 都要重写,所以我在尝试在 Godot 画面前叠加 WebView 的方法,这样我们依然可以继续用现有的 UI 框架。
但是,到底该怎么做呢?
我找了一圈发现并没有什么比较合适的库所以我基于自己贫瘠的 Android 开发知识,拿到 Godot 所在的 Activity 的根 View,直接往根 View 顶上叠一个原生 WebView。
所幸,我通过 adb shell uiautomator dump 命令拿到了 Godot 所在的 Activity 的根 View 的 XML 结构,知道了根 View 是 FrameLayout,这样没有什么很复杂的布局代码要写了。
GodotApp.java 这个文件,它是 Godot 的入口类,我们在这个类中可以拿到 Godot 所在的 Activity 的根 View。
public class GodotApp extends Application {
// ...other code...
private final Runnable createWebView = () -> {
var rootView = (FrameLayout) this.findViewById(android.R.id.content).getRootView();
Log.d("createWebView", rootView.getClass().getName());
};
@Override
public void onGodotMainLoopStarted() {
super.onGodotMainLoopStarted();
runOnUiThread(createWebView);
}
// ...other code...
}
runOnUiThread 方法来确保在主线程执行。 var webview = new WebView(this);
webview.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
webview.getSettings().setJavaScriptEnabled(true);
webview.getSettings().setDomStorageEnabled(true);
webview.setWebContentsDebuggingEnabled(true);
// 对 AIRI 来说很重要,因为我们需要透过 UI 来看 Godot 的场景
webview.setBackgroundColor(Color.TRANSPARENT);
webview.loadUrl("https://lemonbookpro.local:5273/");
rootView.addView(webview);
如果它按预期运行,应该是能看到这样的效果的。
<video src="./assets/airi-pocket-android-godot-vrm-bg.mp4" autoplay loop muted></video>
按官方推荐的做法,其实我们应该写一个 Godot Android 插件的,但是柠猫这次为了快速验证这个想法,所以直接在 GodotApp.java 中写了。
然而,iOS 侧就没那么幸运了,只能写插件了。
iOS 侧在创建好插件之后,我们并不能在里面找到 AppDelegate 相关的代码,只能在插件配置文件里定义插件入口了:
[config]
name="GodotWebView"
binary="GodotWebView.xcframework"
initialization="init_godot_webview"
deinitialization="deinit_godot_webview"
这里的 initialization 和 deinitialization 是插件的初始化和销毁回调,需要在 Objective-C 中实现,所以无论如何我们也需要这么一点点桥来把 Swift 和 Objective-C 联系起来。
#import <Foundation/Foundation.h>
extern "C" void godot_webview_swift_init(void);
extern "C" void godot_webview_swift_deinit(void);
void init_godot_webview() {
godot_webview_swift_init();
}
void deinit_godot_webview() {
godot_webview_swift_deinit();
}
类似的,在 iOS 上也需要找到主窗口的根视图:
private func resolveHostWindow() -> UIWindow? {
let activeScenes = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.filter { scene in
scene.activationState == .foregroundActive || scene.activationState == .foregroundInactive
}
logInfo("Resolving host window; activeSceneCount=\(activeScenes.count)")
for scene in activeScenes {
let windows = scene.windows
logInfo(
"Inspecting scene \(describe(scene: scene)); windowCount=\(windows.count); windows=\(windows.map { describe(view: $0) }.joined(separator: ", "))"
)
if let keyWindow = windows.first(where: \.isKeyWindow) {
logInfo("Selected key window \(describe(view: keyWindow))")
return keyWindow
}
if let firstWindow = windows.first {
logInfo("Selected first window \(describe(view: firstWindow))")
return firstWindow
}
}
logError("No eligible foreground scene/window found")
return nil
}
创建 WebView 实例的方法也是类似的:
let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.allowsInlineMediaPlayback = true
webViewConfiguration.defaultWebpagePreferences.allowsContentJavaScript = true
let webView = WKWebView(frame: .zero, configuration: webViewConfiguration)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear
webView.scrollView.contentInsetAdjustmentBehavior = .never
webView.accessibilityIdentifier = "GodotWebView"
containerView.addSubview(webView)
pinToEdges(webView, in: containerView)
AI 教了我一种用 yml 来描述项目的工具 xcodegen 这样我们可以不在项目里存一大堆 Xcode 工程文件了,但是我还没找到什么可以像 Android 那样拿到视图树的方法。
到目前为止,这还���是成功叠上了 WebView,我们还没有实现 Godot 和 WebView 的通信,比如点击事件、键盘输入、触摸事件等等。
还有 Live2D、VRM 模型的渲染,我已经把它们放进去了,也进行了 Profile,但是这是下一篇 DevLog 的内容了。
今天的内容就到这里,感谢看到这里。