Tian chi

⚠️版权声明:文章可以转载,但请注明文章来源:http://itanch.github.io/

  • 主页
  • 技术
  • 记录
所有文章 友链 关于我

Tian chi

⚠️版权声明:文章可以转载,但请注明文章来源:http://itanch.github.io/

  • 主页
  • 技术
  • 记录

Android系统源码阅读(13):Input消息的分发过程

2017-02-14

Android系统源码阅读(13):Input消息的分发过程

请对照AOSP版本:6.0.1_r50。学校电脑好渣,看源码时卡半天


先回顾一下前两篇文章。在设备没有事件输入的时候,InputReader和InputDispatcher都处于睡眠状态。当输入事件发生,InputReader首先被激活,然后发送读取消息,激活Dispatcher。Dispatcher被激活以后,将消息发送给当前激活窗口的主线程,然后睡眠等待主线程处理完这个事件。主线程被激活后,会处理相应的消息,处理完毕后反馈给Dispatcher,从而Dispatcher可以继续发送消息。

1. InputReader获取事件

回顾一下第11章4.2中,InputReader线程在获取事件以后,会调用processEventsLocked(mEventBuffer, count);处理事件。

这里写图片描述

1.1

这里先根据event的种类进行分门别类的处理。

frameworks/native/services/inputflinger/InputReader.cpp :

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
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
for (const RawEvent* rawEvent = rawEvents; count;) {
int32_t type = rawEvent->type;
size_t batchSize = 1;
if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
//如果这里获得是合成事件
//这里一次要将该输入设备中的一组事件都获取出来
int32_t deviceId = rawEvent->deviceId;
while (batchSize < count) {
if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT
|| rawEvent[batchSize].deviceId != deviceId) {
break;
}
batchSize += 1;
}
//处理这些事件
processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
} else {
//这些是一些设备状况的事件,没必要将这些消息发送出去,留给自己处理就可以了
switch (rawEvent->type) {
case EventHubInterface::DEVICE_ADDED:
addDeviceLocked(rawEvent->when, rawEvent->deviceId);
break;
case EventHubInterface::DEVICE_REMOVED:
removeDeviceLocked(rawEvent->when, rawEvent->deviceId);
break;
case EventHubInterface::FINISHED_DEVICE_SCAN:
handleConfigurationChangedLocked(rawEvent->when);
break;
default:
ALOG_ASSERT(false); // can't happen
break;
}
}
count -= batchSize;
rawEvent += batchSize;
}
}

1.2

准备将事件交给设备进行处理。

frameworks/native/services/inputflinger/InputReader.cpp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void InputReader::processEventsForDeviceLocked(int32_t deviceId,
const RawEvent* rawEvents, size_t count) {
//判断设备是否是已知的
ssize_t deviceIndex = mDevices.indexOfKey(deviceId);
if (deviceIndex < 0) {
ALOGW("Discarding event for unknown deviceId %d.", deviceId);
return;
}
//获得设备
InputDevice* device = mDevices.valueAt(deviceIndex);
if (device->isIgnored()) {
//ALOGD("Discarding event for ignored deviceId %d.", deviceId);
return;
}
//交给设备进行处理
device->process(rawEvents, count);
}

1.3

用device中的mapper去映射传入的事件,然后再处理。

frameworks/native/services/inputflinger/InputReader.cpp :

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
void InputDevice::process(const RawEvent* rawEvents, size_t count) {
// Process all of the events in order for each mapper.
// We cannot simply ask each mapper to process them in bulk because mappers may
// have side-effects that must be interleaved. For example, joystick movement events and
// gamepad button presses are handled by different mappers but they should be dispatched
// in the order received.
//一个设备可能有多种类型的event,所有有多个mapper,但是需要保持event的顺序性
//所以这里采用先循环event,再循环mapper的方式
size_t numMappers = mMappers.size();
for (const RawEvent* rawEvent = rawEvents; count--; rawEvent++) {
if (mDropUntilNextSync) {
if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) {
mDropUntilNextSync = false;
//..
} else {
//..
}
} else if (rawEvent->type == EV_SYN && rawEvent->code == SYN_DROPPED) {
//..
mDropUntilNextSync = true;
reset(rawEvent->when);
} else {
for (size_t i = 0; i < numMappers; i++) {
InputMapper* mapper = mMappers[i];
//用每一种mapper尝试处理event
mapper->process(rawEvent);
}
}
}
}

1.4

这里InputMapper种类很多,每个事件处理方法各不相同,所以在这里不再详述。其中有如下的InputMapper:

SwitchInputMapper, VibratorInputMapper, KeyboardInputMapper, CursorInputMapper, TouchInputMapper, SingleTouchInputMapper, MultiTouchInputMapper, JoystickInputMapper

