0%

摘自《认知觉醒:开启自我改变的原动力》荐读笔记

读完《认知觉醒》2年,来交一份答卷。2021年9月9日,我在微信读书遇到《认知觉醒》,和大部分人一样,翻开书就被吸引了。
那时的我,32岁,大厂工作,但不太顺利。公司内斗,行业衰退,应该算是北上广的“中年危机”吧。站在那个时间点上,我很迷茫,很焦虑,好像被溺到深水里,拼命挣扎,渴望能探出水面,但又十分徒劳。
在人生的某些时刻,我曾有过同样的感受,比如大学毕业找不到工作,从小城市来北京后遭遇失业,只是前两次危机的顺利解决,让我很快松懈下来,并没有思考问题背后的本质。
以至于30岁的这次危机再次如期而至。当时读书很少,对人生、工作、生活的理解都很浅薄,所以也不知道应该怎么解决,只能闷着头走路,探索。
恰好遇到这本书,周岭老师在书中提到,「早读冥写跑」人生五件套,是每个人最基础的成长之道。
这给我一些启发,既然没什么方向,索性就从人生五件套开始改变自己吧,没想到竟然一路坚持下来,还慢慢养成了7个好习惯,形成了我自己的人生七件套。
所以,站在两年的这个节点,我想分享一下,读完这本书后自己的改变。
一来是对本书的反馈,我认为这是一种很好的习惯,书籍也好,文章也罢,绝大部分作者都渴望收到读者的回音。说到底,作者和读者是一种互相“驯化”的关系,如果没有回馈,这种关系就不会牢固,总会觉得缺点什么。
二来,是最近进入了新的平台期,感觉没什么动力。所以,想做个总结,看看这一路坚持而积累的成果,希望能激励自己,再出发。当然,如果你能看到这篇书评,并有幸也能从中获得一些力量,那这些文字就又多了一层意义。
我的人生七件套,「早读冥写跑复记」。
早:
这两年,我在坚持早起,每天早起1.5小时,累计早起516天,粗略估算一下,这让我多出了750+小时。我每天早起后会读书,几乎大部分时候都会进入心流状态,每次还没读过瘾,1个小时就过去了。如果按10个小时读完一本书的话,我大概利用早起的时间读了75本书。
开始早起前,我每天会睡到8点多,晚上熬夜,所以每次起床都很挣扎,起床后为了避免迟到,又急匆匆的挤地铁上班,一天下来迷迷糊糊,没有精气神。开始早起后,睡的也早了,我可以从容的起床、洗漱、吃早饭,还有1个多小时的读书学习时间,这种感觉让我每天都动力满满。
很多人觉得早起痛苦,但其实只需要一个转念。
你想,在每个清晨,阳光刚透过窗子,大部分人还在睡懒觉的时候,你已经早起洗漱完毕,倒上一杯热茶或咖啡,坐在书桌前,开始了一天的学习,这是一件多有意义的事情!
读:
这两年,我几乎每天都在阅读,累计阅读668天,读完了140+本,听完了100+本。一开始读的很慢,但渐渐越读越快,2021读了20多本,2022读了50本,今年的目标是102本,目前已经读了一多半。这些书都化成了我的养料,以各种各样的方式,支撑我前行。
毫不夸张的说,是阅读重塑了我,正是有了阅读,我才会打开认知,才会有了这两年持续的改变。
冥:
最近1年,我也开始学着冥想,累计冥想249天。冥想不是神秘的宗教仪式,而是一种锻炼专注能力、元认知能力的方法。练习冥想前,我的觉知能力很差,很容易生气,很容易愤怒,很容易沉浸在负面情绪里。
但练习冥想后,我的觉察能力大幅提升,现在一有负面情绪,就能很快意识到,并开始分析,这个情绪哪儿来的?
写:
这两年,我开始坚持写作,累计写作487天,写了100万+字,2021写了几万字,2022写了60万字,今年已经写了40多万字。每读完一本书就会写一篇书评,已经写了82篇。
一开始写的很烂、很慢,一篇2000字的文章要写4个小时,但慢慢就越写越快、越写越好了,现在一篇2000字的文章1个多小时就能写完。
而且,我明显感觉到,我的学习能力、思考能力、分析能力、表达能力都大幅提高。
更重要的是,我发现我很喜欢写作的感觉,虽然也有写不下去的痛苦,但更多时候会进入心流状态,内心被激情和愉悦填满。
周岭老师说,我们要拥有自己的作品,带着作品和别人社交,这是一种更高级的方式。你无需考虑如何说话,如何取悦他人,只要你持续写出有价值的作品,就会不断有人被你吸引,和你交朋友。
我开了公号,今年有了1万+粉丝,微读也有了1.3万关注,这就是持续写作的魅力。
跑:
这两年,也开始坚持运动了,累计运动472天。每天晚上我会做10分钟力量锻炼,俯卧撑、仰卧起坐、蹲起等等,没想到竟然练出了腹肌、胸肌和肱二头肌,虽然不是爆炸性的,但我感觉很好,再小的事情,只要坚持做,就会回报你结果,这也让我越来越有耐心。
我也开始跑步,2022跑了200公里,2023计划跑500公里,目前已完成350公里。一开始跑3公里我就会很喘,现在跑10公里都不觉得很累,身体素质明显变强了。更重要的是,感觉意志力也变强了,以前做事耐性很差,现在越来越能坚持了。
复:
这两年,也开始学会做每日复盘,已经累计复盘532天。2021复盘了81天,2022复盘了279天,今年已经坚持复盘200多天。
每天总结一下工作情况,写成功日记和感恩日记,在重要事件上进行深入反思。复盘给我带来最大的改变是,以前总喜欢抱怨别人,总觉得自己没问题,但现在一遇到事情,就会下意识思考,自己能做点什么。
人其实没办法改变别人,只能改变自己,向外求是不会有结果的,最好的办法是反求诸己。思考自己最终想要的结果是什么,然后承担责任,解决问题。
记:
这两年,也在坚持做时间记录,已经累计记录392天。每天记录时间开销,什么时间,做了什么事情,到晚上再做一个复盘总结。今天的时间都去哪儿了?有没有浪费时间?有没有使用不合理的地方?明天需要怎么调整。
这倒不是让自己成为一个工作狂魔,时间怎么分配由我自己决定,但不要让时间在自己毫不知情的情况下,被娱乐、短视频、资讯吸走。
做时间记录前,我对时间花到哪儿了,完全没有概念,但开始记录后,我对时间就敏感了很多,即使我被短视频吸走了,也能很快觉醒出来,而不会沉浸在里面一两个小时。或者说,是多了一种紧迫感,知道有更重要的事情等着我去做。
以上,就是我的人生七件套。现在回想,能有这些的改变,多亏了有阅读。
如果没有读《认知觉醒》,我就不会知道,阅读量<思考量<行动量<改变量,就不会通过阅读、思考和行动去改变自己,而仍旧沉浸在读完一本接一本书的快感里。
如果没有读《早起的奇迹》《把时间当作朋友》,我就不会知道,那些厉害的人都在早起学习,也就不会通过早起修炼自己,而是每天依旧在睡懒觉。
如果没有读《学会写作》《写作法宝》,我就不会知道,普通人改变自己命运最好的方式是写作,也就不会通过写作深度思考,而是每天仍旧过得浑浑噩噩。
如果没有读《运动改造大脑》《跑步治愈》,我就会不知道,坚持跑步有那么多意义,也就不会通过跑步锻炼身体,而是每天继续消耗自己。
如果没有读《十分钟冥想》《冥想》,我就不会知道,冥想能够培养人的觉察能力,也就不会通过冥想锻炼自己的大脑,而是继续被负面情绪控制。
如果没有读《复盘》《好好学习》我就不会知道,复盘能让人在犯错中成长,也就不会通过复盘反求诸己,而是仍旧在怨天尤人……
当然,这一切的起点都源于《认知觉醒》,在我最困难,最无助,最彷徨的时候,是这本书指引我走上改变之路。
所以,如果你想改变自己,不妨从阅读开始,从读《认知觉醒》开始,从“早读冥写跑复记”开始。
最后,假如你也和曾经的我一样,此时此刻刚好陷入困境,正饱受折磨,那么我想告诉你:“尽管眼下十分艰难,可日后这段经历说不定就会开花结果。”
这个世界有不少类似“缝隙”的地方,只要走运,找到适合自己的“缝隙”,就好歹能生存下去。
以前,我总想试图改变自己的命运,但这实在异常艰难。后来我想,一个人如果能找到自己的“缝隙”,以自己喜欢的方式生活,就已经是上天巨大的恩赐了。
因此,无论如何,请奋力前行。

