Skip to content

系统设计题 — 面试题篇

更新: 5/15/2026 字数: 0 字 时长: 0 分钟


Q1: 设计一个图片加载框架(类似 Glide)

考察点:缓存设计、线程调度、生命周期管理

完整回答

需求:从网络/本地加载图片到 ImageView,支持缓存、变换、生命周期感知。

核心架构

ImageLoader.with(context).load(url).transform(圆角).into(imageView)

三级缓存

  1. 活动资源缓存(WeakReference,正在使用的图片)
  2. 内存缓存(LruCache,最大堆内存的 1/8)
  3. 磁盘缓存(DiskLruCache,原始图 + 变换后的图)

缓存 key = url + 目标尺寸 + 变换参数 的 hash。

加载流程

  1. 生成缓存 key
  2. 查活动资源 → 查内存缓存 → 查磁盘缓存 → 网络请求
  3. 网络下载 → 写磁盘 → 解码(inSampleSize 采样)→ 变换 → 写内存 → 显示

生命周期管理:注入空 Fragment 监听 Activity/Fragment 生命周期。onStop 暂停,onDestroy 取消请求并释放资源。

防止图片错位:ImageView 设置 tag 为当前 url,加载完成后检查 tag 是否匹配。

线程调度:网络/磁盘 IO 在 IO 线程池,解码在 CPU 线程池,显示切回主线程。

追问:怎么避免 OOM?

  1. inSampleSize 采样:根据 ImageView 尺寸计算采样率
  2. RGB_565 替代 ARGB_8888(省一半内存)
  3. inBitmap 复用 Bitmap 内存
  4. LruCache 控制内存缓存上限
  5. 大图使用 BitmapRegionDecoder 局部加载

Q2: 设计一个消息推送系统

考察点:长连接、可靠性、省电

完整回答

架构:客户端通过长连接(WebSocket/TCP)与推送网关通信。

连接管理

  • 建立长连接后,定期发送心跳保活
  • 心跳间隔自适应:WiFi 5分钟,移动网络 2分钟
  • 连接断开后指数退避重连(1s → 2s → 4s → 8s,上限 5分钟)

消息可靠性

  • 服务端发送消息带 msgId
  • 客户端收到后发送 ACK
  • 服务端超时未收到 ACK 则重发
  • 客户端用 msgId 去重,避免重复处理

离线消息

  • 客户端上线后发送最后收到的 msgId
  • 服务端返回之后的所有消息(分页拉取)

省电优化

  • 合并心跳与数据传输
  • 适配 Doze 模式(使用高优先级 FCM 唤醒)
  • 后台时降低心跳频率

追问:心跳间隔怎么确定?

NAT 超时时间决定了心跳间隔上限。不同运营商/网络环境 NAT 超时不同(通常 1-5 分钟)。可以用二分法探测:从大间隔开始,连接断开则缩短,逐步找到最优间隔。


Q3: 设计一个路由框架(类似 ARouter)

考察点:组件化通信、编译期处理

完整回答

核心功能:通过 path 跳转页面,支持参数传递、拦截器、降级。

路由表生成

  • 定义 @Route(path = "/user/detail") 注解
  • APT 编译期扫描注解,为每个模块生成路由表类
  • 应用启动时加载所有路由表到内存 HashMap

跳转流程

Router.navigate("/user/detail?id=123")
  → 解析 path 和参数
  → 执行拦截器链(登录检查 → 权限验证 → ...)
  → 查路由表找到目标 Class
  → 构建 Intent,设置参数
  → startActivity

拦截器:责任链模式,每个拦截器可以放行、拦截或重定向。

降级策略

  • 路由表中找不到 → 打开 H5 兜底页
  • 服务端下发路由映射表,支持动态路由

追问:怎么实现跨模块的服务调用(不是页面跳转)?

接口下沉 + SPI:公共层定义接口,业务模块实现并注册。通过 Router.getService(IUserService::class) 获取实现。注册方式可以用 APT 自动扫描 @Service 注解。


Q4: 设计一个埋点系统

考察点:数据采集、性能、可靠性

完整回答

采集层

  • 页面浏览:Lifecycle 自动监听 onResume/onPause
  • 点击事件:AOP(ASM 字节码插桩)自动采集 onClick
  • 曝光事件:监听 RecyclerView 滚动,计算 item 可见面积 >50% 且停留 >500ms
  • 自定义事件:手动调用 Tracker.track("event_name", params)

处理层

  • 统一数据格式(事件名、参数、时间戳、设备信息、用户ID)
  • 本地缓存到 SQLite(保证崩溃不丢数据)