这些方法处理到最后,会调用getListener()->notifyXXX(&args),让Dispatcher进行分发,XXX根据不同的Mapper有相应的名字。在创建InputReader时,将InputDispatcher作为参数传入,同时建立了QueuedInputListener来存放这个InputDispatcher,估计是准备将来处理多个InputDispatcher。所以这里getListener获取的就是当初建立的InputDispatcher对象。

1.5

这里虽然已经开始调用InputDispatcher的函数,但是还是在InputReader线程中。这里开始向InputDispatcher的队列中插入事件,并且把InputDispatcher唤醒了。因为notifyXXX函数同样是针对不同的输入有着不同的处理,所以不再详述,截取一段MotionEvent的代码片段。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//针对每种event,都会想将其封装成一个EventEntry
// Just enqueue a new motion event.
MotionEntry* newEntry = new MotionEntry(args->eventTime,
args->deviceId, args->source, policyFlags,
args->action, args->actionButton, args->flags,
args->metaState, args->buttonState,
args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime,
args->displayId,
args->pointerCount, args->pointerProperties, args->pointerCoords, 0, 0);
//然后加入队列
needWake = enqueueInboundEventLocked(newEntry);
//...
//唤醒Looper线程
if (needWake) {
mLooper->wake();
}

将一个事件放入队列之后,会根据needWake参数决定是否要唤醒线程。如果要唤醒,则调用Looper的wake函数就可以了,和原来道理一样。在有些时候,有事件添加进去,不一定要唤醒线程,比如线程正在等待应用反馈事件处理完毕的消息。

1.6

实实在在的将这个EventEntry放入队列mInboundQueue中了。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

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
bool InputDispatcher::enqueueInboundEventLocked(EventEntry* entry) {
//如果队列空了,需要唤醒
bool needWake = mInboundQueue.isEmpty();
//将事件加入队列
mInboundQueue.enqueueAtTail(entry);
traceInboundQueueLengthLocked();
switch (entry->type) {
//这里会优化App切换的事件,如果上一个App还有事件没处理完,也没反馈事件处理完毕消息
//则清空之前的事件,切换下一个应用
case EventEntry::TYPE_KEY: {
// Optimize app switch latency.
// If the application takes too long to catch up then we drop all events preceding
// the app switch key.
KeyEntry* keyEntry = static_cast<KeyEntry*>(entry);
if (isAppSwitchKeyEventLocked(keyEntry)) {
if (keyEntry->action == AKEY_EVENT_ACTION_DOWN) {
mAppSwitchSawKeyDown = true;
} else if (keyEntry->action == AKEY_EVENT_ACTION_UP) {
if (mAppSwitchSawKeyDown) {
#if DEBUG_APP_SWITCH
ALOGD("App switch is pending!");
#endif
mAppSwitchDueTime = keyEntry->eventTime + APP_SWITCH_TIMEOUT;
mAppSwitchSawKeyDown = false;
needWake = true;
}
}
}
break;
}
//当一个非当前激活app的点击事件发生,会清空之前的事件
//从这个新的点击事件开始
case EventEntry::TYPE_MOTION: {
// Optimize case where the current application is unresponsive and the user
// decides to touch a window in a different application.
// If the application takes too long to catch up then we drop all events preceding
// the touch into the other window.
MotionEntry* motionEntry = static_cast<MotionEntry*>(entry);
if (motionEntry->action == AMOTION_EVENT_ACTION_DOWN
&& (motionEntry->source & AINPUT_SOURCE_CLASS_POINTER)
&& mInputTargetWaitCause == INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY
&& mInputTargetWaitApplicationHandle != NULL) {
int32_t displayId = motionEntry->displayId;
int32_t x = int32_t(motionEntry->pointerCoords[0].
getAxisValue(AMOTION_EVENT_AXIS_X));
int32_t y = int32_t(motionEntry->pointerCoords[0].
getAxisValue(AMOTION_EVENT_AXIS_Y));
sp<InputWindowHandle> touchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y);
if (touchedWindowHandle != NULL
&& touchedWindowHandle->inputApplicationHandle
!= mInputTargetWaitApplicationHandle) {
// User touched a different application than the one we are waiting on.
// Flag the event, and start pruning the input queue.
mNextUnblockedEvent = motionEntry;
needWake = true;
}
}
break;
}
}
return needWake;
}

这里做了两种优化,主要是在当前App窗口处理事件过慢,同时你又触发其他App的事件时,Dispatcher就会丢弃先前的事件,从这个开始唤醒Dispatcher。这样做很合情合理,用户在使用时,会遇到App由于开发者水平有限导致处理事件过慢情况,这时用户等的不耐烦,则应该让用户轻松的切换到其它App,而不是阻塞在那。所以,事件无法响应只会发生在App内部,而不会影响应用的切换,从而提升用户体验。App的质量问题不会影响系统的运转,Android在这点上做的很人性。

2. InputDispatcher分发事件

在第11章中3.2中,Dispatcher调用函数dispatchOnceInnerLocked(&nextWakeupTime);来分配队列中的事件。