Handler机制组成元素之间的关系

Handler机制主要有Handler、MessageQueue、Message、Looper几个元素构成。
它们之间的关系是:

  • 一个线程只有一个Looper实例
  • Looper中持有队列mQueue
  • Handler持有队列mQueue和Looper对象。在构造Handler实例时如果没有Looper入参,那就默认使用当前线程的Looper,ThreadLocal<Looper>.get()
  • Message中持有handler和next message
  • MessageQueue中持有当前message
  • 生产消息:Handler发送的信息通过MessageQueue.enqueueMessage将消息入队
  • 消费消息:Looper.loopOnce中将MessageQueue的消息取出,调用Message.target.dispatchMessage,target属性就标记了消息最终交给哪个Handler处理,所以这里的含义是在生产Msg的Handler中执行处理逻辑;如果MessageQueue信息为空,就会执行被挂起的IdleHandler。

dispatchMessage方法分析

dispatchMessage方法关系到消息队列中消息所对应的处理逻辑最终在哪如何被处理

title:android.os.Handle.dispatchMessage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**  
* Handle system messages here.
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

方法有三个逻辑分支,都是处理MessageQueue抛出的Msg:

  1. msg.callback不为空
    message.callback.run() Message的callback成员是一个Runnable对象
  2. Handler.mCallback不为空
    由Handler.Callback的接口实现来处理
  3. msg.callback和Handler.mCallback都为空
    由Handler.handleMessage方法处理,子类没重写则默认不处理

Handler处理并发实现

title:android.os.MessageQueue.enqueueMessage消息入队
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
boolean enqueueMessage(Message msg, long when) {  
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
// 因为可能在任意对象操作入队,而只会在looper所绑定的线程出队,所以这里加对象锁,保证入队出队操作是线程安全的
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
// 当前队列为空
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
// 按调度时间调整队列位置
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}

// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
title:android.os.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
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
103
104
105
Message next() {  
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis); // 用来检查消息队列中是否有新的消息要处理,当队列为空时,`nativePollOnce` 会使线程等待直到:1. 有新消息到达。2. 被唤醒去处理其他任务(例如,定时事件、输入事件等)。3. 明确使用 `wakeUp()` 方法唤醒。

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 {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 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 {
// No more messages.
nextPollTimeoutMillis = -1;
}

// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}

// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true; // 没有消息,休眠
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
title:android.os.Looper
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
private static boolean loopOnce(final Looper me,  
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;

final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;

if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}

final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (me.mSlowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
me.mSlowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
me.mSlowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();

return true;
}

/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
@SuppressWarnings("AndroidFrameworkBinderIdentity")
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
if (me.mInLoop) {
Slog.w(TAG, "Loop again would have the queued messages be executed"
+ " before this one completed.");
}

me.mInLoop = true;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);

me.mSlowDeliveryDetected = false;

for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}

Handler的并发处理其实就是消息入队和出队被处理的过程

  1. 可以在任意线程将消息入队,具体线程由Handlder.sendMsg的方法栈决定
  2. 只会在Looper.loop方法中执行出队,而Looper.loop只会在指定的一个线程中执行的,也就是消息最终被处理的线程
  3. 可以看出入队和出队可能是在不同的线程中执行的,在MessageQueue中通过对象锁来保证线程安全

Hanlder与ANR的关系

消息阻塞机制

当主线程阻塞超过5s之后,就会触发ANR;前面我们知道,在Looper开启死循环取消息的时候,如果消息队列中没有消息的时候,就可能会被block,调用了nativePollOnce,那么为什么没有阻塞主线程呢?

其实应该把这分为两件事来看,looper.loop是用来处理消息,当没有消息的时候,主线程就休息了,不需要干任何事;像input事件,其实就是一个Message,当它加入到消息队列的时候,会调用nativeWake唤醒主线程,主线程来处理这个消息,只有处理这个消息超时,才会发生ANR,而不是死循环会导致ANR。

案例分析

