0%

软件开发不只是API调用和基础语法,以下是应用开发的全貌:

一、相信数据

如果你觉得系统正常运转,你应该拿出数据支撑你的想法。更重要的是,当研究证明你是错的时,你应该欣然接受实事。
这就需要做测试来验证你的代码,也需要一些流程来生成对应的数据。
你做的任何事都需要数据来辅助你下一步的决策。否则你将不知道自己做的事是对还是错。

二、软件开发不只是编码

除了编码,开发人员还需要协调、沟通、分析、设计、测试、项目管理等

三、代码就是和人沟通

如果我们真的只是“为计算机编程”,那么我们都将编写字节码。计算机最能理解它。但我们用的是一种“折衷”语言,这种语言其他人可以理解,也可以翻译成计算机可以理解的东西。

好的软件开发是一个交流的过程。这是为了确保人们理解你在做什么。你的工作是与下一个阅读它的人交流。找到最好的表达方式可能需要同理心。

四、好的流程很重要

康威定律预言,你的软件注定要反映你的团队及其沟通结构。流程是沟通的结构。

想象一下一架起飞的飞机:飞行员、副驾驶、机组人员和空中交通管制人员之间有一段非常有条理的对话。这确保了每个人都关注关键问题,每个人都有发言权。“机翼还在飞机上吗?””“检查。“没有别的飞机挡住我们的去路吗?”“可以起飞了。”

五、用结果证明自己,而不是身份低位

最糟糕的开发组织要么等级森严,要么每个开发人员有太多老板。这通常反映了管理者对地位的渴望。

考虑“角色”,而不是“地位”。我工作过的最好的组织都首先认可那些推动事情的人,而不管他们在组织中扮演什么角色。

六、每个人都能从别人那学到

如果您认为人们的种族、性别或其他因素是判断他们的技能或他们必须教给您的东西的好方法,那么您就限制了自己作为软件开发人员的发展。

七、测试你所有的猜想,并随时准备改变这些猜想

当指导年轻的开发者时,我总是强调你不应该证明自己是对的,而应该证明自己是错的。我还鼓励他们用证明自己正确的热情去做这件事。

逻辑理论往往有一种方法可以证明你是错的。如果没有,这可能不是一个很好的理论。如果你不能证明它是错的,那么,只有那时,也许你可以试着证明它是对的。这与“相信数据”类似,但这不仅仅是关于数据,还有你使用数据的方式。

参考来源


一、相关类和结构

二、基本使用

2.1 主线程的事件循环
1
2
3
4
5
6
7
8
9
10
11
12
13
package android.app;

public final class ActivityThread extends ClientTransactionHandler
implements ActivityThreadInternal {

public static void main(String[] args) {
//...
Looper.prepareMainLooper();

Looper.loop();
}
}

2.2 构建自己的事件循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LooperThread extends Thread {
public Handler mHandler;

public void run() {
Looper.prepare();

mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};

Looper.loop();
}
}

三、原理剖析

3.1 构建事件循环
3.2 发送消息
3.3 消息执行
3.4 同步屏障
3.4.1 消息的分类
  • 同步消息
  • 异步消息
  • 屏障消息
3.4.2 同步屏障是什么

一种特殊的Message,其target=null

3.4.3 同步屏障工作原理
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() {
//...

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
//...
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {//碰到同步屏障
// Stalled by a barrier. Find the next asynchronous message in the queue.
// do while循环遍历消息链表
// 跳出循环时,msg指向离表头最近的一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
//...
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
//将msg从消息链表中移除
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
//返回异步消息
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
//...
}
//...
}
}

当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息。

3.4.4 实际应用

Android应用框架中为了更快的响应UI刷新事件在ViewRootImpl.scheduleTraversals中使用了同步屏障

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//设置同步障碍,确保mTraversalRunnable优先被执行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//内部通过Handler发送了一个异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

四、其他

Epoll 实现原理

Linux select/poll机制原理分析

一、概述

  • 资产回报率(ROA)
  • 净资产回报率(ROE)
  • 投入资本回报率(ROIC)
  • 使用资本回报率(ROCE)

