Android学习总结之kotlin协程面试篇

一、协程基础概念与原理类真题

真题 1:协程是线程吗?为什么说它是轻量级的?(字节跳动 / 美团)

解答:

  • 本质区别
    线程是操作系统调度的最小单位(内核态),协程是用户态的轻量级执行单元,由协程库(如 kotlinx.coroutines)管理。
  • 轻量级核心优势
    1. 内存占用极小 :每个线程默认分配 1MB 栈内存,协程共享宿主线程栈,单个协程仅需几十字节(如 Continuation 对象)。
    2. 无内核态切换 :协程切换在用户态通过 Continuation 实现,避免线程切换的上下文开销(寄存器保存 / 恢复)。
    3. 超高并发能力:单线程可运行上万协程,而线程受限于系统资源(如 Android 手机通常仅支持几百个线程)。

代码佐证

java 复制代码
// 启动 10万协程(内存无明显波动)
CoroutineScope().launch {
    repeat(100_000) { launch { delay(1000) } }
}

真题 2:挂起函数的本质是什么?为什么不能在普通函数中调用?(阿里 / 腾讯)

解答:

  • 编译期转换(CPS 模式)
    挂起函数(suspend)被编译器转换为接收 Continuation 参数的函数,通过状态机保存执行现场。
    示例

    Kotlin 复制代码
    suspend fun fetchData(): String { /* 耗时操作 */ }

    编译后等价于:

    Kotlin 复制代码
    fun fetchData(continuation: Continuation<String>): Any? {
        // 保存当前局部变量,挂起时通过 continuation.resume() 恢复
    }
  • 调用限制
    挂起函数依赖 Continuation 上下文,只能在协程体内或其他挂起函数中调用,避免脱离协程调度机制导致的状态丢失。

反例

Kotlin 复制代码
fun normalFunction() {
    fetchData() // 报错!必须在协程或另一个挂起函数中调用
}

二、调度器与线程切换类真题

真题 3:Dispatchers.IO 和 Dispatchers.Default 有什么区别?如何选择?

解答:

调度器 底层线程池 适用场景 最佳实践
Dispatchers.IO 共享的 IO 优化线程池 磁盘读写、网络请求(IO 密集型) 耗时 IO 操作(如 Retrofit、文件读取)
Dispatchers.Default 共享的 CPU 密集型线程池 计算任务(JSON 解析、数据排序) CPU 耗时操作(避免阻塞 IO 线程)

核心区别

  • Dispatchers.IO 会复用线程,适合处理耗时但不占用 CPU 的操作(如等待网络响应时线程可处理其他任务)。
  • Dispatchers.Default 限制线程数量(默认 CPU 核心数 × 2),避免 CPU 密集型任务占用过多资源。

错误案例

Kotlin 复制代码
// 错误:在 IO 调度器执行 CPU 密集型任务(浪费线程池资源)
withContext(Dispatchers.IO) { complexCalculation() } 

// 正确:CPU 任务用 Default,IO 任务用 IO
launch(Dispatchers.Default) { complexCalculation() }
launch(Dispatchers.IO) { networkRequest() }

真题 4:withContext 如何实现线程切换?原理是什么?

解答:

  • 实现原理
    withContext(newContext) 创建新的协程上下文,通过 CoroutineDispatcher.dispatch 将协程派发到目标线程。
    关键步骤
    1. 保存当前协程状态(局部变量、挂起点)到 Continuation 对象。
    2. 调度器(如 Dispatchers.Main)将 Continuation 提交到目标线程(如主线程 Looper)。
    3. 目标线程恢复协程执行,继续后续逻辑(如 UI 更新)。

源码视角(简化版):

Kotlin 复制代码
suspend fun <T> withContext(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
    val oldContext = currentCoroutineContext()
    val newContext = oldContext + context
    return newContext[ContinuationInterceptor]!!.interceptContinuation { cont ->
        // 调度 cont 到新线程
    }.invokeCancellable(block)
}