title:"线程休眠"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x7185b6a8 self=0xb400007375b4bbe0
| sysTid=3433 nice=0 cgrp=default sched=0/0 handle=0x749c9844f8
| state=S schedstat=( 800801640 66783841 881 ) utm=60 stm=19 core=0 HZ=100
| stack=0x7fc20cb000-0x7fc20cd000 stackSize=8192KB
| held mutexes=
native: #00 pc 000000000009ca68 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 0000000000019d88 /system/lib64/libutils.so (android::Looper::pollInner(int)+184)
native: #02 pc 0000000000019c68 /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+112)
native: #03 pc 0000000000112194 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loop(Looper.java:183)
at android.app.ActivityThread.main(ActivityThread.java:7723)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:997)

在我们分析ANR日志时,经常会看到这样表现,结合上面我们对于Handler的了解,这个时候其实就是没有消息了,我们看已经调用了nativePollOnce方法,此时主线程就休眠了,等待下一个消息到来。

title:"ANR堆栈"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x7185b6a8 self=0xb400007375b4bbe0
| sysTid=3906 nice=-10 cgrp=default sched=0/0 handle=0x749c9844f8
| state=S schedstat=( 2591708189 61276010 2414 ) utm=220 stm=38 core=5 HZ=100
| stack=0x7fc20cb000-0x7fc20cd000 stackSize=8192KB
| held mutexes=
// ......
- waiting to lock <0x0167ghe6d> (a java.lang.Object) held by thread 5
// ...... 方法调用
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7723)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:997)

在这段堆栈中,我们看到主线程已经是出问题了,处于Blocked的状态,那么在Handler调用dispatchMessage方法的时候,是调用了handleCallback,说明此时是调用了post方法,在post方法中,主线程一直想要获取其他线程持有的一把锁,导致了超时产生了ANR。

日常开发中很多地方都见到了LayoutInflater.from().inflate()方法去将一个布局文件的内容填充为一个View,特别是inflate()这个方法,这个方法的参数有布局文件id,root和attachToRoot,那么这个root和attachToRoot参数有什么作用呢?

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
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {  
synchronized (mConstructorArgs) {

View result = root;

try {
advanceToRootNode(parser);
final String name = parser.getName();

if (TAG_MERGE.equals(name)) {
// merge标签处理
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}

rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}

// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}

} catch (XmlPullParserException e) {

} catch (Exception e) {

} finally {
// Don't retain static reference on context.

}

return result;
}
}

分析root与attachToRoot的四种组合情况,可以得出以下结论:

root null null not null not null
attachToRoot true false true false
返回创建的不会带params的view,不作为root的子view 创建出来的View没有设置params就使用,所以被创建出来的布局,最外层设置的宽高是无效的,并最终返回了创建出来的View对象 view带param添加到root中,返回root 返回创建的带params的view,不作为root的子view
  • root = null,无论attachToRoot取什么值都是一样的效果

Kotlin 的泛型和 Any 都是 Kotlin 语言中的重要概念,它们在处理类型和数据时有不同的用法和作用。下面详细说明它们的联系和区别:

联系

  1. 类型参数的上界: 每个 Kotlin 的泛型类型参数都有一个默认的上界,即 Any?。这意味着如果你没有指定泛型类型参数的上界,默认情况下它可以是任何类型,包括 null

    1
    class Box<T>(val value: T)  // T 的默认上界是 Any?
  2. 兼容性: 泛型类型参数可以指定上界为 Any,这意味着它只能接受非空类型。

    1
    class Box<T : Any>(val value: T)  // T 的上界是 Any,所以 T 不能是 null

区别

  1. 基本概念:
  • 泛型(Generics):提供了类型参数化能力,使得类和函数可以处理多个不同的类型,而不需要在定义时指定具体的类型。

    1
    class Box<T>(val value: T)  // T 是一个泛型类型参数
  • AnyAny 是所有非空类型的根类型,相当于 Java 的 ObjectAny? 是所有类型(包括 null)的根类型。

  1. 类型安全和约束:
  • 泛型类型可以指定上界来约束它可以接受的类型。没有指定上界的泛型类型参数的上界默认是 Any?,即允许任何类型,包括 null
1
class Box<T : Number>(val value: T)  // T 的上界是 Number,所以只能接受数字类型
  • Any 只能接受非空类型,如果需要允许空值,应该使用 Any?
1
2
3
4
5
6
7
fun printValue(value: Any) {  // 只接受非空的任何值
println(value)
}

fun printValue(value: Any?) { // 可以接受任何值,包括 null
println(value)
}
  1. 类型擦除: Kotlin 的泛型在运行时类型信息会被擦除,类型参数信息只在编译期间有效。而 Any 是具体的类型,不会被类型擦除。
1
2
3
4
5
6
7
fun <T> isString(value: T): Boolean {
return value is String // 会出现类型擦除的问题,此处 is String 仍然会编译通过但有警告
}

fun isString(value: Any): Boolean {
return value is String // 不会被类型擦除,运行时仍然有效
}
  1. 类型参数的使用: 泛型类型参数可以在类、接口和函数中使用,使得代码更加通用和灵活。
1
2
3
4
5
6
7
class Container<T>(val content: T) {
fun getContent(): T = content
}

fun <T> createContainer(item: T): Container<T> {
return Container(item)
}

总结来说,Kotlin 的泛型提供了类型参数化的能力,使代码更灵活和可重用,而 Any 是所有非空类型的根类型,提供了对象类型的基本功能。泛型在编译期间处理,而 Any 在运行时也有效,两者在处理类型时有不同的侧重点和用法。

Kotlin 的泛型系统支持定义上界和下界约束,包括协变和逆变。这些特性使得 Kotlin 的泛型类型系统更加灵活和强大。下面是对 Kotlin 泛型上界和下界的详细说明:

泛型的上界 (Upper Bounds)

上界用于限制泛型类型参数的类型范围。默认情况下,泛型类型参数具有一个隐式的上界 Any?。你可以通过显式声明上界来进一步限制类型参数的类型范围。

示例

  1. 默认隐式上界

    1
    class Box<T>(val value: T)  // T 的隐式上界是 Any?
  2. 显式上界

    1
    class Box<T : Number>(val value: T)  // T 的上界是 Number,因此只能接受 Number 的子类型
  3. 多重上界: 如果需要使用多个上界,可以使用 where 子句来进行声明。

    1
    2
    3
    4
    5
    6
    fun <T> ensureAllNonNull(vararg args: T) where T : Any, T : Comparable<T> {
    for (arg in args) {
    checkNotNull(arg)
    println(arg)
    }
    }

