扫脸功能做成 SDK,为什么我没有把结果页和历史记录一起搬进去

上一篇文章里,我写的是这两周做扫脸 SDK 时,为什么先稳住原来的功能。

这一篇想讲后面更容易判断错的一件事:扫脸相关代码很多,但不是所有代码都应该放进 SDK。

做 SDK 很容易有一种冲动:既然要封装,那就干脆把扫脸目录里的东西都移走。

这样看起来最彻底,但一做就会遇到另一个问题:如果这么处理,SDK 反而会变成当前 App 的另一个外壳。它里面不仅有相机、人脸识别和结果计算,还会混进结果页、历史记录、PDF、会员、埋点、首页卡片和一堆只属于当前产品的展示规则。

这样的 SDK 即使能跑,也不一定好用。

因为它复用出去以后,别的项目接到的就不只是扫脸能力,还会被迫接受我这个 App 的一整套业务习惯。

所以这次拆分扫脸 SDK,我花了很多时间做了一件事:分清楚哪些代码应该成为 SDK 的能力,哪些代码应该继续留在 App 里。

一、先把扫脸代码分成两类

扫脸功能在项目里已经能跑。用户进页面,打开相机,识别人脸,进入倒计时,完成测量,最后看到健康结果。

从用户角度看,这是一件完整的事,但从代码角度看,它至少可以分成两类。

第一类,是别的 App 也会需要的通用能力:

  1. 打开前置摄像头。
  2. 处理每一帧画面。
  3. 用人脸关键点模型识别人脸位置。
  4. 判断人脸是否太近、太远、晃动、遮挡。
  5. 从额头、脸颊这些区域里取颜色变化。
  6. 整理红绿蓝三路信号。
  7. 判断什么时候可以开始测量,什么时候结束测量。
  8. 算出心率、血压、血糖、风险和总分。

这些东西适合放进 SDK。

原因很直接:只要是做同类扫脸检测,基本都会遇到这些问题。

第二类,是当前 App 自己的业务规则:

  1. 扫脸入口放在哪里。
  2. 权限弹窗怎么引导。
  3. 结果页长什么样。
  4. 历史记录怎么存。
  5. 首页卡片怎么展示。
  6. PDF 里写哪些文案。
  7. 健康指标弹窗和疾病风险弹窗怎么组织内容。
  8. 会员、埋点、导航、弹窗怎么处理。

这些不适合放进 SDK。

因为它们换一个产品,规则很可能就变了:

  • 今天这个 App 里叫"偏高",另一个 App 里可能叫"需关注";
  • 这里 PDF 要展示 8 项指标,另一个产品可能只展示 4 项;
  • 这里首页有历史进度,另一个产品可能根本没有首页卡片。

如果把这些都塞进 SDK,SDK就不是一个可以复用的扫脸工具。

二、最容易误判的是结果数据

这次最容易误判的地方,反而是健康结果相关的数据。

因为这些数据看起来很像 SDK 应该负责的东西。

比如心率、血压、血糖、BMI、疾病风险、健康总分,这些当然应该由 SDK 计算。

但原项目里的结果数据并不只存这些数字,它还被很多页面拿来做展示:

  1. 结果页要判断每项是正常、偏高还是偏低。
  2. 健康指标弹窗要展示解释文案。
  3. 疾病风险弹窗要区分低风险、异常和高风险。
  4. PDF 要按同一套规则生成报告。
  5. 心脏年龄页面要根据真实年龄判断展示状态。
  6. 首页和历史记录也要读同一份结果。

也就是说,同一个数据模型里混着两件事:

一件是 SDK 应该算出来的数字,另一件是 App 自己决定怎么展示这些数字。

如果我只看到"健康结果"这几个字,就把整个模型搬进 SDK,后面会出现一个很麻烦的问题:

SDK 每改一次字段或展示规则,结果页、PDF、历史记录和首页都可能跟着改。

这不是封装 SDK,这是把页面和 SDK 绑在一起。

所以这里我没有把 App 里的结果模型整块删掉,而是把它拆成了两层理解:

  1. SDK 负责算出稳定的健康指标和总分。
  2. App 继续负责这些指标在页面上怎么叫、怎么分颜色、怎么写文案。

这一步很重要,它决定了后面主工程里有些扫脸相关文件还会留下来,但它们不再负责算法,只负责让当前 App 的页面继续稳定运行。

三、为什么还要留一小段中间代码

SDK 接进主工程以后,我没有让页面直接到处使用 SDK 的类型,主工程里保留了一小段中间代码,专门做数据整理。

它的作用很简单:

  1. 页面、历史记录、PDF 继续使用 App 原来的数据结构。
  2. 需要 SDK 计算时,把 App 数据整理成 SDK 需要的请求。
  3. SDK 算完以后,再整理回 App 现在已经在使用的数据结构。

实际代码里,大概是这种形式:

swift 复制代码
let request = ZZHFaceScanSDK.EstimationRequest(
    redSignal: redSignal,
    greenSignal: greenSignal,
    blueSignal: blueSignal,
    sampleRate: sampleRate,
    userProfile: profile.toSDKProfile(),
    measurementContext: context.toSDKContext()
)

switch engine.estimate(request) {
case let .success(result):
    return .success(result.toHostEstimateResult())
case let .failure(error):
    return .failure(.estimationFailed(
        code: error.code,
        message: error.localizedDescription
    ))
}

这段代码本身不做估算,也不重新实现算法,它只负责把 App 和 SDK 隔开。

这样做有两个好处:

第一,结果页、历史记录和 PDF 不需要跟着 SDK 的类型变化到处改。

第二,SDK 可以保持干净。它只关心自己需要什么输入、输出什么结果,不需要知道当前 App 的页面怎么组织、历史怎么保存、PDF 怎么生成。

这类中间代码看起来像"多了一层",但在复杂项目里,它反而是在减少风险。

四、总分计算可以进 SDK,展示规则不能跟着进去

总分计算这块,我最后是交给 SDK 处理的,原因也很清楚:总分是健康结果的一部分,别的 App 接这个 SDK 时也会需要同一套计算方式。

但交给 SDK 的时候,我只传评分需要的字段。

swift 复制代码
func toSDKHealthScoreRequest(userAge: Int) -> FaceScanHealthScoreRequest {
    FaceScanHealthScoreRequest(
        heartRate: heartRate,
        respiratoryRate: respiratoryRate,
        hrv: hrv,
        systolicBP: systolicBP,
        diastolicBP: diastolicBP,
        stressIndex: stressIndex,
        cardiacLoad: cardiacLoad,
        bmi: bmi,
        signalQuality: signalQuality,
        userAge: userAge
    )
}

这里没有传页面颜色,没有传文案,没有传历史记录,也没有传 PDF 相关数据。

SDK 只需要知道参与评分的指标。

至于页面上怎么展示"正常"、"偏高"、"偏低",这些继续留在 App:

swift 复制代码
enum SC_FaceScanHealthDisplayRules {
    static func bloodPressureText(for metrics: HealthMetrics) -> String {
        "\(Int(metrics.systolicBP))/\(Int(metrics.diastolicBP))"
    }

    static func heartRateStatus(for metrics: HealthMetrics) -> HealthStatus {
        if metrics.heartRate < 60 {
            return .low
        } else if metrics.heartRate > 100 {
            return .high
        }
        return .normal
    }
}

这段代码不属于 SDK 的核心能力,它解决的是当前 App 页面要怎么展示结果:血压字符串怎么拼、心率状态怎么分、颜色怎么取、弹窗和 PDF 怎么保持一致。

五、写入本地扫脸结果历史记录之前,对扫脸结果的稳定处理,暂时也不放进 SDK

还有一段代码,我一开始也考虑过要不要放进 SDK:写入本地扫脸结果历史记录之前,对扫脸结果的稳定处理。

扫脸测量不像用户手动输入体重,它受光线、距离、晃动、人脸状态影响,同一个用户连续测几次,结果可能会有波动。

所以 App 在把结果写入历史记录前,会结合用户资料和历史结果做一次处理,避免结果浮动太大。

这听起来 SDK 也能做,但继续往下看,会发现它依赖很多当前 App 的东西:

  1. 本地历史记录怎么存。
  2. 哪条历史记录可以作为参考。
  3. 当前用户资料怎么区分。
  4. 同一天和跨天结果怎么处理。
  5. 用户手动改系统日期时怎么兜底。

这些已经超出了纯扫脸能力,属于当前 App 对历史数据的处理方式。

所以这块我没有继续往 SDK 里迁,更合理的做法是:

  • SDK 给出测量结果;
  • App 根据自己的历史和产品规则,决定入库前要不要做稳定处理。

以后如果真的要把这块做成 SDK 能力,也应该重新设计接口,让 App 只把必要的历史参考值传进去,而不是让 SDK 直接理解当前 App 的历史数据和用户资料规则。

六、SDK 不是越大越好

这次做完以后,我对扫脸 SDK 的边界有一个更明确的判断:

SDK 应该变强,但不应该变"重"。

变强,指的是它能独立处理相机、人脸识别、测量判断、信号处理、健康指标计算、风险计算和总分重算。

不变重,指的是它不要把结果页、首页、历史记录、PDF、会员、埋点和当前 App 的展示规则都带进去。

如果 SDK 什么都管,接入方反而不自由,它不能只拿SDK提供的扫脸相关的能力,还必须接受这套页面、这套文案、这套历史规则和这套产品结构。

这就违背了做 SDK 的初衷,所以,我更愿意把边界划成这样:

  1. SDK 负责可复用的扫脸能力。
  2. App 负责自己的页面和业务规则。
  3. 中间保留少量数据整理代码,用来隔开 SDK 和 App。

这样主工程确实不会一下子少掉所有的扫脸相关的代码文件,但留下来的代码更清楚:哪些是在调用 SDK,哪些是在服务当前 App 的页面和历史,哪些只是为了隔离数据结构。

能删的旧算法代码要删,但不该进 SDK 的业务代码,也不能为了"看起来更彻底"硬塞进去。

这是我这次做扫脸 SDK 时,必须守住的一个边界。

相关推荐
茶底世界之下2 小时前
诡异!String 参数在闭包里变成了 <uninitialized>,我排查了整整两天
ios·xcode·swift
harder3214 小时前
iOS IPA 马甲包送审风险评估工具
ios
SameX5 小时前
存钱 App 开发手记:restitution 0.3 是怎么试出来的,以及 86400 秒不等于一天
ios
MonkeyKing8 小时前
蓝蓝牙核心基础概念详解:2.4GHz频段、跳频、信道、广播、连接、配对
android·ios
鹤卿1239 小时前
Masonry
macos·ios·cocoa
JoyCong199810 小时前
开启iPad创造力!装上它平板能当电脑用
ios·电脑·ipad
WaywardOne1 天前
一.iOS Objective-C Runtime 原理
前端·ios
WaywardOne1 天前
二.iOS内存管理
前端·ios·面试