0%

QUIC 概览

QUIC协议预览

  所以,QUIC 与 http3 啥关系呢? QUIC 用来替代 TCP、SSL/TLS 的传输协议,在传输层之上还有应用层,如http、ftp、imap 等,理论上这些协议都能运行在 QUIC 之上。运行在 QUIC 之上的 HTTP 协议被称为 https 。 QUIC的几个重要特性如下面介绍:

1、只要1个RTT建立连接

  Http2 基于TLS首次建立连接需要 3 个RTT,而 http3 首次建立连接只需要 1 RTT,首次连接后,后续连接只需要 0 RTT。如图:

QUIC协议预览

那么,http3 如何做到,且看连接过程:

  1. 首次连接时,客户端发送 Inchoate Client Hello 给服务端。
  2. 服务端生成 g、p、a,再根据 g、p、a 算出 A,然后将 g、p、A 放到 Server Config 中再发送 Rejection 消息给客户端。
  3. 客户端接收到 g、p、A 后,再自己生成 b ,根据 g、p、b 计算出 B,根据 A、p、b 算出初始秘钥 K ,B 和 K 计算好之后,客户端用 K 加密 http 数据,连同 B 一起发送给服务端
  4. 服务端收到 B 后,根据 a、p、B 生成与客户端相同的秘钥,再用这个秘钥解密收到的 http 数据。为了前向安全,服务端会更新自己的随机数 a 和 公钥,再生成新的密钥 S,然后把公钥、Http 返回的数据通过 Server Hello 发送给客户
  5. 客户端收到 Server Hello 后,生成与服务端一致的新秘钥 S ,后面的传输都用 S 加密。

2、连接迁移(QUIC连接不受四元组影响)

  QUIC 连接不受四元组(源IP、源端口、目的IP、目的端口)影响,道理很简单:QUIC 不以四元组作为标识,而是使用一个 64 位的随机数,即使 IP 或者端口变化,称之为 Connection ID ,只要 这个 Connection ID 没变化,那么连接依然可以维持连接

  而使用TCP的话,切换网络时至少会有一个因素发生变化,当连接发生变化时,如果还使用原来的TCP连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以,我们有时候发现切换到一个新网络时,即使新网络很好,但内容还是要加载很久。如果实现得好,当检测到网络变化时立刻建立新TCP连接,但这样建立连接还是需要好几百毫秒。

连接迁移

3、队头阻塞/多路复用

  TCP 是个面向连接的协议,即发送请求后需要收到 ACK 消息,如果每次请求都要收到上次请求的 ACK 消息后再请求,无疑效率很低。

  后来http1.1 做了改进,允许一个TCP连接同时发送多个请求,在这个背景下,谈谈http1.1的队头阻塞:一个TCP连接同时传输了 10 个请求,其中 1、2、3个请求已经被客户端接收,但是第 4 个请求丢失,那么后面第 5~10 个请求都被阻塞,需要等第 4 个请求处理完毕才能被处理,这样就浪费了带宽。因此,http一般又允许 6 个TCP 连接,这样可以更加充分利用 带宽,但是每个连接中队头阻塞的问题还是存在。

  http2 的多路复用解决了 http1.x 中的队头阻塞问题,它不用等上一个请求的所有数据包传输完毕之后才能下一个请求,http2每个请求被拆分多个 Fragme 通过一条 TCP 连接同时被传输,这样即使一个请求被阻塞,也不影响其他请求,如下所示:

连接迁移

由于http2 还是基于 TCP 的,虽然在上述粒度上已经解决队头阻塞,但TCP 处理数据时有严格的前后顺序,先发送的 Frame 要先被处理,这样,即使发送了 4 个请求,1,3,4 都已经到达,但是只能先处理了 1, 而 3 、 4 到了也不能被处理,这时候整条连接都被阻塞,如图所示:

连接迁移

不仅如此,http2基于TLS ,TLS协议也存在队头阻塞问题,因为TLS 也是基于 TCP ,它将一堆数据加密,加密完成后又拆分成多个 TCP 包传输,丢了任何一个都不能解密。

  那 QUIC 是如何解决的呢?有2点:

  • QUIC 的传输单元是 Packet,加密单元也是 Packet ,整个加密、传输、解密都是基于 Packet,这样就能避免 TLS 的队头阻塞
  • QUIC 基于 UDP,UDP 的数据包在接收端没有处理顺序,即使中间丢失了包,也不会阻塞整条连接。

拥塞控制

TCP 的拥塞控制就3个方法: 慢启动、拥塞避免、快速恢复 (博客中应该写错了)

QUIC 可以在应用层修改 拥塞控制策略;而TCP 要修改的话,只能在系统层面,

流量控制

TCP 会对每个TCP 连接进行流量控制,意思是让对方不要发送太快,TCP 主要通过 滑动窗口 来实现这个功能。

QUIC 只需要建立一条连接,这条连接同时传输多条 Stream ,好比一条路,两头分别有仓库,道路中间很多车辆运送物资。QUIC 流量控制有两个级别: 连接级别 和 Stream 级别。就好比纪要控制这条路的总流量,不要一下子很多车辆涌进来,货物来不及处理,也不能一辆车一下子运送很多东西,这样货物也来不及处理。那到底怎么实现呢?它的实现也与滑动窗口类似,接收方会发送 WINDOW_UPDATE frame 告诉发送方你可以再多发送些数据过来。

以上内容参考腾讯技术团队的博客

识别插件的类

方案:将所有的插件 dex 合并到 宿主中,具体步骤为:

  1. 根据宿主的 ClassLoader,获取宿主的 dexElements 字段

    具体步骤为: 首先反射出 BaseDexClassLoader 的 pathList 字段,它是 DexPathList 类型的;第二步,反射出 DexPathList 的 dexElements 字段,他是个数组

  2. 根据插件的 dexFile ,反射出一个 Element 类型对象,这就是插件 dex

  3. 把插件 dex 和 宿主 dexElements 合并成一个新的 dex 数组,替换宿主之前的 dexElements 字段

欺骗系统

启动时,其实是启动 PlaceHoldActivity ,将真正要启动的 TargetActivity 放在 Intent 的数据里面,主要步骤:

1、Hook 到 ActivityManagerNative (它里面的gDefault,这个字段是个单例),把 TargetActivity 替换为 PlaceHoldActivity

2、Hook 到 H(Handler) 类的 mCallback 字段,将 PlaceHoldActivity 换成真正的 TargetActivity。或者,如果不想使用这种 Hook,可以在创建Activity (execStartActivity)对象的时候再替换回来

资源使用

原理

  1. 通过反射,构建一个 AssetManager 对象 mAssetManager,之后,反射调用 mAssetManager 的 addAssetPath 方法,将宿主和插件的 dexPath 全部添加进去,这样,这个新的 mAssetManager 拥有宿主以及插件中的所有的资源了。
  2. 根据上述的 mAssetManager 构建处一个 Resources 对象 mResources
  3. 使用 newTheme 创建一个 Theme 对象 mTheme
  4. 在 Activity 中重写 getAssets()、getResources()、getTheme() 方法,分别返回上述的mAssetManager、mResources、mTheme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void loadResources(){
try{
AssetManager assetManager= AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
}catch (Exception e) {
e.printStackTrace();
}
Resources superResources = super.getResources();
mResources = new Resources(mAssetManager, superResources.getDisplayMetrics(), superResources.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}

@Override
public AssetManager getAssets() {
if (mAssetManager == null) {
return super.getAssets();
}
return mAssetManager;
}

@Override
public Resources getResources() {
if (mResources == null) {
return super.getResources();
}
return mResources;
}

@Override
public Resources.Theme getTheme() {
if (mTheme == null) {
return super.getTheme();
}
return mTheme;
}

注意一点,由于将所有资源集合在一块,所以资源id可能冲突,解决方案:修改AAPT命令,将宿主和插件之间的id前缀都区分开,比如宿主的资源id以 0x71 开头,插件1以 0x72,插件2以 0x73 等,避免冲突。

还有,如果插件需要使用宿主的资源,那么,可以在宿主中将id写死,具体方式为自定义一个public.xml ,在里面写死。然后插件中以 provided 的方式将宿主的aar引入(这样,打包的时候就不会将这个aar打进去),引用资源的时候,直接使用这个写死的id就行。

1
2
3
4
5
<!--public.xml-->
<?xml version="1.0" encoding="utf-8">
<resources>
<public type="string" name="welcom" id="0x71050024"/>
</resources>

启动模式(LaunchMode)

普通使用的都是Standard 模式,如果要解决其他 3 种 LaunchMode 问题,使用的是占位Activity的思想,即事先为这3种 LaunchMode 创建很多 PlaceHoldActivity。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
SingleInstancePlaceHoldActivity1
SingleInstancePlaceHoldActivity2
SingleInstancePlaceHoldActivity3


SingleTaskPlaceHoldActivity1
SingleTaskPlaceHoldActivity2
SingleTaskPlaceHoldActivity3


SingleTopPlaceHoldActivity1
SingleTopPlaceHoldActivity2
SingleTopPlaceHoldActivity3

接下来,从服务器下载一个 Json ,指定插件Activity 要使用哪个 PlaceHoldActivity ,这里所说的 Activity 只包括 LauncherMode 为 SingleTop、SingleTask、SingleInstance 的 Activity。我们无法指定插件 Activity 的 LaunchMode ,但是通过这种指定占位,当 ActivityA 和 SingleTaskPlaceHoldActivity1 建立对应关系之后,ActivityA 的 LaunchMode 就是 SingleTask 了!

但是这里还有个小bug,无论是SingleTop 还是 SingleTask ,再回到这个Activity时,并不会触发它的 onCreate ,而是触发 它的 onNewIntent 方法!为此,我们需要在Hook的 Handler.Callback 中拦截 onNewIntent 方法,把占位的 PlaceHoldActivity 替换回插件Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MockClass2 implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
switch(msg.what) {
case 112:
handleNewIntent(msg);
break;
}
}

private void handleNewIntent(Message msg) {
Object obj = msg.obj;
ArrayList intents = (ArrayList)RefInvoke.getFieldObject(obj, "intents");
for(Object object: intents) {
Intent raw = (Intent)object;
Intent target = raw.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
if(target != null) {
raw.setComponent(target.getComponent());
if(target.getExtras() != null) {
raw.putExtras(target.getExtras());
}
break;
}
}
}
}

多态的不同方式

当一个子类继承父类的时候,这就是子类型多态;另一种熟悉的多态是参数多态,泛型就是参数多态常见的形式。

对第三方类扩展

考虑业务类 ClassA、ClassB是第三方引入的并且不能修改,如果我们想要给它们扩展一些方法,比如将对象转换为 Json 字符串,那么利用以前多态技术就会显得麻烦。幸运的是,Kotlin 支持扩展语法:

