面试题答案
一键面试性能优化策略
- 协程调度器的选择与配置
- 根据任务类型选择调度器:
- IO密集型任务:对于如网络请求、文件读写等IO密集型任务,使用
Dispatchers.IO
调度器。它会使用一个共享的线程池,适合执行这类长时间等待外部资源的任务。例如,在进行网络请求获取数据时,将协程调度到Dispatchers.IO
。 - CPU密集型任务:对于计算密集型任务,如图片处理、加密计算等,选用
Dispatchers.Default
调度器。它会根据设备CPU核心数来分配线程,以充分利用CPU资源。比如在对图片进行滤镜处理时,将任务放在Dispatchers.Default
调度器上执行。 - 主线程任务:与UI交互相关的任务,如更新UI,必须使用
Dispatchers.Main
调度器。这确保了UI操作在主线程进行,避免出现线程安全问题。例如,当从网络获取到数据后要更新UI上的列表,就需要在Dispatchers.Main
上执行。
- IO密集型任务:对于如网络请求、文件读写等IO密集型任务,使用
- 自定义调度器:在一些特殊场景下,可以自定义调度器。比如,如果项目中有一组特定的任务,需要与其他任务隔离线程执行,可以创建一个自定义线程池,并使用
newSingleThreadContext
或newFixedThreadPoolContext
来创建自定义调度器。例如,对于一些需要独立运行且不能被其他任务干扰的后台定时任务,可以创建一个单线程的自定义调度器。
- 根据任务类型选择调度器:
- 协程复用机制
- 协程池:可以通过创建一个协程池来复用协程。在Kotlin中没有直接的协程池概念,但可以借助线程池来模拟。例如,使用
Executors.newFixedThreadPool
创建一个固定大小的线程池,然后通过asCoroutineDispatcher
方法将其转换为协程调度器。这样,协程可以复用线程池中的线程,减少线程创建和销毁的开销。在一个需要频繁进行异步计算的模块中,可以使用这种方式创建协程池。 - 复用已有的协程作用域:在项目中,尽量复用已有的协程作用域,而不是每次都创建新的作用域。比如,在一个Activity或Fragment中,可以定义一个
viewModelScope
或lifecycleScope
,在这个作用域内启动多个协程。这样,当Activity或Fragment销毁时,所有在该作用域内的协程会自动取消,避免内存泄漏。例如,在一个新闻详情页的Fragment中,无论是获取新闻内容、加载评论等操作,都在lifecycleScope
内启动协程。
- 协程池:可以通过创建一个协程池来复用协程。在Kotlin中没有直接的协程池概念,但可以借助线程池来模拟。例如,使用
- 减少不必要的挂起操作:仔细检查代码中协程的挂起操作,确保每个挂起都是必要的。例如,在一些数据获取逻辑中,如果可以先在本地缓存中获取部分数据,就先获取本地数据,然后再通过挂起操作获取网络更新的数据,这样可以减少协程挂起的时间,提高性能。在一个电商APP的商品详情页,先从本地数据库获取商品基本信息,再通过网络请求获取最新的价格等信息。
架构设计策略
- 分层架构设计
- 数据层:
- 职责:负责数据的获取和存储。对于网络数据,使用Retrofit等网络框架结合协程进行异步请求。对于本地数据,使用Room等数据库框架结合协程进行操作。例如,在一个社交APP中,使用Retrofit获取用户的好友列表数据,使用Room存储本地聊天记录。
- 协程管理:在数据层,可以创建一个独立的
CoroutineScope
,用于管理所有数据获取和存储相关的协程。并且在数据层可以使用Dispatchers.IO
调度器,因为这一层主要是IO操作。
- 业务逻辑层:
- 职责:处理业务逻辑,如数据的加工、验证等。从数据层获取数据后,进行业务规则的处理。例如,在电商APP中,计算商品的折扣价格、判断库存是否足够等。
- 协程管理:业务逻辑层可以复用数据层获取数据的协程结果,在处理复杂业务逻辑时,可以根据需要启动新的协程。这里可以根据具体任务类型选择合适的调度器,如果是计算相关的,可选择
Dispatchers.Default
;如果是与数据获取相关,可复用数据层的Dispatchers.IO
。
- 表示层:
- 职责:负责与用户交互,展示数据和接收用户输入。在Android项目中,这一层主要涉及Activity、Fragment等组件。它从业务逻辑层获取处理后的数据,并更新UI。
- 协程管理:在表示层,使用
Dispatchers.Main
调度器来更新UI。通过lifecycleScope
或viewModelScope
来启动协程获取数据并更新UI,确保在组件生命周期结束时协程能正确取消。
- 数据层:
- 模块化设计:将项目按照功能模块进行划分,每个模块有自己独立的协程管理和架构。例如,在一个大型的综合类APP中,将社交模块、电商模块、新闻模块等分开设计。每个模块内部有自己的数据层、业务逻辑层和表示层,并且每个模块可以根据自身特点选择合适的协程调度器和协程复用机制。这样可以降低模块之间的耦合度,便于维护和扩展。
实际项目落地
- 优化前的状况:在之前参与的一个物流跟踪APP项目中,在高并发场景下,如多个快递同时查询物流信息时,出现了明显的卡顿。协程管理混乱,很多地方随意创建协程作用域和调度器,导致资源浪费。
- 性能优化落地:
- 调度器调整:对网络请求相关的协程统一使用
Dispatchers.IO
调度器,对于计算快递预计送达时间等CPU密集型任务使用Dispatchers.Default
调度器。在代码中,通过在协程构建器(如launch
或suspendCoroutine
)中指定调度器来实现。例如:
- 调度器调整:对网络请求相关的协程统一使用
launch(Dispatchers.IO) {
val response = networkService.getLogisticsInfo(trackingNumber)
withContext(Dispatchers.Main) {
// 更新UI展示物流信息
}
}
- 协程复用:创建了一个网络请求协程池,使用
Executors.newFixedThreadPool(5).asCoroutineDispatcher()
创建一个大小为5的线程池作为协程调度器。在网络请求模块中,所有网络请求协程都使用这个调度器,复用线程资源。同时,在Activity和Fragment中统一使用lifecycleScope
来启动协程,避免重复创建作用域。
- 架构设计落地:
- 分层架构实现:按照分层架构设计,将项目分为数据层、业务逻辑层和表示层。数据层封装了所有网络请求和本地数据库操作,业务逻辑层处理物流数据的解析、状态判断等,表示层负责展示物流信息给用户。例如,在数据层创建了
LogisticsDataSource
类,里面使用Retrofit和Room结合协程获取和存储物流数据;业务逻辑层的LogisticsLogic
类从LogisticsDataSource
获取数据并进行处理;表示层的LogisticsFragment
通过lifecycleScope
从LogisticsLogic
获取数据并更新UI。 - 模块化设计:将快递查询、包裹管理等功能划分为不同模块。每个模块有自己独立的分层架构,并且可以根据自身需求对协程进行管理。比如包裹管理模块可能更侧重于本地数据操作,在数据层可以针对本地数据库操作进一步优化协程调度。
- 分层架构实现:按照分层架构设计,将项目分为数据层、业务逻辑层和表示层。数据层封装了所有网络请求和本地数据库操作,业务逻辑层处理物流数据的解析、状态判断等,表示层负责展示物流信息给用户。例如,在数据层创建了