Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

移动端系统设计方法论

** 章节边界:** 本章讲统一方法, 并以图片系统完成一次端到端设计示范; 业务算法实现见 Android 业务算法场景, 按题练习见移动端系统设计题库, 图片库 API 细节见图片加载与缓存.

学习目标

完成本章后, 你能在 25 分钟内按 “澄清→估算→接口→状态→失败→验收” 回答一个移动端系统题, 并能明确哪些数字只是待验证的示例假设.

一, 先澄清, 再画图

先确认用户旅程, 非目标, DAU / 峰值, 在线或离线, 端侧资源预算, 权威数据源, 一致性, 隐私与发布约束. 没有容量, 恢复路径和验收口径的模块图不能证明设计可行.

需求与约束 -> 数据 owner/状态 -> 接口与数据流 -> 缓存/并发/幂等
-> 失败恢复与降级 -> 安全/平台约束 -> 指标 -> 灰度,回滚与验收

参数不是固定最佳实践. 缓存大小, 下载并发, 预取深度和重试次数都要说明内容分布, 设备档位, 网络, 测量方法和调整阈值.

二, 完整示范: 设计商品图片加载与预览系统

1. 澄清与假设

目标: 商品列表首屏和详情大图可展示, 网络波动可恢复, 列表快速滚动不出现错图. 非目标: 本题不设计 CDN 选型, 图片审核和服务端转码集群.

** 数字示例, 非真实业务数据也不是推荐固定参数:** 假设 DAU 为 100 万; 每人每天 20 次列表会话, 每次请求 30 张缩略图; 峰值系数 12; 平均缩略图传输 80 KiB. 则日请求数为 1,000,000 × 20 × 30 = 6 亿; 平均请求速率约为 600,000,000 / 86,400 ≈ 6,944 req/s; 示例峰值约 6,944 × 12 ≈ 83,328 req/s; 若都未命中, 峰值下行约 83,328 × 80 KiB ≈ 6.36 GiB/s. 实际应按 CDN 日志, 分辨率分布, 命中率和区域流量重算.

端侧内存也按目标尺寸估算. 示例中一张 360 × 240 的 ARGB_8888 位图约为 360 × 240 × 4 = 345,600 B, 约 338 KiB. 屏幕同时保留 12 张约 3.96 MiB, 这只是位图像素数据, 尚未包含对象, 解码瞬时峰值, 其他 UI 和 GPU/图形内存. 缓存预算应基于低端设备压测, OOM/GC 与命中率曲线决定.

2. 契约和架构

以下为接口草图, 需放进真实 Android 工程并补齐线程, 错误类型和依赖; 本章以 callback 风格示意 (bind/callback/RequestHandle), 题库统一用 Async<T> 抽象, 二者映射见 60 顶部契约说明:

interface ImageRepository {
    suspend fun load(request: ImageRequest): ResourceResult
}

enum class Priority { VISIBLE, PREFETCH }

data class ImageRequest(
    val model: String,
    val widthPx: Int,
    val heightPx: Int,
    val transformKey: String,
    val signature: String?,
    val priority: Priority
)

/** Repository payload; the concrete implementation may wrap a Bitmap, Drawable, or GPU resource. */
interface ImageResource

sealed interface ResourceResult {
    data class Success(
        val resource: ImageResource,
        val source: ImageSource
    ) : ResourceResult
    data class Failure(val error: ImageError) : ResourceResult
    data object Cancelled : ResourceResult
}

interface ImageTarget {
    var currentRequestId: String?
}

interface RequestHandle {
    val requestId: String
    fun cancel()
}

interface ImageRequestManager {
    fun bind(
        target: ImageTarget,
        request: ImageRequest,
        callback: (ImageResult) -> Unit
    ): RequestHandle
}

sealed interface ImageResult {
    val requestId: String
    data class Success(
        override val requestId: String,
        val resource: ImageResource,
        val source: ImageSource
    ) : ImageResult
    data class Failure(
        override val requestId: String,
        val error: ImageError
    ) : ImageResult
    data class Cancelled(override val requestId: String) : ImageResult
}