三、作用域与生命周期管理类真题

真题 5:为什么不推荐使用 GlobalScope?如何避免协程泄漏?(字节跳动 / 百度)

解答:

  • GlobalScope 风险
    作用域生命周期与进程绑定,启动的协程不会随组件(如 Activity)销毁而取消,导致内存泄漏(如协程中持有 Activity 引用)。

正确做法

  1. 绑定组件生命周期

    • Activity/Fragment 使用 lifecycleScope(AndroidX 库提供,自动随组件销毁取消)。
    • ViewModel 使用 viewModelScope(随 ViewModel 销毁取消)。
    Kotlin 复制代码
    // Activity 中安全启动协程
    lifecycleScope.launch(Dispatchers.IO) {
        val data = fetchData()
        withContext(Dispatchers.Main) { uiUpdate(data) }
    }
  2. 自定义作用域时手动管理

    Kotlin 复制代码
    // 手动创建作用域,确保调用 cancel()
    val myScope = CoroutineScope(Job() + Dispatchers.IO)
    // 取消所有子协程
    myScope.cancel()

反例对比

Kotlin 复制代码
// 危险!Activity 销毁后协程仍运行
GlobalScope.launch { 
    delay(10_000)
    activity?.showToast("泄漏!") // NPE 风险 
}

真题 6:协程的结构化并发是什么?有什么优势?(美团 / 京东)

解答:

  • 定义 :协程必须在作用域(CoroutineScope)内启动,子协程自动继承父作用域的生命周期,父作用域取消时所有子协程同步取消。

  • 核心优势

    1. 避免泄漏:无需手动管理每个协程的取消,作用域销毁时统一清理。
    2. 异常传播 :父协程捕获异常时,子协程自动取消(如 launch 中未处理的异常会终止整个作用域)。

示例

复制代码
CoroutineScope().launch { // 父协程
    launch { // 子协程 1
        error("崩溃") // 未处理异常,父协程及所有子协程取消
    }
    launch { // 子协程 2
        delay(1000) // 提前被取消
    }
}

四、协程构建器与高级特性类真题

真题 7:launch 和 async 有什么区别?如何获取 async 的返回值?

解答:

构建器 阻塞当前线程 返回值类型 主要用途 获取结果方式
launch Job(无返回值) 启动无需结果的异步任务 通过 Job.cancel()
async Deferred<T> 启动需要返回值的异步任务 deferred.await()

使用场景

Kotlin 复制代码
// 并行获取两个数据(耗时 1s)
val deferred1 = async { fetchUser() }
val deferred2 = async { fetchOrder() }
val result = deferred1.await() + deferred2.await() // 合并结果

注意async 默认以 CoroutineStart.DEFAULT 启动(立即调度),可通过 start = CoroutineStart.LAZY 延迟启动:

Kotlin 复制代码
val lazyDeferred = async(start = CoroutineStart.LAZY) { heavyTask() }
// 按需启动
if (needData) lazyDeferred.start()

真题 8:如何处理协程中的异常?CoroutineExceptionHandler 如何使用?

解答:

  • 三种异常处理方式
    1. try-catch 块 :捕获当前协程内的异常(不影响其他子协程)。

      Kotlin 复制代码
      launch {
          try {
              riskyOperation()
          } catch (e: Exception) {
              logError(e)
          }
      }
    2. 作用域异常处理器 :通过 CoroutineScope 构造函数传入 CoroutineExceptionHandler

      Kotlin 复制代码
      val scope = CoroutineScope(CoroutineExceptionHandler { _, e -> 
          handleGlobalError(e) // 处理所有未捕获异常
      })
    3. 全局异常处理 (不推荐):通过 CoroutineExceptionHandler 绑定到 Dispatchers.Main

