ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

【Android面试】Kotlin语言专题

【Android面试】Kotlin语言专题

文章目录

  • 一. 基本语法
    • 1. 泛型的Out和In关键字
    • 2. UnsafeVariance
    • 3. lateinit和by lazy
    • 4. 扩展方法及其原理
    • 5. 如何理解委托
    • 6. 内联
    • 7. 高阶函数
    • 8. 伴随对象
    • 9. 泛型实化
    • 10. Unit与void的区别
    • 11. lambda
    • 12. run,let,also,with,apply高阶函数总结
  • 二. 协程
    • 1. 协程是什么
    • 2. 协程的启动
    • 3. 协程的挂起和阻塞概念
    • 4. 理解Suspend关键字
    • 5. Job和SupervisorJob
    • 6. CoroutineDispatcher调度器
    • 7. CoroutineContext 上下文组合规则与元素合并原理
    • 8. 协程的启动模式
    • 9. 协程的作用域
    • 10. 讲讲 Continuation 接口与 suspend 挂起函数底层状态机实现
    • 11. 协程非阻塞的底层本质,为什么线程做不到高并发
    • 12. coroutineScope 与 supervisorScope 核心区别 & 源码行为
    • 13. GlobalScope 为什么不推荐使用?会引发什么问题
    • 14. viewModelScope 底层 Job 类型 & 设计原因
    • 15. Dispatchers.IO 与 Default 线程池机制 & 选型规范
    • 16. withContext 原理、和 async+await 对比 & 最佳实践
    • 17. Job 完整生命周期 & cancel () 与 cancelChildren () 区别
    • 18. launch 与 async 异常抛出差异 底层原因
    • 19. CoroutineExceptionHandler 使用限制与生效条件
    • 20. 深刻解释协程的实现原理
  • 三. Flow
    • 1. Flow工作原理
    • 2. Cold Flow冷流和Hot Flow热流
    • 3. StateFlow和SharedFlow
    • 4. Flow 背压产生原因 & buffer/conflate/collectLatest 区别
    • 5. flowOn 操作符线程切换规则 为什么只影响上游
    • 6. StateFlow 数据倒灌原因 & 解决方案

协程的资料可参考该文章第五节

一. 基本语法

1. 泛型的Out和In关键字

【Kotlin进阶】泛型的高级特性

  • Out:协变——生产者
    协变:只能读取不能写入
    Out T等价于 ? extends T
    案例1:支持协变的List
  • In:逆变——消费者
    逆变:只能写入不能读取
    In T等价于 ? super T
    案例2:支持逆变的Comparator

2. UnsafeVariance

UnsafeVariance 是一个特殊的注解(annotation),用于处理泛型类型参数的协变(covariance)和逆变(contravariance)问题。它通常用于在编译器无法自动推断出类型安全的情况下,强制告诉编译器忽略某些类型检查,从而允许协变或逆变的使用。

publicinterfaceList<outE>:Collection<E>{override val size:Intoverride funisEmpty():Booleanoverride funcontains(element:@UnsafeVarianceE):Booleanoverride funiterator():Iterator<E>publicoperator funget(index:Int):E}

3. lateinit和by lazy

  • 二者都用于延迟初始化,差异如下:

  • by lazy的具体使用场景
// VIEWMODEL 的懒加载初始化(推荐方式)privateval viewModel:MyViewModelby lazy{ViewModelProvider(this).get(MyViewModel::class.java)}//单例数据库实例懒加载(ROOM 数据库)val database:AppDatabaseby lazy{Room.databaseBuilder(App.instance,AppDatabase::class.java,"app_db").build()}

4. 扩展方法及其原理

扩展方法(Extension Function) 是一种允许我们为已有类添加新方法的机制,而无需继承该类或使用装饰器等设计模式。

  • 原理
    Kotlin 编译器在编译时会将扩展函数转换为带有接收者参数的静态函数
    实际上并没有修改原有类的结构,只是在调用处将接收者作为第一个参数传入。
    扩展函数不能访问类的私有成员,因为它不是类的内部成员。
  • 使用场景
    简化 View 操作
    为 Context 或 Activity 添加常用功能
    为数据类型添加格式化方法