1
2
3
fun ClassA.toJson(): String = {
...
}

就可以很方便地添加了toJson方法,需要注意的是,扩展属性和方法的实现运行在ClassA的实例,他们的定义操作不会修改 ClassA 类本身,所以被扩展的第三方类免于被污染。

特设多态与运算符重载

除了子类型多态、参数多态外,还有一种灵活的多态——特设多态。可能概念不好理解,举个具体的例子,你想定义一个通用的sum方法,也许会这么写:

1
fun <T> sum(x: T, y: T): T = x + y

但编译器会报错,因为某些类型不一定支持加法操作。这时候,我们希望定义一种通用的“加法语义上的操作”,可以定义一个通用的 Summable 接口,然后让需要支持假发操作的类来实现它:

1
2
3
4
5
6
7
interface Sumable<T> {
fun plusThat(that: T): T
}

data class Len(val v: Int): Sumable<Len> {
override fun plusThat(that: Len) = Len(this.v + that.v)
}

可以发现,这并没有什么问题。然而,如果要针对不可修改的第三方类扩展加法操作时,这种方式也会遇到问题。于是,又想到了Kotlin的扩展,针对以上例子,我们完全可以采用扩展的语法来解决问题,此外,Kotlin 原生支持运算符重载可以很好解决上述问题:

1
2
3
4
5
6
7
8
9
data class Area(val value: Double)

operator fun Area.plus(that: Area): Area {
return Area(this.value + that.value)
}

fun main(args: Array<String>) {
println(Area(1.0) + Area(2.0)) //运行结果: Area(value=3.0)
}

通过 operator 关键字以及Kotlin中内置可重载的运算符plus,就实现了功能。operator 的作用是,将一个函数标记为重载一个操作符或者实现一个约定,这里的plus是Kotlin规定的函数名。除了plus,我们还可以通过重载减法(minus)、乘法(times)、触发(div)、取余(mod,在kotlin 1.1 版本开始被 rem 替代)。此外,回忆kotlin中常用语法,也是用这种神奇的语言特性实现的,比如:

1
a in list// 转换为 list.contains(a)

扩展: 为别的类添加方法、属性

继续深入Kotlin 的特设多态语言特性

扩展与开放封闭原则

熟悉设计模式的读者知道,在修改现有代码时,我们应该遵循开放封闭原则,对扩展开放,对修改封闭。然而实际并不乐观,比如Android开发,为实现某个需求,引入了第三方库,但是需求发生变动后,当前库无法满足需求,且库的作者没有升级计划。这时候你也许就会考虑对源码修改,这就违背了开放封闭原则。

Java中一种惯常做法是继承第三方类,添加新功能,但是,强行的继承可能违背“里氏替换原则”。更合理的方案,就是通过Kotlin的扩展功能。

使用扩展函数、属性

扩展函数的声明的关键字是 。此外,我们需要一个“接收者类型”(通常是类或者接口)来作为它的前缀,以为 MutableList扩展exchange方法为例:

1
2
3
4
5
fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
val temp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = temp
}

MutableList 是Kotlin标准库中的类,这里作为接收者类型,exchange是扩展函数名。Kotlin的this要比Java的更灵活,这里扩展函数体内的this代表的是接收者类型的对象。注意,Kotlin中是严格区分接收者是否可空的,如果你的函数是可空的,你需要重写一个可空类型的扩展函数

扩展函数的实现机制

扩展函数这么方便,会不会对性能造成影响呢?以 MutableList.exchange 为例,对应的Java代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Metadata(
mv = {1, 5, 1},
k = 2,
d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "CommoKotlin"}
)

public final class KotClassKt {
public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
$this$exchange.set(fromIndex, $this$exchange.get(toIndex));
$this$exchange.set(toIndex, temp);
}
}

结合上述Java代码可以看出,我们可以将扩展函数近似理解为静态方法。我们知道静态方法的特点:不依赖类的特定实例,被该类所有的实例共享,并且,用public修饰,本质上也就是个全局方法,所以,扩展函数不会带来额外的性能消耗

扩展函数的作用域

一般来说,我们习惯将扩展函数直接定义在包内,例如之前的 exchange 例子,我们可以将其放在 com.example.extension包下:

1
2
3
4
5
package com.example.extension

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
...
}

我们知道,在同一个包内是可以直接调用exchange方法的,如果需要在其他包中调用,只需要import即可,这与Java全局静态方法类似。与此同时,在实际开发中,我们可能会将扩展函数定义在一个 Class 内部统一管理:

1
2
3
4
5
6
7
class Extends {
fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
val temp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = temp
}
}

但你会发现,之前的exchange方法无法调用了!我们看下它的Java源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Metadata(
mv = {1, 5, 1},
k = 1,
d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J \u0010\u0003\u001a\u00020\u0004*\b\u0012\u0004\u0012\u00020\u00060\u00052\u0006\u0010\u0007\u001a\u00020\u00062\u0006\u0010\b\u001a\u00020\u0006¨\u0006\t"},
d2 = {"Lcom/glassx/Extends;", "", "()V", "exchange", "", "", "", "fromIndex", "toIndex", "CommoKotlin"}
)
public final class Extends {
public final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
$this$exchange.set(fromIndex, $this$exchange.get(toIndex));
$this$exchange.set(toIndex, temp);
}
}

我们才发现exchange方法没有static关键字了,所以,当扩展方法在一个 Class 内部时,我们只能在该类和该类的子类中调用

扩展属性

与扩展函数一样,我们还能为一个类添加扩展属性,比如为MutableList添加一个判断和是否为偶数的属性:

1
2
3
4
5
6
val MutableList<Int>.sumIsEven: Boolean
get() = this.sum() % 2 == 0

//使用
val list = mutableListOf(2,2,4)
list.sumIsEven

但是,如果你准备给这个属性添加默认值,并且写出如下代码:

1
2
val MutableList<Int>.sumIsEven: Boolean = false
get() = this.sum() % 2 == 0

代码会编译不通过,告知扩展属性不能有初始化器。这是为什么呢?其实,与扩展函数一样,其本质也是对应Java中的静态方法,反编译成Java后,会看到一个 getSumIsEven 的静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Metadata(
mv = {1, 5, 1},
k = 2,
d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u000b\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\"\u001b\u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00028F¢\u0006\u0006\u001a\u0004\b\u0004\u0010\u0005¨\u0006\u0006"},
d2 = {"sumIsEven", "", "", "", "getSumIsEven", "(Ljava/util/List;)Z", "CommoKotlin"}
)
public final class ExtendsKt {
public static final boolean getSumIsEven(@NotNull List $this$sumIsEven) {
Intrinsics.checkNotNullParameter($this$sumIsEven, "$this$sumIsEven");
return CollectionsKt.sumOfInt((Iterable)$this$sumIsEven) % 2 == 0;
}
}

由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的,它们的行为只能由显式提供的 getters 和setters 定义

幕后字段:如果属性中存在访问器使用默认实现,那么Kotlin 会自动提供幕后字段 field ,其仅可用于 getter 和 setter 中。

扩展的特殊情况

类似Java的静态扩展函数

在Kotlin中,如果要声明一个静态的扩展函数,必须要有 伴生对象(companion object)上,所以我们要这样定义带有伴生对象的类:

1
2
3
4
5
class Son {
companion object {
val age = 10
}
}

在已有伴生对象的情况下,如果不想再Son中定义扩展函数,而是在Son的伴生对象上定义,可以这么写:

1
2
3
4
5
6
7
8
fun Son.Companion.foo() {
println("age = $age")
}

//这样在没有Son的实例对象的情况下,也能调用,使用:
fun main(args: Array<String>) {
Son.foo()
}

成员方法优先级总是高于扩展函数

如果扩展函数和现有类的成员方法一样,那么优先调用成员方法,这一点好理解,我们不应该更改原有实现。

类的实例与接收者实例

略,没看清楚表达什么

标准库中的扩展函数:run、let、also、takeIf

Android中扩展的应用

扩展不是万能的

null引用

null 做了哪些恶

  • null 存在歧义,一个值为null可以代表很多含义,比如:未初始化、值不合法、值不需要、值不存在

    一个典型的例子就是 HashMap 保存数据,Java中的HashMap允许 key 为null,比如一个教室,我们将座位号与坐在上面的人保存到HashMap中,在获取这些位置信息的时候,null就产生了歧义,到底是座位不存在还是座位没人。

  • 难以避免的 NPE

  • 冗余的防御使代码,各种判空

可空类型

Java8里面有Optional ,这个不作讨论。

Kotlin的可空类型

在Kotlin中,我们可以在任何类型后面加上 ? ,比如 “Int?”,实际等同于 “Int? = Int or null”

  1. 安全的调用 ?.

  2. Kotlin的Elvis操作符(也称为合并运算符) ?: ,比如,学生不戴眼镜,度数就为 -1,则可以类似如下表示:

val result = student.glasses?.degreeOfMyopia ?: -1

  1. 非空断言 !! ,有时候我们想确保一个学生是戴眼镜的,那么: val result = student!!.glasses

其中,关于第2点,Kotlin能够这样写,具体实现其实还是根据 if-else 去逐层判断,并没有什么魔法。这样判断的原因无非就是:兼容Java、性能考虑

类型检查

在经过is判断会后之后使用就无须强制转换,如下使用方法:

1
2
3
4
when(obj) {
is String -> print(obj.length)
else -> print("not a String")
}

智能类型转换

除了上述的智能转换,对于可空类型我们也可以使用 Smart Casts:

1
2
val stu: Student = Student(Glasses(189.00))
if(stu.glasses != null) println(stu.glasses.degreeOfMyopia)

不过,根据官方文档,当且仅当Kotlin编译器确定在类型检查后该变量不会再改变,才会产生Smart Casts。利用这点,我们能够确保多线程应用安全,举个例子:

1
2
3
4
5
6
7
8
9
10
class Kot {
var stu: Student? = getStu()
fun dealStu(){
if(stu != null) {
//还是不能这样写,编译器会因为可能在其他线程会修改该值有风险而报错,如果 stu 改成val 的就不会有这样的情况
print(stu.glasses)
}
}

}

当然,我们也能利用let来更优雅一点:

1
2
3
fun dealStu(){
stu?.let {print(it.glasses)}
}

在开发过程中,难免碰到类型转换,一般使用类似:

1
2
3
var stu: Student = getStu as Student?
//当然,也能使用以下方法,二者效果是一样的
var stu: Student = getStu as? Student

除此之外,有些同学可能会认为需要频繁类型转换,所以会配合泛型封装一个“有效的”类型转换方法:

1
fun <T> cast(original: Any): T? = original as? T

使用上了 as? ,看起来没问题,那我们应该可以这样用:

1
val ans = cast<String>(140163L)

