第一行代码读书笔记 Kotlin Android

郭神的Android第一行代码更新了,听到消息的我立马就下单了,拿到书就开始如饥似渴地翻阅着.写的特别棒,学到了很多东西,下面是一些我不太熟悉的东西.

开源项目如下:

1. 快速入门Kotlin语言

  • 优先使用val
  • object实际上修饰类之后,是一个单例.并不是静态方法.
  • 函数式api: any:是否存在一个满足条件,all: 是否全部都满足
  • 空安全辅助: ?. ?: ?.let , 其中let的这种方式,对于全局变量空安全也是适用的,而if判空则不行.
  • lateinit 延迟初始化,有时候一些全局变量在使用的时候肯定是不为空的(使用之前就肯定能得到初始化),但是,每次在使用的时候依然需要进行判空处理,麻烦.直接用lateinit就行.

2. 作用域函数

kotlin内置,对数据进行操作转换等

class User(var name: String, private val age: Int) {
    fun agePrint() {
        println(age)
    }
}

fun main() {
    val user = User("张三", 22)

    //let和run都会返回闭包的执行结果,区别在于let有闭包参数,而run没有
    val letResult = user.let { "let 输出点东西 ${it.name}" }
    println(letResult)
    //run 可以使用this访问 user的公有变量
    val runResult = user.run { "run 输出点东西 ${this.name}" }
    println(runResult)

    //also和apply都不返回闭包的执行结果,返回的是当前执行的对象,这里返回的是user
    //also有闭包参数,而apply没有闭包参数
    user.also { it.name }.apply { this.name }.also { it.name }.also { it.name }

    //takeIf的闭包返回一个判断结果,为false时,takeIf会返回空
    //takeUnless与takeIf刚好相反,闭包的判断结果,为true时函数会返回空
    user.takeIf { it.name.isNotEmpty() }?.also { println("姓名为${it.name}") } ?: println("姓名为空")
    user.takeUnless { it.name.isNotEmpty() }?.also { println("姓名为空") } ?: println("姓名为${user.name}")

    //重复执行当前包  it是当前的次数(0..4)
    repeat(5) {
        println("重复输出 ${user.name} $it")
    }

    //with 比较特殊,不是以扩展方法的形式存在的,而是一个顶级函数
    //eg: 可以在这里传入一个View,然后赋值一些初始化什么的
    with(user) {
        this.name = "lisi"
    }
    
}

  • let和run都会返回闭包的执行结果,区别在于let有闭包参数,而run没有
  • let主要用于配合?.判空
  • also和apply都不返回闭包的执行结果,返回的是当前执行的对象.
  • also有闭包参数,而apply没有闭包参数
  • takeIf的闭包返回一个判断结果,为false时,takeIf会返回空
  • takeUnless与takeIf刚好相反,闭包的判断结果,为true时函数会返回空
  • 重复执行当前包 it是当前的次数(0..4)
  • with 比较特殊,不是以扩展方法的形式存在的,而是一个顶级函数

3. Kotlin 定义静态方法

3.1 object

用object修饰的类,实际上是单例类,在Kotlin中调用时是类名加方法直接使用.

3.2 companion object

用companion object修饰的方法也能通过类名加.直接调用,但是这时通过伴生对象实现的.在原有类中生成一个伴生类,Kotlin会保证这个伴生类只有一个对象.

3.3 @JvmStatic注解

给单例类(object)和伴生对象的方法加@JvmStatic注解,这时编译器会将这些方法编译成真正的静态方法.

3.4 顶层方法

Kotlin会将所有的顶层方法全部编译成静态方法.

4. 密封类

密封类用来表示受限的类继承结构: 当一个值为有限集中的类型,而不能有任何其他类型时.

就比如说,应用内的广告源只有3种,那么我们就可以使用密封类来定义.然后在when判断的时候就非常方便,如果缺少了一个判断,则会编译报错.

sealed class Result

class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> "Error is ${result.error.message}"
}

5. 扩展函数

class TestExpand(private val name: String, val age: Int)

fun TestExpand.expandMethod() {
    //扩展函数不能访问原有类的私有属性
    println(age)
}
  • 不修改某个类源码的情况下,动态地添加新的函数.className.
  • 扩展函数不能访问原有类的私有属性
  • 底层实际上是用写了个静态函数来实现的,将类的实例传入这个静态函数,然后搞一些操作

6. 高阶函数

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数

6.1 高阶函数定义

前面是参数,后面是返回值类型(String,int) -> Unit.

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    return operation(num1, num2)
}