上报层

  • 批量上报:每 30 秒或累积 50 条触发上报
  • 实时上报:关键事件(支付、崩溃)立即发送
  • 失败重试:指数退避,最多 3 次
  • 网络恢复后补报缓存数据

性能保证

  • 采集在主线程(最小操作),格式化和上报在后台线程
  • 上报使用 gzip 压缩,减少流量

追问:曝光去重怎么做?

维护一个 Set 记录当前页面已曝光的 item ID。同一个 item 在同一页面生命周期内只上报一次。页面销毁时清空 Set。


Q5: 设计一个日志系统

考察点:高性能写入、文件管理

完整回答

API 设计

kotlin
Logger.d(TAG, "debug message")
Logger.e(TAG, "error", exception)

分级:VERBOSE < DEBUG < INFO < WARN < ERROR。Release 包只记录 INFO 以上。

写入方案:mmap 内存映射文件。写入内存页即完成,OS 异步刷盘。即使崩溃数据也不丢失。比 FileOutputStream 快 10 倍以上。

文件管理

  • 按天分文件:log_2024-01-15.log
  • 单文件上限 10MB,超过则新建
  • 保留最近 7 天,定期清理
  • 上报前 gzip 压缩(压缩率约 80%)

上报策略

  • ERROR 级别实时上报
  • 其他级别定期上报或用户反馈时上报
  • 支持服务端动态调整日志级别(线上问题排查时临时开启 DEBUG)

追问:为什么用 mmap 而不是直接写文件?

直接写文件(FileOutputStream)每次 write 都是系统调用,且需要 flush/fsync 保证数据落盘。mmap 将文件映射到内存,写入就是内存操作,OS 负责异步刷盘。崩溃时 OS 也会将脏页刷盘,数据不丢失。


Q6: 设计一个网络层框架

考察点:网络架构设计

完整回答

核心架构:基于 OkHttp 封装,拦截器链模式。

拦截器设计

  1. 日志拦截器:记录请求/响应信息
  2. 认证拦截器:自动添加 Token,401 时自动刷新 Token 并重试
  3. 缓存拦截器:自定义缓存策略(强缓存 + 协商缓存)
  4. 重试拦截器:指数退避重试,只对幂等请求(GET)重试
  5. 监控拦截器:统计请求耗时、成功率、错误码分布

Token 刷新

  • 401 时 synchronized 加锁刷新 Token
  • 双重检查:进入锁后先检查 Token 是否已被其他线程刷新
  • 刷新成功后用新 Token 重试原请求
  • 刷新失败则跳转登录页

错误处理

  • 网络异常 → 检查本地缓存 → 有则返回缓存数据 + 标记为缓存
  • 服务端错误 → 统一错误码映射 → 展示友好提示
  • 超时 → 重试(GET)或提示用户

追问:怎么做网络监控?

通过 OkHttp 的 EventListener 监听请求各阶段耗时:DNS 解析、TCP 连接、TLS 握手、请求发送、响应接收。聚合统计后上报,用于发现慢请求和网络质量问题。


实习面试补充:简化版系统设计题

实习系统设计题通常不会要求完整大厂方案,更看重你能否拆模块、讲流程、考虑缓存和异常。

Q7: 设计一个简单图片加载器,你会怎么做?

考察点:模块拆分、缓存意识

完整回答

可以按以下模块拆:

  1. 接口层:提供 load(url).into(imageView) 这样的调用方式。
  2. 内存缓存:使用 LruCache 缓存 Bitmap,避免重复解码和请求。
  3. 磁盘缓存:缓存下载后的图片文件,减少网络请求。
  4. 网络加载:通过 OkHttp 下载图片。
  5. 解码压缩:根据 ImageView 尺寸采样,避免加载过大图片导致 OOM。
  6. 线程切换:下载和解码在子线程,最终回到主线程设置图片。

追问:列表中图片错位怎么办?

RecyclerView item 复用时,ImageView 可能已经绑定了新的 URL。设置图片前要校验当前 ImageView 绑定的 URL 是否还是请求发起时的 URL。


Q8: Feed 列表分页加载怎么设计?

考察点:列表分页、异常处理

完整回答

基础方案:

  • 首次进入页面请求第一页数据。
  • 滑动到底部附近触发加载下一页。
  • 使用 page/pageSizecursor 作为分页参数。
  • Adapter 增量追加数据,避免全量刷新。
  • 页面维护 loading、empty、error、content 状态。

异常处理:

  • 首次加载失败显示重试页。
  • 下一页加载失败显示底部重试。
  • 防止重复请求,加载中不再次触发。
  • 下拉刷新时清空旧分页状态,重新请求第一页。

加分点:可以结合本地缓存,让弱网下先展示缓存数据,再刷新网络数据。