移动端工程师面试指导
移动端面试既考平台深度,也考设备特有的约束:生命周期、内存、离线状态,以及在受限硬件上保持流畅的 UI。考察会有平台基础,外加 App 架构。
面试官重点考察什么
平台深度
iOS(Swift)或 Android(Kotlin)的生命周期、线程与内存。
UI 与性能
流畅的列表、渲染、卡顿与电量考量。
App 架构
MVVM/MVI、导航、依赖注入与模块化。
离线与同步
本地存储、缓存、冲突解决与断网处理。
发布工程
应用商店构建、崩溃上报与灰度发布。
移动端工程师常见面试题示例
- 技术面讲讲 Activity/ViewController 的生命周期以及常见的坑。好回答应覆盖
- onCreate/onStart/onResume对应关系
- 配置变更处理
- Fragment生命周期嵌套
- 内存不足时的状态保存与恢复
查看范例答案
Activity生命周期包括onCreate、onStart、onResume、onPause、onStop、onDestroy。onCreate用于初始化,onStart使界面可见,onResume获得焦点开始交互。常见坑:在onPause中执行耗时操作导致ANR,应使用onStop或后台任务;配置变更(如屏幕旋转)会销毁重建Activity,需通过onSaveInstanceState保存临时数据,或在Manifest中配置configChanges属性;Fragment生命周期与Activity嵌套时,需注意Fragment的onAttach在Activity的onCreate之前调用,避免空指针;低内存时系统可能直接杀死进程,应利用ViewModel或onSaveInstanceState恢复重要状态。最佳实践是将数据持久化到数据库,而非依赖临时状态。
- 技术面在低端机上,你如何让一个长滚动列表保持流畅?好回答应覆盖
- 虚拟化/回收机制
- 图片异步加载与缓存
- 减少过度绘制
- 使用Profile工具定位瓶颈
- 减少布局层级
查看范例答案
核心是使用RecyclerView(Android)或UICollectionView(iOS)的回收复用机制。具体措施:1) 图片使用三级缓存(内存、磁盘、网络)并异步加载,避免主线程解码;2) 使用轻量级布局,减少嵌套,例如ConstraintLayout或扁平化布局;3) 减少过度绘制,如移除不必要背景;4) 使用DiffUtil(Android)或自动diff算法(iOS)避免全量更新;5) 对复杂item使用ViewHolder模式,避免findViewById;6) 使用Profile工具(Android Studio Profiler, Xcode Instruments)找出卡顿点。常见坑:在getView/onBindViewHolder中做耗时操作,或未复用convertView导致频繁创建对象。
- 技术面你会如何设计支持离线、稍后再同步的能力?好回答应覆盖
- 本地数据库存储(Room/CoreData)
- 同步队列与版本控制
- 冲突解决策略
- 网络状态监听与自动触发
查看范例答案
设计一个离线优先架构:1) 本地使用SQLite(Room in Android, CoreData in iOS)存储数据,所有写操作先写入本地;2) 维护一个操作日志(outbox / sync queue),记录所有待同步的修改(增删改),每条记录带时间戳或版本号;3) 监听网络状态变化,当网络恢复时按序同步操作日志,同步后更新本地状态;4) 服务端使用乐观锁或基于时间戳的冲突解决,例如最后写入者获胜(LWW)或自定义合并策略。需要注意:不要在同步期间阻塞用户操作,使用后台任务(WorkManager, BGTaskScheduler)进行同步。常见坑:未处理同步过程中网络中断导致的重复操作,需实现幂等性。
- 编程实现一个带内存和磁盘两级的图片缓存。好回答应覆盖
- LRU淘汰策略
- 内存缓存与磁盘缓存分离
- 图片压缩与格式选择
- 异步加载与回调
查看范例答案
两级缓存:内存缓存使用LRU(如Android的LruCache),磁盘缓存使用DiskLruCache或简单文件系统。图片从网络加载时首先检查内存,未命中则检查磁盘,最后下载并写入两级缓存。需要处理图片解码(BitmapFactory)与缩放到所需尺寸,避免OOM。使用异步线程(ExecutorService或协程)加载,通过回调或LiveData返回。常见坑:磁盘缓存文件名需要编码URL,避免非法字符;需要控制磁盘缓存总大小。
参考代码kotlin // 简化实现,未包含线程安全等 class ImageCache(private val cacheDir: File, maxMemory: Int, maxDisk: Long) { private val memoryCache = object : LruCache<String, Bitmap>(maxMemory) { override fun sizeOf(key: String, value: Bitmap) = value.byteCount } private val diskCache = DiskLruCache.open(cacheDir, 1, 1, maxDisk) fun get(key: String): Bitmap? { memoryCache.get(key)?.let { return it } return diskCache[key]?.let { inputStream -> val bitmap = BitmapFactory.decodeStream(inputStream) memoryCache.put(key, bitmap) bitmap } } fun put(key: String, bitmap: Bitmap) { memoryCache.put(key, bitmap) diskCache.edit(key)?.let { editor -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, editor.newOutputStream(0)) editor.commit() } } } - 编程实现一个随用户滚动加载更多的分页列表。好回答应覆盖
- OnScrollListener或Paging3
- 加载状态管理(loading/error/empty)
- 避免重复触发
- 预加载阈值
查看范例答案
使用RecyclerView的OnScrollListener监听滑动,当最后可见项位置接近总项数时触发加载更多。维护一个分页状态:当前页码、加载中标识、是否还有更多数据。当用户滚动到接近底部(例如最后一项可见且未加载中)时,从网络或本地加载下一页数据,追加到列表数据集中并通知适配器。同时考虑错误处理和重试机制。可使用Paging 3库简化,但需理解其原理。常见坑:在加载中时重复触发请求;未处理网络错误导致无限重试;数据顺序不一致。
参考代码kotlin class PaginationAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val items = mutableListOf<String>() private var isLoading = false private var isLastPage = false private val PAGE_SIZE = 20 private var currentPage = 0 override fun getItemCount() = items.size + if (isLastPage) 0 else 1 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (position == items.size) { // show loading spinner } else { // bind item } } fun loadData(page: Int) { if (isLoading || isLastPage) return isLoading = true GlobalScope.launch(Dispatchers.Default) { delay(1500) val newItems = (page * PAGE_SIZE until (page + 1) * PAGE_SIZE).map { "Item $it" } withContext(Dispatchers.Main) { items.addAll(newItems) isLoading = false if (newItems.size < PAGE_SIZE) isLastPage = true notifyDataSetChanged() } } } } // 在Activity或Fragment中 recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val layoutManager = recyclerView.layoutManager as LinearLayoutManager val totalItemCount = layoutManager.itemCount val lastVisibleItem = layoutManager.findLastVisibleItemPosition() if (!isLoading && totalItemCount - lastVisibleItem <= 1) { adapter.loadData(currentPage++) } } }) - 系统设计设计一个能离线工作、重连后同步的移动端聊天 App。好回答应覆盖
- 本地消息数据库
- WebSocket长连接与离线消息队列
- 同步协议(消息ID+时间戳)
- 冲突处理
查看范例答案
整体架构:本地使用SQLite保存所有消息和联系人,每条消息有一个本地唯一ID和服务端ID映射。用户发送消息时先存入本地数据库,状态为“发送中”,然后放入一个待发送队列。同时保持WebSocket连接(或使用MQTT)实时接收消息。当网络断开时,消息停留在队列中;网络恢复后,按序发送队列中的消息,服务器返回确认后更新状态为“已发送”。为保证消息顺序,客户端和服务端使用递增的时间戳或sequence number。离线期间收到的消息,服务器会在客户端重连后通过同步接口(基于最后同步时间戳)批量推送。需要考虑消息撤回、已读回执等复杂情况。冲突处理:以服务器时间戳为准,或使用CRDT。常见坑:消息重复(需幂等),同步期间CPU/网络过度消耗。
- 行为面讲一个你排查过的棘手崩溃或内存 bug。好回答应覆盖
- 具体问题描述
- 工具使用(Instruments/Android Profiler)
- 分析过程
- 根因与修复
查看范例答案
例如,一个Android应用在特定页面频繁崩溃,原因是Fragment中持有Activity的引用导致内存泄漏,当Activity重建后旧Fragment仍然存活,造成空指针。使用Android Profiler观察内存,看到大量Activity实例未被回收。通过分析堆转储(Heap Dump),发现一个匿名内部类由于Handler持有Activity引用,在异步任务未完成时导致了泄漏。修复方法:使用静态内部类+弱引用,或使用LifecycleObserver自动清理。另一个例子:iOS中循环引用导致dealloc不调用,使用Xcode Instruments的Leaks工具发现闭包中强引用self。修复:使用[weak self]或[unowned self]。
- 行为面你如何在新功能与 App 体积和性能之间取得平衡?好回答应覆盖
- 功能优先级与A/B测试
- 增量下载(App Bundle/动态框架)
- 性能预算与监控
- 延迟加载与懒初始化
查看范例答案
平衡策略:1) 对新功能进行性能预算,例如增加的内存、CPU使用率、安装包大小,如果超过阈值则进行优化或降级;2) 使用App Bundle(Android)或动态框架(iOS)按需分发功能,减少初始安装包大小;3) 采用A/B测试,仅对部分用户开放新功能,收集性能数据后再全量发布;4) 对非核心功能使用懒加载,例如启动时不初始化,只有用户首次使用时才加载;5) 定期使用CI pipeline运行性能测试,监控关键指标(启动时间、帧率、内存)。常见坑:过度优化导致开发效率下降,或忽略用户真实体验。决策时需考虑功能价值与性能成本的ROI。
不同级别的考察差异
如何准备
- 先讲生命周期和内存——它们是最常见的失分点。
- 主动讨论离线和弱网;移动端面试官就期待你提这些。
- 用数据说话:性能剖析、崩溃率和帧耗时,胜过空泛而谈。
常见问题
移动端面试会考数据结构编程吗?
通常会,外加平台相关的编程,比如实现缓存或分页列表,以及 App 架构的讨论。
移动端岗位会问哪些架构题?
会问 MVVM/MVI、导航、依赖注入,以及设计支持离线和同步的 App。
如何准备移动端工程师面试?
复习平台生命周期和内存,从零写一个小功能,并在模拟面试中演练架构决策。
用即时 AI 反馈刷移动端工程师面试题
Offersly 会根据你的简历和目标岗位定制一场模拟面试,并从相关性、深度、清晰度和正确性四个维度为每个回答打分。