泛型的下界 (Lower Bounds)

Kotlin 不直接提供显式声明下界的语法,但下界的概念可以通过协变和逆变来实现。协变和逆变是用于描述类型参数之间的子类型关系的概念,它们定义了类型参数在子类型化关系中的行为。

协变 (Covariance)

协变允许使用子类型来替代泛型类或接口的类型参数。在 Kotlin 中,协变使用关键字 out 来定义,表示泛型类型参数只能出现在输出位置(返回类型),不能出现在输入位置(函数参数)。

1
2
3
4
5
6
7
interface Source<out T> {
fun nextT(): T
}

fun demo(source: Source<String>) {
val anySource: Source<Any> = source // 协变,String 是 Any 的子类型,所以 Source<String> 可以赋值给 Source<Any>
}

逆变 (Contravariance)

逆变允许使用超类型来替代泛型类或接口的类型参数。在 Kotlin 中,逆变使用关键字 in 来定义,表示泛型类型参数只能出现在输入位置(函数参数),不能出现在输出位置(返回类型)。

1
2
3
4
5
6
7
interface Consumer<in T> {
fun consume(item: T)
}

fun demo(consumer: Consumer<Number>) {
val intConsumer: Consumer<Int> = consumer // 逆变,Int 是 Number 的子类型,所以 Consumer<Number> 可以赋值给 Consumer<Int>
}

使用示例

以下是一个综合示例如 Box 类和函数如何利用泛型的上界和协变逆变:

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
// 定义一个具有上界的泛型类
class Box<T : Number>(val value: T)

fun <T : Number> sum(value1: T, value2: T): Double {
return value1.toDouble() + value2.toDouble()
}

// 定义一个协变接口
interface Source<out T> {
fun nextT(): T
}

// 定义一个逆变接口
interface Consumer<in T> {
fun consume(item: T)
}

fun main() {
// 使用具有上界的泛型类
val intBox = Box(1)
val doubleBox = Box(1.0)

println(sum(1, 2)) // 输出: 3.0
println(sum(1.0, 2.0)) // 输出: 3.0

// 示例协变
val stringSource: Source<String> = object : Source<String> {
override fun nextT(): String {
return "Hello"
}
}
val anySource: Source<Any> = stringSource
println(anySource.nextT()) // 输出: Hello

// 示例逆变
val numberConsumer: Consumer<Number> = object : Consumer<Number> {
override fun consume(item: Number) {
println("Consumed $item")
}
}
val intConsumer: Consumer<Int> = numberConsumer
intConsumer.consume(10) // 输出: Consumed 10
}

总结

  • 上界:限制泛型类型参数的类型范围,默认是 Any?,可以显式地使用 :<类型> 来指定。
  • 下界:Kotlin 没有直接的下界语法,但可以通过协变(out)和逆变(in)来实现特定的行为。
  • 协变:使用 out 关键字表示,只能用于返回类型,允许将子类型泛型对象赋值给超类型的变量。
  • 逆变:使用 in 关键字表示,只能用于输入类型,允许将超类型泛型对象赋值给子类型的变量。

开闭原则 OCP(Open-Closed Principle)

对扩展开放,对修改关闭

根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的

开闭原则的实现方法
为了满足开闭原则的对修改关闭原则以及扩展开放原则,应该对软件系统中的不变的部分加以抽象,在面向对象的设计中

  • 可以把这些不变的部分加以抽象成不变的接口,这些不变的接口可以应对未来的扩展;
  • 接口的最小功能设计原则。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现;
  • 模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

接口可以被复用,但接口的实现却不一定能被复用。
接口是稳定的,关闭的,但接口的实现是可变的,开放的。
可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能,实现软件系统的柔性扩展。

迪米特法则(最少握手) LoD(Law Of Demeter or Principle of Least Knowledge)

一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

里氏替换原则 LSP(Liskov Substitution Principle)

任何超类出现的地方可以用任一派生类进行替换,实践中可用以校验继承是否有必要以及是否合法

只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

  • 不应该在代码中出现if/else之类对派生类类型进行判断的条件。
  • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

同时LSP体现了:

  • 类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。
  • 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。

重构违反LSP的设计

如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

  • 创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。
  • 从B到A的继承关系改为关联关系。

单一职责原则  SRP(Single Resposibility Principle)

永远不要让一个类存在多个改变的理由。

要控制类的粒度大小,将对象解耦,提高其内聚性,即一个对象不要承担太多职责,一个方法尽量做好一件事,提高其原子性。

接口隔离原则 ISP(Interface Insolation Principle)

不能强迫用户去依赖那些他们不使用的接口。

  • 按分类设计接口,避免耦合

依赖倒置原则  DIP(Dependency Inversion Principle)

A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. 抽象不应该依赖于细节,细节应该依赖于抽象
C.针对接口编程,不要针对实现编程

符合DIP原则的操作:

  • 在高层模块与低层模块之间,引入一个抽象接口层
  • 避免依赖传递

组合/聚合复用原则

尽量使用组合/聚合,不要使用类继承

  • 厘清 is-a 与 has-a,区别继承与组合
  • 复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

什么时候才应该使用继承
只有当以下的条件全部被满足时,才应当使用继承关系:

1)派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分”Has-A”和”Is-A”。只有”Is-A”关系才符合继承关系,”Has-A”关系应当用聚合来描述。
2)永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。
3)派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。
4)只有在分类学角度上有意义时,才可以使用继承。

思考:如何培养code sense

  • 多写多想,从实践中摸索总结
  • 注重代码设计,做好设计再coding
  • 提升bad code识别能力,如重复代码的出现、hard code
  • 基于OOP原则,内化设计模式且与其常用场景结合消化,训练强化的结果

参考自:面向对象设计的七大设计原则详解

近期实现的一个功能中,由于后台需要的请求入参数据量过大,而req的载体有大小限制,所以采用了先使用pb文件存储数据上云,云文件索引作为req入参的方案。
本文记录其中使用pb与Java互转的过程。

前言

protobuf是谷歌推出的序列化协议,有比json所占字节少体积小、序列化传输快的特点。再加上其同样有平台无关的特性,protobuf得以广泛应用。

