面试或许会问到的问题

  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
      • 协程效果域:通常由coroutineScopesupervisorScope函数创立,协程效果域能够用于保证协程在退出时一切资源都被正确释放。
      • 反常处理和撤销:反常处理能够运用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跳转形式能够解决。可是要处理的或许是你是什么当地跳过来是,回来办法要处理一下。