5. 如何理解委托

把一个对象的职责委托给另外一个对象

  • 属性委托:by lazy
  • 类的委托:通过by关键字
  • 使用场景:
    使用 lazy 委托延迟初始化 View 或资源
    使用委托简化 ViewModel 或 Repository 的获取

6. 内联

【Kotlin内联函数】

  • 定义
    编译时把调用代码插入到函数中去,避免方法调用的开销
  • inline: 内联,将函数体插入调用处,提升性能
  • noinline:避免参数被内联
  • crossline:限制非局部返回

7. 高阶函数

高阶函数是指可以接收其他函数作为参数或者返回一个函数的函数

  • 案例:
    let,run,also这些关键字

8. 伴随对象

Kotin中没有静态成员或函数,伴随对象Companion object用于“没有类实例的情况下但需要访问类内部的函数”。
单例模式
如果需要与java代码集成,加上注解@JvmStatic

9. 泛型实化

inline + Reified实现泛型实化,用于在运行时直接获取泛型类型参数的具体类型的

privateval retrofit=Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build();fun<T>create(serviceClass:Class<T>):T=retrofit.create(serviceClass);inline fun<reifiedT>create():T=create(T::class.java)// 不加refiled inline T::class.java这里会报错

10. Unit与void的区别

Unit 是 Kotlin 中的一个对象(object),表示没有有意义的返回值

publicobjectUnit{overridefuntoString()="kotlin.Unit"}

void 是Java 的关键字,表示函数不返回任何值。它不是对象,也不是类型,不能作为参数或变量类型使用。

11. lambda

添加链接描述

12. run,let,also,with,apply高阶函数总结

二. 协程

1. 协程是什么

官方回答:协程视为一种轻量级线程,可用于提高并发代码的性能
关键词:轻量级,并发
轻量级:它不映射到本机线程,因此不需要在处理器上进行上下文切换,因此协程速度更快(线程由操作系统管理,协程由用户管理)
结构化并发:Kotlin 协程支持结构化并发模型,通过 CoroutineScope 来管理协程的生命周期,确保所有协程在完成或取消时不会泄漏。
并发与并行的区别
一手画圆,一手画方,两只手同时操作,左右互搏,这个是并行;但是呢,我先左手画一笔,右手画一笔,同一时候只有一只手在操作,来回交替,直到完成图案,这个就是并发
支持以同步的方式编写异步代码
协程是更高效和更简单的方式管理并发的框架,其轻量级线程编写在实际线程框架之上,通过利用函数的协作性质来充分利用它

2. 协程的启动

launch / async / runBlocking / coroutineScope / supervisorScope

3. 协程的挂起和阻塞概念

  • 挂起
    挂起是协程中的概念,指协程暂停自身的执行,但并不会阻塞底层线程
    挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作
    协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;挂起函数在执行完成之后,协程会重新切回它原先的线程。

  • 阻塞
    阻塞是指线程在执行某个操作时被暂停,直到该操作完成,而不能执行其他任务。比如sleep方法

4. 理解Suspend关键字

原理

  • 定义
    用于标记可挂起函数,使其能在不阻塞线程的前提下暂停和恢复执行

  • 工作原理
    当你在 Kotlin 中使用 suspend 修饰一个函数时,Kotlin 编译器会在编译阶段将该函数转换为一个状态机(一个实现了 Continuation 接口的类),这个状态机用于管理函数的挂起与恢复。

  • 使用限制
    其他挂起函数内部
    协程作用域内(如 launch、async、runBlocking)
    若挂起函数内部无实际挂起逻辑(如未调用 delay 或 withContext),编译器会提示 redundant suspend modifier(多余的 suspend 修饰符)

  • suspend 仅标记函数可挂起,​线程切换需主动调用调度器​(如 withContext)

  • 挂起函数定义
    suspend 修饰的函数称为挂起函数,表示该函数内部可能存在耗时操作(如网络请求、文件读写)

  • 常见挂起函数