关键区别

  • try-catch 仅捕获当前协程块内的异常。
  • CoroutineExceptionHandler 捕获作用域内所有未处理异常,并终止整个作用域(包括子协程)。

一、Android 实战类真题

真题 1:如何在 Android 中安全地使用协程更新 UI?

解答:

  • 线程安全原则
    Android UI 操作必须在主线程执行,协程通过 withContext(Dispatchers.Main) 切换到主线程。
    示例代码

    Kotlin 复制代码
    lifecycleScope.launch(Dispatchers.IO) {
        val data = fetchDataFromNetwork() // IO 线程
        withContext(Dispatchers.Main) {
            textView.text = data // 安全更新 UI
        }
    }
  • 原理
    Dispatchers.Main 封装了主线程的 Looper,通过 Handler 将 UI 操作提交到主线程消息队列,避免直接阻塞主线程。

  • 关键策略

    1. 显式切换主线程 :耗时操作(如网络请求、数据库查询)在非主线程调度器(Dispatchers.IO/Default)执行,完成后通过 withContext(Dispatchers.Main) 切回主线程更新 UI。
    2. 避免隐性线程风险 :协程默认继承调用者线程,若在非主线程启动协程且未指定调度器,直接操作 UI 会导致崩溃。务必通过 launch(Dispatchers.IO)withContext 明确线程分工。

错误案例

Kotlin 复制代码
// 危险!在非主线程直接更新 UI(导致崩溃)
launch { textView.text = "错误" } // 未指定调度器,默认使用调用者线程(可能非主线程)

真题 2:协程如何与 Retrofit 结合实现网络请求?

解答:

  • Retrofit 适配协程
    使用 Retrofit 2.6+ 的 suspend 函数支持,直接返回 ResponseResult
    接口定义

    Kotlin 复制代码
    interface ApiService {
        @GET("users")
        suspend fun getUsers(): Response<List<User>>
    }
  • 协程调用

    Kotlin 复制代码
    viewModelScope.launch(Dispatchers.IO) {
        try {
            val response = apiService.getUsers()
            withContext(Dispatchers.Main) {
                if (response.isSuccessful) {
                    usersLiveData.value = response.body()
                } else {
                    showError(response.errorBody())
                }
            }
        } catch (e: Exception) {
            withContext(Dispatchers.Main) { showError(e) }
        }
    }
  • 优势
    避免回调嵌套,代码简洁;自动处理线程切换,保证主线程安全。

  • 消除回调地狱 :Retrofit 支持 suspend 函数后,网络请求可直接以同步写法实现异步逻辑,无需嵌套回调。

  • 生命周期安全 :在 ViewModel 或组件作用域(viewModelScope/lifecycleScope)内启动协程,框架自动管理协程与组件的生命周期绑定,避免泄漏。

  • 最佳实践
    将网络 / 数据库操作封装为挂起函数,在协程体内通过调度器切换线程,最终通过 LiveDataStateFlow 通知 UI 层,形成「异步处理 - 线程切换 - 响应式更新」的完整链路。

真题 3:如何在 Room 数据库中使用协程?

解答:

  • Room 适配协程
    在 DAO 中定义 suspend 函数,Room 自动生成协程适配代码。
    DAO 示例

    Kotlin 复制代码
    @Dao
    interface UserDao {
        @Query("SELECT * FROM users")
        suspend fun getUsers(): List<User>
    }
  • 协程调用

    Kotlin 复制代码
    viewModelScope.launch(Dispatchers.IO) {
        val users = userDao.getUsers()
        withContext(Dispatchers.Main) {
            usersLiveData.value = users
        }
    }
  • 性能优化
    使用 Flow 实现响应式数据更新(需结合 flowOn(Dispatchers.IO) 切换线程)。

    Kotlin 复制代码
    @Dao
    interface UserDao {
        @Query("SELECT * FROM users")
        fun getUsersFlow(): Flow<List<User>> // 自动适配协程
    }

二、性能优化类真题

