
每次帮人做模拟面试复盘我都能感受到一个现象大家背的题不少但一到追问环节就露怯。原因很简单高频题目虽然就那么几个区域但真正的分水岭不在“会不会背答案”而在“有没有从原理到实践都打通”。这篇文章把我在反复的面试复盘里见到的 30 个 Android 高频问题整理出来每题给出答案要点、考察方向以及我自己踩过坑后的一些真实体会。适合三种人准备跳槽的 Android 开发者、刚开始系统性复习的中级工程师、以及想自查知识盲区的人。1. Java 与 Kotlin 语法底层最先翻车也最容易翻盘的语言题1.1 equals 和 hashCode 为什么总被连在一起问这道题几乎是 Java 类面试的开场白。大多数人都能背出“equals 相等则 hashCode 必须相等反过来不成立”但面试官想听的绝对不是这条结论而是结论背后的哈希表原理。HashMap 会用 hashCode 定位桶下标再用 equals 处理落在同一桶里的碰撞。如果你重写了 equals 却没重写 hashCode两个业务上完全相等的对象会发现 hashCode 不同从而落进不同桶里get 的时候永远找不到。我自己见过一个真实案例有人拿订单号做强标识只重写了 equals放进 HashSet 后 contains 一直返回 false找了大半天才发现是 hashCode 的锅。可以延伸一句不同对象的 hashCode 相同叫碰撞哈希表里每个槽位装的其实是一条链表或红黑树。把这个结构意识说出来这道题就稳了。1.2 String、StringBuffer、StringBuilder不可变与可变的内在逻辑String 为什么不可变只背“被 final 修饰”是不够的。String 在 Java 里被广泛用作 HashMap 的 key、类加载的类名、网络地址标识如果它可变哈希表和常量池的语义就会全面崩掉。更关键的是String 内部那个字符数组是 final 的任何修改操作返回的都是新对象原串不会变。StringBuffer 和 StringBuilder 的区别大家都会说前者方法加了 synchronized线程安全后者没有单线程用。但这里有个容易露馅的细节StringBuffer 的“线程安全”也只是单个方法级别的安全比如多线程交替 append 没问题但如果你先 append 再 insert这两个操作之间没有整体加锁复合操作依然不是原子的。很多人简历里写“熟悉线程安全”一深问就在这里漏了。实际做的过程中要记住一个铁律循环拼接字符串一定要用 StringBuilder否则每次循环都会产生新的 String 对象GC 压力随之而来。这个问题在后面的性能优化章节还会再见面。1.3 volatile 与 synchronized并发题的第一道坎volatile 和 synchronized 表面看都是并发工具本质完全不同。volatile 解决可见性和有序性它不加锁允许多线程同时写synchronized 解决原子性、可见性、有序性靠监视器锁串行化临界区。这道题最大的坑是volatile 修饰的变量执行 i 并不是原子的。i 在字节码层面至少分读、加、写三步volatile 只能让第三步的写入对其他线程可见却挡不住多个线程同时读到旧值。如果面试官继续追问双重检查锁单例里为什么要加 volatile答案不是防重入而是防止指令重排序后某个线程拿到了“构造方法还没执行完”的对象实例。能把这一层说得清清楚楚比单纯背单例模板有用得多。进阶一点可以提 Java 内存模型里的 happens-before 规则锁释放、volatile 写、线程 start 和 join都会建立先行发生关系。能随口把这些规则说出来面试官基本会判断你是系统读过 JMM 的人。1.4 Kotlin 协程真的比线程轻量吗这题现在几乎每个 Android 岗位都会问。回答的第一句可以是“协程是轻量级线程”但接下来一定要展开协程并没有减少 CPU 计算量它只是把并发模型从内核线程调度换成了用户态状态机挂起与恢复。挂起的本质不是阻塞线程而是让出执行权不占线程栈所以可以同时挂起成千上万个协程。重点要讲清楚挂起和恢复的原理。suspend 函数在编译后会被改造成一个带 Continuation 参数的状态机函数每个挂起点就是状态机里的一个分支resume 继续执行。所以协程底层是一段可挂起可恢复的代码块而不是一个常驻线程。面试官还会追问一个高频误区GlobalScope 为什么不能随便用因为它没有作用域约束生命周期不受页面控制页面销毁后协程还在跑轻则浪费资源重则回调已经销毁的 UI 实例。正确做法是使用 lifecycleScope 或 viewModelScope让协程跟着宿主走。再补充一句 Dispatchers.IO 和 Dispatchers.Default 其实共用同一个线程池仅靠任务标记区分就意味着你真正读过协程源码。1.5 重载与重写静态分派和动态分派重载是编译期决定的普通方法选择编译器根据参数的静态类型选方法这叫静态分派重写是运行期根据对象的实际类型动态选择方法这叫动态分派。典型的问法一个 Parent 类型引用指向 Child 对象调用某个被重写的方法实际执行的是 Child 的实现而如果方法只是重载编译器只看变量声明类型。加分项是提一下 invokevirtual 指令和虚方法表。JVM 在类加载时会给每个类建一张虚方法表记录所有虚方法的入口地址运行时的方法分派本质上就是查这张表。再深入一步可以提接口默认方法出现之后接口继承也带来了新的分派逻辑语言本身也在不断演进。这题不难但能体现你对字节码机制有没有真正理解。2. 四大组件与启动流程绕不开的基本盘2.1 Activity 启动模式不止在背四种 launchModestandard、singleTop、singleTask、singleInstance 四个名词几乎是必背题但面试官更想听的是你能不能结合真实场景选型。singleTop 适合接收推送通知详情页通知来了页面已经在栈顶就不重复创建直接走 onNewIntent 更新数据。singleTask 适合主页场景App 在后台很久了点通知进二级页时能顺手把整个任务栈拉回前台。singleInstance 现在很少用了因为从 Android 10 开始普通应用已经无法用它在后台启动 Activity很多国产定制系统限制更早。这里有个容易被忽略的考点taskAffinity 和 allowTaskReparenting。设置 singleTask 时往往要配合 taskAffinity 指定任务栈否则它会复用默认栈。自己做过 launcher 类应用的人应该很清楚很多诡异现象最后都定位到 taskAffinity 没配。2.2 屏幕旋转时的完整生命周期不配置 configChanges 的情况下旋转屏幕会走 onPause、onStop、onDestroy、onCreate、onStart、onResume 这一整串。很多人背了顺序但答不出为什么旋转意味着资源配置变化系统认为需要重建一个适应新尺寸的 Activity于是先销毁旧的再创建新的。onSaveInstanceState 的调用时机在 onPause 之后、onStop 之前。注意它只在非主动销毁时被调用用户主动 finish 或按返回键不会走这里。恢复数据有两个选择在 onCreate 里拿 Bundle或者在 onRestoreInstanceState 里拿。一个真实踩坑点是千万不要把大对象塞进这个 Bundle比如 Bitmap超过一定大小会直接抛 TransactionTooLargeException。保存少量状态可以大对象请交给 ViewModel。能补充说明 Fragment 在旋转时也会被 FragmentManager 自动保存和恢复本题的完成度会立刻高一截。2.3 Service 的两种启动方式startService 和 bindService 的区别是必背题。startService 启动后Service 和调用方没有绑定关系调用方走了 Service 还在跑需要主动 stopService 或 stopSelfbindService 则是客户端拿到 Binder 代理双方生命周期绑定最后一个客户端 unbind 时Service 会走 onUnbind 并销毁。真正容易丢分的是混合场景先 startService 再 bindService那 Service 必须同时 stopService 和 unbind 才能真正销毁。很多做音乐播放器的人在后台播放需求里把两个状态混在一起导致 Service 杀不掉或者生命周期错乱。我在实际项目里推荐的处理方式是onCreate 里 startForeground页面启动时 bindService页面销毁时 unbindService最后退出整个播放器时 stopService。这样既能控制播放又能保证系统不会轻易杀掉它。另外不要忘记前台服务的限制。Android 8.0 之后后台服务无法长时间运行Android 10 之后后台启动前台服务也被限制后台播放音乐等场景必须声明 foregroundServiceType。如果面试官问 Service 却完全不提前台服务限制那大概率是在等你的临场发挥。2.4 广播动态注册、静态注册与高版本限制静态注册写在 Manifest 里进程被杀也能被系统拉起动态注册在代码里 registerReceiver必须配合 unregisterReceiver生命周期跟着注册方走。Android 8.0 限制了隐式广播的静态注册大量自定义广播被迫改成动态注册这是版本适配的经典考点。动态注册最容易踩的坑是不反注册。registerReceiver 之后系统端的注册表里就会留一个 Receiver 对象如果 Activity 销毁了却没有 unregister进程还活着系统回调就会指向一个泄漏的 Receiver。真实项目里经常看到第三方广告 SDK 在 onResume 注册网络监听onPause 漏了注销导致内存泄漏警告刷屏。高版本之后LocalBroadcastManager 也不再被官方推荐一般是用 LiveData、StateFlow 或轻量事件总线替代。能把这些限制和替代方案都理清楚说明你适配过 Android 版本不只是一个只会写 registerReceiver 的新手。2.5 ContentProvider 为什么能抢先初始化ContentProvider 很少被单独拎出来问但它是理解启动流程的关键钥匙。App 启动时Application 的 attachBaseContext 先执行然后系统会按 Manifest 里声明的 provider 列表依次安装最后才回调 Application 的 onCreate。也就是说ContentProvider 的 onCreate 比 Application 的 onCreate 还要早。这个时序被很多 SDK 利用来实现“无侵入初始化”不需要用户手动调用 init 方法。WorkManager、数据上报组件、部分地图 SDK 就是通过 ContentProvider 自启动的。负责的方面也有ContentProvider 太多会拖慢冷启动所以启动优化的一个方向就是合并或移除无必要的 Provider。考察这个点面试官是在确认你真正读过系统启动流程而不是只知道 Application 生命周期。3. Handler 与 Binder占据半壁江山的机制题3.1 Handler 机制的完整链路Handler 题几乎必问而且一定会层层追问。完整链路是Handler 发送 MessageMessage 进入 MessageQueueLooper.loop() 开启死循环不断从队列取消息并 dispatchMessage最终回调 Handler 的 handleMessage。四个角色缺一不可。容易丢分的点有三个。第一MessageQueue 虽然叫 Queue但结枀上是链表结构的消息池Message 在 send 后会被回收进池子复用不能长期持有 Message 里的数据。第二Looper.loop() 为什么不会卡死主线程因为主线程本身就是一个无限事件循环所谓卡死是指事件来不及处理。如果移除 loopMessageQueue 就没人消费了所有 UI 操作都会堆积不执行。第三IdleHandler 是加分点当队列暂时没有紧急消息时系统会执行空闲任务比如延迟埋点、预加载下一屏。能主动聊到 IdleHandler 的候选人基本就是认真啃过 Handler 源码的。3.2 ThreadLocal 到底做了什么ThreadLocal 经常和 Handler 一起考因为 Looper 就是存在 ThreadLocal 里的。每个线程通过 ThreadLocal 拿到属于自己的 Looper互不干扰。原理要讲到 Thread 类内部有一个 ThreadLocalMapkey 是 ThreadLocal 对象本身value 是你要存的值。同一个 ThreadLocal 在不同线程 get拿到的是各自 Map 里对应的 value因为每个线程的 Map 相互独立所以实现了线程隔离。但这里有个让很多人翻车的细节ThreadLocalMap 的 key 被设计成弱引用value 却是强引用。如果外部 ThreadLocal 引用被置空而线程还活着比如在线程池的核心线程里value 就永远被这个 Map 强持有造成内存泄漏。规范用法是用完就 remove。能把这点讲出来说明你真的写过长驻场景的线程池并且踩过内存坑。3.3 Handler 内存泄漏的三种解法Handler 持有 Activity 是经典泄漏场景。原理是Handler 发送出去的 Message 带着 target 引用如果 Message 还留在 MessageQueue 里没处理而 Handler 是 Activity 的内部类隐式持有外部类引用Activity 就无法被回收。常见于延时 Runnable 和周期性消息。解法至少有三种。第一种是静态内部类 Handler WeakReference 持有 Activity兜底写法第二种是 onDestroy 时移除所有消息和回调第三种是用生命周期感知的组件来管理任务比如 Lifecycle 或 ViewModel。我个人推荐后两种做主线。原因是弱引用只切断了引用链并不能阻止延时回调本身就是你不想执行的那个动作如果你根本不移除消息回调还是会在错误的时间触发。还有个更隐蔽的场景是 HandlerThread 没有主动 quit。子线程的 Looper 会一直阻塞在 loop 里即使所有任务都跑完了。很多长连接项目把 HandlerThread 当常驻线程必须记得在合适的生命周期里控制它否则线程会一直占着资源。3.4 Binder 为什么是 Android IPC 的首选Binder 是 Android 面试里最深的一道题。回答框架是先比较 Linux 原生 IPC 方式。管道、消息队列、信号量、Socket、共享内存都不太适合 Android共享内存性能虽好但管理和同步太复杂Socket 性能差管道和消息队列性能也不行更重要的是安全模型不满足“只暴露我想暴露的能力”。Binder 采用 mmap 做一次内存拷贝。调用方数据拷到内核缓冲区再通过 mmap 映射直接读到接收方用户空间省掉了从内核缓冲区再拷到用户态的一次复制所以叫“一次拷贝”。对比传统 IPC 往往是两次拷贝这是 Binder 性能上的关键优势。安全方面Binder 在内核里为每个进程建立了 UID/PID 标识协议自带身份校验不像共享内存那样需要用户层自己维护权限。再补一句实操经验每个 App 进程的 Binder 线程池默认数量有限大量跨进程通信时可能把 BinderThread 占满导致调用卡死。这一句往往能体现你处理过真正的跨进程性能问题。3.5 子线程到底能不能更新 UI这个问题看起来简单但答案不能只说“不能”。早期 Android 确实在 ViewRootImpl 里通过 checkThread 检查调用线程非 UI 线程更新 UI 会抛异常。但更准确的理解是UI 操作必须在 View 指定的线程通常是 UI 线程而子线程可以通过 SurfaceView 或 TextureView 的 Surface 在自己的渲染线程绘制也可以 post 到 UI 线程再执行。面试官真正想听的是为什么采用单线程模型。因为 View 和绘制管线本身不是线程安全的与其给整个绘制系统加一把大锁不如规定所有操作都在一个线程里串行执行这是 Handler 存在的重要理由。补充一个细节子线程调用 view.post() 为什么是安全的post 内部会把 Runnable 通过 ViewRootImpl 的 Handler 投递到 UI 线程队列如果 View 还没 attach 到 Window它会等 attach 后再执行。所以 view.post 是安全更新 UI 的常用方式比 runOnUiThread 更好用因为它保证了 View 已经 attach。4. View 体系与触摸事件自定义 View 面试题的高频来源4.1 measure / layout / draw 的完整链路自定义 View 考察的核心就是这三个流程。measure 阶段根据父 View 的 MeasureSpec 和自身 LayoutParams 确定测量尺寸有三种模式EXACTLY、AT_MOST、UNSPECIFIED。最容易踩的坑是重写 onMeasure 时直接 setMeasuredDimension 写死高度完全忽略父容器的约束。比如 RecyclerView 的 item 高度设置 wrap_content你却在 MeasureSpec 是 AT_MOST 时给了一个超大尺寸item 就会铺满屏幕。layout 阶段由父 View 调用 child.layout 确定子 View 的位置ViewGroup 则要在 onLayout 里遍历所有子 View 安排布局。draw 阶段按顺序执行drawBackground、onDraw、dispatchDraw、onDrawForeground。能理解这个顺序才能解释为什么自定义 View 的背景和前景总是出现微妙的问题。回答时落到 ViewGroup 层面更容易加分。测量是双层循环父 measure 子子 measure 完可能又会触发父重新 measurelayout 是深度优先遍历draw 是整棵树的递归绘制。能提到 getChildMeasureSpec 和 ViewGroup 的 measure 逻辑面试官会认为你真的调过自定义 ViewGroup。4.2 触摸事件的三个方法事件分发考的是三个方法dispatchTouchEvent、onInterceptTouchEvent只有 ViewGroup 有、onTouchEvent。整体流程是先自上而下分发再自下而上回溯处理。典型场景题是RecyclerView 里嵌套一个可以横向滑动的自定义 View如何处理事件冲突。标准做法是子 View 滑动时调用 parent.requestDisallowInterceptTouchEvent(true)请求父 View 不要拦截。但要注意如果父 View 在 ACTION_UP 强制拦截回家或者子 View 消费不了事件父 View 还是会接管。另一个加分点是 OnTouchListener 和 onTouchEvent 的执行顺序OnTouchListener 返回 true 就会拦截事件不再进入 onTouchEventonClick 在 onTouchEvent 的 UP 事件且 pressed 状态命中时触发。把消费链条完整说出来就不像只会背三个方法名了。往深答时还要提 ACTION_CANCEL。当一个事件流因为父 View 重新拦截而中断系统会补发一个 CANCEL 让子 View 回滚状态。很多新手拦截代码不处理 CANCEL导致按钮按压状态残留这个小细节经常是实战里最难查的问题。4.3 requestLayout 与 invalidate 的区别这是自定义 View 高频分类题。invalidate 只触发 draw 阶段和 onDraw不做测量和布局标记当前 View 的 dirty 区域重刷。requestLayout 会触发 measure、layout、draw 的完整链路不仅影响当前 View还会影响整个 ViewGroup 子树。有一个点非常重要requestLayout 不代表马上执行。它会挂一个运行标志由 Choreographer 在下一帧的遍历里执行而且可能连续调多次只做一次完整流程。所以动画里如果只是更新颜色或文字位置用 invalidate 局部重绘就够了只有真正改变了宽高或子 View 的位置才需要 requestLayout。频繁调用 requestLayout 会导致整棵子树反复测量和布局这是卡顿优化里容易被低估的原因。能提到 requestLayout 最终会调用 ViewRootImpl.requestLayout 并经过 Choreographer 合并说明你对渲染线程的协作已经有概念了。4.4 自定义 View 最容易被忽略的坑面试时我常用的提问模板是如果自定义 View 的宽高是 wrap_content你怎么处理很多人没有重写 onMeasure直接在构造函数里给默认尺寸但 wrap_content 场景下 Enter 的是 AT_MOST 的 MeasureSpec你给固定值其实是拿最大可用空间当尺寸表现出来就是控件铺满父布局。第二个高频坑是 onDraw 里创建对象和分配内存。绘制阶段调用频率非常高一帧 60 次可能每次都会走 onDraw。如果在这里 new 一个 Paint 或 Path短时间内会分配大量短命对象直接推高 GC 频率。正确做法是成员变量在初始化时建好draw 时只复用它。第三个是线程安全。很多人喜欢在子线程先改 View 的数据再调 invalidate。invalidate 本身线程安全它可以被任意线程调用但真正的风险是你的数据源没有同步。如果 UI 线程一边读数据绘制子线程一边改数据即使每次 invalidate 都不会报错画面也会偶发错乱。标准解法是数据改动也切回 UI 线程或者子线程改完的是一份快照拿到快照再更新 View。4.5 Choreographer、帧率与卡顿的关系自定义 View 讲深了必然会碰到 Choreographer。它负责协调输入、动画、绘制三个子系统每 16.6 毫秒对着垂直同步信号发起一帧的绘制。一帧里要做的事情包括输入处理、动画更新、measure/layout/draw、同步屏障清理。如果这些工作超过了 16ms帧率就掉到 30用户感知就是卡顿。常考的进阶点是同步屏障。MessageQueue 里可以插入一个同步屏障消息之后所有同步消息都被拦住只有异步消息能继续执行。Choreographer 的帧回调就是异步消息通过同步屏障获得优先执行资格。这道题的关键是把 Handler 机制和渲染机制打通主线程的“消息处理”和“一帧帧渲染”其实是同一套消息泵的两个不同角色。到这里基本就是中级向高级的分界线。能从容讲出 Choreographer 与 Looper、同步屏障、VSYNC 之间的关系面试官会倾向于认为你有解决复杂渲染问题的能力。5. 网络、数据与线程业务开发的地基题5.1 HTTPS 握手流程与证书校验HTTPS 题基本都会考。要点不是“HTTP 加了一层加密”而是整个握手流程。先建 TCP 连接客户端发 ClientHello 指定 TLS 版本和加密套件列表服务端返回 ServerHello、证书、密钥交换参数。客户端验证证书链验证通过后生成对称密钥用服务端公钥加密发过去双方确认后切换对称加密传数据。之所以是混合加密是因为非对称加密性能差但适合安全交换密钥对称加密快但需要预先约定共同密钥。容易被追问的是证书校验。客户端要验证证书是否由受信任 CA 签发、是否过期、域名是否匹配Android 里如果要连自签名证书服务必须自己配置 TrustManager。很多人图省事直接信任所有证书这在线上是致命问题等于中间人攻击畅通无阻。回答时如果能提一句 SNI说明你在同一 IP 多证书的场景里踩过坑。这道题里自己抓过包、配过证书的人通常能拿到高分因为细节一追问就能感觉到是真做过还是背过。5.2 TCP 三次握手为什么不能是两次三次握手的标准答案是同步初始序列号、确认双方收发能力。但更容易让面试官眼前一亮的是“防止历史重复连接”这个角度。假设客户端发了一个 SYN 因为网络延迟卡了很久超时重传后又建立了连接而老的 SYN 后来才到达服务端。服务端认为这是新连接分配资源并回 SYNACK可客户端根本不理会。两次握手的情况下服务端会长期等待一个不存在的客户端浪费资源。三次握手时客户端可以通过确认的序列号判断这是不是历史的旧连接如果是就回 RST 终止。能答到这个层面比单纯背“确认收发能力”更完整。顺带可以提四次挥手为什么需要四次因为半关闭机制主动关闭方发 FIN 后另一方可能还有数据要发所以 ACK 和 FIN 是分开的。虽然不是必考但能接住追问总是好事。5.3 线程池的七个参数和执行流程这题考的是 ThreadPoolExecutor 的七个参数核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。执行流程是核心线程满了就入队队列满了就扩到最大线程数最大也满了就走拒绝策略。如果用无界队列线程数永远不会超过核心线程数这一点很多人理解反了。拒绝策略在真实项目里更有价值。自带四种AbortPolicy 抛异常、CallerRunsPolicy 调用者执行、DiscardPolicy 丢弃、DiscardOldestPolicy 丢弃最老任务。实际开发中很少用 AbortPolicy因为会直接中断业务CallerRunsPolicy 能起背压作用但可能拖慢调用方本身定制策略可以做日志和补偿入队。Android 开发里这题还会关联到为什么不推荐 Executors.newFixedThreadPool因为它的任务队列是无界的任务堆积严重时内存会膨胀。这个补充非常加分能证明你不是只看过教程而是处理过线上 OOM。5.4 SharedPreferences 的缺陷与数据存储替代SP 是入门级知识但现在越来越常被拿来追问。第一条是 apply 异步落盘之前修改会在内存里直接生效可写盘失败或进程被杀数据就丢了。commit 虽然同步但卡 UI。第二条是 SP 全量加载第一次 getSharedPreferences 会把整个 XML 文件的所有键值读进内存如果配置文件很大冷启动阶段就会多出一块不小的 IO 耗时。替代方案里 MMKV 用的是 mmap 映射写操作直接改内存映射页崩溃也不容易丢一半数据性能明显优于 SP。Jetpack DataStore 也是推荐方向Preferences DataStore 类似键值对但基于 Flow 支持协程Proto DataStore 适合结构化数据。回答时如果结合自己项目里的存储选型讲比单纯背对比表有说服力得多。5.5 SQLite 事务、索引与 Room 场景SQLite 基础题不会太难但概念要清。事务的核心是原子性要么全成功要么全失败。SQLite 里每条 SQL 默认在一个事务里批量插入时一定要用 beginTransaction 包裹否则每条 insert 都要独立走一遍日志写盘性能差距可能是几十倍。我见过真实项目里 for 循环逐条往大表里插数据几千条等了非常久改成事务后秒级完成。索引为什么快因为 B 树把查找从全表扫描的 O(n) 降到了 O(log n)。但索引也有代价写入时要维护多棵 B 树所以索引要建在查询频繁且写入不那么猛烈的字段上。有一个经典追问是加了索引为什么反而更慢大概率是优化器选了一个低选择性的索引或者谓词条件不够匹配索引结构需要重新 analyze。Room 对 SQLite 的关系就是 ORM 加编译期 SQL 校验加 LiveData/Flow 联动观察数据库变化。常规问题里能说清楚 Transaction 的作用和与协程的整合就够了。6. 性能优化与架构决定 offer 级别的分水岭6.1 ANR 的本质与定位链路ANR 题到高级岗位基本是必考。背五大数据类型只是基础BroadcastReceiver 超时、Service 超时、InputDispatching 超时、ContentProvider 超时、JobService 超时。真正值钱的还是定位链路。定位思路一般按三步走第一步看 logcat 里的 ANR in 输出里面包含进程名、耗时时间、主线程状态第二步看 /data/anr/traces.txt 里主线程堆栈直接从调用栈看出卡在哪个方法、在等哪把锁第三步结合 CPU 占用判断如果 CPU 跑满那可能有死循环或频繁 GC如果 CPU 不高但 ANR那大概率是主线程在等 IO 或锁。我印象很深的一次线上 ANRtraces 里主线程卡在日志 SDK 的 synchronized 方法上。第三方库写日志时持锁网络请求回调也在抢同一把锁最后把日志库的同步写改成异步消息队列问题才消失。能讲出这种案例面试官通常会很愿意继续深聊。6.2 内存泄漏常见场景与 LeakCanary 原理第一问通常是泄漏和溢出的区别。泄漏是对象本可回收却被强引着溢出是内存确实不够分但因果关系紧密泄漏多了自然会溢出。高频泄漏场景至少有五类Handler 和 Runnable 未移除、静态变量持有 Activity 或 View、单例持有 Activity Context、流未关闭、注册回调后未反注册。我在真实项目里排查到最多的其实是单例持有 Context一个全局播放器单例保存了页面的 Activity Context页面关闭后它一直持着导致整个 Activity 无法回收。LeakCanary 原理也要会讲。它注册 ActivityLifecycleCallbacks 监听页面销毁销毁的实例放进弱引用队列一定时间后没有被回收就主动 GC 一次再查还是没回收就做堆转储并分析引用链。面试里能说出“二次 GC 确认”这个细节已经是明显的加分项。6.3 冷启动流程与启动耗时优化冷启动完整流程是系统 fork 进程、加载 Application 类、执行 attachBaseContext 和 onCreate、初始化 ContentProvider、启动 Activity、渲染第一帧。启动优化的本质就是看从点击图标到第一帧可交互花了多少时间。优化手段我按收益排序。第一是减少 Application.onCreate 里的同步初始化能延迟的都延迟到首帧后或 IdleHandler第二是合并或移除自动初始化的 ContentProvider它串行遍历会拖慢进程第三是复杂项目可以用协程化启动器做任务依赖调度但小项目要谨慎引入复杂度可能大于收益第四是 windowBackground 设置预览但这是感知优化不能当真正的性能提升。容易被忽略的是类加载耗时。方法数太多ClassLoader 加载类的时间也会变长所以精简依赖、按需加载同样能改善冷启动。性能优化题很难靠背模板拿高分结合案例给前后数据对比最有说服力。6.4 MVC、MVP 与 MVVM架构题的答题框架架构对比几乎是高级岗位标配。MVC 里 Activity 同时承担 Controller 和 View 的职责自然越来越臃肿MVP 把 View 抽象成接口Presenter 负责逻辑但 View 接口会很碎片Presenter 也会堆大量页面逻辑MVVM 用 ViewModel 暴露状态给 View数据驱动 UI 更新靠 LiveData/Flow 或 DataBinding 解耦。真正的加分点是讲清 ViewModel 和 LiveData 的细节。ViewModel 为什么旋转时不销毁因为 ViewModelStore 是 ComponentActivity 的一个成员旋转时 ViewModelStoreOwner 虽然换了新实例但同一个 ViewModelStore 被复用了ViewModel 只有真正 finish 时才会 onCleared。LiveData 为什么旋转不泄漏因为 Observer 被 LifecycleOwner 自动管理旧 Observer 会被移除。同时也要说 MVVM 的坑。如果全部用 LiveData 和双向绑定流事件追踪会变困难ViewModel 里如果放太多状态页面重建时要做很多还原计算。架构选型要结合团队和规模不要为了架构而架构。6.5 LiveData、Flow 与 Compose现代 UI 方案还会怎么问现在的面试题明显往 Compose 和 Kotlin Flow 靠。Compose 的数据驱动 UI 模型和传统 View 不同状态变化会触发重组重组范围是读取了该状态的 UI 部分而不是整棵树。这个知识点可以直接解释很多性能问题为什么把状态读取放到 lambda 外部会导致整个 Content 重组用 derivedStateOf 可以减少不必要的重算。Flow 与 LiveData 的选择也是热点。LiveData 优势是生命周期感知、用法简单、粘性事件直接Flow 优势是更丰富的操作符、背压处理、不依赖 Android 框架StateFlow 可以做类似 LiveData 的状态管理。StateFlow 有 distinctUntilChanged 特性会过滤掉连续重复值这是经常用来区分“只是背概念”和“真用过”的细节。如果聊到 Compose 性能最好提到 remember、mutableStateOf 和 stable 的概念。和传统自定义 View 一样Compose 也有布局和重组代价的问题答案里带上一个你自己写过的性能观察会非常加分。我自己的体会是面试题永远不是背出来的是用出来的。这 30 个高频问题放在一起看是一份查漏补缺清单拆开来看每个知识点背后都连着至少一个真实场景的坑。准备的时候建议对着自己最近做过的项目把每个知识点对应到某一次调试、某一次性能优化、某一次崩溃排查里。全部对上之后再去面试状态会完全不一样。祝各位面到想要的岗位也真的能从面试中反推出自己的短板。