系统设计题 — 面试题篇
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
Q1: 设计一个图片加载框架(类似 Glide)
考察点:缓存设计、线程调度、生命周期管理
完整回答:
需求:从网络/本地加载图片到 ImageView,支持缓存、变换、生命周期感知。
核心架构:
ImageLoader.with(context).load(url).transform(圆角).into(imageView)三级缓存:
- 活动资源缓存(WeakReference,正在使用的图片)
- 内存缓存(LruCache,最大堆内存的 1/8)
- 磁盘缓存(DiskLruCache,原始图 + 变换后的图)
缓存 key = url + 目标尺寸 + 变换参数 的 hash。
加载流程:
- 生成缓存 key
- 查活动资源 → 查内存缓存 → 查磁盘缓存 → 网络请求
- 网络下载 → 写磁盘 → 解码(inSampleSize 采样)→ 变换 → 写内存 → 显示
生命周期管理:注入空 Fragment 监听 Activity/Fragment 生命周期。onStop 暂停,onDestroy 取消请求并释放资源。
防止图片错位:ImageView 设置 tag 为当前 url,加载完成后检查 tag 是否匹配。
线程调度:网络/磁盘 IO 在 IO 线程池,解码在 CPU 线程池,显示切回主线程。
追问:怎么避免 OOM?
- inSampleSize 采样:根据 ImageView 尺寸计算采样率
- RGB_565 替代 ARGB_8888(省一半内存)
- inBitmap 复用 Bitmap 内存
- LruCache 控制内存缓存上限
- 大图使用 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 设计:
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 封装,拦截器链模式。
拦截器设计:
- 日志拦截器:记录请求/响应信息
- 认证拦截器:自动添加 Token,401 时自动刷新 Token 并重试
- 缓存拦截器:自定义缓存策略(强缓存 + 协商缓存)
- 重试拦截器:指数退避重试,只对幂等请求(GET)重试
- 监控拦截器:统计请求耗时、成功率、错误码分布
Token 刷新:
- 401 时 synchronized 加锁刷新 Token
- 双重检查:进入锁后先检查 Token 是否已被其他线程刷新
- 刷新成功后用新 Token 重试原请求
- 刷新失败则跳转登录页
错误处理:
- 网络异常 → 检查本地缓存 → 有则返回缓存数据 + 标记为缓存
- 服务端错误 → 统一错误码映射 → 展示友好提示
- 超时 → 重试(GET)或提示用户
追问:怎么做网络监控?
通过 OkHttp 的 EventListener 监听请求各阶段耗时:DNS 解析、TCP 连接、TLS 握手、请求发送、响应接收。聚合统计后上报,用于发现慢请求和网络质量问题。
实习面试补充:简化版系统设计题
实习系统设计题通常不会要求完整大厂方案,更看重你能否拆模块、讲流程、考虑缓存和异常。
Q7: 设计一个简单图片加载器,你会怎么做?
考察点:模块拆分、缓存意识
完整回答:
可以按以下模块拆:
- 接口层:提供
load(url).into(imageView)这样的调用方式。 - 内存缓存:使用 LruCache 缓存 Bitmap,避免重复解码和请求。
- 磁盘缓存:缓存下载后的图片文件,减少网络请求。
- 网络加载:通过 OkHttp 下载图片。
- 解码压缩:根据 ImageView 尺寸采样,避免加载过大图片导致 OOM。
- 线程切换:下载和解码在子线程,最终回到主线程设置图片。
追问:列表中图片错位怎么办?
RecyclerView item 复用时,ImageView 可能已经绑定了新的 URL。设置图片前要校验当前 ImageView 绑定的 URL 是否还是请求发起时的 URL。
Q8: Feed 列表分页加载怎么设计?
考察点:列表分页、异常处理
完整回答:
基础方案:
- 首次进入页面请求第一页数据。
- 滑动到底部附近触发加载下一页。
- 使用
page/pageSize或cursor作为分页参数。 - Adapter 增量追加数据,避免全量刷新。
- 页面维护 loading、empty、error、content 状态。
异常处理:
- 首次加载失败显示重试页。
- 下一页加载失败显示底部重试。
- 防止重复请求,加载中不再次触发。
- 下拉刷新时清空旧分页状态,重新请求第一页。
加分点:可以结合本地缓存,让弱网下先展示缓存数据,再刷新网络数据。