不同的回报率,实际上是衡量公司的不同视角。

二、资产回报率(ROA)

2.1 基本概念

运用全部资金获取利润能力的集中体现

$$
ROA=\frac{净利润}{总资产}
$$

2.2 概念拆分

$$
ROA = 净利润率NPM * 资产利用率AU = \frac{净利润}{营业总收入} * \frac{营业总收入}{总资产} = \frac{(主营业务收入+非主营业务收入)}{总资产}
$$

2.3 概念理解

ROA这个回报率衡量的是资产的回报率。
但是一个公司资产负债表上的资产价值更多的是反映了历史而不是当前,
而且不同的行业的ROA具有不可比性。
最简单的例子,很多类金融企业有无息流动负债,增加了总资产,降低了ROA,
但是无偿使用上下游资金实际上是公司有竞争力的体现。

三、净资产回报率(ROE)

2.1 基本概念

ROE的视角是从股东的角度看问题,单纯从股权的角度衡量回报,而不考虑公司的资本结构及负债情况。衡量公司赚钱的效率。

$$
净资产收益率 = \frac{净利润}{净资产}
$$

A公司:10亿净资产赚2亿。ROE = 20%
B公司:50亿净资产赚5亿。ROE = 10%

2.2 概念拆分

$$
净资产收益率 = \frac{净利润}{净资产} = \frac{净利润}{营业收入} * \frac{营业收入}{总资产} * \frac{总资产}{净资产}
$$

$$
净资产收益率 = 净利润 * 资产周转率 * 权益乘数
$$

$$
权益乘数 = \frac{总资产}{净资产}
$$

$$
资产周转率 = \frac{营业收入}{总资产}
$$

影响因素:

  • 净利润:反应盈利能力
  • 资产周转率:反应资产使用效率
  • 权益乘数:反应负债程度

提升ROE的办法:

  • 提高周转率
  • 廉价的债务杠杆
  • 更高的债务杠杆
  • 更低的所得税
  • 更高的运营利润率
2.3 概念理解

ROE对股东来说意义最大,是股票复利增长的源泉。
ROE可以在不同行业与不同企业之间横向比较。
相当于股票这种“股权债券”的收益率。
对投资人来说无论是投资铁路还是互联网,都相当于是买了“股权债券”。
在同等风险情况下,当然是收益率越高越好。
而且,ROE还可以与政府债券、企业债券的收益率跨资产类别横向比较。

不足

ROE的问题在于无法反映债务杠杆对净利润的影响。
如果一个企业借更多的债,一般就能产生更多净利润,ROE也会提高。
但是这样的提高是以牺牲企业经营稳健程度为代价的。
因此,分析ROE一定要结合杜邦公式,并与同行业进行比较。


四、投入资本回报率(ROIC)

ROIC是从资本的角度看问题,综合考虑股权与债权,衡量投资的效率。与ROIC对应的是平均资金成本(WACC)。如果ROIC小于WACC,就说明投入资本的回报小于平均资本成本,公司是在浪费资本。

ROIC=息税前收益(EBIT)*(1-税率)/投入资本

$$
ROIC = \frac{息税前收益(EBIT)*(1-税率)}{投入资本}
$$

$$
投入资本 = 股东权益 + 有息负债
$$

$$
ROIC = \frac{EBIT(1-税率)}{有息负债+权益)}
$$

公式中分子是指一家公司如果完全以权益筹资所应报告的净利润,分母是指公司所有要求回报的现金来源的总和,也就是说,尽管应付账款也是公司的一种现金来源,但因其未附带明确的成本而被排除在外。实质上,ROIC是生产经营活动中所有投入资本赚取的收益率,而不论这些投入资本是被称为债务还是权益。

投入资本回报率是EBIT(税及利息前盈利)经过税率调整比上投入的资本,也就是股东权益加负债减去非运营现金和投资。之所以用EBIT就是因为利息是由债务产生的,是对债权的回报,当计算债权加股权的总的投入资本的回报率时应该把利息加回来。