用法看起来也挺合理,但是在调用的时候,会抛出 Long cannot be cast to String 这样的异常。这其实是类型擦除的后果。Kotlin的设计者们同样注意到这点,加入了 reified关键字,可以理解为具体化,利用它我们可以在方法体内访问泛型指定的 JVM 对象(注意,还要在方法前加入 inline 修饰)。代码使用如下:

1
inline fun <reified T> cast(original: Any): T? = original as? T

比Java 更面向对象的设计

Java并不能真正意义上被称作一门“纯面向对象”的语言,因为它的原始类型(如int)的值与函数并不能视作对象。

Any: 非空类型的根类型

与Object 作为 Java类层级的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类。与Java不同,Kotlin不区分原始类型和其他类型,Koltin中,所有类型的最终根类型都是Any。另外,Kotlin把Java方法参数和返回类型中用到的Object类型看做 Any ,当Kotlin函数中使用Any时,它会被编译成 Java字节码中的 Object

Any?:所有类型的根类型

如果说Any是所有非空类型的根类型,那么 Any? 才是所有类型(可空和非空类型)的根类型。如果只有Java这门编程语言的经验,很容易陷入一个误区:继承关系决定父子类型关系。因为在Java中,类与类型大部分情况下都是“等价的”。

事实上,“继承”和“子类型化”是两个完全不同的概念。如Kotlin中的Int是Number的子类,那么在需要Number类型的地方传入 Int类型是没问题的。这是“继承”强调的“实现上的复用”。而子类型化是一种类型语义的关系,与实现没关系。虽然Any 与 Any? 看起来没有继承关系,然而在我们需要用 Any? 类型值的地方,显然可以传入一个类型为 Any 的值,反之却不然!

所以,我们可以大胆地说,Any? 是 Any 的父类型,而且是所有类型的根类型。

Any? 与 Any??。 你可能会问,那 Any??是不是 Any? 的父类型?如果成立,岂不是意味着没有所谓的所有类型的根类型了?其实,Kotlin的可空类型可以看做数学上的并集。如果用类型的并集表示 Any ,可以写成 Any U Null ,那么 Any?? 就可以写成 Any U Null U Null ,等价于 Any U Null ,即 Any??等价于 Any? 。

Nothing 与 Nothing?

顾名思义,Nothing是没有实例的类型。Kotlin 中 return、throw等(流程控制中与跳转相关的表达式)返回值都是 Nothing。Nothing 只能包含一个值:null,本质上与null没有区别,所以我们可以使用null作为任何可空类型的值。

自动装箱与拆箱

Kotlin 中没有 int、float、double、long这类的原始类型,取而代之的是它们对应的引用类型包装类: Int、Float、Double、Long。看起来让Kotlin比Java更加接近纯面向对象设计,但这样说其实是不够严谨的。

以Int 为例,虽然它可以像Integer 一样提供额外的操作函数,但这两个类型在底层实现上存在差异,看一段代码:

1
2
3
val x1 = 18 //kotlin
int x2 = 18;//Java
Integer x3 = 18;//Java

但是我们观察 Kotlin 编译完成后的字节码可以发现,Kotlin中的Int在JVM中实际以int 存储(对应字节码类型为 I)。但是作为一个“包装类型”,Int编译后应该装箱才对,难道Kotlin不会自动装箱?其实可以再看看 Int? 的字节码,就可以得出结论:

  • Kotlin 中的 Int 类型等同于 int
  • Kotlin 中的 Int? 等同于 Integer !

Int 作为一种小技巧,让Int看起来是引用类型。

“新”的数组类型

Kotlin 中可以这样创造数组:

1
2
3
4
5
6
7
8
9
val funList = arrayOf()//声明长度为0的数组
val funList = arrayOf(n1,n2,n3,....nt)//声明并初始化长度t的数组

//由于Smart Casts ,编译器能够推出funList的元素类型。当然,我们也能手动指定:

val funList = arrayOf<T>(n1,n2,...nt)

//与array类似,我们可以这样定义原始类型的数组
val xArray = intArrayOf(1,2,3)

要注意的是,IntArray等类型并不是 Array的子类。还有,Kotlin对原始类型的特殊优化,主要体现在避免了自动装箱带来的开销!

泛型: 让类型更安全

首先,Kotlin中也有泛型。

泛型:类型安全的利刃

大家都知道1.5以前的Java的List需要靠强制转换来取值的,并且可以存入各种各样类型的值,在编译期还发现不了,这就很难受了。所以后来就出了泛型,泛型主要优势有几点:

  • 类型检查,在编译时就检查出错误。
  • 更加语义化,List便可以知道里面存储的是 String 类型。
    自动类型转换,获取数据时不需要手动强转,更安全。
  • 能写出更通用的代码。

Kotlin中使用泛型

首先,在Kotlin中,使用 val arrayListt = ArrayList() 这种方式是不允许的,但在Java中可以这么做,这是因为在Java中 1.5版本后才引入的,Java为了兼容可以这么做。但是,由于Kotlin具有类型推导能力,所以 val arrayListt = arrayListOf("one","two") 是允许的。

类型约束:设定类型上界

我们知道,泛型本身就有类型约束的作用,比如,你无法向一个String类型List中添加一个Double对象。那么这里说的是约束什么呢?用例子来看下,假设我们有一个盘子:

1
class Plate<T>(val t: T)

这个盘子类有一个泛型参数,表示可以接收各种东西,如水果或者主食。但是,如果有一天想把盘子归类,有些只能放水果,有些只能放菜,又该如何呢?还是看例子,我们可以定义一个水果类(Fruit),并声明 Apple 和 Banana 来继承它,并定义出水果盘子:

1
2
3
4
5
6
open class Fruit()
class Apple(): Fruit()
class Banana(): Fruit()

//定义一个水果盘子:
class FruitPlate<T: Fruit>()

上述的FruitPlate中T被限定了只能是 Fruit 类及其子类类型,其他类则不被允许。这种约束我们叫做上界约束,和Java的语法类似,只不过java 用extends关键字。假如我们要求水果盘子不一定装水果,有时候还能空着,那应该怎么办呢?我们可以在泛型参数类型后面加一个”?”即可:

1
class FruitPlate<T: Fruit?>(val t: T)

**如果,泛型有多个条件,怎么办?比如,有一把刀,只能用来切长在地上的水果,我们可以用 where 关键字这样实现:

1
2
3
4
5
6
7
interface Ground{}

class Watermelon(): Fruit(), Ground

fun <T> cut(t: T) where T: Fruit, T: Ground {
print("you can cut me")
}

这个where关键字就限定了多个条件,水果以及长在地上。

泛型的背后: 类型擦除

Java 为何无法声明一个泛型数组

我们看个简单的例子,Apple是Fruit的子类:

1
2
3
4
5
6
Apple[] appleArray = new Apple[10];
Fruit[] fruitArray = appleArray;//允许的
fruitArray[0] = new Banana();//编译通过,运行报错

List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = appleList;//不允许

为什么数组可以这么做,而List就不行呢?关键的一点,数组是协变的,而List是不变的,换句话说,Object[] 是所有对象数组的父类,而 List 却不是 List的父类!我们知道Java中的List会类型擦除,具体表现如下:

1
2
3
4
5
6
println(appleArray.getClass());
println(appleList.getClass());