真题 4:如何优化协程中的分页加载?

解答:

  • 分页加载策略

    1. 按需加载:滑动到列表底部时触发下一页请求。
    2. 并行加载 :使用 async 并行获取多个页面数据(如预加载下一页)。
      示例代码
    Kotlin 复制代码
    private fun loadNextPage(page: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            val deferred = async { apiService.getPage(page) }
            val data = deferred.await()
            withContext(Dispatchers.Main) { updateUI(data) }
        }
    }
  • 避免重复请求
    使用 StateFlowLiveData 跟踪加载状态,防止多次触发同一页请求。

  • 使用 Flow 响应式编程
    Flow 是协程的数据流处理工具,可通过 flowOn 切换线程,collect 收集数据,天然支持背压(Backpressure),适合处理列表分页加载、实时消息同步等场景。例如:

    1. 分页加载时,滑动到列表底部触发 flow.emit(nextPageData),自动切换到 IO 线程请求数据,主线程更新 UI。
    2. 结合 StateFlow/SharedFlow 实现数据的可观察性,替代传统的 LiveData 回调,代码更简洁且线程安全。
  • 并行预加载 :对已知的后续操作(如下一页数据),用 async 并行启动协程,利用等待当前结果的时间预加载数据,减少用户等待时间。

真题 5:如何处理协程中的内存泄漏?

解答:

  • 结构化并发原则
    协程必须绑定到组件生命周期(如 lifecycleScope/viewModelScope),避免使用 GlobalScope
    正确做法

    Kotlin 复制代码
    // Activity 中使用 lifecycleScope
    lifecycleScope.launch { /* 协程任务 */ }
    
    // ViewModel 中使用 viewModelScope
    viewModelScope.launch { /* 协程任务 */ }
  • 上下文管理
    单例中避免持有 Activity 上下文,优先使用 Application 上下文。
    反例

    Kotlin 复制代码
    // 危险!单例持有 Activity 上下文导致泄漏
    class BadSingleton(private val context: Context) {
        init { /* 初始化操作 */ }
    }

    正例

    Kotlin 复制代码
    class GoodSingleton(private val context: Context) {
        private val appContext = context.applicationContext // 使用 Application 上下文
    }

真题 6:如何优化协程的启动性能?

解答:

  • 按任务类型选择调度器
    1. IO 密集型任务(网络 / 磁盘) :用 Dispatchers.IO,其内部线程池会复用线程,避免频繁创建开销(如网络请求等待时线程可处理其他任务)。
    2. CPU 密集型任务(数据解析 / 计算) :用 Dispatchers.Default,其线程数限制为 CPU 核心数 × 2,防止过度占用 CPU 资源。
    3. UI 操作 :必须用 Dispatchers.Main,通过主线程 Looper 安全更新界面。
  • 减少上下文切换 :避免在协程体内频繁调用 withContext 切换线程,尽量在同一调度器内完成相关操作(如先在 IO 线程读取文件,再在同线程解析数据,最后切回主线程)。
  • 延迟启动(LAZY 模式)
    使用 async(start = CoroutineStart.LAZY) 延迟执行耗时操作,减少初始化开销。

    Kotlin 复制代码
    val lazyDeferred = async(start = CoroutineStart.LAZY) { loadHeavyData() }
    // 按需启动
    if (needData) lazyDeferred.start()
  • 复用线程池
    避免频繁创建新线程,使用 Dispatchers.IO 或自定义线程池。
    自定义线程池

    Kotlin 复制代码
    val ioDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
    // 使用 ioDispatcher 启动协程
    CoroutineScope(ioDispatcher).launch { /* 任务 */ }
  • 减少上下文切换
    在协程体内尽可能完成同一线程的任务,避免频繁调用 withContext

三、高级实战与性能优化真题

真题 7:如何实现协程与 LiveData 的高效结合?(阿里 / 字节跳动)