投入资本也可以表示为净运营资金(流动运营资产减去无息流动负债)加固定资产,加无形资产及商誉,加其他运营资产。

ROIC的问题在于没有考虑无息流动负债的影响。ROIC实际上是衡量投入的总资本的使用效率,与平均资金成本WACC相结合可以揭示一个企业的真正效率。ROIC < WACC 是卖空大师Jim Chanos经常用来寻找卖空对象的一个重要指标。

但是,ROIC对于类金融企业的分析不太容易。一些类金融企业如国美、苏宁、联想、格力等,占用了大量上下游资金作为无息流动负债,因此提高了EBIT。但是,和有息负债一样,无息负债也存在风险。虽然不用付利息,但是一旦经营形式改变,这些资金很有可能像“热钱”一样撤走。国美在黄光裕被捕后就陷入了这样的危险境地,只要供应商撤资,国美就会破产。这些企业虽然ROIC很高,但是静态不稳定,动态稳定。

五、使用资本回报率(ROCE = Return on Capital Employed)

$$
ROCE資本運用報酬率 = \frac{息税前收益(EBIT)}{已動用資本(Capital Employed)}
$$

$$
已動用資本 = 總資產 -流動負債
$$

ROCE实质上是从企业价值(EnterpriseValue,EV)的角度,从并购的角度看资本的回报率。ROCE中的使用资本是股东权益加有息负债再减现金。这实质上相当于1倍PB并购企业时的企业价值。因为如果你以1倍PB并购一个企业,你要付的价格就是股东权益,并且承担所有负债,但也获得所有现金。

ROCE实际上是从一个并购者的角度看问题,如果并购一个企业,付出企业价值EV后的回报率有多少?这与ROIC最大的不同是:ROIC从企业的拥有者的角度看问题,衡量投入资本的使用效率与回报率。而ROCE是从潜在并购者的角度看问题,衡量如果1倍PB并购付出EV后的回报率,也就是并购到底值不值的问题。这接近价值投资买股票就是买公司的原则。

ROCE:使用资本回报率是EBIT(税及利息前盈利)比上使用的资本,也就是所有有息负债加上股东权益。这个指标与ROIC相近,但是使用的是EBIT而没有经过税率调整,因此可以用来比较不同税率的企业。ROCE也容易忽视公司无息负债的融资效应。

总之,本质上是从四个不同的视角看问题,衡量不同的指标。只有全面综合考虑,才能更全面的看问题

六、参考来源

雪球-老韩7
ROCE資本運用報酬率


我们有一个会被重复调用的lambda表达式:

1
2
3
4
5
6
7
8
9
10
11
import kotlin.*

fun main() {
val f: () -> Unit = {
print("A")
print("B")
print("C")
}
f() // ABC
f() // ABC
}

我们给这个lambda加个参数:(String) -> Unit ,因为参数是个函数,为了能调用到函数,我们把它叫做emit吧:

1
2
3
4
5
6
7
8
9
10
11
import kotlin.*

fun main() {
val f: ((String) -> Unit) -> Unit = { emit -> // 1
emit("A")
emit("B")
emit("C")
}
f { print(it) } // ABC
f { print(it) } // ABC
}

添加的参数看起来有点乱🤔,稍微修改一下,抽出来吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import kotlin.*

fun interface FlowCollector {
fun emit(value: String)
}

fun main() {
val f: (FlowCollector) -> Unit = {
it.emit("A")
it.emit("B")
it.emit("C")
}
f { print(it) } // ABC
f { print(it) } // ABC
}

好像能把it的调用逻辑去掉,这样更简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import kotlin.*

fun interface FlowCollector {
fun emit(value: String)
}

fun main() {
val f: FlowCollector.() -> Unit = {
emit("A")
emit("B")
emit("C")
}
f { print(it) } // ABC
f { print(it) } // ABC
}

调用lambda表达式不是很方便。如果把它抽成接口🤔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import kotlin.*

fun interface FlowCollector {
fun emit(value: String)
}

