注册

面试整理-kotlin与jetpack

面试可能会问到的问题



  1. 内联函数与高阶函数
  2. 对委托的理解
  3. 扩展方法以及其原理
  4. 协变与逆变
  5. 协程相关知识(创建方式、原理)
  6. jetpack使用过哪些库
  7. LiveData和LifeCycle的原理
  8. Viewmodel的原理
  9. WorkManager的使用场景
  10. Navigation使用过程中有哪些坑

内联函数和高阶函数



关键不是问你什么概念,而是看你在实际使用中有没有注意这些细节



概念



  • 内联函数:编译时把调用代码插入到函数中,避免方法调用的开销。
  • 高阶函数:接受一个或多个函数类型的参数,并/或返回一个函数类型的值

概念就这两句话,实际使用的时候却有很大的用途。比如我们常用的apply、run、let这些其实就是一个内联高阶函数。


// apply 
public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this }
// run
public inline fun <T, R> T.run(block: T.() -> R): R { return block() }
// let
public inline fun <T, R> T.let(block: (T) -> R): R { return block(this) }

使用心得



  1. 有时候为了代码整洁,我们不会让一个方法超过一屏幕,会把里面的方法抽成几个小的方法,但是方法会涉及到入栈出栈,而内联函数就可以保证代码的整洁又避免了方法进栈出栈的开销。这个是我们稍微注意一下很方便做的优化。
  2. 为了简化函数的调用我们可以使用高阶函数,除了系统提供的apply、run、let这些外,自己其实平时也会写一些高阶函数,比如下面的例子




    • 使用高阶函数增加代码可读性



// 使用高阶函数简化网络请求处理
fun <T> Call<T>.enqueue(
onSuccess: (response: Response<T>) -> Unit,
onError: (error: Throwable) -> Unit,
onCancel: () -> Unit
) {
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
onSuccess(response)
} else {
onError(Exception("Request failed with code ${response.code()}"))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
if (!call.isCanceled) {
onError(t)
} else {
onCancel()
}
}
})
}

---
// 使用的时候
call.enqueue(
onSuccess = { response ->
// 在这里处理网络请求成功的逻辑

},
onError = { error ->
// 在这里处理网络请求失败的逻辑
},
onCancel = {
// 在这里处理网络请求取消的逻辑
}
)





    • 使用高阶函数减少无用回调,方便使用



// 使用高阶函数简化回调函数
fun EditText.doOnTextChanged(action: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
action(s, start, before, count)
}

override fun afterTextChanged(s: Editable?) {
}
})
}

// 使用的时候只需要关系一个回调就可以
editText.doOnTextChanged { text, _, _, _ ->
// 在这里处理输入框文本变化的逻辑
}


这两个例子很好的说明了高阶函数的作用,可以简化一些操作,也可以增强可读性。其实还有一些其他的作用比如用高阶函数实现RecycleView初始化时的函数式编程、对一些方法添加缓存等等。
只要涉及到对原有方法的增强或者简化或者添加多一层封装实现链式调用都可以考虑使用高阶函数。


对委托的理解



因为委托在开发中真的非常好用,问这个问题就想看看你有没有真的理解委托



首先委托的概念就是把一个对象的职责委托给另外一个对象,在kotlin中有属性的委托和类的委托。属性的委托比如by lazy,他的作用是使用到的时候才加载简化了判空代码也节省了性能。类的委托通常是一个接口委托一个对象interface by Class。目的是对一个类的解耦方便以后相同功能的代码复用。例子就不举例了,就是但凡开发中想到有些代码是可以复用的时候可以考虑能不能写成一个接口去交给委托类去实现。


问到by lazy可能还会问你与lateinit的区别。



  • lateinit:延时加载,只是告诉编译器不用检查这个变量的初始化,不能使用val修饰
  • by lazy:懒加载,lazy是一个内联高阶函数,通过传入自身来做一些初始化的判断。

扩展方法以及其原理



扩展函数也是使用kotlin时非常好用的一个特性,多多少少可能也会提一嘴。



实际开发中我们的点击事件、资源获取等都可以使用。好处就不多说了,比如加入防抖,或者获取资源时的捕获异常,都可以减少日后添加需求时的开发量


private var lastClickTime = 0L
fun View.setSingleClickListener(delay: Long = 500, onClick: () -> Unit) {
setOnClickListener {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime > delay) {
onClick()
lastClickTime = currentTime
}
}
}


  • 原理
    Kotlin 中的扩展方法其实是一种静态的语法糖,本质上是一个静态函数,不是实例函数。编译器会将扩展方法转化为静态函数的调用。
    比如


