理解SDK架构:公共层与平台抽象层的设计原则

做跨平台SDK这几年,我最大的体会是:架构设计的好坏,直接决定了你能走多远。今天聊聊SDK内部的两个核心层次——公共层(Common Layer)和平台抽象层(Platform Abstraction Layer,简称PAL)。

说白了,公共层就是你只写一次的业务逻辑。平台抽象层则是你为每个平台单独实现的“适配器”。二者之间的边界划得越清楚,后续维护就越轻松。

公共层:你只写一次的业务逻辑

公共层是SDK的心脏。它负责所有与平台无关的核心功能。我习惯把它理解为一个“纯净的计算环境”——没有文件路径、没有网络接口、没有UI渲染,只有纯粹的算法和数据流转。

举个例子。一个跨平台登录SDK,公共层里应该放这些:

  • 用户认证流程(登录、登出、刷新)
  • 数据模型(User、Session、AuthResult)
  • 协议解析(JSON/Protobuf序列化与反序列化)
  • 状态机管理(登录态切换、超时处理)

我在项目中遇到过一种反面教材:有人把第三方SDK的初始化代码直接写在了公共层里。结果换了支付平台,整个核心逻辑都要拆了重写。嗯,这个坑千万别踩。

我的习惯:公共层里不要出现任何平台相关的头文件或API调用。维持“纯C/C++”或“纯Java/Kotlin”的实现,不依赖任何平台框架。

平台抽象层:隔离差异的防火墙

平台抽象层是公共层与具体操作系统之间的桥梁。它的职责只有一个:把平台差异封装成统一接口。

想一想,为什么需要这层?因为Windows的线程API、Android的Handler、iOS的GCD,实现思路完全不同。PAL就是这层“翻译官”。

我建议用纯虚类(接口)来定义PAL。例如:

// 平台抽象层定义
class IPlatformFileSystem {
public:
    virtual ~IPlatformFileSystem() = default;
    virtual bool            CreateFile(const std::string& path) = 0;
    virtual std::string     GetAppDataDir() = 0;
};

// Windows 实现
class WinFileSystem : public IPlatformFileSystem {
public:
    bool CreateFile(const std::string& path) override {
        HANDLE h = CreateFileA(path.c_str(), ...);
        return h != INVALID_HANDLE_VALUE;
    }
    std::string GetAppDataDir() override {
        // 调用 Windows API
    }
};

// Linux/macOS 实现
class PosixFileSystem : public IPlatformFileSystem {
    // 完全不同的实现,但接口一致
};

你发现没有?公共层永远只跟 IPlatformFileSystem 打交道。它不知道也无需知道底层是Win还是Linux。

我踩过的坑是:早期做跨平台SDK时,把文件路径处理直接写死在公共层里。结果上架后发现iOS和Android的沙箱路径完全不一样,紧急打了补丁。从此以后,所有文件系统操作全部通过PAL路由。

维度 公共层 平台抽象层
依赖方向 不依赖任何平台API 依赖具体OS API
复用性 一套代码全平台复用 每平台单独实现
测试难度 单元测试易写,可mock 需模拟器或真机
变动频率 低(业务稳定则少改) 高(系统升级、新设备适配)

设计原则:别让架构倒向一边

写了不少SDK后,我总结了几条原则,分享给你:

原则一:依赖倒置

公共层定义接口,PAL实现接口。公共层不直接#include任何PAL的具体实现。

怎么理解?就是公共层只持有IPlatformXxx*指针。初始化时由外部注入具体实现。这样一来,公共层永远不会因为平台升级而重新编译。

原则二:接口最小化

PAL接口越少越好。每个接口只做一件且必做之事。

举个例子。一个IPlatformStorage接口,别把读写文件、数据库操作、偏好设置全塞进去。拆成IPlatformFileSystem和IPlatformPreferences两个接口。为什么?因为不同平台的文件系统和偏好设置实现差异极大,混在一起容易让公共层逻辑跟着乱。

我曾经犯过的错:PAL接口参数里传了平台特有的结构体(比如Windows的HANDLE)。结果导致公共层被迫引入windows.h,整个跨平台设计功亏一篑。切记:PAL接口参数必须是跨平台类型(int、string、vector等)。

原则三:单一职责

每个PAL接口只封装一个维度的平台能力。

按照这个原则,你应该这样拆分:

  • IPlatformThread —— 线程与异步
  • IPlatformNetwork —— 网络请求
  • IPlatformUI —— 原生UI调用
  • IPlatformSensor —— 传感器数据

你会发现,每个接口的实现文件都很瘦。Windows的线程接口可能就200行,iOS的线程接口也就150行。这才是健康的架构。

公共层与PAL的协作流程

下面的SVG图展示了完整的调用链路。应用层 -> 公共层 -> PAL接口 -> 具体实现。箭头代表了依赖方向和控制流向。

SDK 三层架构协作图 应用层 (App) 公共层 (Common Layer) 业务逻辑 | 数据模型 | 状态机 平台抽象层接口 (PAL Interface) IPlatformFileSystem | IPlatformThread | IPlatformNetwork Windows 实现 WinFileSystem | WinThread macOS / iOS 实现 PosixFileSystem | GCDThread Android 实现 JNIFileSystem | HandlerThread 说明:实线箭头表示调用/依赖方向;虚线表示PAL接口与具体实现之间的绑定关系。 公共层只依赖PAL接口,不感知具体平台实现——这就是依赖倒置原则的落地。

验证架构的“好”与“坏”

怎么判断你的PAL设计是否合理?我提供一个自检清单:

  1. 能不能替换? 把PAL实现从Windows切换到Linux,公共层代码一行不改吗?
  2. 接口稳定吗? 过去三个月,PAL接口头文件变动过几次?只有新增,没有删除或重命名,才算稳定。
  3. 测试友好吗? 单元测试时能不能用一个MockPlatform替换真实实现?能,说明PAL设计到位。
  4. 有没有平台泄漏? 搜索公共层代码,是否出现了#ifdef WIN32、__APPLE__等宏?如果有,说明平台细节渗入了核心逻辑,需要重构。

避坑指南:我曾经遇到一个团队,为了图省事,在公共层里直接写 #ifdef __ANDROID__ 来处理文件路径。结果后面增加鸿蒙支持时,所有分支都得改。花了三周才把耦合拆干净。相信我,平台宏就是架构的毒药,能不用就不用。

一句话总结

公共层是你的业务护城河,平台抽象层是护城河外的防火墙。把城墙砌在正确的边界上,比什么都重要。