interface Flow {
fun collect(collector: FlowCollector)
}

fun main() {
val builder: FlowCollector.() -> Unit = {
emit("A")
emit("B")
emit("C")
}
val flow: Flow = object : Flow {
override fun collect(collector: FlowCollector) {
collector.builder()
}
}
flow.collect { print(it) } // ABC
flow.collect { print(it) } // ABC
}

抽成可以重复调用的构建器:

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
import kotlin.*

fun interface FlowCollector {
fun emit(value: String)
}

interface Flow {
fun collect(collector: FlowCollector)
}

fun flow(builder: FlowCollector.() -> Unit) = object : Flow {
override fun collect(collector: FlowCollector) {
collector.builder()
}
}

fun main() {
val f: Flow = flow {
emit("A")
emit("B")
emit("C")
}
f.collect { print(it) } // ABC
f.collect { print(it) } // ABC
}

泛型化参数更通用:

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
import kotlin.*

fun interface FlowCollector<T> {
suspend fun emit(value: T)
}

interface Flow<T> {
suspend fun collect(collector: FlowCollector<T>)
}

fun <T> flow(builder: suspend FlowCollector<T>.() -> Unit) = object : Flow<T> {
override suspend fun collect(collector: FlowCollector<T>) {
collector.builder()
}
}

suspend fun main() {
val f: Flow<String> = flow {
emit("A")
emit("B")
emit("C")
}
f.collect { print(it) } // ABC
f.collect { print(it) } // ABC
}

参考

一、什么是状态

状态是一些被View、Widget订阅或观察的对象,它包含数据。任何状态(数据)的变化都会通知给观察它的View、Widget。

  • 网络断开时展示的Toast
  • 发布的博客及其评论
  • 点击按钮时的水波纹动画

二、Compose中的状态

2.1 记住状态

Composable函数可以使用remember在初始化时存储一个对象到内存中。每次recomposition时这个对象都会被返回。

2.2 可观察状态

mutableStateOf函数可以创建一个可观察的可变数据,此数据的变化将触发recomposition

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun MySwitch() {
// I have a State in Compose's MutableState
val checked = remember { mutableStateOf(false) }
Switch(
// Observe the State by accessing the value property
checked = checked.value,
onCheckedChange = {
checked.value = it
}
)
}
2.3 其他状态类型

除了使用MutableState<T>持有状态外,你还可以使用以下这些可观察数据类型来维护状态。

  • LiveData
  • Flow
  • RxJava2

但是,在使用时需要将其转成State<T>类型

  • LiveData<T>.observeAsState()
  • Flow<T>.collectAsState()
2.4 stateless

在Composable函数中保存状态将降低它的可复用性,通过将状态向外抽离来增加其复用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }

HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
2.5 状态恢复

当Activity重建时,想要保留状态,并在重建后恢复状态,可以使用rememberSaveable将状态保存到Bundle中。

  • Parcelize
  • MapSaver
  • ListSaver

三、参考

State and Jetpack Compose
Jetpack Compose State Guideline
Managing State in Android

一、什么是Side-Effect

wiki:在计算机科学中,当函数、表达式、操作会修改它作用域之外的状态变量值时,我们就说它具有副作用。也就是说除返回值之外其他的对外修改都叫副作用。
常见的例子包括:修改非本地变量、修改参数传进来的可变引用、执行I/O等。

二、Compose中的Side-Effect

因为Jetpack Compose是由一系列的@Composable函数组成的,@Composable函数在执行时可能会被跳过(出于优化的角度)或执行多次(recomposition)。
那对于函数内部的网络请求、对外部状态的修改怎么办?有可能执行多次?有可能不执行?这就产生了某种不可预期的错误状态,或者泄露。
为了解决此类问题,Compose提供了相应的API,这些API主要聚焦于对这些副作用的生命周期进行管理。

这些API可以分成两大类:

  • SuspendedEffect
    • rememberCoroutineScope
    • launchedEffect
  • Non-Suspended Side Effects
    • DisposableEffect
    • SideEffect