这里写图片描述

2.1

从队列中获取event,然后准备处理。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
nsecs_t currentTime = now();
// Reset the key repeat timer whenever normal dispatch is suspended while the
// device is in a non-interactive state. This is to ensure that we abort a key
// repeat if the device is just coming out of sleep.
if (!mDispatchEnabled) {
resetKeyRepeatLocked();
}
// If dispatching is frozen, do not process timeouts or try to deliver any new events.
if (mDispatchFrozen) {
return;
}
//对App切换的情况的优化
// Optimize latency of app switches.
// Essentially we start a short timeout when an app switch key (HOME / ENDCALL) has
// been pressed. When it expires, we preempt dispatch and drop all other pending events.
bool isAppSwitchDue = mAppSwitchDueTime <= currentTime;
if (mAppSwitchDueTime < *nextWakeupTime) {
//如果有切换App的event,且时间小于设定的时间
//则将等待事件设为小者
*nextWakeupTime = mAppSwitchDueTime;
}
// Ready to start a new event.
// If we don't already have a pending event, go grab one.
if (! mPendingEvent) {
if (mInboundQueue.isEmpty()) {
//队列是空的情况
if (isAppSwitchDue) {
// The inbound queue is empty so the app switch key we were waiting
// for will never arrive. Stop waiting for it.
resetPendingAppSwitchLocked(false);
isAppSwitchDue = false;
}
// Synthesize a key repeat if appropriate.
//如果有连续重复事件发生,则制造重复事件
if (mKeyRepeatState.lastKeyEntry) {
if (currentTime >= mKeyRepeatState.nextRepeatTime) {
mPendingEvent = synthesizeKeyRepeatLocked(currentTime);
} else {
if (mKeyRepeatState.nextRepeatTime < *nextWakeupTime) {
*nextWakeupTime = mKeyRepeatState.nextRepeatTime;
}
}
}
// Nothing to do if there is no pending event.
//如果真无事可做,下次睡眠可能比较久
if (!mPendingEvent) {
return;
}
} else {
// Inbound queue has at least one entry.
//从队列中获取一个Event
mPendingEvent = mInboundQueue.dequeueAtHead();
traceInboundQueueLengthLocked();
}
// Poke user activity for this event.
if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
pokeUserActivityLocked(mPendingEvent);
}
//重置ANRT
// Get ready to dispatch the event.
resetANRTimeoutsLocked();
}
// Now we have an event to dispatch.
// All events are eventually dequeued and processed this way, even if we intend to drop them.
bool done = false;
DropReason dropReason = DROP_REASON_NOT_DROPPED;
if (!(mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER)) {
dropReason = DROP_REASON_POLICY;
} else if (!mDispatchEnabled) {
dropReason = DROP_REASON_DISABLED;
}
if (mNextUnblockedEvent == mPendingEvent) {
mNextUnblockedEvent = NULL;
}
//分类处理Event
switch (mPendingEvent->type) {
case EventEntry::TYPE_CONFIGURATION_CHANGED: {
ConfigurationChangedEntry* typedEntry =
static_cast<ConfigurationChangedEntry*>(mPendingEvent);
done = dispatchConfigurationChangedLocked(currentTime, typedEntry);
dropReason = DROP_REASON_NOT_DROPPED; // configuration changes are never dropped
break;
}
case EventEntry::TYPE_DEVICE_RESET: {
DeviceResetEntry* typedEntry =
static_cast<DeviceResetEntry*>(mPendingEvent);
done = dispatchDeviceResetLocked(currentTime, typedEntry);
dropReason = DROP_REASON_NOT_DROPPED; // device resets are never dropped
break;
}
case EventEntry::TYPE_KEY: {
KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);
if (isAppSwitchDue) {
if (isAppSwitchKeyEventLocked(typedEntry)) {
//这个Event就是SwitchEvent,我已经处理
//重置App切换的状态为false
resetPendingAppSwitchLocked(true);
isAppSwitchDue = false;
} else if (dropReason == DROP_REASON_NOT_DROPPED) {
//如果是其他事件,则丢弃,因为正在switch
dropReason = DROP_REASON_APP_SWITCH;
}
}
if (dropReason == DROP_REASON_NOT_DROPPED
&& isStaleEventLocked(currentTime, typedEntry)) {
dropReason = DROP_REASON_STALE;
}
if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
dropReason = DROP_REASON_BLOCKED;
}
//分发KeyEvent
done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
break;
}
case EventEntry::TYPE_MOTION: {
MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {
dropReason = DROP_REASON_APP_SWITCH;
}
if (dropReason == DROP_REASON_NOT_DROPPED
&& isStaleEventLocked(currentTime, typedEntry)) {
dropReason = DROP_REASON_STALE;
}
if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
dropReason = DROP_REASON_BLOCKED;
}
//分发Motion Event
done = dispatchMotionLocked(currentTime, typedEntry,
&dropReason, nextWakeupTime);
break;
}
default:
ALOG_ASSERT(false);
break;
}
if (done) {
if (dropReason != DROP_REASON_NOT_DROPPED) {
dropInboundEventLocked(mPendingEvent, dropReason);
}
mLastDropReason = dropReason;
//将mPendingEvent置为null
releasePendingEventLocked();
//我已经处理的event,所以需要将睡眠设置时间小一点
*nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately
}
}

