一、前言
安卓开发快七年了,说到底还没有完全地用kotlin实战过一个项目。这段时间打算再次重新捡起Java、Kotlin、Android。也算是对这么多年安卓知识的一次总结吧。 太多零零碎碎的知识了,一直没有认真的总结过,打算用三个月的时间再好好捡起来这些。
二、第一个Kotlin项目
Kotlin的基础语法就不从头开始了,我们就从安卓切入,将kotlin化的安卓项目遇到的一些关键函数以及思想整理一下。
三、New Project
这次在选择语言的时候记得选择主语言Kotlin
Build Config language 选择 Kotlin DSL
点击Finsh后就得到我们的第一个Kotlin项目啦
四、var和val
var:可变变量.可以通过重新分配来更改为另一个值的变量,这种声明变量的方式和java中声明变量的方式一样
val:只读变量.这种声明变量的方式相当于java中的final变量。一个val创建的时候必须初始化,因为以后不能被改变。
kotlin
private lateinit var binding: ActivityMainBinding
五、lateinit 和 by lazy
在这里初始化安卓项目时使用了mvvm中的viewBinding,ActivityMainBinding使用了lateinit关键字,和by lazy放在一起对比一下。
5.1 lateinit(延迟初始化)
- 用于var变量:不可修饰基本类型(Int、Boolean),可修饰非空类型引用(如String、View等)
这里我们后面讨论一下为什么不可修饰基本类型
- 使用过程:在调用之前主动赋值,否则空对象调用会崩溃
- 适用场景:生命周期明确、需要在特定的生命周期回调中初始化的对象
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
//先占坑,稍后绑定(不能用val,必须是var)
lateinit var navView: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
println("赋值之前")
checkNavView()
navView = binding.navView
println("赋值之后")
checkNavView()
val navController = findNavController(R.id.nav_host_fragment_activity_main)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
fun checkNavView(){
if(::navView.isInitialized){
//检查是否初始化
println("navView 已绑定")
}else{
println("navView 还未绑定")
}
}
}
csharp
2025-04-01 09:46:57.256 17313-17313 System.out com.example.kotlinapplication I 赋值之前
2025-04-01 09:46:57.256 17313-17313 System.out com.example.kotlinapplication I navView 还未绑定
2025-04-01 09:46:57.256 17313-17313 System.out com.example.kotlinapplication I 赋值之后
2025-04-01 09:46:57.256 17313-17313 System.out com.example.kotlinapplication I navView 已绑定
Tips:注意事项
如果直接使用未初始化的对象,会抛出UninitializedPropertyAccessException
php
2025-04-01 09:49:56.993 17658-17658 AndroidRuntime com.example.kotlinapplication E FATAL EXCEPTION: main
Process: com.example.kotlinapplication, PID: 17658
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.kotlinapplication/com.example.kotlinapplication.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property navView has not been initialized
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3338)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3487)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2071)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7561)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property navView has not been initialized
at com.example.kotlinapplication.MainActivity.getNavView(MainActivity.kt:17)
at com.example.kotlinapplication.MainActivity.onCreate(MainActivity.kt:41)
at android.app.Activity.performCreate(Activity.java:7893)
at android.app.Activity.performCreate(Activity.java:7880)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1306)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3313)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3487)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2071)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7561)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)
5.1 by lazy(惰性初始化)
- 用于val变量:初始化后不可变,支持任何类型(包括基本类型)
- 自动初始化:第一次访问时执行初始化代码,之后直接返回缓存值
- 线程安全:默认多线程安全(可配置为高性能非安全模式)
- 适用场景:高成本操作(如读取文件)、单例对象、按需加载的配置
kotlin
class AppConfig {
val config by lazy{
println("读取配置文件...")
loadConfigFromFile()
}
private fun loadConfigFromFile():Map<String,String>{
//耗时处理逻辑
return mapOf("theme" to "dark","language" to "zh")
}
}
scss
//使用 by lazy
val appConfig = AppConfig()
println("第一次触发读取,输出:读取配置文件..." + appConfig.config)
println("直接返回缓存,不再读取" + appConfig.config)
ini
2025-04-01 10:02:33.165 19527-19527 System.out com.example.kotlinapplication I 读取配置文件...
2025-04-01 10:02:33.165 19527-19527 System.out com.example.kotlinapplication I 第一次触发读取,输出:读取配置文件...{theme=dark, language=zh}
2025-04-01 10:02:33.165 19527-19527 System.out com.example.kotlinapplication I 直接返回缓存,不再读取{theme=dark, language=zh}
线程模式
csharp
//高性能模式(非线程安全,适用于单线程)
val fastLazyValue by lazy(LazyThreadSafetyMode.NONE){
//初始化代码(确保只在单线程调用)
}
懒委托提供了几种懒加载模式供选择
1、LazyThreadSafetyMode.SYNCHRONIZED
同步模式,确保只有单个线程可以初始化实例,这种模式下初始化是线程安全的,当by lazy没有指定模式的时候,默认使用这种模式
2、LazyThreadSafetyMode.PUBLICATION:
并发模式,在多线程下允许并发初始化,但是只有第一个返回的值作为实例,这种模式下是线程安全的。和LazyThreadSafetyMode.SYNCHRONIZED最大区别是,这种模式在多线程并发访问下初始化效率是最高的,本质上面是用空间换时间,哪个的线程执行快就让哪个先返回结果,其他线程执行的结果抛弃掉。
3、LazyThreadSafetyMode.NONE
普通模式,这种模式不会使用锁来限制多线程访问,所以是线程不安全的,所以请勿在多线程并发的情况下使用。
lateinit vs by lazy
再回到上面的问题,为什么lateinit不能修饰基本类型(如Int、Boolean等) lateinit的设计初衷是允许延迟初始化一个非空引用类型(如String、MyClass等).但它的底层实现实际上是通过一个可空引用来跟踪初始化状态的。(比如 private var_value:String?=null)来跟踪初始化状态的。当首次访问lateinit变量时,Kotlin会检查该引用是否为null.
而基本类型在JVM上是非空且不可为null的(例如int、boolean).如果允许lateinit修饰基本类型,就无法用null表示"未初始化"的状态, 导致无法实现延迟初始化的机制.
六、对比
标题 | lateinit | by lazy |
---|---|---|
变量类型 | var(可变) | val(不可变) |
初始化方式 | 手动赋值 | 首次访问时自动初始化 |
空值处理 | 必须非空 | 可返回任意类型(包括可空类型) |
适用类型 | 非空对象(非Int等基本类型) | 所有类型(包括基本类型) |
线程安全 | 需自行保证 | 默认安全(可配置) |
崩溃风险 | 未初始化直接使用会崩溃 | 初始化代码异常才会崩溃 |
典型场景 | Android控件绑定、依赖注入 | 单例、配置加载、高成本资源初始化 |
总结
-
lateinit 适用于非空类型的 var 属性,需要自行确保在属性访问前被初始化,不保证线程安全,不能用于基本数据类型,没有默认值
-
by lazy 适用于任何类型(可空和非空都行)的属性(var 和 val),在首次访问时才完成延迟初始化(懒加载),默认线程安全,适用于延迟初始化开销较大的对象