fun String.lastChar(): Char = this.get(this.length - 1)
---
val s = "hello"
val c = s.lastChar() // 转化为 StringKt.lastChar(s)

协变与逆变(out 和 in)



这个问的可能比较少,这个问题其实主要还是看你有没有写过一些大型架构,尤其是像rxjava这种设计到入参出参的。




  • 协变与逆变是数学中的概率,协变就是x跟y正相关图形是往上的,逆变就是x跟y负相关图形是往下的。
  • 协变往上的肯定有个最大的上限,java中的上限就是obj,所以你会看到很多这样的代码out Any或者?extentd Object
  • 逆变往下的肯定有个最小值,所以你会看到很多这样的代码out T或者? super T

这里面还会涉及到一个set和get的问题,协变只能get不能set。比如逆变只能set不能get。这个结论你可以记起来,也可以理解一下,这个是面向对象的基础。举个例子说明


爷爷辈(会玩手机)、爸爸辈(会玩手机会上网)、孙子辈(会玩手机会上网会打游戏)。 比如指定的上限(out、extends)是爷爷辈,如果只是作为返回值,直接返回T就可以,因为不管你返回什么类型,最后都可以用爷爷辈来接。而如果用于set,你可以传个爸爸辈或者孙子辈的进来,里面并不知道你确切的类型就出问题了。


反过来,如果逆变(in、super)指定的下限是孙子辈,用于set就可以,因为孙子已经包含了爷爷、爸爸辈的内容了。而返回就不行,因为你外面返回如果用t接,你不知道是孙子辈还是老一辈。如果返回的是老一辈你外面调用用的是孙子辈打游戏就崩了。


协程相关知识



  • 协程的基本概念:协程是一个轻量级线程。可以用同步的方式编写异步代码,避免了异步代码传参时所引发的回调地狱。核心概念是挂起跟恢复。即协程可以在执行过程中主动挂起,等待某些事件发生后再恢复执行。挂起可以开发者控制比如调用await或者直接用suspend修饰。恢复是编译器的活。我们只管用就好了。
  • 其他的概念其实跟线程差不多



      • 和协程构建器:launchasync创建一个协程




      • 调度器是切换线程的:Dispatchers.IO、Dispatchers.Main




      • 协程作用域:通常由 coroutineScope 或 supervisorScope 函数创建,协程作用域可以用于确保协程在退出时所有资源都被正确释放。




      • 异常处理和取消:异常处理可以使用try-cache也可以使用CoroutineExceptionHandler指定一个协程异常处理的函数。




  • Flow


使用协程肯定会使用的一个机制,可以代替rxJava做一些简单的操作。



使用例子


import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
val flow = flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}

flow
.buffer() // 缓冲区大小
.onEach {
println("Emitting $it")
delay(200)
}
.collectLatest {
println("Collecting $it")
delay(300)
}
}




    • 原理



Flow 是一种基于懒加载的异步数据流,它可以异步产生多个元素,同时也可以异步消费这些元素。Flow 的每个元素都是通过 emit 函数产生的,而这些元素会被包装成一个包含了多个元素的数据流。Flow 还支持各种各样的操作符,如 map、filter、reduce 等等,可以方便地对数据流进行处理和转换。


jetpack使用过哪些库



下面的不一定都用过,说几个自己用过的就好,但是既然用了就要对原理很熟悉,不然别人一问就倒




  1. ViewModel:用于在屏幕旋转或其他配置更改时管理UI数据的生命周期。
  2. LiveData:用于将数据从ViewModel传递到UI组件的观察者模式库。
  3. Room:用于在SQLite数据库上进行类型安全的ORM操作的库。
  4. Navigation:用于管理应用程序导航的库。
  5. WorkManager:用于管理后台任务和作业的库。
  6. Paging:用于处理分页数据的库。
  7. Data Binding:用于将布局文件中的视图绑定到应用程序数据源的库。
  8. Hilt:用于实现依赖注入的库。
  9. Security:提供加密和数据存储的安全功能。
  10. Benchmark:用于测试应用程序性能的库。

LiveData和LifeCycle的原理


LiveData


使用上非常简单,就是上下游的通知,就是一个简化版的rxjava




  1. LiveData持有一个观察者列表,可以添加和删除观察者。
  2. 当LiveData数据发生变化时,会通知观察者列表中的所有观察者。
  3. LiveData可以感知Activity和Fragment的生命周期,当它们处于激活状态时才会通知观察者,避免了内存泄漏和空指针异常。
  4. LiveData还支持线程切换,可以在后台线程更新数据,然后在主线程中通知观察者更新UI。