解答:

  • LiveData 包装协程结果
    使用 LiveDatapostValuesetValue 在主线程更新数据。
    示例代码

    Kotlin 复制代码
    class UserViewModel : ViewModel() {
        private val _users = MutableLiveData<List<User>>()
        val users: LiveData<List<User>> = _users
    
        fun fetchUsers() {
            viewModelScope.launch(Dispatchers.IO) {
                val data = apiService.getUsers()
                _users.postValue(data) // 非主线程调用 postValue
            }
        }
    }
  • 结合 Flow
    使用 flow 发射数据流,通过 flowOn 切换线程,collectLiveData

    Kotlin 复制代码
    class UserViewModel : ViewModel() {
        val usersFlow = flow {
            emit(apiService.getUsers())
        }.flowOn(Dispatchers.IO)
    
        fun observeUsers(owner: LifecycleOwner, observer: Observer<List<User>>) {
            usersFlow.launchIn(viewModelScope).collect {
                _users.postValue(it)
            }
        }
    }

真题 8:如何处理协程中的耗时操作导致的 ANR?

解答:

  • 避免阻塞主线程
    耗时操作(如文件读写、网络请求)必须在非主线程执行。
    错误案例

    Kotlin 复制代码
    // 危险!在主线程执行耗时操作(导致 ANR)
    runBlocking {
        delay(10_000) // 阻塞主线程 10 秒
    }

    正确做法

    Kotlin 复制代码
    viewModelScope.launch(Dispatchers.IO) {
        delay(10_000) // IO 线程执行耗时操作
        withContext(Dispatchers.Main) { /* 更新 UI */ }
    }
  • 超时控制
    使用 withTimeout 限制协程执行时间,防止长时间阻塞。

    Kotlin 复制代码
    viewModelScope.launch(Dispatchers.IO) {
        try {
            withTimeout(5_000) { // 超时时间 5 秒
                val data = apiService.getUsers()
            }
        } catch (e: TimeoutCancellationException) {
            withContext(Dispatchers.Main) { showTimeoutError() }
        }
    }
  • 严格分离主线程任务 :任何耗时超过 16ms(60fps 要求)的操作必须在非主线程调度器执行,通过 launch(Dispatchers.IO)withContext 明确切换。

  • 设置超时控制 :使用 withTimeout 限制协程执行时间,避免因网络延迟、数据异常等导致的长时间阻塞。例如:

    复制代码
    withTimeout(5000) { /* 网络请求或复杂计算 */ }

    超时后协程自动取消,防止主线程因等待结果而卡顿。

  • 避免阻塞式 API :协程中优先使用挂起函数(如 Retrofitsuspend 方法),而非阻塞式的 execute()get(),确保任务以非阻塞方式挂起,释放线程资源。

相关推荐
会飞的架狗师几秒前
为何选择Spring框架学习设计模式与编码技巧?
学习·spring·mybatis
@蓝莓果粒茶11 分钟前
LeetCode第245题_最短单词距离III
c语言·c++·笔记·学习·算法·leetcode·c#
小小星球之旅29 分钟前
redis缓存常见问题
数据库·redis·学习·缓存
Haoea!31 分钟前
Flink03-学习-套接字分词流自动写入工具
开发语言·学习
哆啦A梦的口袋呀42 分钟前
基于Python学习《Head First设计模式》第三章 装饰者模式
python·学习·设计模式
哆啦A梦的口袋呀43 分钟前
基于Python学习《Head First设计模式》第五章 单件模式
python·学习·设计模式
Fastcv1 小时前
手把手教你上传安卓库到Central Portal
android·maven·jcenter
whysqwhw1 小时前
安卓应用线程与架构问题
android
小鱼干coc1 小时前
Android 轻松实现 增强版灵活的 滑动式表格视图
android
Le_ee1 小时前
dvwa6——Insecure CAPTCHA
android·安全·网络安全·靶场·dvwa