常见的使用场景:

  1. 前端与后端的通信数据载体
  2. 多端复用的数据结构,如草稿库
  3. 作为轻量级文件存储数据(本次的使用场景)

ProtoBuf转成Java过程记录

编写proto文件

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
syntax = "proto2";  

package demo;

option java_multiple_files = true;
option java_package = "com.darrenyuen.demo.protos";
option java_outer_classname = "DemoProtos";

message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}

repeated PhoneNumber phones = 4;
}

message AddressBook {
repeated Person people = 1;
}

命令编译

proto语法是跨平台的,所以我们需要需要对应平台的编译器工具,编译成java文件,比如笔者在MacOs下,需要下载这个平台的编译工具。

1
2
3
4
protoc --java_out=输出目录 编译文件 -I=编译文件所在的文件夹

# 编译.proto文件为Kotlin文件
# protoc --proto_path=src/test/proto --kotlin_out=src/test/kotlin src/test/proto/addressbook.proto
  • protoc:Protobuf 编译器的命令行工具,用于编译 Protobuf 文件。
  • --java_out=输出目录:指定生成的 Java 代码的输出目录。编译器会将生成的代码放置在这个目录下。
  • 编译文件:Protobuf 文件的路径,这些文件包含了消息定义。
  • -I=编译文件所在的文件夹:指定 Protobuf 文件所在的目录。编译器会在这个目录下搜索指定的文件。
    执行编译后,便可拷贝生成java文件到工程中使用

集成插件编译

  1. 集成gradle插件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 根目录 build.gradle文件,引入插件
    buildscript {
    repositories {
    mavenCentral()
    }

    dependencies {
    classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18'
    }
    }
    ``
    1
    2
    // app目录 build.gradle 引入pb-java库
    implementation 'com.google.protobuf:protobuf-java:3.8.0'
1
2
3
4
// app目录 build.gradle 声明使用插件
plugins {
id 'com.google.protobuf'
}
  1. 编译配置
    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
    // app目录 build.gradle
    sourceSets {
    main {
    proto {
    srcDir 'src/main/proto'
    }
    }
    }

    protobuf {
    protoc {
    artifact = 'com.google.protobuf:protoc:3.18.1'
    }
    generateProtoTasks {
    all().each { task ->
    task.builtins {
    java {
    option "lite"
    }
    // Generates Python code
    // python { }
    // Generates Kotlin code
    // kotlin { }
    }
    }
    }
    }
  2. 使用结果文件
    编译后,结果文件会生成并存储在{PROJECT_ROOT}/app/build/generated/source/proto中,可拷贝到工程中使用

Java数据序列化成pb文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val filePath = "dir/${System.currentTimeMillis()}.pb"  
var outputStream: FileOutputStream? = null
try {
val demoParam = addressBookBuilder.build()
FileUtils.createOrExistsFile(File(filePath))
outputStream = FileOutputStream(filePath)
try {
demoParam.writeTo(outputStream)
} catch (t: Throwable) {

}
} catch (t: Throwable) {

} finally {
outputStream?.close()
}

参考文章:
如何在Android上从零使用Protobuf
适用于 Gradle 的 Protobuf 插件

(一)OpenGL函数库
格式:

<库前缀><根命令><可选的参数个数><可选的参数类型> 库前缀有 gl、glu、aux、glut、wgl、glx、agl 等等。

1、核心函数库主要可以分为以下几类函数
(1) 绘制基本的几何图元函数。如:glBegain().

(2) 矩阵操作、几何变换和投影变换的函数。如:矩阵入栈glPushMatrix(),还有矩阵的出栈、转载、相乘,此外还有几何变换函数glTranslate*(),投影变换函数glOrtho()和视口变换函数glViewport()等等。

(3) 颜色、光照和材质函数。

(4) 显示列表函数,主要有创建、结束、生成、删除和调用显示列表的函数glNewList()、glEndList()、glGenLists()、glDeleteLists()和glCallList()。

(5) 纹理映射函数,主要有一维和二维纹理函数,设置纹理参数、纹理环境和纹理坐标的函数glTexParameter*()、glTexEnv*()和glTetCoord*()等。

(6) 特殊效果函数。

(7) 选着和反馈函数。

(8) 曲线与曲面的绘制函数。

(9) 状态设置与查询函数。

(10) 光栅化、像素函数。

2、OpenGL实用库(The OpenGL Utility Library)(GLU)
包含有43个函数,函数名的前缀名为glu.

(1) 辅助纹理贴图函数。

(2) 坐标转换和投影变换函数。

(3) 多边形镶嵌工具。

(4) 二次曲面绘制工具。

(5) 非均匀有理B样条绘制工具。

(6) 错误反馈工具,获取出错信息的字符串gluErrorString()

3、OpenGL辅助库(AUX)
包含有31个函数,函数名前缀名为aux

这部分函数提供窗口管理、输入输出处理以及绘制一些简单的三维物体。

4、OpenGL工具库(OpenGL Utility Toolkit)(GLUT)
包含大约30多个函数,函数前缀名为glut,此函数由glut.dll来负责解释执行。

(1) 窗口操作函数。窗口初始化、窗口大小、窗口位置等函数glutInit() glutInitDisplayMode()、glutInitWindowSize() glutInitWindowPosition()等。

(2) 回调函数。响应刷新消息、键盘消息、鼠标消息、定时器函数等,GlutDisplayFunc()、glutPostRedisplay()、 glutReshapeFunc()、glutTimerFunc()、glutKeyboardFunc()、 glutMouseFunc()。

(3) 创建复杂的三维物体。这些和aux库函数功能相同。如创建球体glutWireSphere().

(4) 函数菜单

(5) 程序运行函数 glutAttachMenu()

5、16个WGL函数,专门用于OpenGL和Windows窗口系统的联接,其前缀名为wgl
(1) 绘制上下文函数。 wglCreateContext()、wglDeleteContext()、wglGetCurrentContent()、wglGetCurrentDC() wglDeleteContent()等。

(2) 文字和文本处理函数。wglUseFontBitmaps()、wglUseFontOutlines()。

(3) 覆盖层、地层和主平面处理函数。wglCopyContext()、wglCreateLayerPlane()、 wglDescribeLayerPlane()、wglReakizeLayerPlatte()等。

(4) 其他函数。wglShareLists()、wglGetProcAddress()等。