//运行结果
class [Ljavat.Apple;
class java.util.ArrayList

Kotlin与Java这个机制是一样的,也会存在类型擦除。但与Java不同的是,Kotlin中的数组是支持泛型的,当然也不再协变

1
2
val appleArray = arrayOfNulls<Apple>(3);
val anyArray: Array<Any?> = appleArray //不允许

这又是为什么呢?

向后兼容的罪

Java的list使用类型擦除是为了兼容老版本。既然类型擦除了,为什么我们在使用泛型的时候,能够进行类型检查,类型自动转呢?这是因为类型检查这些操作在编译器编译前就检查了,所以类型擦除不影响它。然后,我们可以发现,List 的get方法其实也是通过强制转换类型来实现的!

类型擦除的矛盾

通常情况使用泛型我们不在意它的擦除,但是在序列化/反序列化的时候,我们就需要知道类型了,咋办?既然编译后会擦除泛型参数类型,那么我们是不是可以主动指定参数类型来达到运行时获取泛型参数类型效果呢?看下例子:

1
2
3
4
5
6
7
8
9
10
11
//接着上面的Plate的代码写
open class Plate<T> (val t: T, val clazz: Class<T>) {
fun getType(){
print(clazz)
}
}

//使用
val applePlate = Plate(Apple(), Apple::class.java)

applePlate.getType()//会打印 class Apple

上述方法可以解决很多问题了,但是它无法获取泛型的类型,比如:

1
val type = ArraList<String>::class.java//不被允许

有没有其他方式呢?有,可以利用匿名内部类:

1
2
3
4
5
6
7
8
9
val list1 = ArraList<String>()
val list2 = object: ArrayList<String>(){}//匿名内部类

println(list1.javaClass.genericSuperclass)
println(list2.javaClass.genericSuperclass)

//结果
java.util.AbstractList<E>
javaUtil.ArrayList<java.lang.String>

竟然可以了,原理是啥呢?其实,泛型类型擦除并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应 Class 的常量池的。所以,我们能通过相应的方式来获取这个类型信息,使用匿名内部类就可以实现这种需求。我们着手来设计一个能获取所有类型信息的泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open class GenericsToken<T> {
var type: Type = Any::class.java

init {
val superClass = this.javaClass.genericSuperclass
type = (superClass as ParameterizedType).getActualTypeArguments()[0]
}
}

fun main(args: Array<String>) {
val gt = object: GenericsToken<Map<String,String>>(){}
print(gt.type)
}

//结果
java.util.map<java.lang.String, ?extends java.lang.String>

匿名内部类在初始化的时候就会绑定父类或者接口的相应信息,这样就能通过获取父类或者父接口的泛型类信息来实现我们的需求,你可以用这样一个类来获取任何泛型的类型,我们常用的Gson也是使用了相同的设计。比如,我们在Kotlin中可以这样使用Gson来进行泛型类的反序列化:

1
2
3
val json = ...
val rType = object: TypeToken<List<String>>(){}.type
val stringList = Gson.fromJson<List<String>>(json,tType)

使用内联函数获取泛型

其实,Kotlin中除了上述方法外,还可以通过内联函数实现。内联函数在编译的时候,便会将相应函数的字节码插入调用的地方,也就是说参数类型也会被插入字节码中。下面我们就用内联函数实现一个可以获取泛型参数的方法:

1
2
3
inline fun <reified T> getType() {
return T::class.java
}

非常简单,只需要加上 reified 关键词即可。所以,我们可以在Kotlin中改进Gson的使用方式:

1
2
3
4
5
6
inline fun <reified T: Any> Gson.fromJson(json: String): T {
return Gson().fromJson(json, T::class.java)
}

//使用
val list = Gson.fromJson<List<Stirng>>(json)

这里是对Gson进行了扩展,实现很优雅。注意,Java不支持内联函数,所以在Kotlin中声明的普通内联函数可以在Java中调用,被当做常规函数了;而用reified来实例化参数类型的内联函数不能在Java 中调用,因为它永远需要内联的

打破泛型不变

略吧,看迷糊了,下次补上

前言

性能优化这块,很多人只能说出传统的分析方式,比如 ANR 分析,只会通过查看/data/arn/下的log,分析主线程堆栈、cpu、锁等信息。但是这种方法有局限性,有些高版本设备需要root权限才能访问 /data/anr/ 目录,或者,如果只是用户反馈,我们压根没法复现的情况,就很难分析问题了。

卡顿原理与监控

卡顿原理里面需要注意的一点是:存在消息屏障的情况下,*当异步消息被处理完后,如果没有及时把消息屏障消息移除,会导致同步消息一直没有机会处理,一直阻塞在nativePollOnce *

卡顿原理

首先,我们可以看下Looper.loop的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void loop() {
for (;;) {
//1、取消息
Message msg = queue.next(); // might block
...
//2、消息处理前回调
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...

//3、消息开始处理
msg.target.dispatchMessage(msg);// 分发处理消息
...

//4、消息处理完回调
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
...
}

这个loop方法主线程循环,可以长时间运行,从代码可以看出导致卡顿的原因可能有两个地方:

  • 注释1处的取消息 queue.next() 阻塞
  • 注释3处 dispatchMessage 耗时太久

看下 MessageQueue#next 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Message next() {
...
for (;;) {
//1、nextPollTimeoutMillis 不为0则阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 2、先判断当前第一条消息是不是同步屏障消息,
if (msg != null && msg.target == null) {
//3、遇到同步屏障消息,就跳过去取后面的异步消息来处理,同步消息相当于被设立了屏障
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

//4、正常的消息处理,判断是否有延时
if (msg != null) {
if (now < msg.when) {
//4
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
//5、如果此时是同步屏障,没有取到消息,那么下次循环就走到1那里去了,nativePollOnce为-1,会一直阻塞
nextPollTimeoutMillis = -1;
}
...
}


MessageQueue 是一个链表数据结构,它的 next 方法大致流程是这样的:

  1. 首先判断它头结点(第一个消息)是否是同步屏障消息,如果是,则只处理异步msg,同步msg不处理
  2. 如果是同步屏障的话,若没有获取到异步消息,就会走到注释5,设置 nextPollTimeoutMillis = -1 后,下次循环就会在注释 1 处阻塞
  3. 如果获取到正常的 msg ,不管是同步还是异步,处理流程都一样,先在注释4处判断是否演示,如果延时,则会给 nextPollTimeoutMillis 赋值,下次循环到 1 处就会阻塞一段时间;如果不延时,则会return ,交给 handler 处理(确实是交给Handler处理: msg.target.dispatchMessage(msg) ,在这里面会 首先判断msg.callback 、其次是Handler的mCallback,最后才是交给 Handler 的 handleMessage 处理)

简单介绍Linux 中的IO多路复用方案

Linux上IO多路复用方案有 select、poll、epoll,它们 3 个中 epoll 的性能表现是最优秀的,能支持的并发量也最大,简单介绍:

  • select 操作系统提供的函数,通过它,我们可以把一个文件描述符的数组发给操作系统,让操作系统去遍历,确定哪个文件描述符可以读写,然后让我们处理
  • poll:和select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制
  • epoll: 主要针对select 做了3个优化:

1、内核中保存一份文件描述符集合,无序用户每次传入,只需要告诉内核修改的部分

2、内核通过异步IO事件唤醒而不是轮询的方式找到就绪的文件描述符

3、内核仅将有IO事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合

同步屏障

Android 不允许用户调用 发送同步屏障的方法,它是hide的:

1
2
3
4
5
6
7
8
9
 /* @hide
*/
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
...
}

系统一些高优先级的操作会用到同步屏障消息,例如,View在绘制的时候,最终都要调用 ViewRootImpl 的 scheduleTraversals ,会往MessageQueue 中插入同步屏障 msg,之后在unscheduleTraversals中移除 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//插入同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
...
}
}

void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
}
}

为了保证View的绘制过程不被主线程其他任务影响,View在绘制之前会先往MessageQueue 中插入同步屏障消息,然后注册 Vsync 信号监听,Choreographer$FrameDisplayEventReceiver就是做这事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {

@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
...
Message msg = Message.obtain(mHandler, this);
//1、发送异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
...
// 2、doFrame优先执行
doFrame(mTimestampNanos, mFrame);
}
}

收到 Vsync 信号,注释1会发送异步消息,保证注释2中的doFrame 方法(View真正开始绘制的方法,会调用ViewRootImpl 的 doTraversal、performTraversals)优先执行。需要注意的是,App要谨慎使用异步msg,使用不当可能会出现主线程假死的问题,排查也会比较困难

Handler 的dispatchMessage方法

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

这里印证了前面说的msg 处理顺序, msg.callback -> Handler.mCallback -> handleMessage

卡顿监控方案

卡顿监控方案一:通过Looper.loop中的日志打印监控

首先,我们可以回顾上面 Looper.loop 的源码。注释2和注释4会打印日志,中间过程3是处理msg的过程,这样两段日志之间的耗时就是msg处理的耗时。Google 为我们提供了这个接口,我们只需要 Looper.getMainLooper().setMessageLogging(printer) 设置我们自己的printer就行。需要注意的是,监听到发生卡顿之后,dispatchMessage 早已调用结束,已经出栈,此时再去获取主线程的堆栈,堆栈中是不会包含卡顿代码的!

所以,我们需要在后台开一个线程,定时获取主线程堆栈,然后以时间点作为key,堆栈信息作为value,保存到Map中,发生卡顿的时候,只需要取出卡顿时间段内的堆栈信息即可。

不过,这种方法只适合线下使用,因其存在以下缺陷:

  • logging.println 存在字符拼接,频繁调用会创建大量对象,造成内存抖动
  • 获取主线程堆栈,会暂停主线程的运行

卡顿监控方案二:字节码插桩

通过 Gradle Plugin + ASM ,编译期在每个方法开始和结束的位置分别插入一行代码,统计方法耗时!,行业内的轮子有 微信的 Matrix 方案

对于这种插桩方法,需要注意的是:

  • 避免方法数暴增。在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的ID作为参数。
  • 过滤简单函数。过滤一些直接return、i++ 之类的简单函数,并支持黑名单配置,对一些调用非常频繁的函数,需要添加到黑名单来降低整个方案对性能的消耗。

微信对Matrix做了大量优化,整个包体积增大1%~2%,帧率下降 2 帧以内,对性能影响可以接收,不过依然只会在灰度包使用

ANR监控

ANR 原理

ANR的原理可以比喻成埋炸弹拆炸弹的过程,以Service为例,在通知AMS启动服务之前,通过Handler发送演示消息,这就是埋炸弹,若是 10s 内(前台服务是20s)没人来拆炸弹,炸弹就会爆炸。在ActivityThread中创建服务对象时,调用其 onCreate 之后,就会执行remove之前发送的消息,即拆炸弹。

常见的ANR情形如下:

  • Service。 前台服务在20s内未执行完成,后台服务是前台服务的10倍,200s
  • 输入事件。输入事件分发超时5s,包括按键和触摸事件
  • 广播。前台广播在10s内未执行完成,后台60s
  • ContentProvider。在publish过程超时10s;

AppErrors

所有的ANR,最终都会调用 AppErrors 的 appNotResponding 方法,主要包括几个流程:

  1. 写入event log
  2. 写入 main log
  3. 生成tracesFile
  4. 输出ANR logcat (控制台可以看到)
  5. 尝试写入traceFile
  6. 输出drapbox
  7. 后台ANR,直接杀进程
  8. 错误报告
  9. 弹出ANR dialog

关于ANR,可以看gityuan的《彻底理解安卓应用五响应机制》

ANR分析方法:导出ANR文件

导出ANR文件,即导出/data/anr/traces.txt文件,首先查看主线程,搜索main:

搜索main示意

ANR日志有很多信息,可以看到主线程id是1(tid=1),在等待一个锁,这个锁一直被id为22的线程持有,再来看看22号线程的堆栈:

持有锁的线程

22号线程处于Blocked状态,正在等待一个锁,这个锁被id为1的线程持有,同时这个22号线程还持有一个锁,这个锁是主线程想要的。

通过ANR日志可以分析除这个ANR是死锁导致的,并且有具体的堆栈信息。这只是一种,还有其他ANR情况,比如内存不足、CPU被抢占、系统服务没有及时响应。

如果作为线上的话,在ANR发生时,可以将这个traces.txt文件上报到服务器,只不过有些手机需要root权限才能读取 /data/anr目录

ANR监控

1、抓取系统的 traces.txt

  1. 当监控线程发现主线程卡死时,主动向系统发送 SIGNAL_QUIT信号
  2. 等待 /data/anr/traces.txt 文件生成
  3. 文件生成后进行上报

这种方案可以参考手Q的线程死锁监控与自动化分析实践,但是,这种方案存在以下问题:

  • traces.txt 里面包含所有线程信息,上传后需要人工过滤分析
  • 很多高版本系统需要root权限才能读取到 /data/anr 这个目录

2、ANRWatchDog

它的主要原理:

  1. 开启一个线程,死循环,循环中睡眠 5s
  2. 往UI线程post一个Runnable,将_tick 赋值为 0 ,将 _reported 赋值为 false
  3. 线程睡眠 5s 后检查 _tick 和 _report 的字段是否被修改
  4. 如果一直没有被修改,说明主线程post的Runnable 一直没有被执行,说明主线程至少卡顿5s (只能说至少,这里存在5s内的误差)
  5. 将贤臣各堆栈信息输出

但是,这种方案,其实是有缺陷的,它有个时候会捕获不到 ANR ,什么原因呢?

ANRWatchDog缺点

可以用一个图片来表示:

漏检测示意图

这种情况红色表示卡顿:

  1. 假设主线程卡顿了 2s 之后,ANRWatchDog 刚好开始下一轮循环,将 _tick 赋值为5,并往主线程post一个任务,执行 _tick = 0
  2. 主线程过了 3s 之后刚好不卡顿了,将 _tick 置为 0 ,
  3. 等到 ANRWatchDog 睡眠 5s 之后,发现 _tick = 0 ,判断并没有发生 ANR

针对 ANRWatchDog 存在的问题,可以做一个优化。

3、ANRMoitor

