一、星纪魅族
1、Android中创建线程:
在Android中创建线程主要有三种方式:
- 继承Thread类,重写run方法
- 实现Runnable接口,实现run方法
- 使用Kotlin协程(Coroutines)——严格来说协程不是线程,但可以用于异步任务
方式一:继承Thread类
class MyThread : Thread() {
override fun run() {
// 执行耗时操作
}
}
// 启动线程
MyThread().start()
方式二:实现Runnable接口
class MyRunnable : Runnable {
override fun run() {
// 执行耗时操作
}
}
// 启动线程
val myRunnable = MyRunnable()
Thread(myRunnable).start()
方式三:使用Kotlin协程(需要添加依赖)
// 在ViewModel或Activity等中使用
viewModelScope.launch {
// 在后台执行耗时操作
val result = withContext(Dispatchers.IO) {
// 执行耗时操作,比如网络请求
return@withContext "结果"
}
// 回到主线程更新UI
textView.text = result
}
注意事项:
- 一个线程只能启动一次,即start()方法只能调用一次,否则会抛出IllegalThreadStateException。
- 线程的执行顺序不一定与启动顺序一致,这取决于操作系统的调度。
- 在Android中,子线程不能直接更新UI,必须通过Handler等机制将更新操作切换到主线程。
2、java中创建线程过程:
// Thread.java 核心代码分析
public synchronized void start() {
// 1. 检查线程状态
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 2. 将线程加入线程组
group.add(this);
// 3. 调用native方法创建底层线程
boolean started = false;
try {
start0(); // Native方法调用
started = true;
} finally {
// 4. 如果启动失败,从线程组移除
if (!started) {
group.threadStartFailed(this);
}
}
}
// Native方法(由JVM实现)
private native void start0();
3、Android中创建线程过程:
Android基于Linux内核,线程创建过程如下:
1. 应用层(Java/Kotlin)
↓
2. ART/Dalvik虚拟机(JNI调用)
↓
3. Bionic C库(pthread_create)
↓
4. Linux内核(clone系统调用)
↓
5. 内核创建task_struct结构体
↓
6. 返回线程ID给应用层
public class ThreadDemo {
public static void main(String[] args) {
// 完整创建过程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行体
System.out.println("Thread running");
}
});
// 设置线程属性
thread.setName("MyThread");
thread.setPriority(Thread.NORM_PRIORITY);
// 启动线程(关键步骤)
thread.start();
}
}
NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED
创建过程对应状态变化:
1. new Thread() → NEW状态
2. thread.start() → RUNNABLE状态(可运行)
3. 操作系统调度 → RUNNING状态(实际运行)
4. 执行完毕 → TERMINATED状态
4、使用Executors工厂类创建## (了解,但不推荐)
Executors类提供了几种创建线程池的静态方法:
// 1. 固定线程数量的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 2. 单个线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 3. 可缓存的线程池,线程数量动态变化
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 4. 定时任务线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
但是,在Android中,由于设备资源有限,不推荐使用Executors创建线程池,因为:
FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue,可能导致内存溢出。CachedThreadPool和ScheduledThreadPool允许创建无限数量的线程,也可能导致内存溢出。
5、使用ThreadPoolExecutor构造函数创建(推荐)
通过ThreadPoolExecutor构造函数,我们可以自定义线程池的各个参数,从而更好地控制线程池的行为。构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
- corePoolSize:核心线程数,即使线程空闲也不会被销毁,除非设置
allowCoreThreadTimeOut。 - maximumPoolSize:最大线程数,当工作队列满了之后,线程池可以创建的最大线程数。
- keepAliveTime:非核心线程空闲时的存活时间,超过这个时间就会被销毁。
- unit:存活时间的单位。
- workQueue:工作队列,用于保存等待执行的任务。
- threadFactory:线程工厂,用于创建新线程,可以设置线程名称、优先级等。
- handler:拒绝策略,当线程池无法执行新任务时的处理策略。
int corePoolSize = 2;
int maximumPoolSize = 5;
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "MyThread #" + mCount.getAndIncrement());
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
};
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
6、线程池的工作流程
- 当提交一个新任务时,如果当前线程数小于核心线程数,则创建新线程执行任务。
- 如果当前线程数达到核心线程数,则将任务放入工作队列。
- 如果工作队列已满,且当前线程数小于最大线程数,则创建新线程执行任务。
- 如果当前线程数达到最大线程数,且工作队列已满,则执行拒绝策略。
注意事项
- 线程池的关闭:使用
shutdown()或shutdownNow()方法关闭线程池,否则可能导致内存泄漏。 - 线程池的监控:可以通过
ThreadPoolExecutor提供的方法(如getActiveCount())监控线程池状态。 - 避免内存泄漏:在Activity或Fragment中使用线程池时,注意在组件销毁时取消任务,避免持有组件引用。
// 通用线程池配置模板
fun createThreadPool(
name: String,
coreSize: Int = 2,
maxSize: Int = 8,
queueSize: Int = 128
): ThreadPoolExecutor {
return ThreadPoolExecutor(
coreSize, maxSize, 30L, TimeUnit.SECONDS,
LinkedBlockingQueue(queueSize),
ThreadFactory { r ->
Thread(r, "$name-${System.currentTimeMillis()}")
.apply { priority = Thread.NORM_PRIORITY }
},
ThreadPoolExecutor.CallerRunsPolicy()
).apply {
allowCoreThreadTimeOut(true) // 允许回收核心线程
}
}
7、ThreadPoolExecutor 的拒绝策略有多少种?
hreadPoolExecutor 中内置了四种拒绝策略,它们都是实现了RejectedExecutionHandler接口的内部类。此外,我们也可以自定义拒绝策略。
下面是四种内置拒绝策略的详细说明:
- AbortPolicy(默认策略) :当线程池无法处理新任务时,会抛出RejectedExecutionException异常。
- CallerRunsPolicy(调用者运行策略):当线程池无法处理新任务时,会由调用者所在的线程来执行该任务。
- DiscardPolicy(丢弃策略):当线程池无法处理新任务时,会直接丢弃该任务,不会有任何通知。
- DiscardOldestPolicy(丢弃最老策略):当线程池无法处理新任务时,会丢弃队列中最老的任务(即队列头部的任务),然后尝试重新提交当前任务。
8、handler 中view.postDelayed(Runnable { },100) 怎么做的延时的,总结一下
在Android中,
View.postDelayed(Runnable, long)实际上是通过Handler实现的延时执行。因为每个View都关联了一个Handler(具体来说是ViewRootImpl中的Handler),当我们调用View.postDelayed()时,它会将Runnable任务发送到关联的Handler的消息队列中,并设置延迟时间。
具体步骤总结:
- 获取Handler:View在attach到窗口时,会关联一个ViewRootImpl,而ViewRootImpl中有一个Handler(实际上是主线程的Handler,因为ViewRootImpl是在主线程创建的)。所以
View.postDelayed()实际上是通过ViewRootImpl中的Handler来执行的。 - 发送延迟消息:当我们调用
View.postDelayed(runnable, delayMillis)时,它会将这个runnable包装成一个Message(或者直接使用Handler的postDelayed方法),然后发送到Handler的消息队列中,并设置延迟时间。 - 消息队列处理:Handler所在线程(通常是主线程)的Looper会不断从消息队列中取出消息,当到达指定的延迟时间时,就会处理这个消息,执行我们传入的Runnable。
- 执行Runnable:当消息被处理时,会调用Runnable的run方法,从而执行我们传入的代码。
在View类中,postDelayed()方法最终会调用到:
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
} else {
// 如果View还没有attach到窗口,使用ViewRootImpl的Handler,但是ViewRootImpl可能为空,所以会使用一个备用的主线程Handler
// 这个备用的Handler是通过RunQueue实现的,它会在View被attach到窗口时,将任务转移到正确的Handler
ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
return true;
}
}
其中,AttachInfo是View内部的一个静态类,它保存了View附加到窗口时的一些信息,包括一个Handler(mHandler)。这个Handler实际上是ViewRootImpl中的Handler,也就是主线程的Handler。
因此,View.postDelayed()本质上就是通过主线程的Handler来延迟执行一个Runnable。
总结:
View.postDelayed(Runnable, long)是通过将Runnable发送到主线程的Handler的消息队列中,并设置延迟时间来实现延时的。当延迟时间到达后,主线程的Looper会从消息队列中取出并执行这个Runnable。
延时执行流程图
View.postDelayed(runnable, 100)
↓
Handler.postDelayed(msg, 100)
↓
sendMessageDelayed(msg, 100)
↓
sendMessageAtTime(msg, currentTime + 100)
↓
MessageQueue.enqueueMessage(msg, when)
↓
消息按 when 时间插入队列(时间排序)
↓
MessageQueue.next() 循环检查
↓
如果 now < msg.when,计算等待时间
↓
nativePollOnce(ptr, timeout) 阻塞
↓
时间到达 → 返回消息给 Handler
↓
Handler.dispatchMessage(msg)
↓
执行 runnable.run()
View.postDelayed() 的延时机制核心:
- 基于 Handler 消息队列:所有
postDelayed最终都通过 Handler 实现 - 时间排序:消息队列按执行时间 (
when) 排序 - 精准等待:使用
nativePollOnce在 Native 层精确等待指定时间 - 时间基准:使用
SystemClock.uptimeMillis(),不受系统时间修改影响 - 线程关联:
View.postDelayed()最终在主线程执行
9、CoroutineContext 是什么?
一个保存协程执行环境的键值对集合,决定了协程如何执行。
CoroutineContext由以下几个主要元素构成:
-
Job:控制协程的生命周期,例如启动和取消协程。
-
CoroutineDispatcher:决定协程在哪个线程或线程池上运行。常见的Dispatcher有:
- Dispatchers.Main:在主线程(通常是UI线程)上运行。
- Dispatchers.IO:适用于I/O密集型任务。
- Dispatchers.Default:适用于CPU密集型任务。
- Dispatchers.Unconfined:不限制在特定线程上。
-
CoroutineName:为协程指定一个名称,便于调试和日志记录。
-
CoroutineExceptionHandler:处理协程中未捕获的异常。
这些元素可以通过加号(+)操作符组合在一起,形成一个上下文。当协程被创建时,它会从父协程继承上下文,并可以覆盖或添加新的元素。
例如,可以这样组合一个上下文:
val context = Dispatchers.Main + CoroutineName("MyCoroutine") + CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
CoroutineContext支持以下操作:
- 组合:使用
+操作符将多个上下文元素组合在一起。例如:Job() + Dispatchers.Main - 获取元素:通过Key来获取对应的元素。例如:
context[CoroutineName] - 移除元素:使用
minusKey函数移除指定Key的元素。
一句话总结:CoroutineContext是协程的"配置中心",管理线程、异常、生命周期等执行环境。
10、CoroutineContext 元素讲解?
1. Job
Job是协程的句柄,用于管理协程的生命 周期。Job可以有一个父Job,父Job取消时,所有子Job也会被取消。Job有三种状态:isActive、isCompleted、isCancelled。
2. CoroutineDispatcher
调度器决定了协程在哪个线程或线程池上运行。常见的调度器有:
- Dispatchers.Default:用于CPU密集型任务,使用共享的线程池。
- Dispatchers.IO:用于IO密集型任务,如网络请求、文件操作等。
- Dispatchers.Main:在主线程上执行,用于更新UI(在Android中)。
- Dispatchers.Unconfined:不限制在特定线程,但一般不推荐使用。
3. CoroutineName
为协程指定一个名称,方便调试和日志记录。可以通过coroutineContext[CoroutineName]来获取当前协程的名称。
4. CoroutineExceptionHandler
用于处理协程中未捕获的异常。注意:只有在根协程中设置的异常处理器才会被调用,子协程的异常会向上传播。
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
fun main() = runBlocking {
// 创建一个上下文,包含Job、Dispatcher、CoroutineName和ExceptionHandler
val context: CoroutineContext = Job() + Dispatchers.Default + CoroutineName("MyCoroutine") + CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
// 使用这个上下文启动一个协程
val job = launch(context) {
println("Running in ${Thread.currentThread().name}, name: ${coroutineContext[CoroutineName]?.name}")
delay(100)
throw RuntimeException("Test exception")
}
job.join()
}
11、Job 是什么?它与协程有什么关系?
// Job 是协程的句柄,代表一个可取消的后台任务
val job = launch {
// 协程体
}
// 关系:每个协程都会返回一个 Job 对象
// 协程是执行逻辑,Job 是管理这个执行的生命周期
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
12、Job 的几种状态?
// 主要状态:
// 1. New (新建) - 创建但未启动
// 2. Active (活跃) - 启动后运行中
// 3. Completing (完成中) - 执行完毕,等待子协程
// 4. Cancelling (取消中) - 正在取消
// 5. Cancelled (已取消) - 已取消
// 6. Completed (已完成) - 已正常完成
13、如何取消一个 Job?
val job = launch {
repeat(1000) { i ->
delay(500)
println("job: $i")
}
}
// 方式1:直接取消
job.cancel()
// 方式2:取消并指定原因
job.cancel(CancellationException("用户取消"))
// 方式3:取消整个作用域
coroutineScope.cancel()
14、cancel() 和 cancelAndJoin() 的区别?
// cancel(): 异步取消,立即返回
job.cancel()
// cancelAndJoin(): 取消并等待协程结束
job.cancelAndJoin() // 等价于 job.cancel() + job.join()
15、Job 的父子关系有什么作用?
// 父 Job 取消时,所有子 Job 都会被取消
val parentJob = launch {
launch { // 子 Job 1
delay(1000)
println("Child 1")
}
launch { // 子 Job 2
delay(2000)
println("Child 2")
}
}
// 取消父 Job,所有子 Job 都会取消
parentJob.cancel()
16、Job 和 SupervisorJob 的区别?
// Job: 子协程失败会传播给父协程
val job = Job()
launch(job) {
launch {
throw Exception() // 会取消整个 job 作用域
}
launch {
delay(1000)
println("不会执行") // 也被取消
}
}
// SupervisorJob: 子协程失败不会影响其他子协程
val supervisorJob = SupervisorJob()
launch(supervisorJob) {
launch {
throw Exception() // 只影响自己
}
launch {
delay(1000)
println("正常执行") // 不受影响
}
}
17、coroutineScope 和 supervisorScope 的区别?
// coroutineScope: 子协程失败会传播
coroutineScope {
launch {
throw Exception() // 会取消整个作用域
}
launch {
delay(1000)
println("不会执行")
}
}
// supervisorScope: 子协程独立
supervisorScope {
launch {
throw Exception() // 只影响自己
}
launch {
delay(1000)
println("正常执行") // 不受影响
}
}
18、Job 的 join() 和 await() 有什么区别?
// join(): Job 的方法,等待完成(无返回值)
val job = launch { delay(1000) }
job.join() // 等待 job 完成
// await(): Deferred 的方法,等待并获取结果
val deferred = async { "result" }
val result = deferred.await() // 获取返回值 "result"
19、如何实现 Job 的依赖关系?
// 使用 join() 实现依赖
val job1 = launch { delay(1000) }
val job2 = launch {
job1.join() // 等待 job1 完成
println("job1 完成后执行")
}
// 或者使用 async 组合结果
val result1 = async { doTask1() }
val result2 = async { doTask2() }
val finalResult = result1.await() + result2.await()
20、如何在 ViewModel 中正确处理 Job?
class MyViewModel : ViewModel() {
private val jobs = mutableListOf<Job>()
fun fetchData() {
// 方式1:使用 viewModelScope(推荐)
viewModelScope.launch {
// 自动管理生命周期
}
// 方式2:手动管理
val job = CoroutineScope(Dispatchers.IO).launch {
// 需要手动管理
}
jobs.add(job)
}
override fun onCleared() {
super.onCleared()
jobs.forEach { it.cancel() }
}
}
21、如何避免 Job 的内存泄漏?
// 反例:Activity 被销毁但 Job 还在运行
class MyActivity : AppCompatActivity() {
private var job: Job? = null
override fun onCreate() {
job = GlobalScope.launch { // ❌ 使用 GlobalScope
while (isActive) {
delay(1000)
updateUI() // 可能访问已销毁的 Activity
}
}
}
// 正确做法
override fun onCreate() {
// 使用 lifecycleScope
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
while (isActive) {
delay(1000)
updateUI() // 只在活跃状态执行
}
}
}
}
}
22、为什么有时候 cancel() 不能立即取消协程?
launch {
var i = 0
while (i < 5) {
// ❌ 错误:没有检查取消状态
Thread.sleep(1000) // 阻塞线程,无法响应取消
// ✅ 正确:使用 delay()
delay(1000) // 可响应取消
i++
}
}
// 协程取消是协作式的,需要在代码中检查取消状态
23、withContext 返回的 Job 是什么?
val result = withContext(Dispatchers.IO) {
// 这个 block 会立即执行
"result"
}
// withContext 不是启动新协程,而是切换上下文执行代码
// 它返回的是代码块的执行结果,不是一个可管理的 Job
24、必须掌握的核心点:
- Job 是协程的句柄,用于管理生命周期
- 结构化并发:父子 Job 的生命周期绑定
- 取消是协作式的:需要检查
isActive或使用可取消的挂起函数 - 两种 Job 类型:普通 Job 和 SupervisorJob
- 异常处理差异:
launch异常会取消父 Job,async异常需要await()时捕获
25、什么是AIDL?
- AIDL是Android接口定义语言,用于定义跨进程通信的接口。
- 它允许不同进程中的对象进行方法调用和数据交换。
26、为什么要使用AIDL?什么场景下使用?
- 当需要跨进程通信(IPC)时,尤其是多个应用共享服务或一个应用需要为多个应用提供服务时。
- 场景:音乐播放器服务、推送服务、多个应用共享数据等。
27、AIDL支持哪些数据类型?
- 基本数据类型(int, long, char, boolean, double等)
- String和CharSequence
- List(其中的元素必须是AIDL支持的类型)
- Map(其中的元素必须是AIDL支持的类型)
- 其他AIDL接口
- 实现了Parcelable的对象
28、AIDL的实现原理是什么?
- AIDL通过Binder机制实现。编译时,AIDL文件会生成一个Java接口,其中包含一个Stub类(继承Binder)和一个Proxy类(代理类)。
- 服务端实现Stub类,客户端通过Proxy类调用方法,实际调用通过Binder驱动传递到服务端。
29、Binder机制是什么?
- Binder是Android中的一种跨进程通信机制,它使用内存共享的方式,效率高于传统IPC(如管道、消息队列等)。
- Binder驱动在内核空间,负责进程间数据的交换。
30、AIDL中的Stub和Proxy是什么?
- Stub:服务端的抽象类,继承Binder,并实现AIDL接口。它负责接收客户端调用,并将数据解析后调用服务端的具体实现。
- Proxy:客户端的代理类,也实现AIDL接口。它负责将方法调用和数据打包,通过Binder发送给服务端。
31、如何创建一个AIDL服务?
步骤:
- 创建AIDL文件,定义接口。
- 实现Service,在onBind方法中返回Stub的实现。
- 在AndroidManifest.xml中声明Service,并设置exported属性为true(如果允许其他应用调用)。
- 客户端绑定服务,将返回的Binder对象转换为AIDL接口。
32、AIDL中如何进行权限验证?
- 在Service的onBind方法中,可以通过调用
Binder.getCallingUid()和Binder.getCallingPid()来获取调用方的UID和PID,然后进行验证。 - 也可以在Service的onTransact方法中验证,或者使用自定义权限。
33、AIDL中如何传递自定义对象?
- 自定义对象必须实现Parcelable接口。
- 为这个对象创建一个AIDL文件(如
MyObject.aidl),并声明为parcelable。 - 在需要使用的AIDL接口文件中导入这个对象。
34、AIDL中的oneway关键字有什么作用?
- 使用oneway修饰方法时,表示该方法为异步调用,客户端不会阻塞等待结果。
- 注意:oneway方法不能有返回值,且不能抛出异常(除了RemoteException)。
35、AIDL中客户端调用服务端方法时,如何确保线程安全?
- 服务端方法默认是在Binder线程池中执行的,所以服务端实现需要考虑多线程问题。
- 如果服务端方法需要更新UI,则需要切换到主线程(使用Handler等)。
36、AIDL的客户端和服务端在同一个进程和不同进程有什么区别?
- 同一个进程:AIDL接口调用不会经过Binder驱动,直接调用服务端实现。
- 不同进程:调用会经过Binder驱动,涉及序列化和反序列化,因此效率较低。
37、AIDL中如何处理回调?
- 在AIDL中定义回调接口,客户端实现该接口,并将回调对象通过AIDL接口注册到服务端。
- 注意:回调是在服务端进程调用的,所以客户端接收回调的方法是在Binder线程池中,更新UI需要切换到主线程。
38、AIDL传输大量数据时如何优化?
- 使用Parcelable时,避免传递过大的数据,可以考虑使用文件描述符(如ParcelFileDescriptor)传递文件,或者使用共享内存(Ashmem)。
- 也可以将数据分块传输,或者使用更高效的数据格式。
39、AIDL服务端如何同时支持多个客户端?
- 服务端默认就是多线程的,Binder线程池会处理多个客户端的请求。但需要注意服务端方法的线程安全。
- 如果需要区分不同客户端,可以在接口中增加标识,或者使用回调时携带客户端信息。
40、AIDL与Messenger、ContentProvider的区别?
- AIDL:功能强大,支持复杂的数据类型和同步/异步调用,但实现稍复杂。
- Messenger:基于AIDL封装,只支持串行消息处理,使用简单,但功能有限。
- ContentProvider:主要用于数据共享,提供CRUD接口,底层也是Binder。
41、AIDL调用失败的可能原因有哪些?
- 服务端未在AndroidManifest.xml中正确声明。
- 客户端和服务端的AIDL接口不一致(版本问题)。
- 权限不足,没有声明或申请所需权限。
- 服务端进程被杀掉,导致Binder死亡。
42、如何监控AIDL的调用性能?
- 可以使用Traceview或Systrace工具分析调用耗时。
- 在代码中记录关键点的时间戳,特别是跨进程调用的次数和耗时。
43、为什么需要 AIDL?哪些场景下使用?
需要跨进程通信的场景:
- 不同应用之间共享服务(如音乐播放器服务)
- 系统服务与应用的通信
- 多进程应用中的进程间通信
- 需要长时间运行的后台服务
44、AIDL 的工作流程是什么?
- 定义AIDL接口文件(.aidl)
- 编译生成Java接口文件
- 服务端实现Stub类
- 客户端通过Proxy调用
- Binder驱动进行进程间通信
45、Binder 机制在 AIDL 中起什么作用?
// Binder是Android跨进程通信的核心
// 1. Binder驱动在内核空间传递数据
// 2. IBinder接口是跨进程调用的桥梁
// 3. Parcel用于数据序列化/反序列化
46、AIDL 中的 in、out、inout 参数有什么区别?
// in: 参数从客户端流向服务端(默认)
void method1(in User user); // 客户端→服务端
// out: 参数从服务端流向客户端
void method2(out User user); // 服务端→客户端
// inout: 双向流通
void method3(inout User user); // 客户端↔服务端
// 注意:基本类型只能使用in,因为out和inout需要能接收返回值
47、ARouter 怎么实现进程间通信的
当发生跨进程跳转时,ARouter不会让调用方直接操作目标进程的组件,而是引入了一个运行在目标进程的跨进程服务(IPCService) 作为代理。整个流程如下图所示:
48、ARouter与标准AIDL的对比
你可以将ARouter的IPCService理解为一个专门为路由功能定制的、内置的AIDL服务。它与标准AIDL对比如下:
| 特性 | ARouter跨进程通信 | 标准AIDL |
|---|---|---|
| 抽象层级 | 高。开发者只需关心路由路径,无需直接操作Binder |
| 。 | 低。需定义AIDL接口,手动处理ServiceConnection和序列化。 |
|---|---|
| 使用场景 | 特定。专为组件化架构中的跨进程页面跳转和服务获取设计 |
| 。 | 通用。适用于任意自定义的跨进程方法调用。 | |
|---|---|---|
| 耦合性 | 低。调用方只依赖路由路径,不依赖目标组件的具体类。 | 高。调用方需要依赖AIDL接口的包。 |
| 便捷性 | 高。和单进程跳转使用相同的ARouter.getInstance().build(path).navigation()API。 | 低。需要完成绑定服务、转换接口、调用方法等一系列步骤。 |
49、ARouter总结
简单来说,ARouter通过在每个需要提供跨进程访问的进程中部署一个统一的“路由代理服务”(IPCService) ,将复杂的原生IPC细节隐藏起来。对于开发者,跨进程跳转与同进程跳转的代码几乎一致,框架在底层自动完成了进程间请求的转发和在本进程内的路由执行。
注:ARouter的跨进程功能主要服务于其组件化路由的核心目标,并非一个通用的IPC框架。对于复杂的自定义跨进程交互,仍需使用AIDL等其他方案
50、Android 开发中,怎么做内存优化的
内存泄漏 vs. 内存溢出 (OOM)
52、系统性优化方案与实战代码
可以从以下三个层面入手,并举例说明:
1.代码与数据层面
- 使用合适数据结构:
SparseArray、ArrayMap替代HashMap可节省内存 - 避免内存抖动:不在
onDraw或循环中创建对象,可使用对象池复用 - 处理Bitmap:这是OOM主因,务必使用
BitmapFactory.Options采样 (inSampleSize),选择RGB_565等低内存配置,并通过Bitmap.recycle()及时回收
- 注意集合清理:及时清除集合内无用的对象引用。
// Bitmap采样加载示例
public static Bitmap decodeSampledBitmap(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 先只读尺寸
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 计算采样率
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 使用更省内存的配置
return BitmapFactory.decodeResource(res, resId, options);
}
2.资源与架构层面
- 及时释放资源:在
onDestroy()中关闭Cursor、文件流,注销BroadcastReceiver等 - 优化UI与资源:使用
ViewStub延迟加载,压缩图片为WebP格式,移除未用资源 - 使用内存缓存:用
LruCache缓存Bitmap等对象,并设置合理上限 - 架构预防泄漏:
- 使用
Application Context代替Activity Context - 将内部类(如
Handler)改为静态内部类,并使用WeakReference弱引用持有外部类(如Activity) - 使用
ViewModel+LiveData,它们与Activity/Fragment生命周期自动关联,能有效避免泄漏。 - 使用
Lifecycle组件确保异步操作在页面销毁时自动取消。
// 使用静态内部类 + 弱引用的Handler示例
private static class SafeHandler extends Handler {
private final WeakReference<MyActivity> mActivityRef;
SafeHandler(MyActivity activity) {
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MyActivity activity = mActivityRef.get();
if (activity != null && !activity.isFinishing()) {
// 安全地更新UI
}
}
}
3.进程与系统层面
在Application或ComponentCallbacks2中实现onTrimMemory(),根据系统内存级别释放缓存等资源
- 对于多进程应用,将内存消耗大的功能(如后台播放)放到独立进程,系统可单独回收。
53、监控、排查工具链
开发期/实验室:
- Android Studio Profiler:核心工具,用于实时监控内存曲线、发现抖动、手动抓取堆快照(Hprof)并进行分配追踪
- LeakCanary:集成到调试版本,自动检测并报告内存泄漏,给出引用链,非常直观
- 线上监控(Buly):可以搭建自动化上报系统,在OOM发生或内存使用异常时,采集Hprof文件并上报,用于分析线上真实问题。
54、Android 中 lifecycleScope、 GlobalScope 、viewModelScope和CoroutineScope他们有什么区别,使用场景是什么?
为了让你一目了然,我将它们的核心区别和使用场景总结为下表:
| 特性 | lifecycleScope | viewModelScope | GlobalScope | 自定义 CoroutineScope |
|---|---|---|---|---|
| 绑定对象 | LifecycleOwner (Activity, Fragment) | ViewModel | 无 (应用进程) | 自定义 (任何对象) |
| 自动取消时机 | Lifecycle 被销毁时 (DESTROYED) | ViewModel 的 onCleared() 时 | 永不自动取消 | 手动控制 (调用 cancel()) |
| 内存安全 | 安全 | 安全 | ⚠️ 极不安全 (易内存泄漏) | 取决于实现 |
| 结构化并发 | 支持 | 支持 | 不支持 | 支持 |
| 主要使用场景 | UI 相关的、生命周期感知的短任务 | ViewModel 内部的业务逻辑与数据准备 | 几乎不推荐,仅限整个应用进程存活期间执行的任务 | 需要精细控制生命周期的复杂场景 |
55、lifecycleScope
-
本质:与
Activity或Fragment的Lifecycle绑定。是每个LifecycleOwner的扩展属性。 -
关键点:当
Lifecycle进入DESTROYED状态时,其下的所有协程会自动取消。这完美解决了在Activity销毁后更新 UI 导致的崩溃问题。
// 在 Activity/Fragment 中发起一个与UI生命周期绑定的请求
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 当 Fragment 视图销毁时,此协程自动取消
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) { repository.fetchData() }
updateUI(data) // 只有生命周期活跃时才会执行到这里
}
}
}
56、viewModelScope
- 本质:与
ViewModel的生命周期绑定。是每个ViewModel的扩展属性。 - 关键点:当
ViewModel被清除(例如因配置更改或Activity正常结束)时,其onCleared()方法被调用,所有协程自动取消。这是执行业务逻辑和准备数据的首选作用域。 - 典型场景:
class MyViewModel(private val repo: MyRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
fun loadData() {
// 数据加载任务与 ViewModel 生命周期绑定
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = repo.fetchData() // 可能是网络或数据库操作
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
// 当 ViewModel 被清除时,`loadData` 中启动的协程会被自动取消,避免资源浪费。
}
57、GlobalScope
- 极度不推荐常规使用
- 本质:一个与应用进程同生命周期的全局作用域。
58、自定义 CoroutineScope
-
本质:通过
CoroutineScope(context)工厂函数创建,生命周期完全由开发者控制。 -
关键点:用于那些无法直接使用
lifecycleScope或viewModelScope,但又需要精细化生命周期管理的场景。 -
典型场景:
- 在自定义
View或Presenter中管理一组相关任务。 - 需要手动提前取消一组任务(例如用户离开某个页面标签页)。
- 实现一个独立的、可重启的任务管理器。
- 在自定义
class MyCustomController {
// 1. 创建一个自定义作用域,使用 SupervisorJob 使子协程失败互不影响
private val customJob = SupervisorJob()
private val customScope = CoroutineScope(Dispatchers.Main + customJob)
fun startWork() {
customScope.launch {
// 执行一系列任务
}
}
fun stopAllWork() {
// 2. 手动取消所有在此作用域内启动的协程
customJob.cancel()
}
fun onDestroy() {
stopAllWork()
// 可选:也可重新创建 job 和 scope 以复用 controller
}
}
59、Scope选择策略与最佳实践
- 默认首选
viewModelScope:在ViewModel中处理所有业务逻辑和数据准备。这是 Android 架构组件的最佳拍档。 - UI 操作使用
lifecycleScope:在Activity/Fragment中直接执行与 UI 生命周期紧密相关的轻量级操作(例如View的动画、点击防抖),或配合repeatOnLifecycle安全地收集Flow。 - 尽量避免
GlobalScope:将它视为一个“代码异味”。如果你的逻辑需要长时间运行,请考虑使用WorkManager或绑定到自定义Service的作用域。 - 在需要精确控制时自定义:当内置作用域无法满足你复杂的生命周期管理需求时,就创建一个自定义作用域,并务必记得在合适的时机手动取消。
简单来说,记住一个核心原则:让你的协程生命周期与负责它的组件的生命周期保持一致。这样,当组件销毁时,所有相关任务都能自动清理,这是避免内存泄漏和异常的关键。
60、ViewModel 是什么?解决了什么问题?
核心答案:ViewModel 是一个生命周期感知的,用于存储和管理 UI 相关数据的类。它的设计目的是:
- 解决数据持久化问题:在屏幕旋转等配置更改导致
Activity重建时,ViewModel中的数据会被保留,无需重新从网络或数据库加载。 - 实现关注点分离:将数据持有和业务逻辑从
Activity/Fragment(UI 控制器) 中剥离出来,让后者专注于处理 UI 和用户交互。 - 实现数据共享:在同一个
Activity的多个Fragment间轻松共享数据。
61、ViewModel 的生命周期是怎样的?
ViewModel的生命周期范围从首次创建(通常在Activity的onCreate中获取)一直到关联的Activity最终 finish 或Fragment最终 detached。- 在 配置更改(如旋转)导致的
Activity短暂销毁重建过程中,ViewModel不会销毁。它会在新的Activity实例中重用。 - 只有当
Activity真正结束(按返回键退出)时,系统才会调用ViewModel的onCleared()方法进行清理。
62、ViewModel 如何实现数据在配置更改后依然存活?
原理简述:ViewModel 对象并非由 Activity 直接持有,而是由 ViewModelStore(通过 ViewModelProvider)在 Activity 的非配置状态(onRetainNonConfigurationInstance 机制,现由 ViewModelStore 内部管理)中保存并传递。
面试回答重点:ViewModel 的生命周期由内部的 ViewModelStore 管理。配置更改时,Activity 对象虽然被销毁,但 ViewModelStore 及其持有的 ViewModel 实例会被系统保留,并关联到新创建的 Activity 实例上。
63、ViewModel 与 onSaveInstanceState 的区别
面试官常让你对比这两者,下表清晰地说明了它们的适用场景:
| 特性 | ViewModel | onSaveInstanceState (Bundle) |
|---|---|---|
| 数据持久性 | 在配置更改时存活,进程被杀死后消失 | 在配置更改和进程被杀死后重建时均可存活 |
| 存储位置 | 内存 | 序列化到磁盘 |
| 数据类型 | 支持复杂、大型对象(如列表、LiveData) | 只适合存储小型、可序列化的简单数据(如ID、状态标志) |
| 设计目的 | 管理UI所需的数据 | 保存瞬时的UI状态(如滚动位置、临时勾选状态) |
结论:两者是互补关系。
- 用
ViewModel保存准备和恢复成本高的数据(如用户列表)。 - 用
onSaveInstanceState保存轻量的UI状态,以便在应用被系统杀死后能精确恢复界面。
64、ViewModel 与 LiveData / Flow 的协作
这是现代 Android 开发的标配模式。
class MyViewModel : ViewModel() {
// 1. 使用 LiveData(状态容器)
private val _uiState = MutableLiveData<UiState>()
val uiState: LiveData<UiState> = _uiState
// 2. 使用 StateFlow / SharedFlow(更现代的协程流)
private val _uiStateFlow = MutableStateFlow<UiState>(UiState.Loading)
val uiStateFlow: StateFlow<UiState> = _uiStateFlow
fun loadData() {
viewModelScope.launch { // 3. 使用 viewModelScope 管理协程
_uiState.value = UiState.Loading
try {
val result = repository.fetchData()
_uiState.value = UiState.Success(result)
_uiStateFlow.value = UiState.Success(result)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
_uiStateFlow.value = UiState.Error(e.message)
}
}
}
}
// 在 Activity/Fragment 中观察(以 Flow 为例)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiStateFlow.collect { uiState ->
// 安全地更新 UI(只在 STARTED 及以上状态接收)
updateUI(uiState)
}
}
}
面试要点:
viewModelScope的作用:确保所有协程在ViewModel销毁时自动取消,避免内存泄漏。- 观察模式的好处:
UI订阅ViewModel的数据,数据变化自动驱动 UI 更新,实现单向数据流。
65、常见陷阱与注意事项
1. 不要向 ViewModel 传递 Context
问题:如果传入 Activity 的 Context,ViewModel 存活期间会一直持有该引用,导致 Activity 无法被回收(内存泄漏)。
正确做法:如果需要 Context,应使用 AndroidViewModel(它持有 Application 的 Context)。
2.Fragment 间共享 ViewModel 的作用域
- 同一 Activity 内共享:使用
by activityViewModels()。 - 同一导航图内共享:使用
by navGraphViewModels(graphId)。
原理:它们使用相同的ViewModelStoreOwner来获取ViewModel实例。
3.SavedStateHandle:让 ViewModel 支持进程死亡恢复
kotlin
class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
// 将数据保存在 SavedStateHandle 中,进程被杀死后也能恢复
var userId: String by mutableStateOf(
savedStateHandle.get<String>("userId") ?: ""
)
private set
fun saveUserId(id: String) {
userId = id
savedStateHandle["userId"] = id // 保存到 Bundle
}
}
可能的高阶追问:
- “
ViewModel的onCleared()方法里应该做什么?” (清理资源,取消自定义的监听或协程)。 - “
ViewModel是单例吗?” (不是传统单例,它的作用域由ViewModelStoreOwner决定,在同一个Owner下是单例)。 - “能直接在新线程中修改
LiveData的value吗?” (不能,需用postValue)。