(二)函数库列表
1、OpenGL应用函数库
gluBeginCurve,gluEndCurve 定义一条不一至的有理的NURBS曲线
gluBeginPolygon,gluEndPolygon 定义一个非凸多边形
gluBeginSurface,gluEndSurface 定义一个NURBS曲线
gluBeginTrim,gluEndTrim 定义一个NURBS整理循环
gluBuild1Dmipmaps 建立一维多重映射
gluBuild2Dmipmaps 建立二维多重映射
gluCylinder 绘制一个圆柱
gluDeleteNurbsRenderer 删除一个NURBS对象
gluDeleQuadric 删除一个二次曲面对象
gluDeleteTess 删除一个镶嵌对象
gluDisk 绘制一个盘子
gluErrorString 根据OpenGL或GLU错误代码产生错误字符串
gluGetNutbsProperty 得到一个NURBS属性
gluGetString 得到一个描述GLU版本号或支持GLU扩展调用的字符串
gluGetTessProperty 得到一个镶嵌对象
gluLoadSamplingMatrices 加载NUMRBS例子和精选矩阵
gluLookAt 设定一个变换视点
gluNewNurbsRenderer 创建一个NURBS对象
gluNewQuadric 建立一个二次曲面对象
gluNewTess 建立一个镶嵌对象
gluNextContour 为其他轮廓的开始做标记
gluNurbsCallback 为NURBS对象设定一个回调
gluNnrbsCurve 设定一个NuRBS曲线的形状
gluNurbsProperty 设定一个NURBS属性
gluNurbsSurface 定义一个NURBS表面的形状
gluOrtho2D 定义一个二位正交投影矩阵
gluPartialDisk 绘制一个盘子的弧
gluPerspective 设置一个透视投影矩阵
gluPickMatrix 定义一个拾取区间
gluProject 将对象坐标映射为窗口坐标
gluPwlCurve 描述一个分段线性NURBS修剪曲线
gluQuadricCallback 为二次曲面对象定义一个回调
gluQuadricDrawStyle 为二次曲面设定合适的绘制风格
gluQuadricNormals 定义二次曲面所用的法向的种类
gluQuadricOrientation 定义二次曲面内部或外部方向
gluQuadricTexture 定义是否带二次其面做纹理帖图
gluScaleImage 将图象变换为任意尺寸
gluSphere 绘制一个球体
gluTessBeginContour,gluTessEndContour 划定一个边界描述
gluTessBeginPolygon,gluTessEndPolygon 划定一个多边形描述
gluTessCallback 为镶嵌对象定义一个回调
gluTessNormal 为一个多边行形定义法向
gluTessProperty 设置镶嵌对象的属性
gluTessVertex 定义在一个多边形上的顶点
gluUnProject 将窗口坐标映射为对象坐标