fun main() {
    val result1 = num1AndNum2(1, 2) { n1, n2 ->
        n1 + n2
    }
    val result2 = num1AndNum2(2, 5) { n1, n2 ->
        n1 - n2
    }
    println(result1)
    println(result2)
}

高阶函数的完整语法:

//给StringBuilder增加扩展方法build,需要传入一个高阶函数,这个高阶函数只作用于StringBuilder,所以需要写成StringBuilder.() -> Unit,这样可以在里面访问到StringBuilder的上下文
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    block()
    return this
}

val result = StringBuilder().build {
        append("Start\n")
        for (s in list) {
            append(s).append("\n")
        }
        append("end")
    }

6.2 内联函数

其实默认情况下,高阶函数会被Kotlin编译器转换成实现了Function接口的匿名类,这其实会造成额外的内存和性能开销.

为了解决这个问题,Kotlin提供了Kotlin提供了内联函数,将使用Lambda表达式带来的运行时开销完全消除.

直接在定义函数的地方前面加上inline关键词即可.Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方.所以可以完全消除Lambda表达式所带来的运行时开销.

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    return operation(num1, num2)
}

7. 泛型和委托

7.1 泛型

  1. 给方法加泛型
fun <T> method(param: T) : T {
    return param;
}
  1. 给类加泛型
class MyClass<T> {
    fun method(param: T) : T {
        return param;
    }
}

7.2 委托

委托是一种设计模式,它的基本理念是: 操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理.

好处: 如果一个类,让大部分的方法实现都是调用辅助对象中的方法,而少部分的方法是自己实现的,这种情况,就是委托模式的意义所在.

  1. 类委托

kotlin支持类委托给另一个辅助对象,语法层面就支持,拥有辅助对象的所有功能,还能自己去实现或构建独有功能. 直接通过关键字by完成,简化自己去编写模板代码

//MySet中有所有Set接口中的功能,和HashSet保持一致, 并且isEmpty是自己实现的
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {

    fun hello() = println("hello")

    //演示,,,,
    override fun isEmpty(): Boolean {
        return true
    }

}
  1. 属性委托

将一个属性的具体实现委托给另一个类去完成.

8. 泛型高级特性

8.1 泛型实化

Kotlin有内联函数,而内联函数是提替换到被调用的地方,所以不存在泛型擦除(泛型对于类型的约束只在编译期存在)问题.

泛型实化书写,首先得是内联函数,其次得加reified关键字,然后可以在函数内获取当前指定泛型的实际类型

inline fun <reified T> getGenericType() = T::class.java

val result1 = getGenericType<Int>()
val result2 = getGenericType<String>()

9. 协程

线程需要依靠操作系统的调度才能实现不同线程之间的切换.而协程可以在编程语言的层面实现不同协程之间的切换,从而大大提升并发编程的运行效率.

协程允许在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制,和操作系统无关.

  • Kotlin for Java的协程并不属于广义的协程,而是一个线程框架
  • 可以用看起来同步的代码写出实质上异步的操作
  • suspend并不是拿来切线程的,而是用来做标记和提醒的,调用的时候需要放协程里面才行.
  • 协程是怎么切线程的:看Kotlin编译成的class对应的Java代码发现,其实就是将代码分块,某一块代码执行在1个线程,另一块代码执行在另一个线程中.在编译的时候就搞了这样的操作.

9.1 创建协程作用域

GlobalScope.launch可以创建一个协程的作用域,传递给launch函数的代码块(Lambda表达式)就是在协程中运行的.

GlobalScope.launch {
    println("codes run is coroutine scope")
    //这里不是主线程   DefaultDispatcher-worker-1
    println(Thread.currentThread().name)
}

GlobalScope.launch创建的是顶层协程,当应用程序结束时也会跟着结束.

可以在协程中加入delay()函数,delay()函数可以让当前协程延迟指定时间后再运行.
delay是非阻塞式的挂起函数,它只会挂起当前协程.而Thread.sleep()会阻塞当前的线程,该线程的所有协程都会被阻塞.

GlobalScope.launch {
    println("codes run is coroutine scope")
    //这里不是主线程
    println(Thread.currentThread().name)

    delay(1000)
    println("延迟之后的输出")
}

runBlocking也能创建一个协程的作用域,它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程.runBlocking函数通常只应该在测试环境下使用,在正式环境中容易产生性能上的问题.

9.2 创建多个协程

fun main() {
    //创建多个协程
    runBlocking {
        launch {
            println("launch1 ${Thread.currentThread().name}")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2 ${Thread.currentThread().name}")
            delay(1000)
            println("launch2 finished")
        }
    }

}

这里的launch函数和刚才的GlobalScope.launch不一样,这个launch只能在协程的作用域下面调用,且会创建一个子协程.子协程的特点是如果外层作用域的协程结束了,那么该作用域下的所有子协程也会一同结束.