2.2

这一步根据Event的种类,略有不同。有dispatchMotionLocked和dispatchKeyLocked等。主要过程类似,首先判断是否需要丢弃该event,然后获得目标Window,再向目标window发送event。代码片如下:

frameworks/native/services/inputflinger/InputDispatcher.cpp :

1
2
3
4
5
6
7
//..
// Identify targets.
Vector<InputTarget> inputTargets;
//..
injectionResult = findTouchedWindowTargetsLocked(currentTime, entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
//..
dispatchEventLocked(currentTime, entry, inputTargets);

这里调用findTouchedWindowTargetsLocked()来获取目标Window。在前面12章3.6中,将mFocusedWindowHandle参数设置为了当前激活的Window,所以目前返回的inputTargets就是将mFocusedWindowHandle封装后的结果。

2.3

向每一个目标发送event。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {
pokeUserActivityLocked(eventEntry);
//向每一个目标发送event
for (size_t i = 0; i < inputTargets.size(); i++) {
const InputTarget& inputTarget = inputTargets.itemAt(i);
//获取目标的connection
ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);
if (connectionIndex >= 0) {
sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
} else {
//..
}
}
}

2.4

frameworks/native/services/inputflinger/InputDispatcher.cpp :

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
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {
// Skip this event if the connection status is not normal.
// We don't want to enqueue additional outbound events if the connection is broken.
if (connection->status != Connection::STATUS_NORMAL) {
return;
}
// Split a motion event if needed.
if (inputTarget->flags & InputTarget::FLAG_SPLIT) {
//Motion event一般为连续的基本event组合而成
//所以可以分割
MotionEntry* originalMotionEntry = static_cast<MotionEntry*>(eventEntry);
if (inputTarget->pointerIds.count() != originalMotionEntry->pointerCount) {
MotionEntry* splitMotionEntry = splitMotionEvent(
originalMotionEntry, inputTarget->pointerIds);
if (!splitMotionEntry) {
return; // split event was dropped
}
enqueueDispatchEntriesLocked(currentTime, connection,
splitMotionEntry, inputTarget);
splitMotionEntry->release();
return;
}
}
// Not splitting. Enqueue dispatch entries for the event as is.
enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}

2.5

这一步先判断目标Connection是否空,然后向其队列加入event。如果原来队列为空,说明可以进一步分发event;如果不为空,说明旧event还没有处理完毕,则不进一步分发。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

1
2
3
4
5
6
7
8
9
bool wasEmpty = connection->outboundQueue.isEmpty();
// Enqueue dispatch entries for the requested modes.
//将event放入outboundQueue中,省略..
// If the outbound queue was previously empty, start the dispatch cycle going.
if (wasEmpty && !connection->outboundQueue.isEmpty()) {
startDispatchCycleLocked(currentTime, connection);
}

2.6

循环的取出队列中的event,然后交给connection发送。

frameworks/native/services/inputflinger/InputDispatcher.cpp :

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
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
const sp<Connection>& connection) {
//将outboundQueue中的entry一一进行处理
while (connection->status == Connection::STATUS_NORMAL
&& !connection->outboundQueue.isEmpty()) {
//获取首部的entry
DispatchEntry* dispatchEntry = connection->outboundQueue.head;
dispatchEntry->deliveryTime = currentTime;
// Publish the event.
status_t status;
EventEntry* eventEntry = dispatchEntry->eventEntry;
switch (eventEntry->type) {
case EventEntry::TYPE_KEY: {
KeyEntry* keyEntry = static_cast<KeyEntry*>(eventEntry);
//..
// Publish the key event.
status = connection->inputPublisher.publishKeyEvent(dispatchEntry->seq,
keyEntry->deviceId, keyEntry->source,
dispatchEntry->resolvedAction, dispatchEntry->resolvedFlags,
keyEntry->keyCode, keyEntry->scanCode,
keyEntry->metaState, keyEntry->repeatCount, keyEntry->downTime,
keyEntry->eventTime);
break;
}
case EventEntry::TYPE_MOTION: {
MotionEntry* motionEntry = static_cast<MotionEntry*>(eventEntry);
// Set the X and Y offset depending on the input source.
//..
// Publish the motion event.
status = connection->inputPublisher.publishMotionEvent(dispatchEntry->seq,
motionEntry->deviceId, motionEntry->source,
dispatchEntry->resolvedAction, motionEntry->actionButton,
dispatchEntry->resolvedFlags, motionEntry->edgeFlags,
motionEntry->metaState, motionEntry->buttonState,
xOffset, yOffset, motionEntry->xPrecision, motionEntry->yPrecision,
motionEntry->downTime, motionEntry->eventTime,
motionEntry->pointerCount, motionEntry->pointerProperties,
usingCoords);
break;
}
default:
return;
}
// Check the result.
// Re-enqueue the event on the wait queue.
//从outboundQueue中移除
connection->outboundQueue.dequeue(dispatchEntry);
traceOutboundQueueLengthLocked(connection);
//加入waitQueue
connection->waitQueue.enqueueAtTail(dispatchEntry);
traceWaitQueueLengthLocked(connection);
}
}

