Android第三次面试总结之activity和线程池篇(补充)
一、线程池高频面试题
1. 为什么 Android 中推荐使用线程池而非手动创建线程?(字节跳动 / 腾讯真题)
- 核心考点:线程池的优势、资源管理、性能优化
- 答案要点:
- 复用线程:避免重复创建 / 销毁线程的开销(单个线程创建耗时约 9ms,复用可降低延迟)。
- 控制并发:通过参数(
corePoolSize
/maxPoolSize
)控制最大并行任务数,防止 OOM(如大量并发网络请求导致内存溢出)。 - 队列管理:任务队列(如
LinkedBlockingQueue
)缓冲未执行任务,避免系统资源耗尽。 - 线程管理:后台线程自动回收,避免内存泄漏(手动创建线程若持有 Activity 引用,易导致 Activity 无法销毁)。
- 大厂真题延伸:
“如果项目中需要处理 1000 个耗时任务(如文件下载),如何设计线程池参数?”
答:根据任务类型选择策略:- 计算密集型:
corePoolSize
≈CPU 核心数(Android 设备常见 4-8 核); - IO 密集型:
corePoolSize
可适当增大(因 IO 等待时线程可空闲,如2*CPU核心数
); - 此处下载任务属 IO 密集型,可设
corePoolSize=5
,maxPoolSize=10
,队列选LinkedBlockingQueue
(有界队列避免内存溢出),拒绝策略用AbortPolicy
(抛异常提醒任务处理失败)。
- 计算密集型:
2. ThreadPoolExecutor 的核心参数有哪些?工作流程是什么?
- 核心参数(必背!):
public ThreadPoolExecutor(int corePoolSize, // 核心线程数,空闲时也会保持存活int maximumPoolSize, // 最大线程数,核心+临时线程总和long keepAliveTime, // 临时线程空闲时的存活时间TimeUnit unit, // 时间单位BlockingQueue<Runnable> workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂(自定义线程名,方便调试)RejectedExecutionHandler handler // 拒绝策略 );
- 工作流程(画图辅助理解):
- 任务提交,若线程数<
corePoolSize
,创建新线程执行; - 若线程数≥
corePoolSize
,任务入队(根据队列类型,如无界队列LinkedBlockingQueue
会一直排队); - 若队列已满且线程数<
maximumPoolSize
,创建临时线程执行; - 若队列已满且线程数≥
maximumPoolSize
,触发拒绝策略(4 种策略需背熟:AbortPolicy
/CallerRunsPolicy
/DiscardPolicy
/DiscardOldestPolicy
)。
- 任务提交,若线程数<
- 踩坑点:
“为什么不推荐使用 Executors 工具类创建线程池?”
答:Executors 创建的线程池存在隐患:newFixedThreadPool
/newSingleThreadExecutor
使用无界队列LinkedBlockingQueue
,任务大量堆积时可能 OOM;newCachedThreadPool
的maximumPoolSize
为Integer.MAX_VALUE
,突发大量任务时会创建海量线程,导致 CPU 过载。
正确做法:直接使用ThreadPoolExecutor
自定义参数,根据业务场景配置。
3. 线程池如何实现任务优先级?
- 核心思路:自定义任务队列,重写
offer()
/poll()
方法实现优先级排序。 - 代码示例:
// 优先级任务队列(实现Comparator) PriorityBlockingQueue<Runnable> priorityQueue = new PriorityBlockingQueue<>(10, (r1, r2) -> {if (r1 instanceof PriorityRunnable && r2 instanceof PriorityRunnable) {return ((PriorityRunnable) r2).getPriority() - ((PriorityRunnable) r2).getPriority(); // 高优先级先执行}return 0; }); // 创建线程池时使用该队列 ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, priorityQueue );
- 面试加分项:结合实际场景(如直播场景中,弹幕渲染任务优先级高于日志上报),说明优先级队列的应用价值。
二、Activity 高频面试题
1. Activity 生命周期完整流程是什么?哪些场景会触发各个回调?
- 关键回调触发场景:
onCreate()
:Activity 创建时调用(初始化 UI、数据),系统异常重建时也会调用(需结合onSaveInstanceState
恢复数据)。onStart()
→onResume()
:Activity 可见 / 可交互时调用(区别:onStart
表示可见但可能在后台,onResume
表示位于前台)。onPause()
:Activity 失去焦点时调用(如跳转到新 Activity,需在此处停止动画、释放 CPU 资源,但避免耗时操作,否则影响新 Activity 启动)。onStop()
:Activity 完全不可见时调用(可执行较耗时操作,如保存数据,但需注意若用户快速返回,可能跳过onStop
直接调用onRestart
)。onDestroy()
:Activity 销毁时调用(释放所有资源,避免内存泄漏,如取消网络请求、解绑 Listener)。
- 大厂真题:
“横竖屏切换时,Activity 的生命周期如何变化?如何避免数据丢失?”
答:- 默认情况下,屏幕旋转会导致 Activity 销毁重建,生命周期走:
onPause()
→onStop()
→onDestroy()
→onCreate()
→onStart()
→onResume()
; - 避免数据丢失方案:
- 重写
onSaveInstanceState(Bundle)
保存临时数据(系统自动调用,在onStop()
前),onRestoreInstanceState(Bundle)
或onCreate(Bundle)
中恢复; - 在 Manifest 中给 Activity 添加
android:configChanges="orientation|screenSize"
,阻止重建,此时仅调用onConfigurationChanged()
。
- 重写
- 默认情况下,屏幕旋转会导致 Activity 销毁重建,生命周期走:
2. Activity 启动模式有哪些?应用场景是什么?
- 四大启动模式(必背!):
模式 核心特性 典型场景 standard
每次启动都创建新实例,默认模式,多实例共存(栈内可存在多个相同 Activity) 普通页面(如商品详情页) singleTop
若栈顶已有实例,不再创建,调用 onNewIntent()
消息通知页(避免重复打开顶部) singleTask
栈内唯一实例,启动时清除栈中该 Activity 以上的所有 Activity(单任务栈) 主页(如微信首页,需清空其他页面) singleInstance
全局唯一实例,独占一个任务栈,其他 Activity 需跳转至该栈调用 来电界面(独立于其他应用) - 进阶问题:
“如何理解任务栈(Task)和启动模式的关系?singleTask
为什么会清空栈顶?”
答:- 任务栈是 Activity 的容器,遵循 “后进先出”,每个应用默认有一个主任务栈;
singleTask
要求实例在栈内唯一,若目标栈中存在该 Activity,会清除其上方的所有 Activity,确保其成为栈顶(类似 “回到主页并清空中间页面”)。
“启动模式和IntentFlag
(如FLAG_ACTIVITY_NEW_TASK
)如何配合使用?”
答:FLAG_ACTIVITY_NEW_TASK
相当于singleTask
模式,优先级高于清单文件配置,二者同时存在时以IntentFlag
为准。
3. Activity A 启动 Activity B,如何回传数据?异常情况下(如 B 被系统杀死)如何保证数据不丢失?
- 常规方法:
- A 通过
startActivityForResult(Intent, requestCode)
启动 B; - B 通过
setResult(resultCode, data)
回传数据,A 在onActivityResult()
中处理; - 注意:Android 10 + 建议使用
ActivityResultContract
替代(更简洁安全,避免内存泄漏)。
- A 通过
- 异常场景处理:
- 若 B 在后台被系统杀死(内存不足),系统会保存其
onSaveInstanceState
数据,重建后onActivityResult()
仍会回调,但需在 B 的onCreate()
中恢复必要数据,确保setResult()
正常调用。
- 若 B 在后台被系统杀死(内存不足),系统会保存其
- 大厂真题延伸:
“如果 A 启动 B 后,B 又启动 C,C 如何直接返回数据给 A?”
答:- 传统方法:C→B→A 逐层回传(繁琐);
- 优化方案:使用事件总线(如 EventBus、LiveData),C 发送事件,A 订阅并处理(需注意解耦和内存泄漏,Activity 销毁时取消订阅)。
面试扩展:
一、线程池深度补充
1. 线程池中的线程为什么不会无限创建?核心源码解析
- 源码关键逻辑(
ThreadPoolExecutor.execute(Runnable command)
):public void execute(Runnable command) {if (command == null) throw new NullPointerException();int c = workerCountOf(ctl.get());// 步骤1:若当前线程数 < 核心线程数,创建核心线程执行任务if (c < corePoolSize) {if (addWorker(command, true)) return;c = workerCountOf(ctl.get());}// 步骤2:若线程数 >= 核心线程数,将任务加入队列if (isRunning(c) && workQueue.offer(command)) {int recheck = workerCountOf(ctl.get());// 再次检查线程池状态(防止加入队列后线程池已关闭)if (!isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false); // 确保至少有一个核心线程存在(allowCoreThreadTimeOut=false时)}// 步骤3:队列已满,创建非核心线程(直到maxPoolSize),失败则触发拒绝策略else if (!addWorker(command, false))reject(command); }
- 核心控制逻辑:
- 通过
ctl
变量(32 位整数,高 3 位表示状态,低 29 位表示线程数)原子性控制线程创建和状态转换; addWorker
方法会检查线程数是否超过maximumPoolSize
,确保线程数不超过上限。
- 通过
2. 如何监控线程池的运行状态?大厂性能优化必问题
- 关键监控指标:
方法 含义 应用场景 getActiveCount()
当前正在执行任务的线程数 判断线程池是否繁忙(如扩容依据) getCompletedTaskCount()
已完成的任务总数 统计任务处理效率 getQueue().size()
等待执行的任务数 预警队列堆积(如超过阈值时扩容) getLargestPoolSize()
线程池曾创建的最大线程数 分析峰值负载,优化 maxPoolSize
- 实战方案:
// 定时打印线程池状态(建议在后台线程执行) ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(); monitor.scheduleAtFixedRate(() -> {log.d("Thread pool status: " +"active=" + executor.getActiveCount() +", queueSize=" + executor.getQueue().size() +", completed=" + executor.getCompletedTaskCount()); }, 0, 5, TimeUnit.SECONDS);
- 大厂真题:
“如果发现线程池队列持续堆积,如何排查和解决?”
答:- 检查任务处理耗时是否过长(可能存在耗时操作未优化);
- 确认
corePoolSize
是否过小(IO 密集型任务可适当增大); - 若使用有界队列,可临时增大队列容量或调整
maxPoolSize
; - 考虑任务优先级,拆分高优先级任务到独立线程池。
3. WorkManager vs 线程池:Android 后台任务如何选择?
- 核心区别:
特性 线程池(ThreadPoolExecutor) WorkManager 生命周期管理 依赖宿主(如 Activity)手动关闭 系统级管理,支持跨进程、跨重启执行 持久化任务 不支持(任务丢失后需手动重启) 支持(通过 WorkRequest
配置持久化)约束条件 无(需自行处理网络、电量等限制) 支持网络状态、充电状态等约束 适用场景 短期、实时性任务(如网络请求) 长期后台任务(如日志上报、定时同步) - 大厂最佳实践:
- 前台实时任务(如界面数据加载):使用线程池 + Activity 生命周期绑定;
- 后台异步任务(如下载、定时任务):优先使用 WorkManager(避免后台服务被系统杀死,兼容 Android 12 + 前台服务限制)。
二、Activity 深度补充(Framework 层原理 + 实战坑点)
1. Activity 启动流程
- 极简流程图:
用户操作 → Launcher发送启动Intent → AMS(ActivityManagerService)查询目标Activity信息 → 若目标进程未启动,创建ProcessRecord → 创建ActivityThread → 创建Activity实例 → 依次调用Activity的生命周期回调(onCreate→onStart→onResume)
- 面试高频问题:
“Activity 启动时,AMS 如何处理任务栈?singleInstance
模式为什么独占一个任务栈?”
答:- AMS 维护一个任务栈列表,每个任务栈有独立的 Task ID;
singleInstance
模式要求 Activity 在全局唯一,因此 AMS 会为其创建新的任务栈,确保其他 Activity 无法进入该栈,实现 “独占”。
2. onSaveInstanceState vs onRestoreInstanceState:必坑细节
- 调用时机:
onSaveInstanceState
:非用户主动销毁时调用(如系统内存不足、配置变更),用户按 Back 键不会调用(因 Activity 是主动销毁,无需保存);onRestoreInstanceState
:在onStart()
之后、onResume()
之前调用,仅当 Activity 有保存的 InstanceState 时触发。
- 数据丢失场景:
“如果在 onSaveInstanceState 中保存了 EditText 内容,为什么旋转屏幕后内容还在,但用户按 Back 键退出再进入就丢失了?”
答:- 旋转屏幕属于配置变更,系统自动保存并恢复
InstanceState
; - 按 Back 键退出时 Activity 被主动销毁,
onSaveInstanceState
不会调用,因此未保存数据,再次进入时需通过onCreate
的参数或ViewModel
恢复。
- 旋转屏幕属于配置变更,系统自动保存并恢复
- 最佳实践:
- 临时 UI 状态(如 EditText 内容、列表滚动位置):用
onSaveInstanceState
; - 持久化数据(如用户登录状态、网络请求结果):用
ViewModel
(不随 Activity 销毁而销毁)或SharedPreferences
。
- 临时 UI 状态(如 EditText 内容、列表滚动位置):用
3. Activity 异常销毁(系统回收)后的恢复策略
- 三大恢复方案对比:
方案 数据类型 生命周期感知 优缺点 onSaveInstanceState 临时 UI 状态(可序列化) 仅处理配置变更 / 内存回收 简单易用,但数据需可序列化,且 Back 键不触发 ViewModel 与界面相关的业务数据 感知 Activity/Fragment 生命周期 不随配置变更销毁,需配合 LiveData 更新 UI SavedStateHandle 键值对数据(Jetpack) 替代传统 InstanceState 类型安全,支持更复杂数据结构(如 List) - 代码示例(ViewModel + SavedStateHandle):
class MyViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {private val _userData = savedStateHandle.getLiveData<String>("user_data")val userData: LiveData<String> get() = _userDatafun saveData(data: String) {savedStateHandle["user_data"] = data // 自动保存,Activity重建时恢复} }
三、最新大厂真题实战
1. 字节跳动:如何设计一个线程池,让高优先级任务优先执行,同时避免低优先级任务饿死?
- 解题思路:
- 使用
PriorityBlockingQueue
作为任务队列,自定义任务优先级比较器; - 限制高优先级任务的并发数,避免低优先级任务长期无法执行(“公平性策略”)。
- 使用
- 关键代码:
// 自定义优先级任务 class PriorityRunnable(private val priority: Int, private val runnable: Runnable) implements Runnable, Comparable<PriorityRunnable> {@Overridepublic void run() { runnable.run(); }@Overridepublic int compareTo(PriorityRunnable o) {return o.priority - this.priority; // 数值越大优先级越高} }// 线程池配置 ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS,new PriorityBlockingQueue<>(),(r) -> new Thread(r, "PriorityThread-" + counter.incrementAndGet()) );
- 进阶回答:
补充 “饥饿解决方案”:设置最低执行配额(如每执行 N 个高优先级任务,必须执行 1 个低优先级任务),或使用公平队列(如按提交顺序混合执行)。
2. 美团:Activity A 启动 Activity B 时,B 的主题设置为透明(android:theme="@android:style/Theme.Translucent"),A 的生命周期如何变化?
- 核心考点:Activity 可见性对生命周期的影响
- 答案:
- 若 B 为透明主题,A 未完全遮挡,A 只会调用
onPause()
(保持部分可见),不会调用onStop()
; - 若 B 为非透明主题(完全覆盖 A),A 会调用
onPause()
→onStop()
。
- 若 B 为透明主题,A 未完全遮挡,A 只会调用
- 延伸问题:
“如何判断一个 Activity 是否在前台可见?”
答:- 通过
isResumed()
(前台可交互)或重写ActivityLifecycleCallbacks
监控所有 Activity 的onResume
/onPause
; - 注意:透明 Activity 可能让底层 Activity 处于 “可见但非焦点” 状态,需结合
getWindowVisibility()
判断。
- 通过