LiveData提供了setValuepostValue两个方法来设置数据通知



  • setValue:方法只能在主线程调用,不依赖Handler机制来回调,
  • postValue:可以在任何线程调,同步到主线程依赖于Handler,需要等待主线程空闲时才会执行更新操作。

LifeCycle


用于监听生命周期,包含三个角色。LifecycleOwner、LifecycleObserver和Lifecycle




  • LifecycleObserver是Lifecycle的观察者。viewmodel默认就实现了这个接口
  • LifecycleOwner是具有生命周期的组件,如Activity、Fragment等,它持有一个Lifecycle对象
  • Lifecycle是LifecycleOwner的生命周期管理器,它定义了生命周期状态和转换关系,并负责通知LifecycleObserver状态变化的事件

了解这三个角色其实就很容易理解了,本质上LifeCycle也是一个观察者模式,管理数据的是LifeCycle,生命周期的状态都是通过它来完成的。而我们写代码的时候要写的一句是getLifecycle().addObserver(xxLifeCycleObserver());是添加一个观察者,这个观察者就能收到相应的通知了。


Viewmodel的原理



这个问题有可能会问你Viewmodel跟Activity哪个先销毁、Viewmodel跟Activity是怎么进行生命周期的绑定的。



Viewmodel的两个重要类:ViewModelProviderViewmodelStore。其实就是我们使用时用到的


// 这里this接收的其实是一个`ViewModelStoreOwner`是一个接口,我们的AppCompatActivity已经实现了
aViewModel = ViewModelProvider(this).get(AViewModel::class.java)


  • ViewModelStore 是一个存储 ViewModel 的容器,用于存储与某个特定的生命周期相关联的 ViewModel


是一个全局的容器,实际上就是一个HashMap。




  • ViewModelProvider用于管理ViewModel实例的创建和获取

其实这里设计的理念也比较好理解,比如旋转屏幕这个场景,我们会使用Viewmodel来保存数据,因为他数据不会被销毁,之所以不被销毁不用想也只是肯定是脱离Activity或者Fragment保存的。



知道了Viewmodel会全局保存这一点,应该会有一些疑问,就是这个Viewmodel是什么时候回收的。



在Activity或者Fragment销毁其实只是移除了他的引用,当内存不足时gc会回收或者手动调用clear方法回收。所以回答Activity和Viewmodel谁的生命周期比较长时就知道了,只要不是手动清除肯定是ViewModel的生命周期比Activity长。


因为ViewModel一直存在,所以如果太多需要做一些优化,原则很简单,就是把ViewModel细分,有些没必要保存的手动清除,有些需要全局的就使用单例。


WorkManager的使用场景



其实就是一个定时任务,人家问你使用场景是看你有没有真正用过。




  1. 需要在特定时间间隔内执行后台任务,例如每天的定时任务或周期性的数据同步。
  2. 执行大型操作,例如上传或下载文件,这些操作需要时间较长,需要在后台执行。
  3. 应用退出时需要保存数据,以便在下一次启动时可以使用。
  4. 执行重复性的任务,例如日志记录或数据清理。

Navigation使用过程中有哪些坑



这个问题首先要明确Navigation是干嘛的才知道有什么坑





  • Navigation翻译过来是导航,其实就是一个管理Fragment的栈类似与我们使用Activity一样,样提供的方法也是一样的比如动画、跳转模式,并且它还可以让我们不用担心Fragment是否被回收直接调用它的跳转,没有的话会帮我们做视图的恢复数据它已经内部处理好了,还支持一些跳转的动画传参等都有相应的api。简而言之,Navigation能做的FragmentManager都能做,只是相对麻烦而已。




  • Navigation优势就不多说了,合适的场景就是线性的跳转,比如A跳B跳C跳D这种,直接一行代码就可以跳转。返回到指定的页面也有方法,比如从D返回到navController.popBackStack(R.id.fragmentA, false)。这里的ture和false要注意,具体的细节就去看官网了。




  • 不太适合的场景就是相互的调用,比如A跳B跳A跳B这种反复的,需要你设置好跳转模式,如果模式不对会出现反复的创建和销毁,这里使用SingleTop跳转模式可以解决。但是要处理的可能是你是什么地方跳过来是,返回方法要处理一下。


作者:浪人笔记
链接:https://juejin.cn/post/7220344703588532261
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册