上面的输出如下:

launch1 main
launch2 main
launch1 finished
launch2 finished

日志是交叉打印的,很明显,这是并发执行的.但是线程却是相同的线程.这里由编程语言来决定如何在多个协程之间进行调度,让谁挂起,让谁运行.调度过程不需要操作系统参与,这使得协程并发效率出奇得高.

9.3 suspend 挂起

当需要将部分代码提取到一个单独的函数中,这个函数是没有协程作用域的.Kotlin提供一个suspend关键字,使用它可以将任意函数声明成挂起函数,挂起函数之间是可以互相调用的.

suspend只能声明挂起函数,而不能提供协程作用域,在里面调用launch(必须在协程作用域调用才行)是不得行的.要想有协程作用域,可以使用coroutineScope.

coroutineScope也是一个挂起函数,因此可以在其他任何挂起函数中调用.coroutineScope会继承外部的协程作用域并创建一个子作用域. 于是可以这样用:

suspend fun printDot() {
    coroutineScope {
        launch {
            println(".")
            delay(1000)
            println("延迟之后的输出")
        }
    }
}

coroutineScope有点类似runBlocking,保证其作用域内的所有代码和子线程全部执行完之前,会一直阻塞当前协程.
但是runBlocking会阻塞当前线程,影响较大.而coroutineScope只会阻塞当前协程,不会影响其他协程,也不会影响其他线程.

可创建新的协程作用域:

  • GlobalScope.launch 可在任何地方调用
  • runBlocking 可在任何地方调用
  • lanuch
  • coruotineScope

9.4 更多的作用域构建器

runBlocking会阻塞线程,只能在测试环境下使用.而GlobalScope.launch是顶层协程,比如在Activity中使用来请求网络,还没请求回来的时候,Activity已关闭,这时需要手动管理(去取消)这个顶层协程,比较麻烦. 调用下面的代码会取消顶层协程.

val job = GlobalScope.launch { }
job.cancel()

但是实际项目中,一般会用CoroutineScope

val job = Job()
//返回的是CoroutineScope对象  这里是调用的CoroutineScope方法
val scope = CoroutineScope(job)
scope.launch {

}

所有使用CoroutineScope对象的launch创建的协程统统会被job所管理(都是在它的作用域下面).大大降低协程维护成本.

9.5 创建协程,并获取其执行结果

使用async函数,就可以获取协程的执行结果.它会创建一个子协程,并返回Deferred对象,然后我们调用其await方法即可知道结果.下面是简单计算一下5+5

runBlocking {
    val result = async {
        delay(100)
        5 + 5
    }.await()
    println(result)
}

注意,调用await方法之后会阻塞当前协程,直到子协程拿到结果,才会执行后面的代码(对应上面是println语句).为了提高效率,可以先拿到返回Deferred对象,最后需要结果的时候才调用await方法

runBlocking {
    val start = System.currentTimeMillis()
    val deferred1 = async {
        delay(1000)
        5 + 5
    }
    val deferred2 = async {
        delay(1000)
        6 + 6
    }
    println("结果是 ${deferred1.await() + deferred2.await()}")
    val end = System.currentTimeMillis()
    println("花费时间: ${end - start}")
}

9.6 withContext

withContext大致是async函数的简化版,它是一个挂起函数,返回结果是withContext函数体内最后一行代码.相当于val result = async{5+5}.await()

runBlocking {
    val result = withContext(Dispatchers.Default) {
        5 + 5
    }
    println(result)
}

调用withContext函数后,函数体内的代码会被立即执行,同时需要指定一个线程参数.这个参数有如下几个值:

  • Dispatchers.Default 会开启子线程,并使用一种较低并发的线程策略.适合计算密集型任务.
  • Dispatchers.IO 会开启子线程,并使用一种较高并发的线程策略.网络请求比较合适
  • Dispatchers.Main 不会开启子线程,而是在Android主线程执行代码.

9.7 使用协程简化回调

suspendCoroutine 函数可以将当前协程立即挂起,然后在一个普通的线程执行lambda表达式中的代码.Lambda表达式的参数是一个Continuation参数,调用它的resume方法或resumeWithException可以让协程恢复执行.

来看一段代码:

suspend fun request(address: String): String {
    return suspendCoroutine { continuation ->
        HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
            override fun onFinish(response: String) {
                continuation.resume(response)
            }

            override fun onError(e: Exception) {
                continuation.resumeWithException(e)
            }
        })
    }
}

GlobalScope.launch {
    val response = request("https://www.baidu.com/")
    Log.d("xfhy", "网络请求结果 : $response")
}