5. Job和SupervisorJob

  • Job
    Job 是协程中最基本的接口,用于表示一个可取消的工作单元
    如果一个子协程异常失败,会取消其父 Job 及其他兄弟协程。
    适用场景:任务之间有强依赖关系
  • SupervisorJob
    SupervisorJob 是 Job 的子接口,但行为不同:子协程的失败不会影响其他子协程或父协程
    使用场景:APP的首页,首页上展示的数据五花八门。如:广告,弹窗,未读状态

6. CoroutineDispatcher调度器

  • Dispatchers.Main
    用于 Android 的主线程(UI 线程),适合执行更新 UI 的操作。
  • Dispatchers.IO
    用于执行 I/O 密集型任务(如网络请求、文件读写等)
  • Dispatchers.Default
    用于 CPU 密集型任务(如数据计算、图像处理等)
  • Dispatchers.Unconfined
    不限制执行的线程,协程会在调用它的线程中运行,但恢复时可能在其他线程

7. CoroutineContext 上下文组合规则与元素合并原理

  • CoroutineContext是不可变集合式接口,采用键值对结构存储各类元素:Job、Dispatcher、异常处理器、协程名称等;
  • 支持+运算符做上下文叠加,同 Key 元素会后者覆盖前者,不同 Key 元素相互合并;
  • 子协程默认继承父协程全部上下文;
  • 主动指定Dispatcher/Job会覆盖父级对应元素,CoroutineExceptionHandler、协程名称等默认继承。

8. 协程的启动模式

  • DEFAULT 默认启动模式
    饿汉启动模式,协程创建后立即开始调度
  • LAZY 懒汉启动模式
    懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度(主动的调用Job的start、join或者await等函数)
  • ATOMIC
    在协程创建后立即开始调度。执行到第一个挂起点之前是不响应cancel 取消操作的,ATOMIC一定要涉及到协程挂起后cancel 取消操作的时候才有意义
  • UNDISPATCHED
    直接开始在当前线程下执行,直到运行到第一个挂起点。不经过任何调度器就开始执行的。当然遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器
    (没有遇到挂起点前,在当前线程下执行;遇到挂起点后,看挂起点具体的场景)

9. 协程的作用域

  • 顶级作用域
    没有父协程的协程所在的作用域称之为顶级作用域
  • 协同作用域
    在协程中启动一个协程,新协程为所在协程的子协程。子协程所在的作用域默认为协同作用域。此时子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。
  • 主从作用域(监督作用域)
    与协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。但是如果父协程被取消,则所有子协程同时也会被取消

10. 讲讲 Continuation 接口与 suspend 挂起函数底层状态机实现

  • Continuation是协程挂起与恢复的核心回调接口,封装了协程后续执行逻辑、上下文、异常回调,是挂起函数的载体。
  • 所有suspend挂起函数,编译期会被 Kotlin 编译器改写,自动新增Continuation类型入参,同时生成状态机代码,通过label标记代码执行位置。
  • 协程挂起时:保存当前执行现场、局部变量、label 状态,交出线程使用权;
  • 任务完成后:通过回调Continuation.resume/resumeWithException恢复状态机,从上次挂起点继续执行。
  • 非协程环境无法调用挂起函数,本质是没有自动生成与传递 Continuation,缺少恢复回调。

11. 协程非阻塞的底层本质,为什么线程做不到高并发

  • 线程由OS 内核调度,上下文切换成本高、内存占用大,线程池数量有限;
  • 协程由用户态调度,运行在线程之上,多个协程复用同一个线程,挂起仅保存少量栈信息,无内核态切换开销;
  • 非阻塞核心:挂起不阻塞线程,线程可立刻复用执行其他协程任务;
  • 单一线程可承载成千上万个挂起协程,因此协程支持海量并发,资源开销远低于线程

