Skip to content

OpenIM 集成指南

本指南介绍如何将 Dynamic Domain SDK 与 OpenIM 结合使用,以解决 IM 消息在特殊网络环境下的封锁问题。

集成思路

OpenIM 的核心逻辑运行在原生层(Go 核心),它不会自动接管 Flutter 的代理设置。因此,我们需要:

  1. 启动 Dynamic Domain 隧道获取本地 SOCKS5 端口。
  2. 将该端口显式传递给 OpenIM SDK。

代码实现

1. 启动并获取代理配置

dart
final dynamicDomain = DynamicDomain();
await dynamicDomain.init("your_app_id");

// 获取配置并启动隧道
String config = await dynamicDomain.fetchRemoteConfig();
await dynamicDomain.startTunnel(config);

// 获取结构化的代理配置
final proxy = dynamicDomain.getProxyConfig();
if (proxy != null) {
  print("HTTP 代理: ${proxy.httpUrl}");
  print("SOCKS5 代理: ${proxy.socksUrl}");
}

2. 配置 OpenIM SDK

在初始化 OpenIM 时,根据其提供的 API 设置代理。

方案 A:一键应用环境变量 (最简便)

由于 OpenIM 的核心是用 Go 编写的,Go 运行时默认会读取进程的环境变量。我们提供了 applyToEnvironment() 方法来自动完成这一操作。

dart
if (proxy != null) {
  // 一键将代理应用到 ALL_PROXY, HTTP_PROXY 等环境变量
  // 这会让底层的 Go Core 自动识别并使用隧道
  await proxy.applyToEnvironment();
}

方案 B:手动显式设置代理 (最稳健)

如果您的 OpenIM SDK 版本支持设置代理服务器地址,请务必优先使用 SOCKS5 协议,因为它对长连接(WebSocket)的支持更稳定。

dart
// 伪代码示例,具体取决于 OpenIM Flutter SDK 的 API
OpenIM.iMManager.setProxy(
  proxyUrl: proxy.socksUrl, 
  type: 'socks5'
);

await OpenIM.iMManager.initSDK(
  platform: ...,
  apiAddr: "https://your-im-api.com",
  wsAddr: "ws://your-im-ws.com",
  ...
);

为什么使用 SOCKS5?

OpenIM 的核心通讯依赖 WebSocket。

  • HTTP 代理:主要针对短连接请求,对长连接的转发效率较低,且容易断连。
  • SOCKS5 代理:在传输层进行转发,完美支持 TCP/UDP 和 WebSocket,是 IM 场景的首选。

开发建议 (Pro Tips)

1. 时序控制

确保在调用 OpenIM.iMManager.initSDK 之前 已经完成了 proxy.applyToEnvironment()。如果 OpenIM 已经启动了连接线程,之后设置的环境变量可能不会立即生效。

2. 证书 Pinning 冲突

如果您的 OpenIM 服务端开启了严格的证书校验(SSL Pinning),经过隧道代理时可能会因为证书指纹改变而报错。此时建议:

  • DynamicDomain 中配置该域名为“直连”(如果该域名未被封锁)。
  • 或者在 OpenIM SDK 中信任隧道的本地 CA(如果适用)。

3. 自动重连逻辑

IM 场景对连接敏感。当 Dynamic Domain 触发自动重连或切换节点时,本地代理端口可能会有短暂的瞬断。

  • 建议:在 OpenIM 的连接回调中监听 onConnectFailed,并配合 dynamicDomain.isConnectionHealthy() 进行智能重连尝试。

Android 依赖冲突处理

在 Android 端同时集成 OpenIM SDK 和 Dynamic Domain SDK 时,由于两者均基于 Gomobile 构建,会产生 go.Seq 等 Go 运行时类的命名空间冲突。表现为编译报错:Duplicate class go.Seq found in modules...

请根据您的 Android Gradle Plugin (AGP) 版本,按以下优先级尝试解决方案:

方案一:使用 pickFirst (最通用)

android/app/build.gradleandroid 闭包内添加:

gradle
android {
    packagingOptions {
        // 强制 Gradle 在遇到冲突类时选择第一个发现的,忽略后续冲突
        pickFirst 'go/Seq.class'
        pickFirst 'go/Seq$GoObject.class'
        pickFirst 'go/Seq$GoRef.class'
        pickFirst 'go/Seq$GoRefQueue.class'
        pickFirst 'go/Seq$GoRefQueue$1.class'
        pickFirst 'go/Seq$Proxy.class'
        pickFirst 'go/Seq$Ref.class'
        pickFirst 'go/Seq$RefMap.class'
        pickFirst 'go/Seq$RefTracker.class'
        pickFirst 'go/Universe.class'
        pickFirst 'go/Universe$proxyerror.class'
        pickFirst 'go/error.class'
    }
}

方案二:通过 configurations 强制排除 (推荐)

android/app/build.gradle 的末尾添加配置,从依赖管理层面允许忽略重复类:

gradle
configurations.all {
    // 允许 Gradle 忽略特定模块(如果冲突类来自某个可识别的 Maven 模块)
    exclude group: 'com.openim.flutter_sdk', module: 'openim_flutter_sdk'
    
    resolutionStrategy {
        // 强制所有冲突的库指向同一个版本(仅当它们来自同一个 Maven 库的不同版本时有效)
        force 'io.openim:core-sdk:3.8.3-patch3' 
    }
}