enum class ImageSource { MEMORY, DISK, NETWORK }
sealed interface ImageError {
    data object NetworkTransient : ImageError // timeout, unavailable, 5xx
    data object AuthOrRequest : ImageError // 401/403 and non-retryable 4xx
    data object DecodeOrUnsupported : ImageError
    data object LocalIoOrStorage : ImageError
}

cacheKey = hash(model, widthPx, heightPx, transformKey, signature); 尺寸, 变换或资源版本不同就不能复用同一结果. signature 可对应服务端版本 / ETag, 资源更新后使旧缓存失效.

ImageRepository.load 只接收可合并的资源请求, 返回不含 target 身份的闭合 ResourceResult, 不能从中推导或伪造 requestId. request-manager wrapper 在每次 bind(target, request, callback) 时生成新的 requestId, 保存 target.currentRequestId, 并返回含该 ID 的 RequestHandle; repository 的 ResourceResult.Success/Failure/Cancelled 由 wrapper 映射为同一 ID 的 ImageResult.Success/Failure/Cancelled 后才交给 listener/target.wrapper 取消 handle 时只向该 owner 交付 ImageResult.Cancelled(requestId), 不要求 repository 生成 target 结果. 合并下载可以服务多个 listener, 但每个 listener 都得到带其自身 requestId 的 ImageResult. VISIBLE 可抢占 / 优先于 PREFETCH, 具体并发数和抢占策略由首图 P95, 废弃字节, 设备内存及网络测量后调整. 取消是本地 listener/owner 语义, 不把 requestId 当服务端幂等键.

Compose/ImageView target
  -> request owner(可见性与取消)
  -> active request 合并 -> 内存缓存 -> 变换后磁盘缓存 -> 源文件缓存
  -> local URI 或 HTTPS/CDN -> 下载 -> 按目标尺寸解码 -> 显示/写缓存

列表行只持有 wrapper 分配的当前 requestId. 复用时 wrapper 先取消或替换旧 listener, 再写入新 ID; 回调仅当 target.currentRequestId == result.requestId 且结果为 Success 才显示. Failure 按错误类别显示错误态或等待重试, Cancelled 不更新 UI; 这避免异步回调把旧图写入新行.

3. 缓存, 状态与时序

缓存策略可为内存 LRU 保存已解码资源, 磁盘缓存保存原始数据或变换结果; 取舍取决于 CPU, 磁盘和重复变换比例. 不要把 onStop() 一律暂停请求当规则, 应按可见性, 预取价值, 流量和业务语义决定.

Idle --bind(target, ImageRequest, callback)--> Loading(requestId)
Loading --ResourceResult.Success(resource, source) / wrapper maps ImageResult.Success(requestId)--> Ready(requestId)
Loading --ResourceResult.Cancelled or owner cancel / wrapper maps ImageResult.Cancelled(requestId)--> Cancelled(requestId)
Loading --cancel/rebind by owner--> Cancelled(requestId) --bind(new requestId)--> Loading
Loading --ResourceResult.Failure(NetworkTransient) / wrapper maps ImageResult.Failure(requestId)--> RetryWaiting(requestId) --timer/network change--> Loading(requestId)
Loading --ResourceResult.Failure(AuthOrRequest/DecodeOrUnsupported/LocalIoOrStorage) / wrapper maps ImageResult.Failure(requestId)--> Failed(requestId) --user retry/new requestId--> Loading
Ready --resource version changes--> Stale(requestId) --bind(new requestId)--> Loading
UI bind target to R1 -> wrapper creates requestId=A, sets target.currentRequestId=A, returns handle A -> repository.load(R1) -> ResourceResult.Success/Failure/Cancelled
wrapper maps ResourceResult.Success/Failure/Cancelled for listener A -> matching ImageResult(requestId=A) -> callback A
UI reuses target for R2 -> wrapper cancels/detaches handle A, creates requestId=B, sets target.currentRequestId=B -> repository.load(R2)
late callback A or ImageResult.Cancelled(A) -> A != target.currentRequestId -> discard for this target
repository ResourceResult.Success for R2 -> wrapper maps ImageResult.Success(requestId=B) -> B == target.currentRequestId -> render and retain according to owner policy

Ready 的 owner 是请求管理器, UI 仅渲染结果; 跨进程死亡不保存 Bitmap, 而保存可重新构造的 ImageRequest/页面数据. 下载中的临时文件必须原子落盘, 不能把半文件当缓存命中.