12. coroutineScope 与 supervisorScope 核心区别 & 源码行为

  • coroutineScope:遵循标准结构化并发,子协程异常会向上传播,直接取消父作用域及所有兄弟协程,全部任务终止;
  • supervisorScope:基于SupervisorJob,子协程异常隔离,单个子任务崩溃不影响其他子协程与父作用域;
  • 两者都会阻塞当前协程,等待内部所有子协程执行完毕才结束;
  • Android 中页面独立埋点、多模块并行请求、互不依赖的 UI 附属任务,统一使用supervisorScope

13. GlobalScope 为什么不推荐使用?会引发什么问题

  • GlobalScope属于全局顶级作用域,无绑定生命周期,不受页面 / 组件生命周期管控
  • 无结构化并发约束,页面销毁后协程仍持续运行,引发内存泄漏、空指针崩溃、无效 UI 更新
  • 无法统一取消,批量任务管理困难,异常无法统一捕获
  • 安卓规范:Activity 用lifecycleScope、ViewModel 用viewModelScope、仓库层自定义业务
    Scope,彻底替代 GlobalScope

14. viewModelScope 底层 Job 类型 & 设计原因

  • viewModelScope内部默认绑定SupervisorJob + Dispatchers.Main;
  • 设计目的:页面部分接口失败不杀死整个 ViewModel 所有任务,避免单一请求崩溃导致整个页面功能瘫痪;
  • ViewModel 销毁时自动触发 Job 取消,自动终止所有协程,杜绝泄漏;
  • 适合 MVVM 架构下多接口并行请求、独立业务任务解耦。

15. Dispatchers.IO 与 Default 线程池机制 & 选型规范

  • Dispatchers.Default:固定核心线程池,适配CPU 密集型任务,如数据解析、图片压缩、复杂计算;
  • Dispatchers.IO:动态扩容线程池,按需创建回收线程,适配IO 密集型:网络请求、文件读写、数据库操作;
  • 错误选型危害:CPU 任务用 IO 会造成线程泛滥、频繁切换;IO 任务用 Default 会堵塞计算线程池;
  • 两者底层共享公共调度器核心,只是线程池扩容策略不同。

16. withContext 原理、和 async+await 对比 & 最佳实践

  • withContext是挂起函数,作用为切换上下文 / 线程,无额外任务容器,执行完自动切回原线程;
  • 无需手动管理 Job,自动跟随外层作用域取消,代码更简洁;
  • 对比async+await:单任务线程切换优先用 withContext;多任务并行、需要获取多个返回值时使用 async;
  • 项目规范:串行网络、数据库、文件操作统一使用 withContext (Dispatchers.IO)。

17. Job 完整生命周期 & cancel () 与 cancelChildren () 区别

  • Job 生命周期:New→Active→Completing→Completed/Cancelled;
  • cancel ():取消当前 Job+全部子 Job,整个协程树终止;
  • cancelChildren ():仅取消所有子协程,当前父协程继续运行;
  • 协程一旦进入 Cancelled 状态,不可重启,需重新创建协程实例。

18. launch 与 async 异常抛出差异 底层原因

  • launch:独立任务型设计,异常立即向上传播,若无捕获直接触发崩溃;
  • async:结果返回型设计,异常被封装在 Deferred 内部,只有调用 await () 才会抛出;
  • 设计初衷:launch 用于单向执行任务,async 用于需要返回结果的异步任务,异常延迟抛出;
  • 并行 async 任务建议统一 try-catch 包裹 await,避免隐性崩溃。

19. CoroutineExceptionHandler 使用限制与生效条件

  • 只能捕获作用域内未被 try-catch 捕获、未被消费的顶级协程异常
  • 无法捕获子协程已捕获的异常、CancellationException 取消异常;
  • 异常处理器遵循就近原则,子上下文 Handler 优先于父级;
  • 仅对 launch 有效,async 异常不会经过该处理器,必须通过 await 捕获。