三、SuspendedEffect

3.1 LaunchedEffect

在首次composition的时候被调用。recomposition时不会再次调用。可以通过改变Key让其重新调用。
它是一个协程作用域,我们可以执行一些挂起函数,当composable函数退出时,协程会被取消。

下面的例子中,启动即执行循环逻辑,每秒修改一次状态值并触发recomposition,但recomposition并不影响LaunchedEffect内的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
fun LaunchedEffect() {
var timer by remember { mutableStateOf(0) }
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
Text("Time $timer")
}

LaunchedEffect(key1 = Unit) {
while (true) {
delay(1000)
timer++
}
}
}
3.2 RememberCoroutineScope

LaunchedEffect的启动和取消跟随Composable函数的生命周期。如果想自己控制生命周期可以使用RememberCoroutineScope

下面的例子中,协程的控制权转移到我们自己手中:

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
@Composable
fun JustRememberCoroutineScope() {
val scope = rememberCoroutineScope()
var timer by remember { mutableStateOf(0) }
var timerStartStop by remember { mutableStateOf(false) }
var job: Job? by remember { mutableStateOf(null) }

Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Time $timer")
Button(onClick = {
timerStartStop = !timerStartStop

if (timerStartStop) {
job?.cancel()
job = scope.launch {
while (true) {
delay(1000)
timer++
}
}
} else {
job?.cancel()
}

}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
}
}

四、Non-Suspended Side Effects

4.1 DisposableEffect

LaunchedEffect一样,立即启动,通过修改key可以再次启动。它提供了一个销毁时的回调,当再次启动时,前一个的onDispose方法会被回调。

下面的例子每次点击按钮改变状态进行recompose,同时会改变DisposableEffect的key,销毁上一个Effect并收到回调。

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
@Composable
fun JustDisposableEffect() {
var timerStartStop by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
}

val context = LocalContext.current

DisposableEffect(key1 = timerStartStop) {
val x = (1..10).random()
Toast.makeText(context, "Start $x", LENGTH_SHORT).show()

onDispose {
Toast.makeText(context, "Stop $x", LENGTH_SHORT).show()
}
}
}
4.2 SideEffect

使用场景:

  • 每次composition / recomposition成功后被调用
  • 用于对外部状态的更新
  • 无需做清理回收操作时

例1:对外部状态的更新

1
2
3
4
5
6
7
8
9
10
@Composable
fun MyScreen(drawerTouchHandler: TouchHandler) {
val drawerState = rememberDrawerState(DrawerValue.Closed)

SideEffect {
drawerTouchHandler.enabled = drawerState.isOpen
}

// ...
}

例2:

1
2
3
4
5
6
7
8
9
10
var i = 0
@Composable
fun MyComposable(){
SideEffect { //this will handle the side effect that may occur
i++
}
Button(onClick = {}){
Text(text = "Click")
}
}

五、参考

Jetpack Compose Effect Handlers
SideEffects and Effects Handling in Jetpack Compose
Jetpack Compose Side Effects Made Easy

一、问题

为了方便Java代码调用Kotlin的object类,我们通常会对object类的方法添加@JvmStatic注解。

然而、当object类实现某个接口时,对应的方法却不能添加@JvmStatic注解。

二、解决

要想解决上述问题,只需要将object类的实现做一些转换:
1
2
3
4
5
6
7
8
9
10
11
12
13

interface Play2 {
fun play()
}

class Singleton2 {

private companion object : Play2 {
@JvmStatic
override fun play() {
}
}
}

三、思考

为什么第一种方式会报错,而第二种方式没问题呢?

通过反编译查看Java代码我们发现:object类的方法会被直接编译成Java的static方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Singleton implements Play {
public static final Singleton INSTANCE;

@JvmStatic
public static void play() {
}

private Singleton() {
}

static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}