2、OpenGL核心函数库
glAccum 操作累加缓冲区
glAddSwapHintRectWIN 定义一组被 SwapBuffers 拷贝的三角形
glAlphaFunc 允许设置 alpha 检测功能
glAreTexturesResident 决定特定的纹理对象是否常驻在纹理内存中
glArrayElement 定义一个被用于顶点渲染的数组成分
glBegin,glEnd 定义一个或一组原始的顶点
glBindTexture 允许建立一个绑定到目标纹理的有名称的纹理
glBitmap 绘制一个位图
glBlendFunc 特殊的像素算法
glCallList 执行一个显示列表
glCallLists 执行一列显示列表
glClear 用当前值清除缓冲区
GlClearAccum 为累加缓冲区指定用于清除的值
glClearColor 为色彩缓冲区指定用于清除的值
glClearDepth 为深度缓冲区指定用于清除的值
glClearStencil 为模板缓冲区指定用于清除的值
glClipPlane 定义被裁剪的一个平面几何体
glColor 设置当前色彩
glColorMask 允许或不允许写色彩组件帧缓冲区
glColorMaterial 使一个材质色彩指向当前的色彩
glColorPointer 定义一列色彩
glColorTableEXT 定义目的一个调色板纹理的调色板的格式和尺寸
glColorSubTableEXT 定义目的纹理的调色板的一部分被替换
glCopyPixels 拷贝帧缓冲区里的像素
glCopyTexImage1D 将像素从帧缓冲区拷贝到一个单空间纹理图象中
glCopyTexImage2D 将像素从帧缓冲区拷贝到一个双空间纹理图象中
glCopyTexSubImage1D 从帧缓冲区拷贝一个单空间纹理的子图象
glCopyTexSubImage2D 从帧缓冲区拷贝一个双空间纹理的子图象
glCullFace 定义前面或后面是否能被精选
glDeleteLists 删除相邻一组显示列表
glDeleteTextures 删除命名的纹理
glDepthFunc 定义用于深度缓冲区对照的数据
glDepthMask 允许或不允许写入深度缓冲区
glDepthRange 定义 z 值从标准的设备坐标映射到窗口坐标
glDrawArrays 定义渲染多个图元
glDrawBuffer 定义选择哪个色彩缓冲区被绘制
glDrawElements 渲染数组数据中的图元
glDrawPixels 将一组像素写入帧缓冲区
glEdgeFlag 定义一个边缘标志数组
glEdgeFlagPointer 定义一个边缘标志数组
glEnable, glDisable 打开或关闭 OpenGL 的特殊功能
glEnableClientState,glDisableClientState 分别打开或关闭数组
glEvalCoord 求解一维和二维贴图
glEvalMesh1,glEvalMesh2 求解一维和二维点或线的网格
glEvalPoint1,glEvalPoint2 生成及求解一个网格中的单点
glFeedbackBuffer 控制反馈模式
glFinish 等待直到 OpenGL 执行结束
glFlush 在有限的时间里强制 OpenGL 的执行
glFogf,glFogi,glFogfv,glFogiv 定义雾参数
glFrontFace 定义多边形的前面和背面
glFrustum 当前矩阵乘上透视矩阵
glGenLists 生成一组空的连续的显示列表
glGenTextures 生成纹理名称
glGetBooleanv,glGetDoublev,glGetFloatv,glGetIntegerv 返回值或所选参数值
glGetClipPlane 返回特定裁减面的系数
glGetColorTableEXT 从当前目标纹理调色板得到颜色表数据
glGetColorTableParameterfvEXT,glGetColorTableParameterivEXT 从颜色表中 得到调色板参数
glGetError 返回错误消息
glGetLightfv,glGetLightiv 返回光源参数值
glGetMapdv,glGetMapfv,glGetMapiv 返回求值程序参数
glGetMaterialfv,glGetMaterialiv 返回材质参数
glGetPixelMapfv,glGetpixelMapuiv,glGetpixelMapusv 返回特定的像素图
glGetPointerv 返回顶点数据数组的地址
glGetPolygonStipple 返回多边形的点图案
glGetString 返回描述当前 OpenGl 连接的字符串
glGetTexEnvfv 返回纹理环境参数
glGetTexGendv,glGetTexGenfv,glGetTexGeniv 返回纹理坐标生成参数
glGetTexImage 返回一个纹理图象
glGetTexLevelParameterfv,glGetTexLevelParameteriv 返回特定的纹理参数的 细节级别
glGetTexParameterfv,glGetTexParameteriv 返回纹理参数值
glHint 定义实现特殊的线索
glIndex 建立当前的色彩索引
glIndexMask 控制写色彩索引缓冲区里的单独位
glIndexPointer 定义一个颜色索引数组
glInitName 初始化名字堆栈
glInterleavedArrays 同时定义和允许几个在一个大的数组集合里的交替数组
glIsEnabled 定义性能是否被允许
glIsList 检测显示列表的存在
glIsTexture 确定一个名字对应一个纹理
glLightf,glLighti,glLightfv,glLightiv 设置光源参数
glLightModelf,glLightModeli,glLightModelfv,glLightModeliv 设置光线模型参数
glLineStipple 设定线点绘图案
glLineWidth 设定光栅线段的宽
glListBase 为 glcallList 设定显示列表的基础
glLoadIdentity 用恒等矩阵替换当前矩阵
glLoadMatrixd,glLoadMatrif 用一个任意矩阵替换当前矩阵
glLoadName 将一个名字调入名字堆栈
glLogicOp 为色彩索引渲染定义一个逻辑像素操作
glMap1d,glMap1f 定义一个一维求值程序
glMap2d,glMap2f 定义一个二维求值程序
glMapGrid1d,glMapGrid1f,glMapgrid2d,glMapGrid2f 定义一个一维或二维网 格
glMaterialf,glMateriali,glMateriafv,glMaterialiv 为光照模型定义材质参数
glMatrixMode 定义哪一个矩阵是当前矩阵
glMultMatrixd,glMultMatrixf 用当前矩阵与任意矩阵相乘
glNewList,glEndList 创建或替换一个显示列表
glNormal 设定当前顶点法向
glNormalPointer 设定一个法向数组
glOrtho 用垂直矩阵与当前矩阵相乘
glPassThrough 在反馈缓冲区做记号
glPixelMapfv,glPixelMapuiv,glPixelMapusv 设定像素交换图
glPixelStoref,glpixelStorei 设定像素存储模式
glPixelTransferf,glPixelTransferi 设定像素存储模式
glPixelZoom 设定像素缩放因数
glPointSize 设定光栅点的直径
glPolygonMode 选择一个多边形的光栅模式
glPolygonOffset 设定 OpenGL 用于计算深度值的比例和单元
glPolygonStipple 设定多边形填充图案
glPrioritizeTextures 设定纹理固定的优先级
glPushAttrib,glPopAttrib 属性堆栈的压入和弹出操作
glPushClientAttrib,glPopClientAttrib 在客户属性堆栈存储和恢复客户状态值
glPushmatrix,glPopMatrix 矩阵堆栈的压入和弹出操作
glPushName,glPopName 名字堆栈的压入和弹出操作
glRasterPos 定义像素操作的光栅位置
glreadBuffer 为像素选择一个源色彩缓冲区
glReadPixels 从帧缓冲区读取一组数据
glRectd,glRectf,glRecti,glRects,glRectdv,glRectfv,glRectiv,glRectsv 绘制一个三 角形
glRenderMode 定义光栅模式
glRotated,glRotatef 将旋转矩阵与当前矩阵相乘
glScaled,glScalef 将一般的比例矩阵与当前矩阵相乘
glScissor 定义裁减框
glSelectBuffer 为选择模式值建立一个缓冲区
glShadeModel 选择平直或平滑着色
glStencilFunc 为模板测试设置功能和参照值
glStencilMask 控制在模板面写单独的位
glStencilOp 设置激活模式测试
glTexCoord 设置当前纹理坐标
glTexCoordPointer 定义一个纹理坐标数组
glTexEnvf,glTexEnvi,glTexEnvfv,glTexEnviv 设定纹理坐标环境参数
glTexGend,glTexgenf,glTexGendv,glTexGenfv,glTexGeniv 控制纹理坐标的生成
glTexImage1D 定义一个一维的纹理图象
glTexImage2D 定义一个二维的纹理图
glTexParameterf,glTexParameteri,glTexParameterfv,glTexParameteriv 设置纹理参数
glTexSubImage1D 定义一个存在的一维纹理图像的一部分,但不能定义新的纹理
glTexSubImage2D 定义一个存在的二维纹理图像的一部分,但不能定义新的纹理
glTranslated,glTranslatef 将变换矩阵与当前矩阵相乘
glVertex 定义一个顶点
glVertexPointer 设定一个顶点数据数组
glViewport 设置视窗

启动模式

  • DEFAULT:默认启动模式,协程会在创建后立即开始调度,如果在调度前协程被取消,其将直接进入取消响应的状态
  • LAZY:懒启动模式,协程被调用才会启动执行,包括调用协程的start,join或者await等方法时才会被调度,如果调度前就取消那么该协程将直接进入异常结束状态
  • ATOMIC:原子启动模式,立即开始调度,在执行第一个挂起点之前协程不能被取消
  • UNDISPATCHED:不受限的启动模式,协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点才会切线程
    立即调度:表示协程的调度器会立即接收调度指令,但具体执行的时机以及在哪个线程上执行,还需要根据调度器的具体情况而定

调度器

  • Default:默认调度器,适合处理后台计算,是一个CPU密集型任务调度器
  • Main:UI调度器,平台相关,在Android上是主线程
  • Unconfined:无所谓调度器,挂起点恢复执行时会在恢复所在的线程上直接执行,如A(创建协程的线程)-B(协程挂起的线程)-B(协程恢复后执行的线程)
  • IO:IO调度器,适合执行IO相关操作,是一个IO密集型任务调度器

