【Android面试】Kotlin语言专题
文章目录一. 基本语法1. 泛型的Out和In关键字2. UnsafeVariance3. lateinit和by lazy4. 扩展方法及其原理5. 如何理解委托6. 内联7. 高阶函数8. 伴随对象9. 泛型实化10. Unit与void的区别11. lambda12. runletalsowithapply高阶函数总结二. 协程1. 协程是什么2. 协程的启动3. 协程的挂起和阻塞概念4. 理解Suspend关键字5. Job和SupervisorJob6. CoroutineDispatcher调度器7. CoroutineContext 上下文组合规则与元素合并原理8. 协程的启动模式9. 协程的作用域10. 讲讲 Continuation 接口与 suspend 挂起函数底层状态机实现11. 协程非阻塞的底层本质为什么线程做不到高并发12. coroutineScope 与 supervisorScope 核心区别 源码行为13. GlobalScope 为什么不推荐使用会引发什么问题14. viewModelScope 底层 Job 类型 设计原因15. Dispatchers.IO 与 Default 线程池机制 选型规范16. withContext 原理、和 asyncawait 对比 最佳实践17. Job 完整生命周期 cancel () 与 cancelChildren () 区别18. launch 与 async 异常抛出差异 底层原因19. CoroutineExceptionHandler 使用限制与生效条件20. 深刻解释协程的实现原理三. Flow1. Flow工作原理2. Cold Flow冷流和Hot Flow热流3. StateFlow和SharedFlow4. Flow 背压产生原因 buffer/conflate/collectLatest 区别5. flowOn 操作符线程切换规则 为什么只影响上游6. StateFlow 数据倒灌原因 解决方案协程的资料可参考该文章第五节一. 基本语法1. 泛型的Out和In关键字【Kotlin进阶】泛型的高级特性Out协变——生产者协变只能读取不能写入Out T等价于 ? extends T案例1支持协变的ListIn逆变——消费者逆变只能写入不能读取In T等价于 ? super T案例2支持逆变的Comparator2. UnsafeVarianceUnsafeVariance 是一个特殊的注解annotation用于处理泛型类型参数的协变covariance和逆变contravariance问题。它通常用于在编译器无法自动推断出类型安全的情况下强制告诉编译器忽略某些类型检查从而允许协变或逆变的使用。publicinterfaceListoutE:CollectionE{override val size:Intoverride funisEmpty():Booleanoverride funcontains(element:UnsafeVarianceE):Booleanoverride funiterator():IteratorEpublicoperator 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. 高阶函数高阶函数是指可以接收其他函数作为参数或者返回一个函数的函数案例letrunalso这些关键字8. 伴随对象Kotin中没有静态成员或函数伴随对象Companion object用于“没有类实例的情况下但需要访问类内部的函数”。单例模式如果需要与java代码集成加上注解JvmStatic9. 泛型实化inline Reified实现泛型实化用于在运行时直接获取泛型类型参数的具体类型的privateval retrofitRetrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build();funTcreate(serviceClass:ClassT):Tretrofit.create(serviceClass);inline funreifiedTcreate():Tcreate(T::class.java)// 不加refiled inline T::class.java这里会报错10. Unit与void的区别Unit 是 Kotlin 中的一个对象object表示没有有意义的返回值publicobjectUnit{overridefuntoString()kotlin.Unit}void 是Java 的关键字表示函数不返回任何值。它不是对象也不是类型不能作为参数或变量类型使用。11. lambda添加链接描述12. runletalsowithapply高阶函数总结二. 协程1. 协程是什么官方回答协程视为一种轻量级线程可用于提高并发代码的性能关键词轻量级,并发轻量级它不映射到本机线程因此不需要在处理器上进行上下文切换因此协程速度更快(线程由操作系统管理协程由用户管理)结构化并发Kotlin 协程支持结构化并发模型通过 CoroutineScope 来管理协程的生命周期确保所有协程在完成或取消时不会泄漏。并发与并行的区别一手画圆一手画方两只手同时操作左右互搏这个是并行但是呢我先左手画一笔右手画一笔同一时候只有一只手在操作来回交替直到完成图案这个就是并发支持以同步的方式编写异步代码协程是更高效和更简单的方式管理并发的框架其轻量级线程编写在实际线程框架之上通过利用函数的协作性质来充分利用它2. 协程的启动launch / async / runBlocking / coroutineScope / supervisorScope3. 协程的挂起和阻塞概念挂起挂起是协程中的概念指协程暂停自身的执行但并不会阻塞底层线程。挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作协程在执行到有 suspend 标记的函数的时候会被 suspend 也就是被挂起而所谓的被挂起就是切个线程挂起函数在执行完成之后协程会重新切回它原先的线程。阻塞阻塞是指线程在执行某个操作时被暂停直到该操作完成而不能执行其他任务。比如sleep方法4. 理解Suspend关键字原理定义用于标记可挂起函数使其能在不阻塞线程的前提下暂停和恢复执行工作原理当你在 Kotlin 中使用 suspend 修饰一个函数时Kotlin 编译器会在编译阶段将该函数转换为一个状态机一个实现了 Continuation 接口的类这个状态机用于管理函数的挂起与恢复。使用限制其他挂起函数内部协程作用域内如 launch、async、runBlocking若挂起函数内部无实际挂起逻辑如未调用 delay 或 withContext编译器会提示 redundant suspend modifier多余的 suspend 修饰符suspend 仅标记函数可挂起​线程切换需主动调用调度器​如 withContext挂起函数定义suspend 修饰的函数称为挂起函数表示该函数内部可能存在耗时操作如网络请求、文件读写常见挂起函数5. Job和SupervisorJobJobJob 是协程中最基本的接口用于表示一个可取消的工作单元如果一个子协程异常失败会取消其父 Job 及其他兄弟协程。适用场景任务之间有强依赖关系SupervisorJobSupervisorJob 是 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 附属任务统一使用supervisorScope13. GlobalScope 为什么不推荐使用会引发什么问题GlobalScope属于全局顶级作用域无绑定生命周期不受页面 / 组件生命周期管控无结构化并发约束页面销毁后协程仍持续运行引发内存泄漏、空指针崩溃、无效 UI 更新无法统一取消批量任务管理困难异常无法统一捕获安卓规范Activity 用lifecycleScope、ViewModel 用viewModelScope、仓库层自定义业务Scope彻底替代 GlobalScope14. 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 原理、和 asyncawait 对比 最佳实践withContext是挂起函数作用为切换上下文 / 线程无额外任务容器执行完自动切回原线程无需手动管理 Job自动跟随外层作用域取消代码更简洁对比asyncawait单任务线程切换优先用 withContext多任务并行、需要获取多个返回值时使用 async项目规范串行网络、数据库、文件操作统一使用 withContext (Dispatchers.IO)。17. Job 完整生命周期 cancel () 与 cancelChildren () 区别Job 生命周期New→Active→Completing→Completed/Cancelledcancel ()取消当前 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 排队执行。若确实需要固定到同一条工作线程用单线程 dispatchersingle-thread executor恢复同样靠排队等待。内核/Linux 的角色主要参与定时/IO 等“等待事件”的底层能力协程的挂起/恢复语义本身在用户态由 continuation 调度队列完成而非内核去“挂起/恢复线程”。三. Flow1. 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添加链接描述添加链接描述StateFlowStateFlow 是一种始终有值的热流它保存当前状态并将最新值发送给所有活跃的收集者。它的行为类似于 LiveData但基于协程。应用场景持有并共享 UI 状态ViewModel 状态管理SharedFlowSharedFlow 是一个广播式事件流可以发送多个值给多个收集者是一种更通用的热流。应用场景分发一次性事件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。