而companion object则会被编译成静态内部类

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
public final class Singleton2 {
/** @deprecated */
@Deprecated
public static final Singleton2.Companion Companion = new Singleton2.Companion((DefaultConstructorMarker)null);

@JvmStatic
public static void play() {
Companion.play();
}

private static final class Companion implements Play2 {
@JvmStatic
public void play() {
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

由于static方法是不能重写(override)的,所以同一个关键字不能作用于一个函数。而通过静态内部类的方式,在类的层次增加静态能力,从而消除了方法的static关键字。

参考

issue-KT-21342

Kotlin提供了一些注解可以帮助开发者更好地兼容Java。下面探索一下Kotlin中JVM注解的使用,以及在Java中使用Kotlin类时注解对我们的影响。

1、@JvmName

用于文件、函数、属性、getter和setter。

1.1 给文件添加@JvmName注解

默认情况下,一个Kotlin文件中的functionproperties会被编译成filenameKt.class,其中的class会被编译成classname.class:

1.2 给函数名添加@JvmName注解
1.3 给getter和setting添加@JvmName
1
2
3
4
5

@get:JvmName("getContent")
@set:JvmName("setContent")
var text = ""

2、@JvmDefault

和Java8一样,Kotlin的接口也支持默认方法实现。即使针对Java7及以下版本,他也能正常编译,因为Kotlin使用静态内部类实现默认方法。

当在Java中实现此接口时,需要复写对应的方法,否则将报错。

如果我们希望它在Java8中不报错,可以使用@JvmDefault注解标识方法:

1
2
3
4
interface Document {
@JvmDefault
fun getType() = "document"
}

3、@JvmStatic

针对object classcompanion object使用此注解,可以避免Java访问时的INSTANCE调用

4、@JvmOverloads

Kotlin的默认参数可以帮助我们减少函数重载,简化方法调用参数。在Java中调用含默认参数的Kotlin函数时,需要提供全部参数。

5、@Throws

Kotlin没有受检测异常,try-catch是非必须的。如果希望在Java调用中检测到异常,可以使用@Throws注解

6、@JvmWildcard & @JvmSuppressWildcard

7、@JvmMultifileClass

当在多个文件中定义的顶层函数或属性想要合并到一个编译的class中时,可以使用此注解。

8、@JvmPackageName

和@JvmName一样,此注解可以修改包名,但是他被标记为internal,只能在kotlin库内部使用,这里不做过多介绍。

9、注解一览

[参考]

1.Guide to JVM Platform Annotations in Kotlin

一、初始化流程

1、WorkManager集成了androidx.startup 库,通过startup库的ContentProvider在应用启动时进行初始化。
work-runtime的Manifest文件:

1
2
3
4
5
6
7
8
9
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge" >
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup" />
</provider>

其初始化类为WorkManagerInitializer,其中调用了WorkManager#initialize,然后调用WorkManager的构造函数。
整个流程初始化了如下一些对象:

二、任务

2.1 定义任务

当我们自定义任务时,需要继承Work类,WorkManager内部对Work的结构定义如下:

其中DiagnosticsWorkerCombineContinueationsWorker是内部使用的两个任务。

2.2 提交任务

我们将任务以请求(WorkRequest)的形式提交(enqueue)给WorkManager。WorkManager会将请求装换成内部的WorkContinuation对象。

WorkContinuation可以将多个OneTimeWorkrequest构建成任意依赖关系的无环图。而WorkContinuation的enqueue方法则是任务被执行调度的入口。

2.3 执行任务

对任务进行不同的操作,如:取消、结束、开始等。WorkManager内部会将这些行为转换成对应的Runnable交给调度系统进行执行。

三、任务的调度

提交给WorkManager的任务,会封装成EnqueueRunnable交给默认的SerialExecutor去执行。
EnqueueRunnable的run方法会先将任务加入数据库中保存。然后进行任务调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void run() {
try {
if (mWorkContinuation.hasCycles()) {
throw new IllegalStateException(
String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
}
//加入数据库
boolean needsScheduling = addToDatabase();
if (needsScheduling) {
// Enable RescheduleReceiver, only when there are Worker's that need scheduling.
final Context context =
mWorkContinuation.getWorkManagerImpl().getApplicationContext();
PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
//进行任务调度
scheduleWorkInBackground();
}
mOperation.setState(Operation.SUCCESS);
} catch (Throwable exception) {
mOperation.setState(new Operation.State.FAILURE(exception));
}
}

四、其他

4.1 任务的约束

五、总结

WorkManager库并不算复杂。这种对任务的管理和调度框架的源码,我们可以多看,学习一下里面的设计思路。有个大概的印象,后续写相关逻辑时可以借鉴。

  • 借助startup初始化你的框架
  • 构建有依赖关系的任务
  • 通过Runnable解耦任务的请求和执行。

一、通过RoomDatabase#Callback预填充数据

如果需要在数据库创建或数据库打开的时候添加一些默认数据,可以使用RoomDatabase#Callback接口并复写它的onCreateonOpen方法。由于DAO对象只能在这两个方法返回后使用,所以我们创建一个新线程来插入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Room.databaseBuilder(context.applicationContext,
DataDatabase::class.java, "Sample.db")
// prepopulate the database after onCreate was called
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// moving to a new thread
ioThread {
getInstance(context).dataDao()
.insert(PREPOPULATE_DATA)
}
}
})
.build()