方案三:使用 packaging.resources.excludes (针对 AGP 7.0+)

对于较新版本的 AGP,可以使用更现代的 packaging 语法:

gradle
android {
    packaging {
        resources {
            excludes += "go/Seq.class"
            excludes += "go/Universe.class"
            excludes += "go/error.class"
            // 添加所有报错中提到的 go/ 开头的 .class 文件
        }
    }
}

方案四:跳过重复类检查任务 (强力方案)

此方案通过禁用 Gradle 的 checkDebugDuplicateClasses 任务来跳过编译前的静态检查。

gradle
project.tasks.configureEach { task ->
    if (task.name.contains("check") && task.name.contains("DuplicateClasses")) {
        task.enabled = false
    }
}

方案五:终极剥离方案 (物理移除冲突类)

适用场景:如果您看到 DexArchiveMergerException: Error while merging dex archivesD8: Type go.Seq$GoObject is defined multiple times,说明方案四虽然跳过了检查,但 D8 编译器在合并代码时依然发现了物理冲突。这是因为 Gomobile 将 Go 运行时硬编码在了 AAR 内部。

核心思路:手动或通过脚本从 tunnel_core.aar 中彻底删除 go/ 运行时目录,使其共用 OpenIM 中的运行时。

操作步骤:

  1. 进入 SDK 目录cd dynamic_domain/android/libs
  2. 执行剥离逻辑(建议使用脚本 strip_go.sh):
bash
#!/bin/bash
# 用法: ./strip_go.sh tunnel_core.aar
AAR_NAME=$1
STRIPPED_NAME="${AAR_NAME%.aar}_stripped.aar"

# 1. 解压 AAR
mkdir -p temp_aar && unzip $AAR_NAME -d temp_aar
# 2. 解压内部 jar
mkdir -p temp_jar && unzip temp_aar/classes.jar -d temp_jar
# 3. 物理删除冲突的 Go 运行时
rm -rf temp_jar/go/
# 4. 重新打包 jar
cd temp_jar && zip -r ../temp_aar/classes.jar . && cd ..
# 5. 重新打包 AAR
cd temp_aar && zip -r ../$STRIPPED_NAME . && cd ..
# 6. 清理
rm -rf temp_aar temp_jar

echo "剥离成功!请在项目中引用: $STRIPPED_NAME"
  1. 更新引用:在 Flutter 项目中,将对 tunnel_core.aar 的引用改为生成的 tunnel_core_stripped.aar

为什么这样是安全的?

go.Seq 等类是 Gomobile 的通用底层代码。剥离后,我们的 SDK 在运行时会自动调用 OpenIM SDK 内部相同的类,由于版本兼容性极高,两者可以完美共存。

注意

修改 Gradle 配置或替换 AAR 后,必须执行以下清理步骤以确保生效:

bash
flutter clean
flutter run

常见问题

Q: 接入后消息依然发送失败? A: 请确认您的 OpenIM 服务端域名是否在 DynamicDomain 的分流白名单中。如果服务端在国内,请确保配置为“直连”;如果在海外,请确保走“隧道代理”。


验收与验证

完成集成后,请务必参考 集成验证清单 进行逐项核对,确保隧道已生效且 OpenIM 流量正常转发。

重要测试说明 (必须阅读)

只能在真机环境测试

由于 Dynamic Domain SDK 涉及底层网络协议(VLESS/REALITY)及原生层(Go/Gomobile)的深度集成,请务必使用物理真机进行调试,并确保通过数据线连接到电脑。

模拟器运行报错

如果您在 iOS 或 Android 模拟器上运行,会出现以下几种典型的异常报错,导致连接断开或崩溃:

1. 核心运行时缺失 (iOS 模拟器)

text
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Invalid argument(s): Couldn't resolve native function 'DOBJC_initializeApi' in 'package:objective_c/objective_c.dylib'
Failed to load dynamic library 'objective_c.framework/objective_c'

原因:模拟器环境缺少必要的原生 FFI 链接库支持。

2. Hive 存储异常 (常见于 OpenIM 组件)

text
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: HiveError: You need to initialize Hive or provide a path to store the box.
#0      BackendManager.open (package:hive/src/backend/vm/backend_manager.dart:21:7)
#3      CacheController.onInit (package:openim_common/src/controller/cache_controller.dart:121:30)

原因:模拟器的文件系统权限或路径映射与真机不一致,导致 OpenIM 依赖的 Hive 缓存组件无法正确初始化。

3. 后台进入前台回调失败

text
flutter: CallHandler method:appEnterForeground, arguments:null

原因:模拟器无法模拟真机的应用生命周期管理,导致 SDK 的心跳恢复逻辑异常。

解决方案

遇到上述报错时,请检查:

  1. 是否使用了 真机
  2. 是否使用了 物理数据线 连接(避免无线调试导致的网络波动干扰)。
  3. 确保真机已经过 flutter cleanflutter run 的完整编译流程。

基于 MIT 许可发布