2.7

这一步同样有多种情况,有publishMotionEvent和publishKeyEvent。基本思路相近。先封装成message,最后都调用了mChannel->sendMessage(&msg)。
frameworks/native/libs/input/InputTransport.cpp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
InputMessage msg;
msg.header.type = InputMessage::TYPE_MOTION;
msg.body.motion.seq = seq;
msg.body.motion.deviceId = deviceId;
msg.body.motion.source = source;
msg.body.motion.action = action;
msg.body.motion.actionButton = actionButton;
msg.body.motion.flags = flags;
msg.body.motion.edgeFlags = edgeFlags;
msg.body.motion.metaState = metaState;
msg.body.motion.buttonState = buttonState;
msg.body.motion.xOffset = xOffset;
msg.body.motion.yOffset = yOffset;
msg.body.motion.xPrecision = xPrecision;
msg.body.motion.yPrecision = yPrecision;
msg.body.motion.downTime = downTime;
msg.body.motion.eventTime = eventTime;
msg.body.motion.pointerCount = pointerCount;
for (uint32_t i = 0; i < pointerCount; i++) {
msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);
msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);
}
return mChannel->sendMessage(&msg);

2.8

这一步就要像保存的文件描述符mFd中写入数据了。

frameworks/native/libs/input/InputTransport.cpp :

1
2
3
4
5
size_t msgLength = msg->size();
ssize_t nWrite;
do {
nWrite = ::send(mFd, msg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
} while (nWrite == -1 && errno == EINTR);

到这里,Dispatcher终于把event通过当初建立的Channel pair发送给了应用window。这里和旧版本很不同,当初需要先将event放入共享内存,然后发送一个信号进行通知。

3. 当前激活Window获得消息

这一节比较复杂,需要回忆大量的前面几章的细节和一定的逻辑推理(连蒙带猜)能力。

先来回忆一下第12章4.5节,在InputChannel注册到Client时,最后一步做了什么。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

1
mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);

这里给主线程的Looper添加的一个需要监听fd,这个fd是Client端的InputChannel的文件描述符。addFd函数的第4个参数是一个回调函数,这里this指NativeInputEventReceiver对象,它是LooperCallback的子类。addFd函数新建了一个Request对象,如下:

system/core/libutils/Looper.cpp :

1
2
3
4
5
6
7
8
9
10
11
Request request;
request.fd = fd;
request.ident = ident;
request.events = events;
request.seq = mNextRequestSeq++;
//回调函数指明是上一步传入的NativeInputEventReceiver对象
request.callback = callback;
request.data = data;
//..
//将request以fd为关键字加入mRequests
mRequests.add(fd, request);

再次回忆第10章1.6,也就是主线程被阻塞的地方。代码如下:
system/core/libutils/Looper.cpp :

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
int Looper::pollInner(int timeoutMillis) {
// Adjust the timeout based on when the next message is due.
//..
//清空mResponses数组
// Poll.
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
// We are about to idle.
mPolling = true;
//等待epoll监测到IO事件
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
//..
// Handle all events.
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd) {
if (epollEvents & EPOLLIN) {
//这里处理的唤醒事件
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
//这里处理的是input事件
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
//将event加入mResponses
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
//..
}
}
}
Done: ;
//遍历每一个存在mResponses的event
// Invoke all response callbacks.
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
// Invoke the callback. Note that the file descriptor may be closed by
// the callback (and potentially even reused) before the function returns so
// we need to be a little careful when removing the file descriptor afterwards.
//这一步开始调用回调函数,说明该文件描述符下有人输入的event
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if (callbackResult == 0) {
removeFd(fd, response.request.seq);
}
// Clear the callback reference in the response structure promptly because we
// will not clear the response vector itself until the next poll.
response.request.callback.clear();
result = POLL_CALLBACK;
}
}
return result;
}

这一步处理wake事件已经在第10章第1节讲述过了,这里需要处理的另一种事件input event。对input event,先将其放入mResponses数组,然后依次调用他们的回调函数。这里就是NativeInputEventReceiver的handleEvent函数了。

这里写图片描述

3.1

