2026小知识点-简(6)

0 阅读12分钟

1、lateinit varby lazy 的区别?

  • lateinit var:用于可变的非空属性,延迟初始化,不能用于原生类型,必须确保使用前已初始化。
  • by lazy:用于不可变val属性,惰性初始化,线程安全,首次访问时执行初始化lambda。

2、Kotlin中如何实现真正的单例?

  • 直接使用 object 关键字声明即可。这是饿汉式单例,线程安全。

3、与Java互调时的空安全注意事项?

  • Java代码中的类型在Kotlin中称为平台类型 (String!),可空性未知。调用时需自己决定如何处理,可以加?安全调用,或用非空断言!!.

4、===== 的区别?**

  • == 调用 equals() 进行结构相等比较。
  • === 判断两个引用是否指向同一个对象(地址相等)。

5、Kotlin的扩展函数

扩展函数让我能够为已有类无缝添加新功能,保持代码的归属清晰

// 为View添加简化显示/隐藏的扩展
fun View.show() {
    this.visibility = View.VISIBLE
}
fun View.hide() {
    this.visibility = View.GONE
}
// 使用
imageView.show()
textView.hide()

// 为String添加一个判空非空的扩展
fun String?.isNotNullOrBlank(): Boolean {
    return !this.isNullOrBlank()
}
val input: String? = ...
if (input.isNotNullOrBlank()) { // 即使input可能为null,调用也安全
    // 处理非空字符串
}

6、面试高频问题与回答策略

  1. Q: 扩展函数是运行时多态吗?能重写父类方法吗?

    • A: 不是。扩展函数是静态解析的,在编译时根据变量的声明类型决定调用哪个,不是运行时多态。它不能重写父类的成员函数,如果与成员函数同名,成员函数永远优先
  2. Q: 高阶函数和Lambda在性能上有开销吗?

    • A: 有,但通常可忽略。每个Lambda表达式在JVM上默认会生成一个匿名类实例,有对象创建开销。但Kotlin使用 inline 内联函数 来优化:如果高阶函数被标记为 inline,编译器会将Lambda的代码直接拷贝到调用处,消除函数调用和对象创建的开销。标准库的 letapplyrunwith 都是内联的。
  3. Q: 带接收者的函数类型 (T.() -> R) 和普通函数类型 ((T) -> R) 有什么区别?

    • A: 这是面试的深度考点。T.() -> R 的函数体内部,this 指向接收者对象 T,可以直接访问其成员。而 (T) -> R 的函数体内,需要通过参数来访问对象。applyrun 的差异就源于此。

7、协程关键作用域类型与使用场景

作用域获取方式 / 特点生命周期典型使用场景
GlobalScopeGlobalScope.launch { ... }与应用进程同寿,不受组件生命周期控制。几乎不用。极易造成内存泄漏(协程无法自动取消),仅用于整个应用级别的顶级任务。
viewModelScopeViewModel 中直接使用。ViewModel 同寿,当 ViewModel 被清除(onCleared)时自动取消。ViewModel 中启动所有协程的标准方式,用于执行业务逻辑。
lifecycleScopeActivity/Fragment 中通过 lifecycleScope 属性使用。Lifecycle 同寿,当生命周期组件销毁时自动取消。Activity/Fragment 中启动与UI生命周期相关的协程(如监听 LiveData)。
MainScope()手动创建:val mainScope = MainScope()由开发者手动管理,需在组件销毁时调用 mainScope.cancel()在非 ViewModel 且需要主线程调度的自定义作用域场景,不如 lifecycleScope 常用。
协程构建器自带作用域coroutineScope { } / supervisorScope { }创建一个新的、独立的作用域,其生命周期受外部作用域管理,并会等待其内部所有子协程完成在协程内部创建并行子任务时使用。supervisorScope 内子协程失败不会相互影响。

8、为什么 GlobalScope 是危险的?

  • A:因为它创建的是非结构化的协程。它的生命周期与应用进程绑定,不会随着 Activity/ViewModel 的销毁而自动取消。如果在其内部捕获了外部引用,极易导致内存泄漏。在Android中应使用 viewModelScopelifecycleScope

9、coroutineScope { }supervisorScope { } 有什么区别?

  • A:这是结构化并发中错误传播策略的区别。
    • coroutineScope:一个子协程失败(抛出异常) ,会取消所有兄弟协程,并且异常会向上传播,导致整个作用域失败。
    • supervisorScope:一个子协程失败,不影响其他兄弟协程。其异常需要在该子协程内部处理,或通过 CoroutineExceptionHandler 捕获。

10、viewModelScope 内部使用了什么 Job

  • AviewModelScope 内部使用 SupervisorJob() 作为其根 Job。这正是因为 ViewModel 中的多个后台任务(如多个网络请求)应该是独立的,一个失败不应导致其他任务被取消。

11、何进行线程切换?

切换线程的核心是使用挂起函数,主要有两种方式:

方式一:withContext —— 临时切换
这是最常用、最安全的方式。它会挂起当前协程,在指定的调度器上执行代码块,然后自动切回原来的调度器。

viewModelScope.launch { // 默认在 Main 线程
    // 在主线程更新UI:开始加载
    showLoading(true)

    // 切换到 IO 线程执行网络请求(挂起点)
    val result = withContext(Dispatchers.IO) {
        repository.fetchDataFromNetwork() // 耗时操作
    }

    // 自动切回 Main 线程(挂起点恢复)
    showLoading(false)
    updateUI(result) // 安全更新UI
}

方式二:为 Flow 指定上游执行上下文 —— flowOn
Flow 是冷流,通过 flowOn 可以改变其上游(flowOn 之前)操作符的执行上下文

kotlin

fun fetchUserFlow(): Flow<User> = flow {
    // 这个发射块在 IO 线程执行
    emit(repository.fetchUser())
}
.map { user -> // 这个 map 操作也在 IO 线程执行
    user.toDomain()
}
.flowOn(Dispatchers.IO) // 👈 指定上游执行上下文
.onEach { user ->
    // 下游收集端(这里)默认回到调用 collect 的上下文(通常是 Main)
    updateUI(user)
}

12、Dispatchers.Main 在 Android 上是如何实现的?不依赖UI线程的库(如单元测试)能用吗?

  • A:它通过协程的 Main 调度器模块实现。在Android上,该模块依赖 androidx.lifecycle:lifecycle-runtime-ktx 等库,它们会通过 ServiceLoader 在运行时注册一个指向主线程 HandlerMain 调度器实现。在单元测试中,可以通过 Dispatchers.setMain( testDispatcher ) 来替换成一个测试调度器,确保测试不依赖真实Android环境。

13、launch(Dispatchers.IO) { ... }withContext(Dispatchers.IO) { ... } 在切换线程上有什么本质区别?

  • Alaunch 启动一个新的、并发的子协程,它和父协程是并行关系。withContext 不创建新的并发协程,它只是挂起当前协程,在其内部顺序执行代码块。withContext 更轻量,是“临时切换”任务的更佳选择。

14、为什么说协程的线程切换开销比线程小?

  • A:协程的线程切换是协作式的,发生在用户态的挂起点。它不涉及操作系统内核的线程上下文切换(需要保存/恢复大量寄存器、内存页表等,开销大)。协程只是简单地恢复一段在另一个线程上准备好的代码,开销极低。

15、Lottie 动画和SVG的区别

维度LottieSVG (Android中常指 VectorDrawable)
本质一套基于JSON的矢量动画数据格式和播放器一种基于XML的静态矢量图形描述语言。Android通过 VectorDrawable 支持其子集。
设计目标完美还原设计师在After Effects中制作的复杂矢量动画,并能在代码中控制。提供可无限缩放而不失真的静态图形
文件来源Adobe After Effects 通过 Bodymovin 插件导出为 .json 文件。Adobe Illustrator, Sketch, Figma 等设计工具导出为 .svg 文件,再转换为Android的XML格式。
动画支持核心优势。支持AE中绝大部分动画特性:形状变换、蒙版、路径动画、修剪路径、缓动曲线等。极其有限。Android的 VectorDrawable 仅支持简单的路径动画(如修剪路径、路径变形)和属性动画(如填充色变化),需在XML中定义 animated-vector
性能特点动画运行时需实时计算和绘制每一帧的矢量路径,是CPU密集型操作,对复杂动画可能造成帧率下降。初次加载时需将矢量路径光栅化为位图(或缓存为 Bitmap),之后绘制开销与普通 Drawable 相当。缩放时重新光栅化,可能带来额外开销。
内存占用较高。需要解析和缓存整个动画的JSON结构以及关键帧数据。较低。存储的是路径数据,内存占用主要取决于路径复杂度。
代码控制强大。可动态控制播放(进度、速度、循环)、监听状态、动态替换颜色或部分图层。较弱。主要通过 ObjectAnimator 控制已定义的属性动画,难以动态修改路径。
兼容性需要引入Lottie库(com.airbnb.android:lottie),但兼容到API 16+。VectorDrawableAnimatedVectorDrawableAPI 21 (Android 5.0) 开始原生支持,通过 AppCompat 可兼容到API 7。

16、*Lottie 的工作原理与优化

