一、背景与目标
- 目标:把 Go 核心服务通过 gomobile 以 AAR 形式内嵌进 Android Flutter 客户端,让普通我在手机上开箱即用,而不是先手动起一个独立的 HTTP 核心。
- 模式约定:
core_mode = external:客户端通过 HTTP 访问外部核心。core_mode = embedded:客户端在本机进程里拉起 Go 核心,通过127.0.0.1回环访问。
- 本次只做“初步能跑”的版本,重点是:
- 跑通 gomobile 构建。
- 在 Android 默认使用内嵌核心。
- 有足够调试信息可以排查问题(尤其是 S3 连接)。
二、核心集成路径概览
1. Go 侧:mobile 包包装 HTTP 核心
- 在
core/mobile/mobile.go里提供用于 gomobile 的导出函数:StartWithParams(...)用于从 Java 侧传入 S3 参数和端口,然后调用内部的Start(*CoreConfig)。Start内部基于config.DefaultConfig()生成配置,根据传入配置覆盖 S3 endpoint、AK/SK、bucket、UseSSL 以及 server 端口,最后通过api.NewServer创建 HTTP 服务器并监听127.0.0.1:<port>。Stop通过http.Server.Shutdown优雅关闭,避免进程残留。
- 这样 gomobile 导出的 API 就非常简单:
startWithParams+stop,内部仍然是原来那套 HTTP 核心。
2. gomobile bind:生成 Android AAR
-
以
core模块为工作目录(core/go.mod里 module 名是e2eepan),使用 Android Studio 自带 JBR 和本机 SDK/NDK:- 设置环境变量:
ANDROID_HOME=D:\Android\SdkANDROID_NDK_HOME=D:\Android\Sdk\ndk\28.2.13676358PATH前缀追加D:\Android\Android Studio\jbr\bin
- 在
core下执行:gomobile bind -target=android -androidapi=21 -o ..\client\android\app\libs\e2eepan-mobile.aar e2eepan/mobile
- 设置环境变量:
-
早期踩坑:
- 一开始在仓库根目录执行,
gomobile找不到go.mod,提示“cannot find main module”,后来改成在core目录执行,按模块名e2eepan/mobile调用后正常。 - PowerShell 中不能用
&&链接命令,这个也踩过一次,后续都用单条命令 + 显式cwd解决。
- 一开始在仓库根目录执行,
-
Android 侧通过
client/android/app/build.gradle.kts中的:implementation(files("libs/e2eepan-mobile.aar"))将 AAR 引入工程。
3. Android 侧:MethodChannel 驱动 Go 内核
- 在
client/android/app/src/main/kotlin/com/e2eepan/e2eepan_client/MainActivity.kt中:- 创建
MethodChannel("e2eepan/core"),接收 Flutter 端调用。 - 支持两个方法:
startEmbeddedCore:读取 Flutter 传下来的 S3 endpoint、AK/SK、bucket、UseSSL 和端口,启一个后台线程调用Mobile.startWithParams(...)。stopEmbeddedCore:同样在后台线程中调用Mobile.stop()。
- 将 Go 启动/关闭都放到子线程,避免在主线程阻塞导致 ANR。
- 创建
- 这样 Android 部分只负责“转译参数 + 在线程里调用 gomobile”,逻辑比较干净。
4. Flutter 侧:AppState 驱动内嵌核心
- 在
client/lib/core/services/native_core_service.dart中:- 定义
MethodChannel('e2eepan/core')。 - 暴露静态方法:
startEmbeddedCore(Map<String, dynamic> config)stopEmbeddedCore()
- 定义
- 在
client/lib/core/state/app_state.dart中:- 增加
_coreMode字段,默认:- Android 默认
'embedded'。 - 其他平台默认
'external'。 - 同步到
SharedPreferences('core_mode'),可调。
- Android 默认
- 启动流程
_initCoreAndNetwork():await _ensureCoreModeLoaded();await _maybeStartEmbeddedCore();await _checkInitialNetworkStatus();
_maybeStartEmbeddedCore在core_mode == 'embedded'且平台是 Android 时:- 组装 S3 配置(来自设置页 / 默认值)和
api.baseUrl。 - 调用
NativeCoreService.startEmbeddedCore(config)。 - 提供
suppressError参数,在正常启动路径中吞掉异常,在调试场景中向外抛出。
- 组装 S3 配置(来自设置页 / 默认值)和
restartEmbeddedCoreIfNeeded用于 S3 配置变更后优雅重启:- 若模式是 embedded:先
stopEmbeddedCore,再_maybeStartEmbeddedCore()。
- 若模式是 embedded:先
- 增加
三、健康检查与 S3 故障暴露
1. 后端 /health 增强
- 原有逻辑:
/health只检查 S3 是否可 Ping,失败时返回status=error和错误字符串。 - 本次调整:
- 在
core/internal/api/server.go的healthCheck中,将 S3 失败时的响应扩展为:status: "error"component: "s3"code: "S3_UNAVAILABLE"error: 具体错误
- 正常时仍返回
status: "ok"和时间戳。
- 在
- 这样客户端在只调用
/health的前提下也能区分:- 核心没起 / 网络不通。
- 核心在,但 S3 掉线或配置错误。
2. 客户端健康检查细化
- 在
client/lib/core/api/api_client.dart中:- 保留原来的
healthCheck(),仍然只返回bool,给老逻辑用。 - 新增
getHealthDetail():- 成功时直接返回
/health的 JSON。 - 失败但有 JSON body(例如 503 时)也尝试解析并返回。
- 无法连接时返回
null。
- 成功时直接返回
- 保留原来的
- 在
AppState中:- 增加
_healthErrorCode字段,对外提供healthErrorCodegetter。 - 构造函数里的 15 秒心跳逻辑改为:
- 调用
api.getHealthDetail()。 - 若返回
status == "ok":认为在线,_isOffline=false,_healthErrorCode=null。 - 若返回
status == "error":认为离线,_isOffline=true,记录code(如S3_UNAVAILABLE)。 - 若返回
null:认为离线但不知道具体原因。
- 调用
_checkInitialNetworkStatus()也切换到getHealthDetail(),保证一启动就能拿到更细的信息。
- 增加
3. 文件页上的错误提示区分
- 在
client/lib/ui/home_page.dart中的空态渲染_buildEmptyPlaceholder(AppState state):- 增加本地判断:
s3Failed = state.isOffline && state.healthErrorCode == 'S3_UNAVAILABLE'
- 根据
s3Failed决定文案:- 如果离线且
s3Failed:- 标题显示
S3 连接失败 - 副标题显示
请检查存储服务配置后重试
- 标题显示
- 如果离线但不是 S3 错误:
- 保持原来的
内核未启动 / 请启动内核后刷新
- 保持原来的
- 在线保持原有“这里空空如也 / 点击右下角按钮上传文件”的文案。
- 如果离线且
- 增加本地判断:
- 效果:
- 内核没起来 / 端口不通:我这边看到的是“内核未启动”。
- 内核在但 S3 配置错误或服务挂掉:我这边看到的是“S3 连接失败”,定位更精准。
四、调试入口与配置集中
1. 新增 DebugPage
- 在
client/lib/ui/debug_page.dart新增一个独立页面,专门放调试功能:- “重启内核”:
- 调用
AppState.debugRestartEmbeddedCore()停止并强制启动内嵌核心。 - 再通过
AppState.debugCheckCoreStatus()组合输出当前核心模式、baseUrl、S3 配置、health 和 bootstrap 状态。 - 结果通过 SnackBar 弹出,便于快速查看。
- 调用
- “设置内核地址”:
- 复用原来隐藏在“关于”里的地址弹窗,读取/写入
core_base_url。 - 可以恢复默认地址,也可以指向局域网其它核心。
- 复用原来隐藏在“关于”里的地址弹窗,读取/写入
- “重启内核”:
- 这两个调试项都集中在单独文件里,随时可以整体移除,不和业务逻辑强耦合。
2. 设置页只保留正式入口
client/lib/ui/settings_page.dart做了瘦身:- 调试相关逻辑移除:
- 删除了隐藏 5 连点“关于”触发核心地址弹窗的彩蛋。
- 删除了设置页里“重启内核”的直接入口。
- 新增一个轻量入口:
- 在“调试”分区只保留一个
调试选项的ListTile,点击后Navigator.push到DebugPage。
- 在“调试”分区只保留一个
- S3 配置对话框:
- 仍然在设置页中提供。
- 修改完配置后调用
appState.restartEmbeddedCoreIfNeeded()尝试重启内嵌核心,并给出提示。
- 调试相关逻辑移除:
五、构建 / 运行流程总结
当前一套“从源码到手机上运行内嵌核心”的标准流程:
-
在 core 下重建 gomobile AAR
- 工作目录:
core - 设置
ANDROID_HOME/ANDROID_NDK_HOME/PATH指向 Android Studio JBR 和 SDK/NDK。 - 执行:
gomobile bind -target=android -androidapi=21 -o ..\client\android\app\libs\e2eepan-mobile.aar e2eepan/mobile
- 工作目录:
-
运行 Flutter 客户端
- 工作目录:
client - 执行:
flutter run
- 选择实际设备(本次是 2311DRK48C),安装并以 debug 模式运行。
- 工作目录:
-
在手机上验证
- 打开 App,默认在 Android 上会选择
core_mode = embedded。 - 在设置 → 调试选项:
- 验证“重启内核”是否正常工作。
- 查看调试输出里的
core_mode/api.baseUrl/S3配置和health/bootstrap状态。
- 在文件页:
- 如果 S3 配置错误或 MinIO 没起,空态应该显示 “S3 连接失败”。
- 如果内核没起 / 端口不通,则显示 “内核未启动”。
- 打开 App,默认在 Android 上会选择
六、本次集成中的几个关键坑
-
gomobile 工作目录与 module 名
- 一开始在仓库根目录执行
gomobile bind,因为根目录没有go.mod,导致go: cannot find main module。 - 正确方式是在
core目录执行,并使用 module 名e2eepan/mobile。
- 一开始在仓库根目录执行
-
PowerShell 的命令组合
- 直接写
cd ... && gomobile ...会因为&&不被当前 PowerShell 版本支持而报错。 - 后面统一使用单条命令并通过工具设置
cwd,避免在 shell 里叠加多条。
- 直接写
-
S3 地址 vs 实际网络拓扑
- 最初健康检查
health: FAILED,结果发现根因是:- 本地
s3key.txt中配置的 MinIO 地址属于宿主机子网。 - 虚拟机网络模式变动后,MinIO 实际跑在
192.168.1.4,客户端仍连老的192.168.74.101。
- 本地
- 调整方式:
- 更新
s3key.txt和默认配置中的 endpoint。 - 再次构建并部署,健康检查恢复正常。
- 更新
- 最初健康检查
-
ANR 与线程模型
- 如果在
MainActivity主线程直接调用Mobile.startWithParams,会在核心启动或 S3 检查耗时较长时导致 ANR。 - 通过
Thread { Mobile.startWithParams(...) }.start()将调用放到后台线程,避免阻塞 UI 线程。
- 如果在
七、后续可以做的事情
-
统一健康检查接口的使用
- 目前有
ApiClient.healthCheck()和ApiClient.getHealthDetail()两条路径,后续可以考虑逐步迁移到后者,以便在更多场景下区分具体错误。
- 目前有
-
NativeCoreService 真正落地
- 当前内嵌内核仍然通过 HTTP 回环访问,将来可以尝试在 Flutter 侧引入真正的 FFI 调用路径,减少 HTTP 层开销。
- 但无论是 HTTP 还是 FFI,都可以复用这次梳理出的:
healthCheck/bootstrap语义。- AppState 中对在线/离线和初始化状态的统一处理。
-
更多调试工具
- 在
DebugPage里继续增加一些调试项,例如:- 一键打印
/health和/bootstrap原始 JSON。 - 显示当前使用的
core_mode和core_base_url。
- 一键打印
- 保持调试页面与业务弱耦合,方便在生产构建中关闭。
- 在
本次内嵌内核的初步集成,已经实现了“内核跟着 App 一起跑、S3 问题可视化”这两个核心目标,为后续彻底切换到 NativeCore / FFI 打下了比较清晰的基础。