将网络请求的代码用suspendCoroutine包装一下,免得每次去手动生成一个匿名类,然后在里面拿到结果的时候调用continuation的resume方法将结果返回,这样在外面即可拿到结果. 使用request方法请求网络,只需要写一句代码即可.上面为了实例,没有加try..catch.

10. Android系统架构

JK14sg.png

Linux内核

Android平台的基础是Linux内核.

硬件抽象层(HAL)

硬件抽象层提供标准界面,向更高级别的Java API框架显示设备硬件功能.

Android Runtime

对于运行Android 5.0(API 21) 或更高版本的设备,每个应用都在棋自己的进程中运行,并且有其自己的Android Runtime(ART)实例.

原生C/C++库

许多核心Android系统组件和服务(例如ART和HAL)构建自原生代码,需要以C和C++编写的原生库.

Java API 框架

可通过以Java语言编写的API使用Android OS 的整个功能集.

系统应用

Android随附一套用于电子邮件,短信,日历,互联网浏览和联系人等的核心应用.

11. Jetpack

Jetpack是一个开发者组件工具集,主要目的是帮助我们写出更加简洁的代码,简化开发过程.Jetpack中的组件,大部分不依赖任何Android系统版本,这些组件通常定义在AndroidX库中,并且拥有非常好的向下兼容性.

Jetpack官网地址

Jetpack全家福

11.1 ViewModel

ViewModel官网地址

ViewModel是用来专门存放于界面相关数据的,尽量将数据存放到ViewModel中,减少Activity的逻辑. ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续存在。下面是ViewModel的生命周期

viewmodel lifecycle

ViewModel 使用及原理解析

11.2 Lifecycles

让任何类都能轻松感知到Activity的生命周期.

Lifecycle 使用及原理解析 一文搞懂

11.3 LiveData

LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者. LiveData是Activity与ViewModel之间通信的桥梁,它内部靠的是Lifecycles组件来自我感知生命周期的变化,从而在Activity销毁的时候及时释放引用,避免产生内存泄露.

LiveData 使用及原理解析

11.4 Room

Android官网的ORM框架(对象关系映射).

11.4.1 Entity

定义封装实际数据的实体类,每个视图类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的.

11.4.2 Dao

数据访问对象,通常会在这里对数据库的各项操作进行封装.

11.4.3 Database

用于定义数据库中的关键信息,包括数据库的版本号,把汗哪些实体类以及提供D奥层的访问实例

11.5 WorkManager

官方推出WorkManager用于处理一些要求定时执行任务的地方,它可以根据操作系统版本自动选择使用AlarmManager实现还是JobScheduler.

WorkManager注册的周期性任务不能保证一定准时执行,这是为了系统减少电量消耗,可能会将出发时间临近的几个任务放在一起执行,这样可大幅度减少CPU被唤醒的次数,从而有效延长电池的使用时间.

基本用法:

  1. 定义一个后台任务,并实现具体的任务逻辑
  2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求
  3. 将该后台任务请求传入WokManager的enqueue()方法中,系统会在合适的时间运行.

12. 将开源库发布到jcenter

  1. 先注册一个bintray账号.网址是https://bintray.com
  2. 然后在我的首页,点击Add New Repository创建一个新的仓库.仓库类型选择Maven,开源许可随便选,可以是Apache-2.0
  3. 回到我们编写的库中,在项目的build.gradle中引入bintray-release
buildscript {
    dependencies {
        classpath 'com.novoda:bintray-release:0.9.1'
    }
}
  1. 然后在开源库Library的build.gradle中填写如下配置
apply plugin: 'com.novoda.bintray-release'
publish {
    userOrg = 'xfhy'//bintray.com用户名
    groupId = 'com.permissionx.xfhy'//jcenter上的路径
    artifactId = 'permissionx'//项目名称
    repoName = "permissionx"
    publishVersion = '1.0.0'//版本号
    desc = 'Make Android runtime permission request easy'//描述,不重要
    website = 'https://github.com/xfhy/PermissionX'//网站,不重要
}

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.novoda:bintray-release:0.9.1'
    }
}
  1. 填写完成之后开始上传,在命令行输入//上传命令: ./gradlew clean build bintrayUpload -PbintrayUser=xfhy -PbintrayKey=xxxxx -PdryRun=false 其中PbintrayKey是Bintray的API key.
  2. 命令执行完成之后,到Bintray中找到之前创建的仓库,点进去详情,点击Add to Jcenter,发送申请.
  3. 几小时就能审核通过,然后就可以使用这个开源库了.
  4. 使用方式implementation 'com.permissionx.xfhy:permissionx:1.0.0'
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342