面试高频点:Lottie是如何工作的?性能瓶颈在哪?

  • 流程.json 文件解析 -> 映射为Lottie的模型层 (LottieComposition) -> 在 Canvas 上逐帧绘制(使用 Path, Paint 等标准API)。

  • 性能瓶颈

    1. 路径复杂度:包含大量节点(Many Polys)的动画,每一帧的路径计算都会消耗大量CPU。
    2. 遮罩和图层叠加:复杂的图层混合会增加绘制开销。
    3. 实时播放:不同于视频,Lottie需要实时计算每一帧。
  • 优化策略

    • 使用 LottieDrawable 并设置缓存策略setCacheStrategy(CacheStrategy.Strong).Weak
    • 开启硬件加速:确保 ViewlayerTypeLAYER_TYPE_HARDWARE
    • 简化动画:与设计师沟通,减少不必要的复杂路径和图层。
    • 预加载和复用:对可能重复使用的 LottieComposition 进行预加载和缓存。
    • 降级方案:对于极复杂的动画,考虑在低端机上使用静帧图或简化版本。

17、SVG (VectorDrawable) 的局限与使用

面试高频点:Android中的 VectorDrawable 和完整SVG有什么区别?

  • 支持子集VectorDrawable 仅支持完整SVG规范的一个子集(主要是路径数据 pathData)。许多高级特性(如滤镜、文字、渐变、<use>标签)不被支持或支持有限。

18、Lottie vs SVG?

这是面试官最想听到的技术选型分析能力。回答时务必结合场景。

场景首选方案核心理由
一个庆祝用的复杂弹窗动画(有图形变形、颜色渐变、序列运动)LottieSVG的动画能力无法实现如此复杂的效果,Lottie可以完美还原设计。
应用的设置图标、返回箭头等静态图标SVG (VectorDrawable)静态图形,无需引入额外库,一套资源适配所有密度,体积小。
一个可暂停、可拖拽进度条的动画教程LottieLottie提供了精准的进度控制和状态监听,可与用户交互深度结合。
一个简单的进度指示器(旋转的圆圈)SVG (AnimatedVectorDrawable)Lottie简单路径动画,两者皆可。若追求极简和无库依赖,可选SVG;若未来可能变复杂,可选Lottie。
需要动态换肤的图标(如主题色变化)Lottie (或 SVG + 代码着色)Lottie可通过 addValueCallback 动态修改图层颜色。SVG也可通过 tint 着色,但控制粒度较粗。

“Lottie和SVG虽然都基于矢量,但定位不同。Lottie的核心是‘动画’,它是一个强大的动画播放器,适合还原复杂的、有交互需求的AE动画,但其性能开销与动画复杂度正相关。SVG(在Android中主要是VectorDrawable)的核心是‘图形’,它是静态矢量图的标准,用于替代PNG图标,拥有极佳的缩放性和小体积,但其原生动画能力非常有限。

在技术选型上,我会遵循一个原则:如果动画复杂、需要交互或精准控制,必选Lottie,但要注意性能优化;如果只是静态图标或极简动画,首选原生的VectorDrawable,以减少依赖和包体积。 在实践中,我通常会在UI规范中与设计师明确:复杂动效用Lottie(提供AE模板),静态图标用SVG(提供导出规范),并建立相应的资源管理流程。”

19、数据库ObjectBox、SQLite和Realm的区别?

对比维度ObjectBoxSQLite (Room)Realm
数据库类型NoSQL 对象数据库关系型数据库NoSQL 对象数据库
核心特点极致性能、零拷贝、低内存占用官方支持、SQL表达能力强大跨平台、易用性高
数据模型直接操作 Kotlin/Java 对象需将对象与SQL表映射 (@Entity, DAO)直接操作对象
性能表现非常高 (基于内存映射)良好
易用性高,API简洁中等,需理解SQL
冷启动延迟 (无需初始化)较高
事务模型显式与隐式事务显式事务显式事务
跨平台支持 (Android, iOS, Linux等)主要Android支持优秀
  • 理解局限性:ObjectBox不适合复杂关联查询(如多表JOIN),此类场景仍是SQLite/Room的强项。

20、核心概念与基础

  1. 定义与初始化

    • BoxStore 是ObjectBox的核心,代表整个数据库,通常在自定义的Application类中初始化并全局持有
    • Box 类似于表,负责对特定实体类的对象进行增删改查操作,通过boxStore.boxFor(Entity::class.java)获取
  2. 实体定义

    • 使用注解@Entity 标记实体类,@Id 标记主键,@Index 创建索引。支持 @Unique 来保证属性唯一性
    • Kotlin支持:在Kotlin中使用时,需注意内联值类等特性可能需要特殊处理。例如,简单的单属性值类移除@Convert注解并使用@get:JvmName可能更有效

21、核心操作与查询

  1. CRUD操作

    • put() 用于插入或更新对象,支持单对象和集合。使用@Unique约束时,重复值会抛出异常
    • 查询使用QueryBuilder,支持链式调用,如.equal().contains()等。
  2. 分页查询

    • 通过 Query 对象的 offset()limit() 方法实现,这是面试常考点。例如,query.offset(20).limit(10) 获取第三页数据(每页10条)。

22、性能优势原理

  • 内存映射:数据文件直接映射到内存,省去序列化/反序列化开销,实现“零拷贝”。
  • 本地对象:直接操作对象,无中间层转换。

23、