针对 ANRWatchDog 的漏检测问题,设计一个 ANRMoitor ,ANRWatchDog 出现问题的主要原因是,因为线程睡眠 5s ,不知道前一秒主线程是否已经出现卡顿了,如果盖层每隔 1s 检测一次,就可以把误差降低到 1s 内。我们想让子线程间隔1s执行一次任务,可以通过 HandlerThread 来实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Volatile
var mainHandlerRunEnd = true

//子线程会间隔1s调用一次这个Runnable
private val mThreadRunnable = Runnable {

blockTime++
//1、标志位 mainHandlerRunEnd 没有被主线程修改,说明有卡顿
if (!mainHandlerRunEnd && !isDebugger()) {
logw(TAG, "mThreadRunnable: main thread may be block at least $blockTime s")
}

//2、卡顿超过5s,触发ANR流程,打印堆栈
if (blockTime >= 5) {
if (!mainHandlerRunEnd && !isDebugger() && !mHadReport) {
mHadReport = true
//5s了,主线程还没更新这个标志,ANR
loge(TAG, "ANR->main thread may be block at least $blockTime s ")
loge(TAG, getMainThreadStack())
//todo 回调出去,这里可以按需把其它线程的堆栈也输出
//todo debug环境可以开一个新进程,弹出堆栈信息
}
}

//3、如果上一秒没有卡顿,那么重置标志位,然后让主线程去修改这个标志位
if (mainHandlerRunEnd) {
mainHandlerRunEnd = false
mMainHandler.post {
mainHandlerRunEnd = true
}

}

//子线程间隔1s调用一次mThreadRunnable
sendDelayThreadMessage()

}

具体流程:

  1. 子线程每隔 1s 执行一次 mThreadRunnable,检测标志位 mainHandlerRunEnd 是否被修改
  2. 假如 mainHandlerRunEnd 被如期修改为 true,则重置 mainHandlerRunEnd 为 false,继续执行步骤 1
  3. 假如 mainHandlerRunEnd 没有被修改为true,说明有卡顿,累计卡顿 5s 就触发 ANR

这种方案也能在线下应用,定位到耗时代码。最好可以结合 ProcessLifecycleOwner ,应用在前台时才开启,否则停止检测。

死锁监控

就是检测等待啥锁,锁被谁持有了

形成闭环

前面分别讲了卡顿监控、ANR监控和死锁监控,可以把它们连接起来,形成闭环:

  1. 发生ANR
  2. 获取主线程堆栈
  3. 检测死锁
  4. 上报服务器
  5. 结合git,定位到最后修改代码的同学,提问题单

以上内容参考蓝师傅的博客

   因项目要求,设计类似抖音的视频列表播放功能。在权衡之后,采用ViewPager2 + ExoPlayer 来实现,由于直接这样使用会引起视频播放混乱的问题,即划入下一个视频的时候,上一个视频还在播放,导致同一时刻多个视频同时播放。因此,我们设定一个单例的 mPlayer ,动态地设置给 PlayView,通过在onPageSelected 回调中,调用 notifyItemChanged 方法,刷新当前的item ,让它的标志 play = true,这样让它获取单例的 player ,从而实现播放。

  还有,在每个 item 里面需要有个dialog,点击这个item上的某个位置就弹出来,并且,如果第 0 个item 符合某个条件,那么进入视频列表页面就会弹出dialog。这时候,诡异的问题就来了,在某个测试版本,进入视频列表页面之后,弹出了一个dialog,但是感觉这个dialog上面会有一个蒙层,点击之后,这个蒙层才会消失,之后,该页面的所有按钮才能点击 !也就是测试所提出的,按钮需要点击两次才有效!这就奇怪了,通过LayoutInspector 也只能看到一个dialog浮在内容上(事实上这是使用不熟导致的,因为看起来,比如 ActivityA 中有3个dialog,那么在选择process的时候,会有3个 ActivityA 在那,所以应该是每有一个window都会展示一下这个activity,所以那时候应该是选了Activity,但是由于内容是一样的,所以看不出来)。

  但是通过打断点,发现同一个视频 id 生成了,在 itemView 中setData 3次,其实 2次是可以理解的 (数据刚加载的时候一次,在 notifyItemChanged 的一次);与此同时,发现同一个视频 id 出现在两个 itemView 中,这就很奇怪了,并且,前两次 是 itemView1 ,在 notifyItemChanged 之后,id 就出现在了 itemViewB 中了!如果没有看过源码的话,这时候应该颠覆了认知了,因为 notifyItemChanged 应该只刷新当前 item 才对,怎么会创建了一个新的 itemView 呢?

  这个问题到这里,其实可以解释得通了:由于第0个视频满足刚进入页面就需要弹dialog,所以弹了一次;这时候,由于第 0 个视频又被承载到了新的 itemView 上,创建了新的View ,所以又 弹出来了一次,也就是说,上面那个阴影是一个dialog。

  找到问题之后,再搜了下,发现如果 RecyclerView 默认带有动画的情况下,notifyItemChanged 会创建一个新的ViewHolder !难怪,那理论上,如果我去掉RecyclerView的动画,应该问题就解决了,于是有了如下代码:

1
2
3
4
5
//Adapter
override fun onAttachedToRecyclerView(rv: RecyclerView) {
super.onAttachedToRecyclerView(rv)
rv.itemAnimator = null
}

果然可以了!现在来总结以下问题:

由于默认的动画导致刷新单个item的时候创建了新的ViewHolder, 从而引起 Dialog 再弹了一次,两个重叠在一起了,解决方案就是将 RecyclerView 的动画去掉

以上内容参考资料腾讯云csdn的博客

run()函数

它的实现如下,其实就是调用传入的block参数(一般是个Lambda代码块)

