第4章 双端共用核心模块设计(数据模型、常量定义、工具类)
双端 SDK 共用核心模块,说白了就是让 Android 和 iOS 两端「说同一种方言」。我的经验是,很多项目一开始各写各的,等联调时才发现字段名对不上、状态码含义不同,改起来比重新写还痛苦。
这一章我带你梳理哪些东西该抽成共用模块,怎么设计才能让双端维护成本最低。
4.1 数据模型——统一协议的第一道坎
数据模型是网络通信的骨架。两端解析同一个 JSON 时,字段名、类型、嵌套结构必须完全一致。
举个实际例子:用户信息模型,在 Android 端用 Kotlin data class,iOS 端用 Swift struct,但 JSON 结构必须长一样。
// Android (Kotlin)
data class UserProfile(
val userId: String,
val nickname: String,
val avatarUrl: String,
val level: Int,
val vipExpireTime: Long?
)
// iOS (Swift)
struct UserProfile: Codable {
let userId: String
let nickname: String
let avatarUrl: String
let level: Int
let vipExpireTime: Int64? // 注意类型对应
}
我在项目中踩过一个坑:数字类型的精度丢失。服务端返回的 `userId` 是 Int64 类型,Android 端用 `Long` 没问题,但 iOS 端默认用 `Int`(32位设备上最大只有 2^31-1),直接解析就溢出了。后来我们约定:所有可能超过 2^31 的整数统一用 String 传输,两端各自转。
4.1.1 序列化/反序列化层
两端虽然语言不同,但序列化框架可以选配对方案。我个人习惯这么用:
| 能力 | Android 端 | iOS 端 | 注意事项 |
|---|---|---|---|
| JSON 解析 | Moshi / Gson | Codable(原生) | 字段命名策略要一致(默认蛇形 vs 驼峰) |
| 日期格式 | @JsonAdapter(DateAdapter) | 自定义 DateFormatter | 统一使用 ISO8601 毫秒时间戳 |
| 枚举处理 | @Json(name = "xxx") | CodingKeys | 保持枚举值字符串一致 |
你想想看,如果服务端字段是 `create_time`,Android 用 `createTime` 映射,iOS 也用了 `createTime`,但 Moshi 默认不转换蛇形,你得手动加注解。所以我们内部协议直接约定:所有字段名使用驼峰,JSON key 也强制驼峰,这样两端都能省掉映射配置。
4.2 常量定义——把「魔法数字」关进笼子
常量最大的用处是减少硬编码。双端共用常量通常包括:错误码、事件类型、键名(SharedPreferences / UserDefaults 的 key)、通知名称、API 版本号。
我的建议:准备一个共用常量文档(或者直接用代码仓库里的 Markdown 文件),每新增一个常量都通知对方。我在团队里用了一个简单的脚本,从 Kotlin 文件里提取常量生成 Swift 代码模板,避免手写不一致。
4.2.1 枚举 vs 静态常量
通常情况下,有穷集合用枚举(比如错误码、状态),字符串/数字用静态常量(比如 SharedPreferences key)。
// Android
object AppConstants {
const val PREFS_KEY_USER_ = "user_"
const val PREFS_KEY_USER_ID = "user_id"
}
enum class ErrorCode(val code: Int, val message: String) {
NETWORK_ERROR(1001, "网络异常"),
AUTH_EXPIRED(1002, "登录过期"),
UNKNOWN(9999, "未知错误")
}
// iOS
struct AppConstants {
static let prefsKeyUserToken = "user_"
static let prefsKeyUserId = "user_id"
}
enum ErrorCode: String {
case networkError = "NETWORK_ERROR"
case authExpired = "AUTH_EXPIRED"
case unknown = "UNKNOWN"
}
这里有个细节:枚举值映射。Android 的枚举可以带 Int 和 String,iOS 的枚举底层是 Int 或 String。如果你想让两端能直接通过枚举值通信,我建议统一用 String 枚举,JSON 里直接传字符串”NETWORK_ERROR”,两端都能解析。
4.2.2 平台差异常量处理
有些常量在两端值不同,但语义相同。比如:页面跳转的 intent action / URL scheme。Android 用 intent 传递,iOS 用 URL scheme,但路径名可以统一。
| 场景 | Android 端常量 | iOS 端常量 |
|---|---|---|
| 跳转个人中心 | "app://user/profile" | "myapp://user/profile" |
| 分享图片路径 | Environment.getExternalStorageDirectory() | NSTemporaryDirectory() |
这种情况下,我们会在共用文档里写清楚:双端各自实现自己的常量文件,但外部暴露的协议/路径字符串要一致。比如跳转 URL 统一用 `"scheme://user/profile"`,具体 scheme 在各自配置里。
4.3 工具类——让公共能力「一次编写,双端复用」
工具类是「底层能力」,比如日志、网络判断、加密、文件操作。双端虽然不能代码复用,但接口和功能行为必须对齐。
4.3.1 日志工具类
日志打印是调试的命脉。我见过最痛的是:Android 用 Logcat 的 tag 格式 `[TAG] message`,iOS 用 `NSLog` 输出 `[时间] [进程] message`,两边日志格式不统一,排查时根本对不上。所以我们自己封装了一个统一日志工具:
// 两端约定输出格式: [LEVEL] [MODULE] message
// Android 内部调用 android.util.Log
class UnifiedLogger {
companion object {
fun d(module: String, message: String) {
Log.d("SDK_$module", message)
}
}
}
// iOS 内部调用 os_log
struct UnifiedLogger {
static func debug(module: String, message: String) {
os_log("[DEBUG] [%@] %@", module, message)
}
}
这样不管用哪个平台,日志过滤时搜索 `[DEBUG]` 就能同时看到两端输出。
4.3.2 网络状态判断
SDK 经常需要检测网络状态。两端的 API 完全不同:Android 用 `ConnectivityManager`,iOS 用 `NWPathMonitor`。但给外部使用的接口可以统一:
isNetworkAvailable() -> BooleanisWifi() -> BooleangetNetworkType() -> String(返回 "WIFI", "4G", "5G", "NONE")
内部实现各自调用平台 API,上层业务代码不用感知。我在项目中就是用这种「抽象接口 + 平台适配器」模式,切换网络监测时只改工具类内部,不影响业务代码。
4.3.3 加密与哈希
SDK 通常需要对 或关键数据进行加密。双端算法的参数必须一致:算法(如 AES-GCM)、密钥长度、IV 生成规则、编码方式(Base64)。
| 能力 | Android 端 | iOS 端 |
|---|---|---|
| AES-GCM 加密 | Cipher.getInstance("AES/GCM/NoPadding") | CryptoKit.AES.GCM |
| SHA256 哈希 | MessageDigest.getInstance("SHA-256") | SHA256.hash(data:) |
| Base64 编解码 | Base64.getEncoder() | Data.base64EncodedString() |
4.3.4 线程切换工具
SDK 内部很多异步操作需要在主线程回调结果。两端的线程模型不同:
- Android:
Handler(Looper.getMainLooper())或runOnUiThread - iOS:
DispatchQueue.main.async
我们统一暴露接口:runOnMainThread(Runnable),各自实现。注意 iOS 端如果当前已经在主线程,再 `async` 也不会死锁,但 Android 端如果在主线程调用 `Handler.post` 会等到当前消息队列处理完才执行,导致回调延迟。我的做法:在 Android 端先判断当前线程,如果是主线程直接同步调用,否则 post。
// Android 示例
fun runOnMainThread(action: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
action()
} else {
mainHandler.post { action() }
}
}
4.4 知识体系图
下面这张图概括了本章的核心内容:共用核心模块的分层结构,以及各模块之间的依赖关系。
图中把「共用核心模块」放在中心位置,左边是数据模型,右边是工具类,下方是常量定义。每个模块再展开原子能力。你搭建 SDK 时,建议先画类似的结构图,再逐项填充代码。