前言
日子过的好快,12月又过了,也就代表2020也要结束了。不管你在这一年中是开心,是难过,是苦闷,还是平淡,都过去了,向前看,老铁们~
新的一年祝大家技术越来越棒,钱越来越多,当然身体健康是最重要的,也希望疫情早点结束 !
照例,12月刊的《面试题思考与解答》奉上。
kotlin为什么被设计出来
kotlin
被设计出来并被Google
推广,主要有以下优势:
- 完全兼容Java
- 更少的空指针异常
- 更少的代码量,更快的开发速度
kotlin工作原理
首先,我们了解下Java
的工作原理:
Java 代码是经过编译才能运行的。首先会编译成class
文件,然后通过java
虚拟机运行,在Android中也就是ART。
所以,任何语言只要能被编译成符合规格的class
文件,就能被java
虚拟机运行,也就能运行在我们的Android手机上,kotlin
亦是如此。
- 另外
Android studio
也提供了一个功能,可以查看kotlin对应的字节码:
Tools -> Kotlin -> Show Kotlin Bytecode
再点击Decomplie
还可以反编译成Java文件。
kotlin的空安全
- java中,我们可以任意初始化一个变量,而不需要赋值,比如
String
,就有它的默认值null。
String a;
如果要调用对象的参数,必须判空:
if (a!=null){ Log.d("lz","length="+a.length()); }else{ Log.d("lz","length=null"); }
- kotlin中,为了保证减少空指针的问题,不允许直接设置为空,可以通过
?=
的方式设置可以为空。
val a: String ? = null
1)赋值的时候,可以直接使用?
来表示这个对象可能为空,如果为空则表达式结果也为空,而不用进行非空判断。
//如果 b 非空,就返回 b.length,否则返回 null val length = b?.length //如果 b 非空,就返回 String类型的b,否则返回 null val str = b as? String
也就是通过问号来表示对象为空则整个表达式结果为空,而不会报错空指针。
2)如果需要设定为空的时候返回的表达式值不为空,可以用操作符?:
来表示,也叫Elvis操作符。
//b为空则表达式返回-1 val length = b?.length ?: -1
3)如果要将值转换为非空类型,就可以使用 !!
来标识非空,但是这种操作符就有可能会抛出空指针异常,如果实际对象为空的话。所以这种操作符相当于去除了空判断。
//如果b为空,空指针异常 val length = b!!.length
val和var
val,全称value
,声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值了,所以相当于java中的final变量。
var,全称variable(可变的)
,所以是用来声明一个可变的变量,可以重复赋值。
kotlin
中这么设计的原因主要是把不可变变量 这种因素和可变变量拿到同一级来设计,也就是说我们以后编码设计变量的时候,必须要考虑这个变量是不可变还是可变的,养成良好习惯,不是以前在java中需要添加final这种稍微繁琐的举动。
扩展函数(Extension Function)
扩展函数,其实就是扩展类的函数,可以在已有的类中添加新的方法,比继承更加简洁优雅方便。
- 扩展函数比如:
fun Activity.showToast( msgId:Int){ Toast.makeText(this,msgId,Toast.LENGTH_SHORT).show() }
这样任何的Activity里面就可以直接调用showToast
方法来展示Toast了。
- 同样,可以设置扩展属性,比如:
var <T> MutableList<T>.lastData: T //获取List中最后一个对象 get() = this[this.size - 1] //设置List中最后一个对象的值 set(value) { this[this.size - 1] = value }
用法:
var strs = mutableListOf<String>() strs.lastData="heihei" Log.e(TAG,"lastdata= ${strs.lastData}")
这里还涉及到两个知识点:
- kotlin中,在使用对象的
get和set
方法,可以直接省略,直接使用属性名即可,会根据表达式的实际功能来添加对应的set或者get
方法。 - kotlin中,对于
$符号
表示串模板
,就是可计算的代码片段,可以将其计算结果链接到字符串中。
扩展属性原理
kotlin这个扩展功能确实设计的很巧妙,那就一起来研究下它的原理:
按照上面的方法,也就是Tools -> Kotlin -> Show Kotlin Bytecode -> Decomplie
, 我们得到showToast扩展函数和使用代码所对应的java代码:
//扩展函数 public final class UtilsKt { public static final void showToast(@NotNull Activity $this$showToast, int msgId) { Intrinsics.checkParameterIsNotNull($this$showToast, "$this$showToast"); Toast.makeText((Context)$this$showToast, msgId, 0).show(); } } //使用 UtilsKt.showToast(this, 1900026);
可以看到所谓的扩展函数不过就是自动生成一个带有当前对象的函数,扩展函数的所在类被public final
修饰,函数被public static final
修饰,然后扩展的那个类被作为方法的一个参数传进去,这样就跟我们用java的时候写的工具类很像。
然后使用的时候就跟我们使用工具类一样调用工具类的方法即可。
可以定义同名的扩展方法吗
在同一个包名下,是不可以定义相同类相同方法名的扩展方法的。但是,在不同包名下,是可以定义的。
比如我在不同的包名下定义了相同的扩展方法:
//Utils2.kt package com.example.studynote.kotlin fun Activity.showToast(msg:String){ Toast.makeText(this,msg,Toast.LENGTH_LONG).show() } //Utils.kt package com.example.studynote fun Activity.showToast(msg:String){ Toast.makeText(this,msg,Toast.LENGTH_SHORT).show() }
具体会用哪个呢?就要看你导入的包是哪个了~
扩展方法可以覆盖掉某个类的已有方法吗
肯定是不能的,如果一个类的扩展方法和它已有方法同名,是可以编译过的。
但是调用的时候会优先调用
类中本来就有的方法,而不是扩展方法。
kotlin中有没有用到;的时候
kotlin中一般会把;
省略,但是有两种情况还是会用到:
- 枚举中,如果有方法的情况,必须用
;
来分割枚举常量列表和方法
enum class Color { RED, BLACK, BLUE, GREEN, WHITE; fun getTopColor():Color { return BLACK } }
- 两个表达式在一行的时候,当然这种有点累赘,为啥要写成一行呢是吧:
var test="nihao" ; var test2="heihei"
let、apply、with、run
- let 默认当前这个对象作为闭包的it参数,返回值为函数最后一行,或者return
fun getInt():Int{ "jimu".let { println(it.length) return 0 } }
- apply 在apply函数范围内,可以任意调用该对象的任意方法,并返回该对象
fun getInt(): Int { return ArrayList<String>().apply { add("jimu") }.size }
- with 返回值是最后一行,这点类似let。可以直接调用对象的方法,这点类似apply。
fun getInt(): Int { return with(ArrayList<String>()){ add("jimu") size } }
- run run和with很像,可以调用对象的任意函数,返回值是最后一行
fun getInt(): Int { return ArrayList<String>().run{ add("jimu") size } }
lateinit和by lazy
上篇说过,Kotlin
有空限制,所以有些变量如果不想设置为空的时候初始化该怎么做呢?这就用到延迟初始化了,lateinit和by lazy
都能实现。
- lateinit
lateinit
用于修饰var变量,它会让编译器暂时忽略初始化这个事情,到后面用的时候我们在进行初始化,但是不能用到基本数据类型,比如int,double
这种。
lateinit var test: String
- by lazy
by lazy
用于val
类型的变量,它会暂时不进行初始化,并且在第一次使用的时候自动调用我们设置好的表达式进行初始化。
val str by lazy { println("Init lazy") "Hello World" }
Kotlin中的构造函数
kotlin中构造函数分为主构造函数和次构造函数。
- 主构造函数
主构造函数没有函数体,直接定义在类名后。每个类都会默认带一个不带参数的构造函数,也可以直接定义参数,如果需要在构造函数中进行初始化工作,可以用init
关键字:
class Student { } class Student(var name: String) { init { Log.e(TAG,"name=$name") } } class Student constructor(var name: String) { init { Log.e(TAG,"name=$name") } }
- 次构造函数
除了类名后这种主构造函数,其他的构造函数方法就是通过constructor
关键字来定义次构造函数,一个类可以定义多个次构造函数。如果主构造函数和次构造函数同时存在的时候,次构造函数必须调用主构造函数。
class Student{ private val username: String constructor(username: String){ this.username = username } } class Student(username: String) { private var username: String private var age: Int init { this.username = username this.age = 10 } constructor(username: String, age: Int) : this(username) { this.age = age } }
协程
Kotlin协程
是对线程的一种封装,同样是用来解决并发任务(异步任务)的方案,可以理解为一种线程框架,特点是挂起时不需要阻塞线程,更好的解决了线程切换,魔鬼调用的问题。
简单举个例子,具体的说明大家可以翻翻以前的文章——协程三问。
GlobalScope.launch(Dispatchers.Main) { var name = ioTask() updateUI(name) var name1 = ioTask() updateUI(name1) var name2 = ioTask() updateUI(name2) } private suspend fun ioTask(): String { var name = "" withContext(Dispatchers.IO) { //耗时操作,比如网络接口访问 name = "jimu" } return name }
GlobalScope.launch
去开启一个协程Dispatchers.Main
表示运行在主线程suspend
关键字用于标记挂起函数的关键字withContext
函数用来构建一个协程作用域,可以标明作用线程,比如这里的Dispatchers.IO。这个函数必须在挂起函数或者协程中执行
说说插值器和估值器
- 插值器
一般指时间插值器TimeInterpolator
,是设置 属性值 从初始值过渡到结束值 的变化规律
,比如匀速,加速,减速等等。可以通过xml属性和java代码设置。
系统默认的插值器是AccelerateDecelerateInterpolator
,即先加速后减速。
//匀速插值器设置 android:interpolator="@android:anim/linear_interpolator" alphaAnimation.setInterpolator(new LinearInterpolator());
属性动画中,插值器的含义就是要设置时间和属性的变化关系,也就是根据动画的进度(0%-100%)通过逻辑计算 计算出当前属性值改变的百分比。比如匀速关系
就是动画进度和属性值改变的进度保持一致,50%时间进度就完成了属性值50%的变化。
//自定义匀速插值器 public class MyLinearInterpolator implements TimeInterpolator { @Override public float getInterpolation(float input) { return input; }
- 估值器
又叫类型估值算法TypeEvaluator
,用来设置 属性值 从初始值过渡到结束值 的变化具体数值
,刚才介绍的插值器是指变化规律
,而这个估值器是决定具体的变化数值
,是用来协助插值器完成动画设置。
比如属性动画设置:
ObjectAnimator anim = ObjectAnimator.ofObject(view, "scale", new IntEvaluator(),1,10); //系统估值器类型 IntEvaluator:针对整型属性 FloatEvaluator:针对浮点型属性 ArgbEvaluator:针对Color属性
可以看看IntEvaluator
源码,其实就是根据三个参数—估值小数(fraction),开始值(startValue)和 结束值(endValue)然后计算具体属性变化的值:
public class IntEvaluator implements TypeEvaluator<Integer> { public Integer evaluate(float fraction, Integer startValue, Integer endValue) { int startInt = startValue; return (int)(startInt + fraction * (endValue - startInt)); } }
所以要实现一个完整的属性动画,需要估值器和插值器进行协同工作:
- 首先由
TimeInterpolator
(插值器)根据时间流逝的百分比计算出当前属性值改变的百分比,并且 插值器 将这个百分比返回,这个时候 插值器 的工作就完成了。 - 比如
插值器
返回的值是0.5,很显然我们要的不是0.5,而是当前属性的值,即当前属性变成了什么值,这就需要估值器
根据当前属性改变的百分比来计算改变后的属性值,根据这个属性值,我们就可以设置当前属性的值了。
使用动画的注意事项
OOM问题
:这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现OOM,这个在实际开发中要尤其注意,尽量避免使用帧动画。内存泄露
:在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄露,通过验证后发现View动画并不存在此问题。兼容性问题
:动画在3.0以下的系统有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配工作。View动画的问题
:View动画是对View的影像做动画,并不是真正改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.GOEN)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决问题。不要使用px
:在进行动画的过程中,要尽量使用dp,使用px会导致在不用的设备上有不用的效果。动画元素的交互
:从3.0开始,将view移动(平移)后,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。在Android3.0以前的系统中,不管是View动画还是属性动画,新位置都无法触发单击事件同时,老位置仍然能触发单击事件(因为属性动画在Android3.0以前是没有的,是通过兼容包实现的,底层也是调用View动画)。硬件加速
:使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性。
关于硬件加速,直观上说就是依赖GPU实现图形绘制加速,同软硬件加速的区别主要是图形的绘制究竟是GPU来处理还是CPU,如果是GPU,就认为是硬件加速绘制,反之,软件绘制