上下文 CoroutineContext

  • 可通过实现AbstractCoroutineContextElement抽象类自定义上下文
  • 常用的有AndroidExceptionPreHandler、CoroutineName
  • Job的作用,没Job实例的协程不能被取消

拦截器

可作为上下文的一部分

可以拦截协程异步回调时的恢复调用,即可以在挂起点恢复执行的位置添加拦截器实现AOP操作。

作用域

作用域的概念,主要用以明确协程之间的父子关系,以及对于取消或者异常处理等传播行为。
协程作用域包括以下三种:

  1. 顶级作用域:没有父协程的协程所在的作用域为顶级作用域
  2. 协同作用域:协程中启动新的协程,新协程为所在协程的子协程,这种情况下子协程所在的作用域默认为协同作用域。子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消
  3. 主从作用域:与协同作用域在协程的父子关系上一致,区别在于处于该作用域下的协程出现未捕获的异常时不会将异常向上传递给父协程
    主从作用域可通过阻断子协程未捕获的异常向上传播实现,如supervisorScope()

除这三种作用域中提到的行为外,父子协程之间还存在以下规则:

  1. 父协程被取消,所有子协程均被取消
  2. 父协程需要等待子协程执行完毕之后才能最终进入完成状态
  3. 子协程会继承父协程的协程上下文中的元素,如果自身有相同的key成员,则覆盖父协程对应的key,覆盖效果仅限子协程作用域

启动协程的方式

launch

  • 返回Job对象,但无法获取协程运行结果

async

  • 返回Deffer对象,可以获取协程运行结果

Deffer中await()方法的作用:

  • 在协程已经执行完成时,立即返回协程执行的结果,如果协程异常结束,则抛出异常
  • 如果协程尚未执行完成,则挂起直到协程执行完成,与join类似

挂起与恢复

挂起的本质是程序执行流程发生异步调用时,当前调用流程的执行状态进入等待状态。
异步调用发生与否,取决于恢复调用函数与对应的挂起函数的调用是否在相同的调用栈上,切换函数调用栈的方法可以是切换到其他线程上执行,也可以是不切换线程但在当前函数返回之后的某一个时刻再执行。

  • 协程内部挂起函数的调用处被称为挂起点,挂起点出现异步调用则当前协程被挂起,直到对应的Continuation的恢复调用函数被调用才会恢复执行
  • 挂起标记 - return非阻塞式挂起当前线程
  • invokeSuspend、resume - 恢复调用

挂起的本质

常用

suspendCoroutine

  • 可处理异步回调的逻辑

suspendCancellableCoroutine

  • 可在协程被取消时收到回调 CancellableContinuation#invokeOnCancellation

Channel

Flow

  • 冷流:有观察者消费时才执行
  • 热流:马上执行

Select

参考文章:Kotlin协程

与其他图片格式对比

WebP是一种由Google开发的现代图像格式,旨在提供更高的压缩率和更好的图像质量,同时保持与现有图像格式相似的视觉质量。与传统的JPEG、PNG和GIF格式相比,WebP具有以下优缺点:

优点:

  1. 更高的压缩率:WebP通常比JPEG格式具有更高的压缩率,可以减小图像文件的大小,加快网页加载速度。
  2. 更好的图像质量:相同文件大小下,WebP格式的图像质量通常比JPEG更好,可以减少图像的失真。
  3. 支持透明度:WebP格式支持透明度,可以创建带有半透明效果的图像,而JPEG不支持透明度。
  4. 动态图像支持:WebP格式支持动态图像,可以替代GIF格式,提供更高的压缩率和更好的图像质量。

缺点:

  1. 兼容性:WebP格式相对较新,不是所有的浏览器和应用程序都支持该格式,可能会导致兼容性问题。
  2. 编解码速度:WebP格式的编解码速度可能比JPEG和PNG慢一些,特别是在处理大型图像时。
  3. 动态图像性能:虽然WebP支持动态图像,但在某些情况下可能性能不如GIF格式。

总的来说,WebP格式在压缩率和图像质量方面有优势,特别适合用于网页图像的优化。但在一些特定情况下,如兼容性和动态图像性能方面可能存在一些缺点。

压缩处理

生成WebP图像通常使用Google的官方工具cwebp命令行工具或libwebp C库。
cwebp支持许多参数来控制输出的WebP图像的质量和大小。以下是一些常用的参数:

  • -q <float>:设置输出的图像质量,范围是0(最差)到100(最好)。默认值是75。
  • -r: 帧率
  • -m <int>:设置压缩效率,范围是0(最快)到6(最慢)。更高的值会产生更小但需要更长时间来编码的文件。默认值是4。
  • -lossless:生成无损的WebP图像。
  • -alpha_q <int>:设置透明度通道的质量。更低的值会产生更小的文件但透明度质量更差。
  • -resize <width> <height>:改变图像的大小到指定的宽度和高度。
  • -o <file>:指定输出文件的名称。
  • -v:显示详细的编码信息。
  • -h:显示帮助信息。

例如,以下命令将JPEG图像转换为质量为80的WebP图像:

1
cwebp -q 80 input.jpg -o output.webp

注意,cwebp的所有参数都是可选的。如果你不提供任何参数,cwebp将使用默认的设置来生成WebP图像。

另外,ffmpeg中也集成了cwebp,可以通过以下命令将视频文件转换成WebP图像:

1
ffmpeg -i input.mp4 -vcodec libwebp -vf scale=480:640 -lossless 0 -loop 0 -r 9 -q 8 output.webp

实践应用

背景:线上能将视频文件转成webp动图的工具的转换过程对用户来说都是一个黑盒操作,我们在使用的时候无法控制转换的参数,如转换的帧率、质量等等,这导致有时得到的webp动图不合期望,故这里开发了一个将视频文件转换成webp动图的mac桌面版工具。

项目链接
https://github.com/Darrenyuen/WebPTransTool

相关技术

  • KMP:开发桌面工具
  • 集成ffmep可执行文件:其中集成的cwebp提供了转换能力

效果展示

  1. 导入需要转换的原始视频,目前仅支持mp4文件和mov文件

  2. 导入后会弹出配置选择弹窗,默认配置是帧率为9、质量为8、使用旧编解码器,转换耗时长与视频大小及配置有关
    !

  3. 转换完成后可以看到