需要注意的是:当app首次启动时,在create和insert之间崩溃的话,数据将永远不会被插入。

二、使用DAO的继承能力

很多DAO里存在一样的InsertUpdateDelete方法。我们可以使用继承来避免这些重复的代码。

1
2
3
4
5
6
7
8
9
interface BaseDao<T> {
@Insert
fun insert(vararg obj: T)
}
@Dao
abstract class DataDao : BaseDao<Data>() {
@Query("SELECT * FROM Data")
abstract fun getData(): List<Data>
}

三、通过@Transaction减少事务查询的模板代码

@Transaction注解的方法会在一个事务里面执行。当这些方法执行异常时,事务也会失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Dao
abstract class UserDao {

@Transaction
open fun updateData(users: List<User>) {
deleteAllUsers()
insertAll(users)
}
@Insert
abstract fun insertAll(users: List<User>)
@Query("DELETE FROM Users")
abstract fun deleteAllUsers()
}

@Query方法带有select语句时,在下列情况下你可能会使用@Transation注解

  • 当查询的结果集很大时,让查询在一次事务里完成能够保证:如果查询结果不满足单个游标窗口,它不会因为在游标窗口交换之间的数据库更改而中断。
  • 如果查询结果是一个带@Relation字段的POJO,此字段会单独查询,所以让他们运行在一个事务中可以确保结果一致。

DeleteUpdateInsert方法包含多个参数时会自动在事务里执行。

四、只读需要的数据

关注App的内存消耗,只加载需要使用的字段,这能提高查询速度减少IO消耗。

1
2
3
4
5
6
7
8
9
@Entity(tableName = "users")
data class User(@PrimaryKey
val id: String,
val userName: String,
val firstName: String,
val lastName: String,
val email: String,
val dateOfBirth: Date,
val registrationDate: Date)

上面的类中,很多字段我们用不到,定义一个类,只包含我们需要的字段:

1
2
3
data class UserMinimal(val userId: String,
val firstName: String,
val lastName: String)

在DAO中,定义方法只查询所需字段:

1
2
3
4
5
@Dao
interface UserDao {
@Query(“SELECT userId, firstName, lastName FROM Users)
fun getUsersMinimal(): List<UserMinimal>
}

五、使用外键

即使Room不直接支持关系,但它允许你定义外键来约束实体之间的关系。

Room的@ForeignKey@Entity注解的一部分,用来支持Sqlite的外键特性。它强制表之间的约束,当你修改表时,保证关系一致性。

看一下UserPet类,Pet有主人,以userId关联。

1
2
3
4
5
6
7
8
@Entity(tableName = "pets",
foreignKeys = arrayOf(
ForeignKey(entity = User::class,
parentColumns = arrayOf("userId"),
childColumns = arrayOf("owner"))))
data class Pet(@PrimaryKey val petId: String,
val name: String,
val owner: String)