Event分为Input和Output,这里是事件输入,所以先看Input分支。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
if (events & ALOOPER_EVENT_INPUT) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
status_t status = consumeEvents(env, false /*consumeBatches*/, -1, NULL);
mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
return status == OK || status == NO_MEMORY ? 1 : 0;
}
if (events & ALOOPER_EVENT_OUTPUT) {
//..
}
return 1;
}

3.2

开始从目标Channel中读出event,然后包装成java层的对象,开始调用java函数进行处理。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

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
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
ScopedLocalRef<jobject> receiverObj(env, NULL);
bool skipCallbacks = false;
//循环从Channel中读出event
for (;;) {
uint32_t seq;
InputEvent* inputEvent;
//这一步开始从Channel中读取event,存入inputEvent中
status_t status = mInputConsumer.consume(&mInputEventFactory,
consumeBatches, frameTime, &seq, &inputEvent);
if (status) {
if (status == WOULD_BLOCK) {
if (!skipCallbacks && !mBatchedInputEventPending
&& mInputConsumer.hasPendingBatch()) {
// There is a pending batch. Come back later.
//..
return OK;
}
}
return status;
}
if (!skipCallbacks) {
if (!receiverObj.get()) {
//这里的mReceiverWeakGlobal是java层的InputEventReceiver
receiverObj.reset(jniGetReferent(env, mReceiverWeakGlobal));
//..
}
//根据不同的InputEvent种类,将其转化为java层的inputEventObj
jobject inputEventObj;
switch (inputEvent->getType()) {
case AINPUT_EVENT_TYPE_KEY:
inputEventObj = android_view_KeyEvent_fromNative(env,
static_cast<KeyEvent*>(inputEvent));
break;
case AINPUT_EVENT_TYPE_MOTION: {
MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
*outConsumedBatch = true;
}
inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
break;
}
default:
assert(false); // InputConsumer should prevent this from ever happening
inputEventObj = NULL;
}
if (inputEventObj) {
//开始调用java层的dispatchInputEvent函数
env->CallVoidMethod(receiverObj.get(),
gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
env->DeleteLocalRef(inputEventObj);
} else {
//..
}
}
if (skipCallbacks) {
//不需要调用回调函数,则可以直接反馈完成信号
mInputConsumer.sendFinishedSignal(seq, false);
}
}
}

我们先看如何从Channel中获取event的,3.3。然后讲解java层的分发过程,3.4。

3.3

从Chuannel中读取一个(一组)event。

frameworks/native/libs/input/InputTransport.cpp :

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
status_t InputConsumer::consume(InputEventFactoryInterface* factory,
bool consumeBatches, nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {
*outSeq = 0;
*outEvent = NULL;
// Fetch the next input message.
// Loop until an event can be returned or no additional events are received.
while (!*outEvent) {
if (mMsgDeferred) {
// mMsg contains a valid input message from the previous call to consume
// that has not yet been processed.
mMsgDeferred = false;
} else {
// Receive a fresh message.
//从Channel中读取一个Message
status_t result = mChannel->receiveMessage(&mMsg);
if (result) {
// Consume the next batched event unless batches are being held for later.
//..
}
}
//根据message种类构造event
switch (mMsg.header.type) {
case InputMessage::TYPE_KEY: {
KeyEvent* keyEvent = factory->createKeyEvent();
if (!keyEvent) return NO_MEMORY;
initializeKeyEvent(keyEvent, &mMsg);
*outSeq = mMsg.body.key.seq;
*outEvent = keyEvent;
break;
}
case AINPUT_EVENT_TYPE_MOTION: {
//对Motion event,需要处理成批的event事件
//..
MotionEvent* motionEvent = factory->createMotionEvent();
if (! motionEvent) return NO_MEMORY;
updateTouchState(&mMsg);
initializeMotionEvent(motionEvent, &mMsg);
*outSeq = mMsg.body.motion.seq;
*outEvent = motionEvent;
break;
}
default:
return UNKNOWN_ERROR;
}
}
return OK;
}

mChannel->receiveMessage(&mMsg)函数在InputChannel中主要如下实现,调用socket函数recv从mFd中读出数据。

frameworks/native/libs/input/InputTransport.cpp :

1
nRead = ::recv(mFd, msg, sizeof(InputMessage), MSG_DONTWAIT);

3.4

回到java层,在获得event后,就需要进行分发了。receiverObj指向的是一个InputEventReceiver对象,这里其实是它的子类WindowInputEventReceiver对象,见第12章第4节。dispatchInputEvent函数还是继承的父类的,没有重写。

3.5

这一步直接调用了下一步。

3.6

将event插入等待事件队列的尾部,然后开始调度这些消息。

frameworks/base/core/java/android/view/ViewRootImpl.java :

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
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
adjustInputEventForCompatibility(event);
//将event和receiver封装在一起
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
// Always enqueue the input event in order, regardless of its time stamp.
// We do this because the application or the IME may inject key events
// in response to touch events and we want to ensure that the injected keys
// are processed in the order they were received and we cannot trust that
// the time stamp of injected events are monotonic.
//找到尾部插入
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
if (processImmediately) {
//立即处理所有的InputEvent
doProcessInputEvents();
} else {
//发送一个提醒消息
scheduleProcessInputEvents();
}
}

