前言
最近在网上看许多面经的文章,有拿到 Offer 的,也有凉凉的,这些文章中都有一个共同的特点,就是单单的只有面试题目。而没有面试过程中需要注意的细节和面试心得。导致有些面试者们在面试来临,就去盲目的去刷这些题目,而没有结合自己做过的那些项目中,遇到的一些技术问题会被面试官问道,回答不上导致面试落选或者薪资上不去。
1、MVVM 架构模式概览
这是使用 MVVM 架构模式+Kotlin 协程+JetPack(ViewModel+LiveData)+Retrofit 的架构,实现 WanAndroid 登录接口的小 DEMO,后续会慢慢完善 WanAndroid 客户端
1、ViewModel
为了从界面控制器 Activity/Fragment 逻辑中分离出视图 View 数据所有权,架构组件为界面控制器提供了 ViewModel 辅助程序类,该类负责为界面准备数据。在配置更改期间会自动保留 ViewModel 对象,以便它们存储的数据立即可供下一个 Activity 或 Fragment 实例使用。
2、LiveData
LiveData 是一种可观察的数据存储器类,具有生命周期感知能力,意指它遵循其他应用组件如 Activity、Fragment 或 Service 生命周期,可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。LiveData 对象通常存储在 ViewModel 对象中,并可通过 getter 方法进行访问。
3、Kotlin 协程
协程依附在线程上,可以实现顺序编写异步代码,自动进行线程切换。并且 ViewModelScope 为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。
4、Retrofit
将服务接口中的网络请求函数声明为 suspend 挂起接口函数,以支持 Kotlin 线程,并将 suspend 函数结果作为 LiveData 对象传送。
2、ViewModel
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)`
复制代码
ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。
3、LiveData
viewModel.user.observe(this, Observer {
复制代码
Toast.makeText(this, it.data?.nickname, Toast.LENGTH_SHORT).show()
复制代码
Toast.makeText(this, it.errorMsg, Toast.LENGTH_SHORT).show()
复制代码
使用 LiveData 具有以下优势:确保界面符合数据状态
LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。观察者可以在每次发生更改时更新界面,而不是在每次应用数据发生更改时更新界面。
复制代码
不会发生内存泄漏
观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。
复制代码
不会因 Activity 停止而导致崩溃
如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
复制代码
界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。
复制代码
数据始终保持最新状态
如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。
复制代码
适当的配置更改
如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
复制代码
共享资源
可以使用单一实例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。
复制代码
4、Kotlin 协程
4.1、异步的本质
什么是异步?
但是对于有前后依赖关系的任务,异步该如何处理呢?
为什么需要异步回调机制?
异步回调机制有什么缺点?
代码结构过分耦合,遇到多重函数回调的嵌套耦合,也就是回调地狱,代码会难以维护。
复制代码
解决回调地狱的方案有什么?
链式调用结构。
常见方式就是使用RxJava,它是反应函数式编程在Java中的实现。
但是RxJava中流的创建、转化与消费都需要使用到各种类和丰富的操作符,加大了RxJava的学习成本。
减少在无封装情况下使用RxJava,因为你无法保证团队里面的每一个成员都能看懂它,并且在修改时都能做出正确选择。
复制代码
在串行的执行中,虽然代码确实是顺序执行的,但其实是在不同的线程上顺序执行的。那为什么在串行的执行中代码执行顺序一致,却还要使用回调呢?
因为串行的执行中,执行是阻塞式的,主线程的阻塞会导致很严重的问题,所以所有的耗时操作不能在主线程中执行,所以就需要多线程并行来执行。
复制代码
在并行的执行中,异步回调其实就是代码的多线程顺序执行。那能不能既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行,自动完成线程的切换工作呢?
那就是Kotlin协程。
Kotlin 的协程是一种无栈协程的实现,它的控制流转依靠对协程体本身编译生成的状态机的状态流转来实现,变量保存也是通过闭包语法来实现的。
复制代码
结论:
异步回调就是代码的多线程顺序执行,而Kotlin协程可以实现顺序编写异步代码,自动进行线程切换。
复制代码
那么协程自动进行线程切换的原理是什么?
Yield:让出CPU,放弃调度控制权,回到上一次Resume的地方
Resume:获取调度控制权,继续执行程序,到上一次Yield的地方
复制代码
例子:
1. GlobalScope.launch发起了一个协程,并在IO线程上执行,
2\. 在协程里,去调用接口获取结果。
3. 拿到结果,使用withContext(Dispatchers.Main)切换到主线程并更新界面
复制代码
4.2、协程的类型
是协程范围,指的是协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行。
复制代码
GlobalScope
指的是与应用进程相同的协程范围,也就是在进程没有结束之前协程内的代码都可以运行。
复制代码
JetPack 中提供的生命周期感知型协程范围:
ViewModelScope,为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。
LifecycleScope,为每个 Lifecycle 对象定义了 LifecycleScope。在此范围内启动的协程会在 Lifecycle 被销毁时取消。
使用 LiveData 时,可能需要异步计算值。可以使用 liveData 构建器函数调用 suspend 函数,并将结果作为 LiveData 对象传送。
复制代码
相关链接:https://developer.android.google.cn/topic/libraries/architecture/coroutines
4.3、协程的启动
launch 方法:
/**
* 重要知识:ViewModel+协程
*/
fun ViewModel.launch(
block: suspend CoroutineScope.() -> Unit,
onError: (e: Throwable) -> Unit = {},
onComplete: () -> Unit = {}
) {
viewModelScope.launch(CoroutineExceptionHandler { _, e -> onError(e) }) {
try {
block.invoke(this)
} finally {
onComplete()
}
}
}
复制代码
源码:
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
}
复制代码
4.3.1、launch 方法解释
context
协程上下文,可以指定协程运行的线程。默认与指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默认运行在一个后台工作线程内。也可以通过显示指定参数来更改协程运行的线程,Dispatchers提供了几个值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。
复制代码
start
协程的启动模式。默认的CoroutineStart.DEFAULT是指协程立即执行,除此之外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。
复制代码
block
协程主体。也就是要在协程内部运行的代码,可以通过lamda表达式的方式方便的编写协程内运行的代码。
复制代码
CoroutineExceptionHandler
指定CoroutineExceptionHandler来处理协程内部的异常。
复制代码
Job
返回值,对当前创建的协程的引用。可以通过Job的start、cancel、join等方法来控制协程的启动和取消。
复制代码
4.4、suspend 挂起函数
suspend 关键字只起到了标志这个函数是一个耗时操作,必须放在协程中执行的作用,而 withContext 方法则进行了线程的切换工作。
协程中的代码自动地切换到其他线程之后又自动地切换回了主线程!顺序编写保证了逻辑上的直观性,协程的自动线程切换又保证了代码的非阻塞性。挂起函数必须在协程或者其他挂起函数中被调用,也就是挂起函数必须直接或者间接地在协程中执行。
那为什么协程中的代码没有在主线程中执行呢?而且执行完毕为什么还会自动地切回主线程呢?
协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的代码重新进入协程所在线程的过程。协程就是通过的这个挂起恢复机制进行线程的切换。
4.5、async await 方法
用 async 方法包裹了 suspend 方法来执行并发请求,并发结果都返回之后,切换到主线程,接着再用 await 方法来获取并发请求结果。
5、Retrofit
HTTP 接口 suspend 挂起函数:
interface ApiService {
@FormUrlEncoded
@POST("user/login")
suspend fun loginForm(@Field("username") username: String,@Field("password") password: String): BaseResponse<User>
}
复制代码
kotlin 泛型:
data class BaseResponse<T>(
val errorCode: Int=0,
val errorMsg:String? = null,
var data: T? = null
)
复制代码
这是使用 MVVM 架构模式+Kotlin 协程+JetPack(ViewModel+LiveData)+Retrofit 的架构,实现 WanAndroid 登录接口的小 DEMO,后续会慢慢完善 WanAndroid 客户端
最后
由于题目很多整理答案的工作量太大,所以仅限于提供知识点,详细的很多问题和参考答案我都整理成了 PDF 文件,需要的小伙伴可以私信我【面试】免费领取或者点击GitHub免费获取!
评论