你可以定义一些行为,当例子中的User在数据库中被删除或修改。你可以做以下一些操作:NO_ACTIONRESTRICTSET_NULLSET_DEFAULTCASCADE,这些操作和Sqlite保持一致。

注意: 在Room中SET_DEFAULTSET_NULL效果一致,因为Room还不允许为数据列设置默认值。

六、使用@Relation简化一对多查询

在前面User-Pet的例子中,我们有个一对多的关系,一个人可以有多个宠物。当我们想获取主人和宠物集合时:

1
2
data class UserAndAllPets (val user: User,
val pets: List<Pet> = ArrayList())

需要两个查询:

1
2
3
4
5
@Query(“SELECT * FROM Users”)
public List<User> getUsers();

@Query(“SELECT * FROM Pets where owner = :userId”)
public List<Pet> getPetsForUser(String userId);

我们需要遍历用户来查宠物。

为了简化此过程,Room的@Relation注解自动关联实体。此注解只能用于ListSet对象。

1
2
3
4
5
6
7
8
class UserAndAllPets {
@Embedded
var user: User? = null

@Relation(parentColumn = “userId”,
entityColumn = “owner”)
var pets: List<Pet> = ArrayList()
}

在DAO中,我们定义一次查询:

1
2
3
@Transaction
@Query(“SELECT * FROM Users”)
List<UserAndAllPets> getUsers();

七、避免可观察查询的假通知

可观察查询,只关心对应ID的对象:

1
2
3
4
5
6
7
@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): LiveData<User>

// or

@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): Flowable<User>

每次此用户更新的时候你都得到一个新通知。但是其他的一些不影响此ID的修改(delete、update、insert)会发送通知。

这种场景背后的原因是什么:
1、Sqlite支持触发器,DELETEUPDATEINSERT发生时或触发。
2、Room创建了一个InvalidationTracker来跟踪观察表的变化。
3、LiveDataFlowable查询依赖InvalidationTracker.Observer#onInvalidated通知,收到通知就做一次再查询操作。

Room只知道表被修改了,不知道为什么修改也不知道什么被修改。因此,再查询操作会重新通知。由于Room不在内存保存数据也不能保证object的equals方法所以他不知道对象是否变化了。

你需要自己保证DAO过滤收到的事件,只对关心的数据做出响应。

如果观察的是Flowable,使用Flowable#distinctUntilChanged

1
2
3
4
5
6
7
8
9
10
11
12
@Dao
abstract class UserDao : BaseDao<User>() {
/**
* Get a user by id.
* @return the user from the table with a specific id.
*/
@Query(“SELECT * FROM Users WHERE userid = :id”)
protected abstract fun getUserById(id: String): Flowable<User>

fun getDistinctUserById(id: String):
Flowable<User> = getUserById(id).distinctUntilChanged()
}

如果观察的是LiveData,可以用MediatorLiveData处理只让有变化的数据发送事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun <T> LiveData<T>.getDistinct(): LiveData<T> {
val distinctLiveData = MediatorLiveData<T>()
distinctLiveData.addSource(this, object : Observer<T> {
private var initialized = false
private var lastObj: T? = null
override fun onChanged(obj: T?) {
if (!initialized) {
initialized = true
lastObj = obj
distinctLiveData.postValue(lastObj)
} else if ((obj == null && lastObj != null)
|| obj != lastObj) {
lastObj = obj
distinctLiveData.postValue(lastObj)
}
}
})
return distinctLiveData
}

在DAO中,让真正关心变化的方法使用public修饰,查询方法用protected修饰

1
2
3
4
5
6
7
@Dao
abstract class UserDao : BaseDao<User>() {
@Query(“SELECT * FROM Users WHERE userid = :id”)
protected abstract fun getUserById(id: String): LiveData<User>

fun getDistinctUserById(id: String): LiveData<User> = getUserById(id).getDistinct()
}

注意: 如果你返回列表用于展示,考虑使用Paging库,它能帮你处理数据的变化。