这里会有两种不同的处理event的方式,一个是立即处理;另一种是向主线程Looper发送MSG_PROCESS_INPUT_EVENTS消息。这里选择立即处理。

3.7

这里会把等待在队列中的event,一口气全处理了。这一步会循环拿出队列中的每一个event,然后调用下一步进行处理。

3.8

准备交给stage处理。

frameworks/base/core/java/android/view/ViewRootImpl.java :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void deliverInputEvent(QueuedInputEvent q) {
//...
//下面开始从某个stage开始
//寻找适合的stage进行处理
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}

3.9

下面就讲解一下stage是什么东西。stage可以说是处理event的不同阶段,如果上一个stage处理不了,就交给下一个stage处理,总有一个stage可以将event处理掉。这里stage的处理顺序图如下所示:

这里写图片描述

  1. NativePrelmeInputStage: Delivers pre-ime input events to a native activity. Does not support pointer events. 其实我也不清楚到底是干什么的,没有想到具体的应用场景。
  2. ViewPreImeInputStage: Delivers pre-ime input events to the view hierarchy. Does not support pointer events. 这一步只处理KeyEvent,在InputMethod处理这个KeyEvent之前,可以截获这个event。典型的例子是处理BACK key。
  3. ImeInputStage: Delivers input events to the ime. Does not support pointer events. 将event交给IputMethodManager处理。
  4. EarlyPostImeInputStage: Performs early processing of post-ime input events. 在交给下一阶段之前,先处理筛选一些event。
  5. NativePostImeInputStage: Delivers post-ime input events to a native activity. 尝试让InputQueue发送event给native activity。
  6. ViewPostImeInputStage: Delivers post-ime input events to the view hierarchy. 将event发送给view的层次结构中。
  7. SyntheticInputStage: Performs synthesis of new input events from unhandled input events. 最后一个阶段,处理综合事件,比如trackball, joystick等。

这里我们暂且研究第6种stage,这个stage和View有直接关系。

3.10

这里根据event的类型分别进行处理。我们先关注一下PointerEvent如何处理的。

3.11

将event交给mView来处理,这里mView就是一个DecorView对象。

3.12

这一步是DecorView继承自View的方法,将event分为TouchEvent和GenericMotionEvent来处理。先看TouchEvent如何处理。

3.13

DecorView重写了该函数。这里调用getCallback函数来获取一个回调对象。该函数是PhoneWindow继承自Window类的方法,获得是mCallback。那么这个mCallback到底是谁呢?

回顾一下Activity的创建过程,在Activity创建以后会调用attach函数对Activity进行一定的初始化,其中就创建了PhoneWindow,同时设置了callback为Activity它自己。所以这一步获得是一个Activity对象,然后调用它的函数继续分发event。

3.14

Event交到Activity手中进行处理。为什么会先交给Activity处理?目的是让开发者可以重写这个函数,从而可以在分发这个事件之前进行截获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//绕一圈有交给PhoneWindow处理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//没有view可以处理,那么activity自己处理
//默认就是放弃,也可以重写实现特定功能
return onTouchEvent(ev);
}

3.15

什么也没做,将event传回DecorView,让它处理。

###3.16
这里DecorView又交给dispatchTouchEvent处理。这里的dispatchTouchEvent是源自父类ViewGroup的函数,而不是自己重写的函数。

###3.17
ViewGroup是View的子类。它管理了一组View在mChildren数组中,按照设计模式的说法叫Composite模式。

这里写图片描述

这一步会依次访问每个子view,判断他们是否可以处理该event,如果能就交给它处理;没人能处理就自己处理。无论哪种方式,都会调用下一步dispatchTransformedTouchEvent函数。

frameworks/base/core/java/android/view/ViewGroup.java :

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
public boolean dispatchTouchEvent(MotionEvent ev) {
//..
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
//如果Window没有被遮住,才进行以下过程
if (onFilterTouchEventForSecurity(ev)) {
//..
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
//..
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
//根据z坐标进行排序,从小到大的顺序
final ArrayList<View> preorderedList = buildOrderedChildList();
//也可以自己定制顺序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//所有子View存放在mChildren中
final View[] children = mChildren;
//按z的值,从大到小开始遍历
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//如果指定了一个view去获得这个event,一直循环到那个view为止
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判断该view是否可以接受这个event,是否在这个view范围内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//如果该child正在处理上一个event
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//尝试去向该child发送event
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
//..
//将child加入mFirstTouchTarget为首的队列头部
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
//..
}
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//没有child可以处理,那么就自己处理
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//..
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//..
}
predecessor = target;
target = next;
}
}
//..
}
return handled;
}