20. 深刻解释协程的实现原理

  • 协程一定跑在线程上:挂起不代表线程被挂起,只是这段协程逻辑暂停,把线程让出来去执行别的任务。
  • 挂起的本质suspend 经编译器改写为“状态机 + Continuation”。遇到挂起点时保存现场并返回COROUTINE_SUSPENDED。
  • 恢复的本质:挂起条件满足后(delay 到期/IO 回调返回),并不是“立刻抢回原线程A”,而是 resume 后交给Dispatcher.dispatch(…),把 continuation 包装成任务投递/入队。
  • 线程A忙怎么办:不会抢占、不并发插入执行;恢复任务进入队列,等线程A空闲(当前任务执行完)再执行。
  • 是否必须回到同一条物理线程A:一般不保证。
  • Dispatchers.IO/Default:只保证回到同一线程池,可能换线程继续执行。
  • Dispatchers.Main:保证回到主线程(同一条主线程),通过Looper/MessageQueue 排队执行。
  • 若确实需要固定到同一条工作线程:用单线程 dispatcher(single-thread executor),恢复同样靠排队等待。
  • 内核/Linux 的角色:主要参与定时/IO 等“等待事件”的底层能力;协程的挂起/恢复语义本身在用户态由 continuation +调度队列完成,而非内核去“挂起/恢复线程”。

三. Flow

1. Flow工作原理

Flow 的核心设计基于响应式流(默认冷流)的概念,原理上是 kotlinx.coroutines 库的一部分。它有三个主要角色:
生产者(Producer):负责发送数据流(通过 flow { emit() } 实现)
中间处理(Intermediate):对数据进行转换、过滤等操作(如 map、filter)
消费者(Collector):接收并处理数据(通过 collect 启动)

2. Cold Flow冷流和Hot Flow热流

  • 冷流
    定义:冷流只有在有消费者(collect)开始收集数据时才会开始执行。
    典型实现类:flow (Flow) // Observable (Rxjava)
    使用场景:适合一次性任务,如网络请求、数据库查询等。
  • 热流
    定义:热流无论是否有消费者收集数据,都会持续发射数据。
    典型实现类:SharedFlow、StateFlow // Subject, livedata
    使用场景:适合广播事件、UI 状态共享、传感器数据等需要实时共享的场景。

3. StateFlow和SharedFlow

添加链接描述
添加链接描述

  • StateFlow
    StateFlow 是一种始终有值热流,它保存当前状态,并将最新值发送给所有活跃的收集者。它的行为类似于 LiveData,但基于协程。
    应用场景:持有并共享 UI 状态(ViewModel 状态管理)
  • SharedFlow
    SharedFlow 是一个广播式事件流,可以发送多个值给多个收集者,是一种更通用的热流。
    应用场景:分发一次性事件(Toast、导航)

4. Flow 背压产生原因 & buffer/conflate/collectLatest 区别

  • 背压:上游数据发送速度 > 下游消费速度,数据堆积导致内存占用过高;
  • buffer:设置缓存池,缓存上游数据,异步消费,避免阻塞生产者;
  • conflate:保留最新数据,丢弃老旧积压数据,适合实时状态刷新;
  • collectLatest:新数据到来时,取消上一次未完成的消费逻辑,适合搜索、列表刷新等场景。

5. flowOn 操作符线程切换规则 为什么只影响上游

  • flowOn 仅改变上游生产者、中间操作符的执行线程;
  • 下游 collect 收集代码始终运行在当前协程上下文线程;
  • 原理:Flow 是冷流懒加载设计,上下游执行环境隔离,各自绑定独立调度器;
  • 多段 flowOn 叠加时,以最近的 flowOn为准覆盖上游线程。

6. StateFlow 数据倒灌原因 & 解决方案

  • 倒灌根源:StateFlow 默认保存当前最新值,新订阅者连接后会立刻回放历史值;
  • 劣势:一次性事件(弹窗、Toast、路由)重复触发;
  • 解决方案:一次性事件改用 SharedFlow、添加事件消费标记、使用distinctUntilChanged过滤重复状态;
  • 架构规范:UI 状态用 StateFlow,事件型通知强制用 SharedFlow。
返回列表