1
2
3
4
5
6
7
8
9
10
11
public inline fun <R> run(block: () -> R): R { 
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

//使用
run {
myFun()
}

apply() 函数

实现如下,主要看后面两行,先是调用了block函数(block里面可以直接使用this?),然后再返回当前调用者this。即执行完block后返回当前的调用者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public inline fun <T> T.apply(block: T.() -> Unit): T { 
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

//使用方法
val a = ArrayList<String>().apply {
add("A")
add("B")
add("C")
println(this)//打印 [A,B,C]
}

println(a)//同样打印 [A,B,C]

let函数

只需要看源代码最后一行即可:意思是把当前调用对象作为参数传入block代码块(意味着一定要用it访问调用对象,能不能this?),最后,返回block执行结果,即作用域中的最后一个对象::

1
2
3
4
5
6
public inline fun <T, R> T.let(block: (T) -> R): R { 
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

also() 函数

源码先调用block(this) ,但是最后返回值是this,也就是,将当前调用者传入block,执行完后返回调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public inline fun <T> T.also(block: (T) -> Unit): T { 
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

//使用
val a = "ABC".also{
println(it)//输出 ABC
}

pintln(a);//输出 ABC

with() 函数

源码传入一个接收者对象 reciver,然后使用该对象去调用传入的Lambda代码块: receiver.block()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public inline fun <T, R> with(receiver: T, block: T.() -> R): R { 
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

//使用
whith(ArrayList<String>()) {
add("A")
add("B")
add("C")

println(this)//ABC
}.let {
println(it)//kotlin.Unit
}

run、let 与 also 对比

由下面代码可能更好理解run实现中 return block() 、let实现中的 return block(this) ,以及 also 源码中的 return this :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//run提供了一个单独的作用域,并且会返回在这个作用域当中的最后一个对象
//例如现在有这么一个场景,用户领取app的奖励,如果用户没有登录弹出登录dialog,如果已经登录则弹出领取奖励的dialog。我们可以使用以下代码来处理这个逻辑
run {
if (islogin) loginDialog else getAwardDialog
}.show()


val original = "abc"

original.let {
println("The original String is $it") // "abc"
it.reversed()
}.let {
println("The reverse String is $it") // "cba"
it.length
}.let {
println("The length of the String is $it") // 3
}

original.also {
println("The original String is $it") // "abc"
it.reversed()
}.also {
println("The reverse String is ${it}") // "abc"
it.length
}.also {
println("The length of the String is ${it}") // "abc"
}
  • run是直接提供一个作用域,并返回作用域里面最后一个对象
  • let 将this传入block中,并且返回 block(this),也就是作用域最后一个对象
  • also 将this传入block中,但是返回的是 this,也就是调用者本身
  • with 呢,是以 with(T) 的形式使用(但是它貌似不能判断null?)
  • apply没有传入this,但是直接返回this

最后,按需使用的情景:

场景

有些内容参考 无嘴小呆子

无需写interface,直接写个高阶函数?这个地方是不是高阶函数?

类的构造方法

Kotlin 中的类及接口

Kotlin中的类与Java的很像:

1
2
3
4
5
class Bird {
val weight: Double = 500.0
val color: String = "blue"
fun fly()
}

反编译成Java的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class Bird {
private final double weight = 500.0D;
@NotNull
private final String color = "blue";

public final double getWeight() {
return this.weight;
}

@NotNull
public final String getColor() {
return this.color;
}

public final void fly() {

}
}

由此可以看出,虽然声明方式很像,但是也存在很多不同:

  • 属性默认值。Kotlin中,除非显式声明延迟初始化,不然就需要指定默认值。
  • 不同的可访问修饰符。Kotlin类中的成员默认是全局可见的(public),而Java默认可见域是包作用域。
  • 方法默认是final修饰的。意味着不能覆写(这条是自己添加的)。

可带有属性和默认方法的接口

我们知道,Java 8 之后,接口支持默认实现,如下所示:

1
2
3
4
5
6
public interface Flayer {
public String kind();
default public void fly() {
System.out.println("I can fly");
}
}

接下来看下Kotlin的接口实现:

1
2
3
4
5
6
7
interface Flyer {
val speed: Int
fun kind()
fun fly() {
println("I can fly");
}
}

同样,我们可以用Kotlin 定义一个带有方法实现的接口,同时,它还支持抽象属性(如例子中的speed属性),然而,Kotlin是基于Java6实现的,那它是如何支持的呢?转换为Java代码看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Flyer {
int getSpeed();

void kind();

void fly();

public static final class DefaultImpls {
public static void fly(Flyer $this) {
String var1 = "I can fly";
System.out.println(var1);
}
}
}

由此我们发现,Kotlin编译器通过定义一个静态内部类 DefaultImpls 来提供fly方法的默认实现。同时,抽象属性是通过一个get方法来实现的!所以呢,我们不能像Java一样,为属性直接赋值,如下这样是错误的:

1
2
3
interface Flyer {
val height = 1000;//error Property initializers are not allowed in interfaces
}

但是Kotlin 提供了另外一种方式来实现这种效果:

1
2
3
4
interface Flyer {
val height
get() = 1000
}

更简洁地构造类的对象

如果要在Java中实现参数个数不同的构造方法,那我们就要重载很多个构造方法,这种方式主要存在2个缺点:

  • 如果要支持任意参数组合来创建对象,那么需要实现的构造方法非常多
  • 每个构造方法中的代码都会冗余,如在构造方法中可能都需要对 age 和color 进行相同的赋值操作。

Kotlin 通过引入新的构造语法来解决这些问题。比如我们可以用一行代码来表示复杂的构造方式:

1
class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue")

如果用Java实现这种参数任意组合的效果,那是非常复杂的。但是如果不写入全部的参数,而只用其中某些参数的时候,需要写参数名,否则会报错:

1
2
3
4
5
//错误的
val bird1 = Bird(1000.00)

//正确示例
val bird2 = Bird(weight = 1000.00, color = "black")

init方法:事实上,我们的构造方法可以拥有多个 init,他们会在对象创建时按照类中从上到下的顺序先后执行

延迟初始化: by lazy 和 lateinit

在Kotlin中,主要使用lateinit 和 by lazy 这两种语法来实现延迟初始化的效果。如果这是一个用 val 声明的变量,我们用 by lazy 来修饰:

1
2
3
4
5
class Bird(val weight: Double, val age: Int, val color: String) {
val sex: String by lazy {
if(color == "yellow") "male" else "female"
}
}

总结 by lazy 语法的特点如下:

  • 该变量必须是引用不可变的,而不能通过var声明
  • 被首次调用时,才会进行赋值操作,一旦赋值,后续将不能更改。

需要注意的是,系统会给 lazy属性默认加上同步锁,也就是 LazyThreadSafetyMode.SYNCHRONIZED ,它在同一时刻只允许一个线程对lazy属性初始化,所以,lazy是线程安全的。当然,你可以自己给lazy指定参数,如: val sex: String by lazy(LazyThreadSafetyMode.NONE)

与lazy 不同,lateinit 主要用于 var 声明的变量,然而它不能用于基本数据类型,如 Int、Long 等,我们需要使用Integet这种包装类作为替代。lateinit 的用法如下:

1
2
3
4
5
6
7
8
class Bird(val weight: Double, val age: Int, val color: String) {
lateinit var sex: String

fun printSex() {
sex = if(color == "yellow") "male" else "female"
println(sex)
}
}

Kotlin只用一个构造方法实现了Java中需要重载才能实现的功能,那么,Kotlin中是否真的只需要一个构造方法呢?

主从构造方法

前面似乎遗漏了一些情况,简化前面的Bird类:

1
2
3
4
5
6
7
class Bird(age: Int) {
val age: Int

init {
this.age = age
}
}

假设当前我们知道鸟的生日,希望可以通过生日来得到鸟的年龄,然后创建一个Bird对象,如何实现?有一种方案就是在别处定义一个工厂方法:

1
fun Bird(birth: DateTme) = Bird(getAgeByBirth(birth))

在哪声明这个工厂方法呢?这种方式的缺点在于,Bird 方法与Bird类在代码层面的分离不够直观。其实我们可以像Java那样新增一个构造方法来解决,Kotlin 也支持多构造方法,与Java的区别是,Kotlin中多个构造方法之间存在主从关系

1
2
3
4
5
6
7
8
9
10
11
class Bird(age: Int) {
val age: Int

init {
this.age = age
}

constructor(birth: DateTime) : this(getAgeByBirth(birth)){

}
}

以上代码的运作方式是:

  • 通过constructor方法定义一个新的构造方法,称为从构造方法。相应地,我们熟悉的构造方法叫做主构造方法,每个类最多存在一个主构造方法,但是可以存在多个从构造方法
  • 如果一个类存在主构造方法,那么每个从构造方法都要直接或间接地委托给它。

不同的访问控制原则

构造完对象,就要考虑访问控制了。

限制修饰符

我们知道,Kotlin中的类和方法默认实现反编译成 Java的时候,会被final修饰,所以,类默认是不能被继承的,方法默认也不能被覆写的,如果要实现继承,类之前需要用open修饰: open class Bird {} ,方法也需要使用open 修饰: open fun fly()

类默认final 真的好吗?

网上有很多人认为默认final有很多缺点,那为什么Kotlin要设计成默认final呢?主要有2个原因:

  • Kotlin 当前是一门以Android为平台的开发语言,在开发中,我们很少会频繁继承一个类,默认final会更加安全。
  • Kotlin的扩展手段更加丰富。不像Java,Kotlin 可以通过多种方式去扩展,而不是通过原始类的手段,典型的莫过于 Android 的Kotlin 扩展库 android-ktx,Google就是通过Kotlin的扩展语法而不是继承来实现。

此外,Kotlin还可以利用密封类来限制一个类的继承,如下所示:

1
2
3
4
5
sealed class Bird {
open fun fly() = "I can fly"

class Eagle: Bird()
}

Kotlin通过 sealed 关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。但是这种方式有它的局限性,即它不能被初始化,为什么呢?这是因为它是基于抽象类实现的,我们看反编译后的Java代码就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Bird {
private Bird() {

}

public Bird(DefaultConstructorMarker $constrctor_maker) {
this();
}

public static final class Eagle extends Bird {
public Eagle() {
super((DefaultConstructorMarker)null)
}
}
}

密封类的使用场景优先,它其实可以看成一种功能更强大的枚举,所以它在模式匹配中可以起到很大的作用

可见性修饰符

除了限制类修饰符外,还有一种可见性修饰符。Kotlin与Java的不同在于:

  • 默认修饰符不同,Kotlin是public,而Java是default
  • Kotlin中有一个独特的 internal
  • Java类只有内部类可以用private修饰,其他类不允许;而Kotlin可以
  • protected访问范围不同。Java中是包、类及子类可访问,而Kotlin只有类和子类可以访问

说下Kotlin 独特的 internal 修饰符,它的作用域被称作模块内访问,那到底什么是模块?以下几种情况可以算作一个模块:

  • 一个Eclipse项目
  • 一个 Intellij IDEA项目
  • 一个Maven项目
  • 一个Gradle项目
  • 一组由一次Ant任务执行编译的代码

那为什么要这种修饰符呢?Java的包内访问不好吗?Java包内访问确实是有问题的,举个例子,你再Java项目中定义了一个类,默认修饰符,那就是包私有的,其他地方将无法访问。然后你id啊宝诚一个类库,供三方使用。但如果有个开发者想使用这个类,除了copy源码以外,还有一个方式就是在程序中创建一个与该类相同名字的包,那么这个包下面的其他类就能直接使用我们前面定义的类了!

而Kotlin这种,模块内可见指的是该类只对一起编译的其他Kotlin文件可见,开发工程与第三方类库不属于同一模块,这时候如果还想用,就只能复制源码了。

Java中我们很少见到private修饰的类,因为Java中的类或者方法没有单独属于某个文件的概念。若要用provate修饰,那么这个只能是其他类的内部类,而Kotlin中则可以用private给单独的类修饰,它的作用域就是当前这个Kotlin文件:

解决多继承问题

Java和Kotlin都不支持多继承。为什么这样呢?是因为多继承会导致继承关系语义上的混淆。

骡子的多继承困惑

C++支持多继承,然而C++中存在一个经典的钻石问题。假如我们有个抽象的 Animal 类,它有个 run() 方法,Horse (马) 和 Donkey(驴) 都继承了Animal,假如支持多继承,Mule(驴)继承了 Horse 和 Donkey ,那么,在 Mule 中的 run() 到底是继承了谁的呢?这就是典型的钻石问题,因为继承关系像个钻石图,如下:

钻石问题

接口实现多继承

在Java中我们经常提及使用接口来实现多继承,其实,如果多个接口中都存在同样的方法,比如上述的 run() ,同样也会导致钻石问题。不过,Kotlin 通过提供 super 关键字来指定继承那个父接口的方法,从而解决了这个问题,如下:

1
2
3
4
5
6
7
8
9
10
11
interface Flyer {
fun kind() = "flying animals"
}

interface Animal {
fun kind() = "flying animals"
}

class Bird(): Flyer, Animal {
override fun kind() = super<Flyer>.kind()
}

通过 super<Flyer>.kind()来指定继承哪个父接口的方法!当然,子类也可以自己实现这个方法,不用父类的,完全没问题。

内部类解决多继承问题

Kotlin的内部类的定义方式和Java 还不一样,如果我们按照Java的习惯来定义 Kotlin中的内部类:

1
2
3
4
5
6
7
8
9
class OuterKotlin {
val name = "not kotlin inner class"

class ErrorInnerKotlin {
fun printName() {
print("thie name is $name")//报错,不能访问name
}
}
}

报错了,和Java 还真不一样。其实,我们这样声明的是Kotlin 的 嵌套类,并非内部类。如果要在Kotlin中声明一个内部类,必须在这个类前面加一个 inner 关键字,即这样子:

1
2
3
4
5
6
7
8
9
class OuterKotlin {
val name = "kotlin inner class"

inner class InnerKotlin {
fun printName() {
print("thie name is $name")
}
}
}

我们知道,Java中在内部类的语法上增加一个 static 关键字,就可以变成 嵌套类;Kotlin则是相反的思路,默认是嵌套类,必须加上 inner 关键字才是一个内部类。

了解内部类之后,可以通过内部类实现上述的骡子类:

1
2
3
4
5
6
7
8
9
10
11
12
class Mule {
fun runFast() {
HorseC().runFast()
}

fun runSlow() {
DonkeyC().runSlow()
}

private inner class HorseC: Horse()
private inner class DonkeyC: Donkey()
}

使用委托代替多继承

Kotlin中的委托只需要通过 by 关键字就可以实现,比如之前学习的 by lazy 语法,其实就是利用了委托实现了延迟初始化。我们看下如何通过委托代替多继承需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface CanFly {
fun fly()
}

interface Caneat {
fun eat()
}

open class Flyer: CanFly {
override fun fly() {
println("I can fly")
}
}

open class Animal : CanEat {
override fun eat() {
println("I can eat")
}
}

//关键
class Bird(flyer: Flyer, animal: Animal): CanFly by flyer, CanEat by animal {}

fun main(args: Array<String>) {
val fyler = Flyer()
val animal = Animal()
val b = Bird(flyer, animal)
b.fly()
b.eat()
}

真正的数据类

繁琐的JavaBean

JavaBean中需要各种setter和getter,如果要支持对象值的比较,还得重写hashCode 和 equals 等方法。

用data class创建数据类

data class 顾名思义就是数据类,这不是Kotlin首创,在Scala等语言中也有。一般我们只需要如下定义即可:

1
data class Bird(var weight: Double, var age: Int, var color: String)

这么一行代码,编译器为我们做了很多事情,来看看反编译后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public final class Bird {
private double weight;
private int age;
@NotNull
private String color;

public final double getWeight() {
return this.weight;
}

public final void setWeight(double var1) {
this.weight = var1;
}

public final int getAge() {
return this.age;
}

public final void setAge(int var1) {
this.age = var1;
}

@NotNull
public final String getColor() {
return this.color;
}

public final void setColor(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.color = var1;
}

public Bird(double weight, int age, @NotNull String color) {
Intrinsics.checkNotNullParameter(color, "color");
super();
this.weight = weight;
this.age = age;
this.color = color;
}

public final double component1() {//Java中没有的
return this.weight;
}

public final int component2() {//Java中没有的
return this.age;
}

@NotNull
public final String component3() {//Java中没有的
return this.color;
}

@NotNull
public final Bird copy(double weight, int age, @NotNull String color) {//Java中没有的
Intrinsics.checkNotNullParameter(color, "color");
return new Bird(weight, age, color);
}

// $FF: synthetic method
public static Bird copy$default(Bird var0, double var1, int var3, String var4, int var5, Object var6) {
if ((var5 & 1) != 0) {
var1 = var0.weight;//copy时,若未指定具体属性的值,则使用被copy对象的属性值,这是浅拷贝
}

if ((var5 & 2) != 0) {
var3 = var0.age;
}

if ((var5 & 4) != 0) {
var4 = var0.color;
}

return var0.copy(var1, var3, var4);
}

@NotNull
public String toString() {
return "Bird(weight=" + this.weight + ", age=" + this.age + ", color=" + this.color + ")";
}

public int hashCode() {
int var10000 = (Double.hashCode(this.weight) * 31 + Integer.hashCode(this.age)) * 31;
String var10001 = this.color;
return var10000 + (var10001 != null ? var10001.hashCode() : 0);
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Bird) {
Bird var2 = (Bird)var1;
if (Double.compare(this.weight, var2.weight) == 0 && this.age == var2.age && Intrinsics.areEqual(this.color, var2.color)) {
return true;
}
}

return false;
} else {
return true;
}
}
}

这就和JavaBean很相似了,同时还有 equals 和 hashCode 的实现。同时,我们发现里面有几个JavaBean中没有的方法,比如 copy、component1、component2、component3。下一节来介绍它们。

copy 、componentN 与 结构

上上述代码可以看到,两个copy方法,可以传入响应的参数来生成不同的对象;同时,如果你未指定具体属性的值,那么新生成的对象的属性值将使用被copy对象的属性值,这就是我们常说的浅拷贝。看个例子:

1
2
3
4
5
6
7
8
//如果Bird的属性是var,即可变的
val b1 = Bird(20.0,1, "blue")
val b2 = b1
b2.age = 2

//如果Bird的属性是val,不可变的,那么更改属性只能通过copy
val b1 = Bird(20.0,1, "blue")
val b2 = b1.copy(age = 2)

注意,Kotlin提供的上述copy方法是浅拷贝的,所以我们要注意使用场景。因为数据类的属性可以被修饰为var,所以不能保证不会出现引用被修改的情况。

接下来看 componentN (其中N为1,2,3…,根据参数个数来定),这个设计到底有什么用?我们或多或少直到怎么将属性绑定到类上,但是对于如何将类的属性绑定到响应变量上却不是很熟悉,比如:

1
2
3
4
5
6
7
8
9
val b1 = Bird(20.0,1, "blue")

//通常方法,也符合Java的思维逻辑
val weight = b1.weight
val age = b1.age
val color = b1.color

//Kotlin 进阶方法
val (weight, age, color) = b1

看到进阶方法的时候,一定感到兴奋了吧,普通方法确实很繁琐。还有一种情形,看Java的代码:

1
2
3
4
5
6
String birdInfo = "20.0,1, blue";
//如果要把值取出来,就得split
String[] temps = birdInfo(",");
double weight = Double.valueOf(temps[0]);
int age = Integer.valueOf(temps[1]);
String color = temps[2];

在我们明明直到值得情况下,还需要这样分割,很繁琐,好在Kotlin提供了更优雅的做法:

1
val (weight, age, color) = birdInfo.split(",")

这个语法也很简洁和直观,其原理也很简单,就是 解构,通过编译器的约定实现解构。当然,Kotlin对于解构也有限制,在数组中它默认最多允许赋值5个变量,因为如果变量过多,效果反而会适得其反,因为到后期你都搞不清哪个值要赋给哪个变量了。除了利用编译器自动生成的 componentN之外,你还可以实现自己的 componentN,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data class Bird(var weight: Double, var age: Int, var color: String) {
var sex = 1

operator fun component4: Int {//注意 operator 关键字
return this.sex
}

constructor(weight: Double, age: Int, color: String, sex: Int) : this(weight, age, color) {
this.sex = sex
}

//使用
fun main(args: Array<String>) {
val b1 = Bird(20.0, 1, "blue", 0)
val (weight, age, color, sex) = b1
...
}
}

除了数组支持解构外,Kotlin也提供了其他常用数据类型,分别是 Pair 和 Triple 前者是二元组,后者是 三元组,,我们可以

用类似以下方法来使用它们:

1
2
3
4
5
6
7
val pair = Pair(20.0,1)
//利用属性顺序获取
val weightP = pair.first
val ageP = pair.second

//使用解构
val (weightP, ageP) = Pair(20.0,1)

从static 到 object

Kotlin中告别了static,因为有了 object 关键字,除了替代static外,它还有更多的功能实现,比如单例对象以及简化匿名表达式等。

伴生对象

看一段常见的Java代码:

1
2
3
4
5
6
7
8
9
public class Prize {
private int type;

static int TYPE_REDPACK = 0;

static boolean isRedpack(Prize prize) {
return prize.type == TYPE_REDPACK;
}
}

这个类中既有静态变量、静态方法,也有普通变量、普通方法,然而,静态变量和静态方法是属于类的,普通变量和普通方法是属于具体对象的,所以在代码解构上职能并不清晰。Kotlin中利用 companion object 两个关键字引入伴生对象来清晰区分。

顾名思义,“伴生”即伴随某个类的对象,它属于这个类所有,全局只有一个单例,因此伴生对象跟Java中static修饰的效果一样,在类装载的时候被初始化

companion object 用花括号包裹了所有静态属性和方法,使得将普通方法和属性清晰区分开来。此外,伴生对象很适合作为工厂,这里就不展开。

天生的单例: object

单例模式最大的一个特点就是在系统中只能存在一个实例对象,所以在java中我们必须通过设置构造方法私有化,以及提供静态方法创建实例的方式来创建单例。在Kotlin中,由于object的存在,我们可以直接用它来实现单例,如下所示:

1
2
3
4
5
6
object DatabaseConfig {
var host: String = "127.0.0.1"
var port: Int = 3306
var userName: String = "root"
var password: String = ""
}

由于object全局声明的对象只有一个,所以它并不用语法上的初始化,甚至都不需要构造方法,因此,我们可以说object创造的是天生的单例。此外,由于 DatabaseConfig 的属性是 var 声明的属性,我们还能修改它们:

1
2
DatabaseConfig.host = "localhost"
DatabaseConfig.port = 3307

由于单例也可以和普通类一样实现接口和继承类,所以可以将其看成一种不需要主动初始化的类,它也可以拥有扩展方法单例对象会在系统加载的时候初始化

object 表达式

主要说的是,利用object来完善匿名内部类,这里不展开说。

不一样的类型声明

Kotlin 采用的是与 Java 相反的类型声明方式,类型名通常在变量名的后面: val a: String = "I am Kotlin"为什么采用这种风格呢?Kotlin官方的FAQ的回答是这样的:

我们相信这样可以使得代码的可读性更好。同时,这也有利于使用一些良好的语法特性,比如省略类型声明。Scala的经验表明,这不是一个错误的选择。

所以,类型放在变量后面的其中一个原因是为了类型省略,这个类型省略其实就是类型推导。

增强的类型推导

类型推导是Kotlin在Java的基础上增强的语言特性之一,即编译器可以在不显式声明类型的情况下,自动推导出它所需要的类型,如下:

1
2
3
4
5
6
7
8
9
val string = "I am Kotlin"
val int = 1314
val long = 1314L
...

//如果我们打印以上的变量类型,如: println(int.javaClass.name),将会获得如下结果:
//java.lang.String
//int
//long

类型推导很大程度上提高了Kotlin这种静态类型语言的开发效率,虽然静态类型语言有很多优点,然而在编码过程中却需要书写大量的类型。

声明函数返回值类型

虽然支持类型推导,但是函数返回值类型必须要显式声明,比如:

fun sum(x: Int, y: Int): Int { return x + y }

此时也许与Java的区别不大,其实Kotlin进一步增强了函数语法,我们可以把 {} 去掉,用等号来定义一个函数:

fun sum(x: Int, y: Int) = x + y

Kotlin支持的这种单行表达式与等号的语法定义的函数,叫做表达式函数体,作为区分,普通的函数声明叫做代码块函数体。但是别高兴太早,我们再来看一段递归程序:

fun foo(n: Int) = if(n == 0) 1 else n * foo(n - 1)

这种情况下,编译器并不能针对递归函数的情况推导类型,因此这里会报错。所以,在一些诸如递归等复杂条件下,及时用表达式定义函数,我们也必须显式声明类型,才能让程序正常工作,代码如下:

fun foo(n: Int): Int = if(n == 0) 1 else n * foo(n - 1)

val 和 var 的使用规则

Kotlin声明变量时,引入了 val 和 var 的概念。var 容易理解,就是变量,在JavaScript 中也有用到,但是 val 是什么呢?如果在 IDEA 中反编译 val 的实现成Java代码就能发现,它是通过 final 这一特性实现的

优先使用val避免副作用

Kotlin支持一开始不定义 val 变量的取值,随后再进行赋值,然而,因为引用不可变,所以val声明的变量只能被赋值一次,并且声明时不能省略变量类型,如下所示:

1
2
3
4
5
fun main(args: Array<String>) {
val a: Int
a = 1
println(a)//输出1
}

由于不可变性,我们可以直到 val 变量在并发环境更安全。

var 的适用场景

既然 val 那么好,为什么要 var 呢?首先,Kotlin 要兼容Java ,这就注定 必须有 var 的存在;其次有一些场景如果不适用 var 就必须得用到 递归 才能实现了,所以var需要存在。

高阶函数和Lambda

Kotlin 天然支持了部分函数式特性,函数式语言的一个典型特征在于函数式头等公民——我们不仅可以像类一样在顶层直接定义一个函数,也可以在函数内部定义一个局部函数!如下所示:

1
2
3
4
5
6
fun foo(x: Int) {
fun double(y: Int): Int {
return y * 2
}
println(double(x))
}

此外,Kotlin还能直接将函数像普通变量一样传递给另一个函数,或在其他函数中被返回,如何理解这个特性?

抽象和高阶函数

概念东西,略

函数的类型

在Kotlin中,函数类型的格式非常简单,举个例子:(Int) -> Unit,我们可以发现,Kotlin中的函数类型需要遵循以下几点:

  • 通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型
  • 必须用一个括号来包裹参数类型,如果多个参数,可以用逗号分割,如: (Int, String?) -> Unit
  • 返回值即使是 Unit ,也必须显式声明

此外,Kotlin 还支持为声明参数指定名字:(errCode: Int, errMsg: String?) -> Unit 这还没完,高阶函数还支持返回另一个函数,所以还能这么做:

1
2
3
(Int) -> ((Int) -> Unit)
//如果把后半部分括号省略,可以写成:
(Int) -> Int -> Unit

方法和成员引用

Kotlin 存在一种特殊的语法,通过两个冒号来四号线对于某个类的方法进行引用。假如有一个CountryTest 类的对象实例 countryTest ,如果要引用它的 isBigEuropeanCountry 方法,就可以这么写:

countryTest::isBigEuropeanCountry

此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量:

1
2
3
4
5
6
class Book(val name: String)

fun main(args: Array<String>) {
val getBook = ::Book
println(getBook("Dive into Kotlin").name)
}

可以发现,getBook 的类型为 (name: String) -> Book 。类似的道理,如果我们要引用某个类中的成员变量,比如Book类中的name,就可以这样引用: Book:name ,以上 Book::name 的类型为 (Book) -> String 。当我们在对Book 类对象的集合应用一些函数式API的时候,就会显得格外有用,比如:

1
2
3
4
5
6
fun main(args: Array<String>) {
val bookNames = listOf(
Book("Thinking in Java")
Book("Dive into Kotlin")
).map(Book::name)
}

匿名函数

Lambda 是语法糖

Kotlin 在JVM 层设计了 Function 类型 (Function0,Function1…Function22)来兼容Java的Lambda表达式,其中后缀数字代表了 Lambda 参数的数量。比如,Function1在源码中就是如下表示的:

1
2
3
interface Function1<in P1, out R>: kotlin.Function<R> {
fun invoke(p1: P1): R
}

可见每个Function 类型都有一个invoke方法,设计Function类型的目的之一就是要兼容Java ,实现在Kotlin 中也能调用Java的Lambda。在 Java 中,实际上不支持把函数作为参数,而是通过函数式接口来实现这一特性。

函数、Lambda和闭包

“柯里化”风格、扩展函数

柯里化略

在我们介绍的Lambda的表达式中,还存在一种特殊的语法,如果一个函数只有一个参数,且该参数为函数类型,那么在调用该函数时,外面的括号就可以省略,例子如下:

1
2
3
4
5
6
7
8
fun omit(block: () -> Unit) {
block
}

//那么我们在调用的时候,可以写成
omit {
println("parentheses is omitted")
}

另一项特性 扩展函数,允许我们在不修改已有类的前提下,给它增加新的方法,示例如下:

1
2
3
fun View.invisible() {
this.visibility = View.INVISIBLE
}

在上述例子中,类型View被称为接收者类型,this对应的是这个类型锁创建的接收者对象,this也能被省略,就像这样:

1
2
3
fun View.invisible() {
visibility = View.INVISIBLE
}

面向表达式编程

现在,罗列下我们已经提及的表达式概念:

  • if表达式
  • 函数体表达式
  • Lambda表达式
  • 函数引用表达式

Unit类型:让函数调用皆为表达式

之所有不能说Java中的函数调用皆是表达式,是因为存在特例 void,在Java中如果声明的函数没有返回值,那么它就要用void修饰:

1
2
3
void foo () {
System.out.println("hahah");
}

所以foo就不具有值和类型信息,就不能算作一个表达式。函数式语言在所有的情况下都具有返回类型,所以kotlin引入了 Unit 来替代 void 关键字。如何理解 Unit ?其实与 int 一样,都是一种类型,然而它不代表任何信息,它就是一个单例,它的实例只有一个 ,可以写为 () 。

for循环和范围表达式

在Java中,经常在for来构建循环体:

1
2
3
for (int i = 0; i < 10; i ++) {
System.out.println(i);
}

但是kotlin会简单很多:

1
2
3
4
5
for (i in 1..10) println(i)  
//当然也能把大括号和i的类型加上
for (i:Int in 1..10) {
println(i)
}

范围表达式,1..10 这种语法是范围表达式(range) 。

官网的表述是:Range表达式是通过rangeTo 函数实现的,通过 .. 操作符与某种类型的对象组成,除了整形的基本类型外,该类型需要实现 java.lang.Comparable 接口

举个例子,由于 String类实现了 Comparable 接口,字符串之间可以比较大小,所以我们可以创建一个字符串区间,如 "abc".."xyz"

另外,kotlin 还提供了步长和倒序以及半开区间:

1
2
3
4
5
6
7
8
//步长
for (i in 1..10 step 2) print(i) //输出 1 3 5 7 9

//倒序
for (i in 10 downTo 1 step 2) print(i) //输出: 10 8 6 4 2

//半开区间
for(i in 1 until 10) print(i) //输出 123456789

用 in 来检查成员关系,,在Kotlin中我们可以用 in 关键字来检查一个元素是否是一个区间或者集合中的成员,比如:"a" in listOf ("b" , "c") ,会返回 false ;在 in 之前加上叹号就是相反结果: "a" !in listOf ("b" , "c") 返回true。更多的应用场景如下:

1
2
3
4
5
6
7
//结合范围表达式
"kot" in "abc".."xyz"

//还能通过withIndex 提供键值元祖
for ((index,value) in array.withIndex) {
println("the element at $index is $value")
}

中缀表达式

前面见识过 in、step、downTo、until 这些写法,都不需要通过点号,而是用中缀表达式来被调用,从而语法更直观。这是如何实现的呢?看下标准库中类似的方法 to 的设计:

1
infix fun <A,B> A.to(that: B): Pair<A, B>

函数可变参数

Java 中采用 “…” 来表示可变参数,Kotlin中通过 varargs 关键字实现可变参数…。需要注意的是,Java 中的可变参数必须是最后一个参数,Ktolin中没有这个限制,但两者都可以在函数体中以数组方式来使用可变参数变量:

1
2
3
4
5
6
7
8
9
fun printLetters(varargs letters: String, count: Int) {
print("${count} letters are ")
for (letter in letters) print(letter) // 输出 3 letters are abc
}

//此外,我们还能使用星号(*)来传入外部的变量作为可变参数的变量:

val letters = arrayOf("a", "b", "c")
printLetters(*letters, count = 3) //同样会输出 3 letters are abc

由于to会返回 Pair 这种键值对的结构数据,因此我们经常会把它与map结合在一起使用,如下:

1
2
3
4
5
mapOf(
1 to "one",
2 to "two",
3 to "three"
)

字符串的定义和操作

kotlin 中有丰富的API,比如: "abcdefg".filter {c -> c in 'a'..'d'} //输出 abcd

定义原生字符串

Java 对原生字符串只能通过转义字符的方法支持。然而,在Kotlin中已经支持直接写原生字符串,使用3个引号的方式(“””),体验下:

1
2
3
4
5
6
7
8
9
val rawString = """
\n Kotlin is awesonme.
\n Kotlin is a better Java. """

print(rawString)

//会打印:
\n Kotlin is awesonme.
\n Kotlin is a better Java.

可以看到非常简洁,如果用Java 来表示会非常复杂,尤其是 Html 代码。

字符串模板

字符串判等

Kotlin 中判等性有两种类型:

  • 结构相等。 通过 == 来判定两个对象的内容是否相等
  • 引用相等。通过 === 来判断两个对象的引用是否一样,与之相反的操作是 !== ,

React简介

React 只专注于 MVC 框架设计模式中的 View 层面的实现。为了大大减少传统前端直接操作DOM 的昂贵花费,React 使用Virtual DOM (虚拟DOM)进行DOM的更新,实现了单向数据流传递。下图清晰地描述了 React 底层与前端浏览器的沟通机制。

Ract框架结构

React的底层特性

传统HTML页面需要更新页面元素时,都是将整个页面重新加载实现重绘,这个代价非常昂贵。后来有了 AJAX 这样的局部更新技术,实现了页面的异步更新,不过AJAX代码编写、维护、性能及更新粒度上还是不够完美。

文档对象模型(Document Object Model, DOM) 是 W3C 组织推荐的处理可扩展标志语言的标准编程接口,在 HTML 网页上,将构成页面的对象元素组织在一个树形的结构中,用来表示文档中对象的标准模型就称为 DOM

React 在底层设计了虚拟DOM,虚拟DOM 与真实 DOM 相互映射,当业务逻辑修改了 Reat 组件中的 state ,React 框架diff算法会通过比较虚拟 DOM 与 真实DOM 之间的差异,找出哪些部分被修改了,最终只更新差异部分。这样实现了React 在前端中的高性能表现。

其实,React 并不会在state 更改的第一时间去执行 diff 算法并立即更新页面,而是将多次操作汇聚成一次批量操作,这样再次提升页面更新重绘的效率。

React Native 简介

第3章:React Native 工作原理与生命周期

React Native框架及工作原理

因为 React Native 底层为React 框架,所以,如果是UI层的变更,那么就映射为虚拟DOM后,调用diff算法计算出变动后的 JSON 映射文件,最终由Native 层将此 JSON 文件映射渲染到原生App的页面元素上,实现了在项目中只需要控制state 以及 props的变更来引起 ios /Android 平台的 UI 变更。

编写的 RN 代码最终会打包成 main.bundle.js 文件供App 加载,此文件可以存在App本地或者服务器上更新。

RN 与原生平台通信

RN 与原生的通信如下图所示,采用了 JavaScriptCore 作为 JS VM,中间通过 JSON 文件与 Bridge 进行通信。若使用 Chrome 进行调试,那么所有的 JavaScript 代码都将运行在 Chrome 的 V8 引擎,与原生代码通过 WebSocket 进行通信。

RN与原生平台通信

组件间通信

  1. 父子组件之间的通信

    在RN中,可以通过 props 的形式实现父组件向子组件传递值,如下例子展示父组件通过调用子组件并赋值子组件的 name 为 React,子组件通过 this.props.name 获取父组件传递过来的 name 字符串 React:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    *父子组件通信,在父组件中调用子组件
    */
    <ChildComponent name='React'/>

    /**
    *子组件实现,通过props获取父组件传递的值
    */
    class ChildComponent extentds Component {
    render(){
    return (
    <Text> Hello {this.props.name}!</Text>
    );
    }
    }
  1. 子父组件通信

    先略了

React Native 中的生命周期

在 RN 程序启动时,内部的虚拟 DOM 开始建立,生命周期时建立在此虚拟DOM 的整个声明周期之中,从虚拟DOM 的初始化到虚拟DOM 的卸载 ,RN 为组件的不同状态建立了不同的生命周期。