3.18

ViewGroup会决定自己处理还是交给child处理。在ViewGroup自己处理,或者child为不再是一个ViewGroup时,则开始调用View的dispatchTouchEvent函数。

frameworks/base/core/java/android/view/ViewGroup.java :

1
2
3
4
5
6
7
8
9
//省略了计算坐标便宜的过程
if (child == null) {
//ViewGroup决定自己处理
handled = super.dispatchTouchEvent(event);
} else {
//交给child处理,child可能是个view
//也可能还是个ViewGroup,这就重复3.17步骤
handled = child.dispatchTouchEvent(event);
}

3.19

这一步就会调用用户自己实现的Listener;如果没有Listener,则会调用view默认的处理函数。

frameworks/base/core/java/android/view/View.java :

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
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
//该View不可访问的状态,返回
return false;
}
}
boolean result = false;
//..
//先判断该View是否被遮挡,没被遮挡才进行处理
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
//获得注册的Listener,看是否有注册OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//调用Listener的回调函数,处理event
result = true;
}
//View也可以采用默认的处理onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
//..
return result;
}

默认处理函数onTouchEvent会处理一些基本的操作,比如button的按下松开的效果,滚动容器产生滚动的效果等。

3.20

在InputStage完成处理event的任务后,会开始返回完成的消息。

3.21

开始调用c++函数。

3.22

将指针转化为NativeInputEventReceiver指针,交给它来处理。

frameworks/base/core/jni/android_view_InputEventReceiver.cpp :

1
2
3
sp<NativeInputEventReceiver> receiver = reinterpret_cast<NativeInputEventReceiver*>(receiverPtr);
status_t status = receiver->finishInputEvent(seq, handled);

3.23

InputConsumer依次处理每个sequence,发送完成信号。

3.24

向channel发送完成消息。

1
2
3
4
5
6
7
status_t InputConsumer::sendUnchainedFinishedSignal(uint32_t seq, bool handled) {
InputMessage msg;
msg.header.type = InputMessage::TYPE_FINISHED;
msg.body.finished.seq = seq;
msg.body.finished.handled = handled;
return mChannel->sendMessage(&msg);
}

向Channel写入事件后,处于睡眠的InputDispatcher被唤醒,开始分发下一个event。

最后说两句

到这里,Input event的处理流程已经分析完了,心好累。

  • 技术
  • Android

扫一扫,分享到微信

微信分享二维码
Android系统源码阅读(14):Zygote和System进程的启动
Android系统源码阅读(12):InputChannel的注册过程
  1. 1. Android系统源码阅读(13):Input消息的分发过程
    1. 1.1. 1. InputReader获取事件
      1. 1.1.1. 1.1
      2. 1.1.2. 1.2
      3. 1.1.3. 1.3
      4. 1.1.4. 1.4
      5. 1.1.5. 1.5
      6. 1.1.6. 1.6
    2. 1.2. 2. InputDispatcher分发事件
      1. 1.2.1. 2.1
      2. 1.2.2. 2.2
      3. 1.2.3. 2.3
      4. 1.2.4. 2.4
      5. 1.2.5. 2.5
      6. 1.2.6. 2.6
      7. 1.2.7. 2.7
      8. 1.2.8. 2.8
    3. 1.3. 3. 当前激活Window获得消息
      1. 1.3.1. 3.1
      2. 1.3.2. 3.2
      3. 1.3.3. 3.3
      4. 1.3.4. 3.4
      5. 1.3.5. 3.5
      6. 1.3.6. 3.6
      7. 1.3.7. 3.7
      8. 1.3.8. 3.8
      9. 1.3.9. 3.9
      10. 1.3.10. 3.10
      11. 1.3.11. 3.11
      12. 1.3.12. 3.12
      13. 1.3.13. 3.13
      14. 1.3.14. 3.14
      15. 1.3.15. 3.15
      16. 1.3.16. 3.18
      17. 1.3.17. 3.19
      18. 1.3.18. 3.20
      19. 1.3.19. 3.21
      20. 1.3.20. 3.22
      21. 1.3.21. 3.23
      22. 1.3.22. 3.24
    4. 1.4. 最后说两句
© 2017 Tian chi
Hexo Theme Yilia by Litten
  • 所有文章
  • 友链
  • 关于我

tag:

  • Java
  • 技术
  • 云
  • 记录
  • Android
  • 设计模式
  • AngularJS
  • Yeoman
  • JavaScript
  • java
  • git
  • 架构
  • MESOS
  • ubuntu
  • MooseFS
  • Netty
  • Torque
  • Cocos2d-x
  • eclipse
  • docker
  • kubernetes
  • spring
  • slurm
  • React Native
  • 豆瓣
  • 电影

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 友情链接1
  • 友情链接2
  • 友情链接3
  • 友情链接4
  • 友情链接5
  • 友情链接6
计算机界的一个小学生