4. 失败, 证据与修复路径

症状证据与定位修复 / 降级验证
快速滚动出现错图target/requestId, 取消日志, 复用录屏绑定时替换旧 listener 并校验 wrapper requestId固定测试集 (覆盖 N 次 bind/reuse) 中, 窗口 [bind, target detach] 内 “显示结果的 cacheKey 不等于当前绑定 cacheKey” 的次数 / 有结果显示的绑定次数 = 0; RecyclerView/Compose 滚动测试记录分子, 分母和版本
OOM 或频繁 GCheap, allocation, 缓存命中和像素尺寸按视图尺寸采样, 缩小预算或淘汰策略低内存设备滚动压测, 对比峰值和帧时间
弱网长时间占位DNS/TLS/下载分段耗时, 网络类型超时, 带 jitter 的有限重试, 低清图或错误态飞行模式/切网/限速矩阵
资源更新仍显示旧图响应 ETag / 版本, cacheKey 命中日志将版本纳入 signature, 失效旧项更新同 URL 资源的集成测试

对幂等 GET 可在满足请求预算时重试; NetworkTransient 可进入 RetryWaiting, AuthOrRequest, DecodeOrUnsupported 与 LocalIoOrStorage 不应盲目重试, Cancelled 终止该 listener. 重试上限, 退避和网络约束必须由实测错误分布和用户等待预算决定.

5. 指标, 发布与验收

示例验收不是通用 SLO: 定义 image_display_latency 为 bind 到首个非占位像素的耗时, 按缓存层和网络分组看 P50/P95; 另看内存/磁盘命中率, 解码失败率, 取消后废弃字节, 错图率, 峰值内存和滚动帧. 灰度前记录基线, 按版本/设备档位/网络分层; 超过预先约定的崩溃, OOM 或 P95 回退阈值时关闭新策略并回滚配置. 验收证据为测试矩阵, 仪表盘链接/导出, 配置版本和 owner, 而非 “看起来更快”.

三, IM: 可靠性时序图

消息可靠性不是由 WebSocket 或推送单独保证, 而由服务端日志, 序列号, 持久化, ACK, 去重和补拉组成.

Client A -> Server: send(clientMsgId, conversationId, body)
Server -> durable message log: assign serverMsgId, sequence
Server -> Client A: accepted(serverMsgId, sequence)
Server -> Client B: deliver(message, sequence)
Client B -> local DB: transactionally persist + advance cursor
Client B -> Server: receivedAck(serverMsgId)
Client B -> UI: render from local DB
reconnect -> Server: pull(afterCursor) -> Client B: dedupe by serverMsgId, fill gaps

发送 clientMsgId 是幂等键; 接收端以 serverMsgId 去重, 以 sequence 发现缺口. 推送仅做唤醒 / 提示, 后台收到后仍以游标补拉. 多设备编辑, 撤回, 过期和已读状态必须有明确的服务端权威规则.

四, 断点续传: 恢复时序图

create task(url, target, expected ETag) -> persist PENDING
GET Range: bytes=start-end + If-Range: ETag
  -> write temp file at offset -> fsync/checkpoint range -> persist progress
network/process death -> read task + ranges -> validate ETag -> resume missing ranges
all ranges complete -> length/hash verify -> atomic rename -> COMPLETED
ETag changed / checksum mismatch -> delete temp metadata -> restart or ask user

分片数由文件大小, 网络/服务端连接限制, 随机写成本, 电量和吞吐实测决定; 不是越多越快. 必须处理无 Range 支持, 磁盘满, 权限/URI 失效, 进程死亡, 响应成功但进度尚未持久化等情形.

练习与预期证据

  1. 用自己的假设重算图片峰值 QPS 和字节流量, 写出公式, 单位和至少一个会使结论失效的条件.
  2. 为图片请求画出取消, 资源更新和进程重建分支; 预期证据是状态 owner, 持久化字段和每条转移触发条件.
  3. 为 IM 或下载器列出一个 “请求成功但响应丢失” 恢复方案; 预期证据是幂等键 / 游标或 ETag, 去重和补偿步骤.

版本与参考资料