Android FrameWork——Touch事件派發過程詳解
對於android的視窗window管理,一直感覺很混亂,總想找個時間好好研究,卻不知如何入手,現在寫的Touch事件派發過程詳解,其實跟android的視窗window管理服務WindowManagerService存在緊密聯絡,所以從這裡入手切入到WindowManagerService的研究,本blog主要講述一個touch事件如何從使用者訊息的採集,到WindowManagerService對Touch事件的派發,再到一個Activity視窗touch事件的派發,並著重講了Activity視窗touch事件的派發,因為這個的理解對我們寫應用很好地處理touch事件很重要
一.使用者事件採集到WindowManagerService和派發
--1.WindowManagerService,顧名思義,它是是一個視窗管理系統服務,它的主要功能包含如下:
--視窗管理,繪製
--轉場動畫--Activity切換動畫
--Z-ordered的維護,Activity視窗顯示前後順序
--輸入法管理
--Token管理
--系統訊息收集執行緒
--系統訊息分發執行緒
這裡,我關注的是系統訊息的收集和系統訊息的分發,其他功能,當我對WindowManagerService有一個完整的研究後在發blog
--2.系統訊息收集和分發執行緒的建立
這個的從WindowManagerService服務的建立說起,與其他系統服務一樣,WindowManagerService在systemServer中建立的:
ServerThread.run
-->WindowManagerService.main
-->WindowManagerService.WMThread.run(構建一個專門執行緒負責WindowManagerService)
-->WindowManagerService s = new WindowManagerService(mContext, mPM,mHaveInputMethods);
--mQueue = new KeyQ();//訊息佇列,在構造KeyQ中會建立一個InputDeviceReader執行緒去讀取使用者輸入訊息
整個過程處理原理很簡單,典型的生產者消費者模型,我先畫個圖,後面針對程式碼進一步說明
--3.InputDeviceReader執行緒,KeyQ構建時,會啟動一個執行緒去讀取使用者訊息,具體程式碼在KeyInputQueue.mThread,在建構函式中,mThread會start,接下來,接研究一下mThread.run:
//使用者輸入事件訊息讀取執行緒
Thread mThread = new Thread("InputDeviceReader") {
public void run() {
RawInputEvent ev = new RawInputEvent();
while (true) {//開始訊息讀取迴圈
try {
InputDevice di;
//本地方法實現,讀取使用者輸入事件
readEvent(ev);
...
synchronized (mFirst) {//mFirst是keyQ佇列頭指標
...
addLocked(di, curTimeNano, ev.flags,RawInputEvent.CLASS_TOUCHSCREEN, me);
...
}
}
}
}
函式我也沒有看大明白:首先呼叫本地方法readEvent(ev);去讀取使用者訊息,這個訊息包括按鍵,觸控,滾輪等所有使用者輸入事件,後面不同的事件型別會有不同的處理,不過最後事件都要新增到keyQ的佇列中,通過addLocked函式
--4佇列新增和讀取函式addLocked,getEvent
addLocked函式比較簡單,就分析一下,有助於對訊息佇列KeyQ的資料結構進行理解:
//event加入inputQueue佇列
private void addLocked(InputDevice device, long whenNano, int flags,
int classType, Object event) {
boolean poke = mFirst.next == mLast;//poke為true表示訊息佇列為空
//從QueuedEvent快取QueuedEvent獲取一個QueuedEvent物件,並填入使用者事件資料,包裝成一個QueuedEvent
QueuedEvent ev = obtainLocked(device, whenNano, flags, classType, event);
QueuedEvent p = mLast.prev;//佇列尾節點為mLast,把ev新增到mlast前
while (p != mFirst && ev.whenNano < p.whenNano) {
p = p.prev;
}
ev.next = p.next;
ev.prev = p;
p.next = ev;
ev.next.prev = ev;
ev.inQueue = true;
if (poke) {//poke為true,意味著在空佇列中添加了一個QueuedEvent,這時系統訊息分發執行緒可能在wait,需要notify一下
long time;
if (MEASURE_LATENCY) {
time = System.nanoTime();
}
mFirst.notify();//喚醒在 mFirst上等待的執行緒
mWakeLock.acquire();
if (MEASURE_LATENCY) {
lt.sample("1 addLocked-queued event ", System.nanoTime() - time);
}
}
}
很簡單,使用mFirst,mLast實現的指標佇列,addLocked是QueuedEvent物件新增函式,對應在系統訊息分發執行緒中會有一個getEvent函式來讀取inputQueue佇列的訊息,我在這裡也先講一下:
QueuedEvent getEvent(long timeoutMS) {
long begin = SystemClock.uptimeMillis();
final long end = begin+timeoutMS;
long now = begin;
synchronized (mFirst) {//獲取mFirst上同步鎖
while (mFirst.next == mLast && end > now) {
try {//mFirst.next == mLast意味佇列為空,同步等待mFirst鎖物件
mWakeLock.release();
mFirst.wait(end-now);
}
catch (InterruptedException e) {
}
now = SystemClock.uptimeMillis();
if (begin > now) {
begin = now;
}
}
if (mFirst.next == mLast) {
return null;
}
QueuedEvent p = mFirst.next;//返回mFirst的下一個節點為處理的QueuedEvent
mFirst.next = p.next;
mFirst.next.prev = mFirst;
p.inQueue = false;
return p;
}
}
通過上面兩個函式得知,訊息佇列是通過mFirst,mLast實現的生產者消費模型的同步連結串列佇列
--5.InputDispatcherThread執行緒
InputDispatcherThread處理InputDeviceReader執行緒存放在KeyInputQueue佇列中的訊息,分發到具體的一個客戶端的IWindow
InputDispatcherThread.run
-->windowManagerService.process{
...
while (true) {
// 從mQueue(KeyQ)獲取一個使用者輸入事件,正上呼叫我上面提到的getEvent方法,若佇列為空,執行緒阻塞掛起
QueuedEvent ev = mQueue.getEvent(
(int)((!configChanged && curTime < nextKeyTime)
? (nextKeyTime-curTime) : 0));
...
try {
if (ev != null) {
...
if (ev.classType == RawInputEvent.CLASS_TOUCHSCREEN)
{//touch事件
eventType = eventType((MotionEvent)ev.event);
} else if (ev.classType == RawInputEvent.CLASS_KEYBOARD ||
ev.classType == RawInputEvent.CLASS_TRACKBALL) {//鍵盤輸入事件
eventType = LocalPowerManager.BUTTON_EVENT;
} else {
eventType = LocalPowerManager.OTHER_EVENT;//其他事件
}
...
switch (ev.classType) {
case RawInputEvent.CLASS_KEYBOARD:
...
dispatchKey((KeyEvent)ev.event, 0, 0);//鍵盤輸入,派發key事件
mQueue.recycleEvent(ev);
break;
case RawInputEvent.CLASS_TOUCHSCREEN:
dispatchPointer(ev, (MotionEvent)ev.event, 0, 0);//touch事件,派發touch事件
break;
case RawInputEvent.CLASS_TRACKBALL:
dispatchTrackball(ev, (MotionEvent)ev.event, 0, 0);//滾輪事件,派發Trackball事件
break;
case RawInputEvent.CLASS_CONFIGURATION_CHANGED:
configChanged = true;
break;
default:
mQueue.recycleEvent(ev);//銷燬事件
break;
}
}
} catch (Exception e) {
Slog.e(TAG,
"Input thread received uncaught exception: " + e, e);
}
}
}
WindowManagerService.dispatchPointer,一旦判斷QueuedEvent為螢幕點選事件,就呼叫函式WindowManagerService.dispatchPointer進行處理:
WindowManagerService.dispatchPointer
-->WindowManagerService.KeyWaiter.waitForNextEventTarget(獲取touch事件要派發的目標windowSate)
-->WindowManagerService.KeyWaiter.findTargetWindow(從一個一個WindowSate的z-order順序列表mWindow中獲取一個能夠接收當前touch事件的WindowSate)
-->WindowSate target = waitForNextEventTarget返回的WindowSate物件
-->target.mClient.dispatchPointer(ev, eventTime, true);(往目標window派發touch訊息)
target.mClient是一個IWindow代理物件IWindow.Proxy,它對應的代理類是ViewRoot.W,通過遠端代理呼叫,WindowManagerService把touch訊息派發到了對應的Activity的PhoneWindow
之後進一步WindowManagerService到Activity訊息的派發在下文中說明
二WindowManagerService派發Touch事件到當前top Activity
--1.先我們看一個system_process的touch事件訊息呼叫堆疊,在WindowManagerService中的函式dispatchPointer,通過一個IWindow的客戶端代理物件把訊息傳送到相應的IWindow服務端,也就是一個IWindow.Stub子類。
Thread [<21> InputDispatcher] (Suspended (breakpoint at line 321 in IWindow$Stub$Proxy))
IWindow$Stub$Proxy.dispatchPointer(MotionEvent, long, boolean) line: 321
WindowManagerService.dispatchPointer(KeyInputQueue$QueuedEvent, MotionEvent, int, int) line: 5270
WindowManagerService$InputDispatcherThread.process() line: 6602
WindowManagerService$InputDispatcherThread.run() line: 6482
--2.通過IWindow.Stub.Proxy代理物件把訊息傳遞給IWindow.Stub物件。code=TRANSACTION_dispatchPointer,IWindow.Stub物件被ViewRoot擁有(成員mWindow,它是一個ViewRoot.W類物件)
--3.在case TRANSACTION_dispatchPointer會呼叫IWindow.Stub子類的實現方法dispatchPointer
--4.IWindow.Stub.dispatchPointer
-->ViewRoot.W.dispatchPointer
-->ViewRoot.dispatchPointer
public void dispatchPointer(MotionEvent event, long eventTime,
boolean callWhenDone) {
Message msg = obtainMessage(DISPATCH_POINTER);
msg.obj = event;
msg.arg1 = callWhenDone ? 1 : 0;
sendMessageAtTime(msg, eventTime);
}
--5.ViewRoot繼承自handle,在handleMessage函式的case-DISPATCH_POINTER會呼叫mView.dispatchTouchEvent(event),
mView是一個PhoneWindow.DecorView物件,在PhoneWindow.openPanel方法會建立一個ViewRoot物件,並設定ViewRoot物件的mView為一個PhoneWindow.decorView成員,PhoneWindow.DecorView是真正的root view,它繼承自FrameLayout,這樣呼叫mView.dispatchTouchEvent(event)
其實就是呼叫PhoneWindow.decorView的dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Callback cb = getCallback();
return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
.dispatchTouchEvent(ev);
}
--6.分析上面一段紅色程式碼,可以寫成return (cb != null) && (mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev)).當cb不為null執行後面,如果mFeatureId<0,執行cb.dispatchTouchEvent(ev),否則執行super.dispatchTouchEvent(ev),也就是FrameLayout.dispatchTouchEvent(ev),那麼callback
cb是什麼呢?是Window類的一個成員mCallback,我下面給一個圖你可以看到何時被賦值的:
setCallback(Callback) : void - android.view.Window
-->attach(Context, ActivityThread, Instrumentation, IBinder, int, Application, Intent, ActivityInfo, CharSequence, Activity, String, Object, HashMap<String, Object>, Configuration) : void - android.app.Activity
--> performLaunchActivity(ActivityRecord, Intent) : Activity - android.app.ActivityThread
performLaunchActivity我們很熟識,因為我前面在講Activity啟動過程詳解時候講過,在啟動一個新的Activity會執行該方法,在該方法裡面會執行attach方法,找到attach方法對應程式碼可以看到:
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow就是一個PhoneWindow,它是Activity的一個內部成員,通過呼叫mWindow的setCallback(this),把新建立的Activity設定為PhoneWindow一個mCallback成員,這樣我們就清楚了,前面的cb就是擁有這個PhoneWindow的Activity,cb.dispatchTouchEvent(ev)也就是執行:Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//getWindow()返回的就是PhoneWindow物件,執行superDispatchTouchEvent,就是執行PhoneWindow.superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//執行Activity.onTouchEvent方法
return onTouchEvent(ev);
}
--7.再看PhoneWindow.superDispatchTouchEvent:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
--> public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
}
}
superDispatchTouchEvent呼叫super.dispatchTouchEvent,我前面講過mDector是一個PhoneWindow.DecorView,它是一個真正Activity的root view,它繼承了FrameLayout,通過super.dispatchTouchEvent他會把touchevent派發給各個activity的子view,也就是我們再Activity.onCreat方法中setContentView時設定的view,touch event時間如何在Activity各個view中進行派發的我後面再作詳細說明,但是從上面我們可以看出一點若Activity下面的子view攔截了touchevent事件(返回true),Activity.onTouchEvent就不會執行。
--8.這部分,我再畫一個靜態類結構圖把前面講到的一些類串起來看一下:
我用紅色箭頭線把整個訊息派發過程過程給串起來,然後system_process程序和ap程序分別用虛線橢圓圈起,這樣以後相信你更理解各個類之間關係。
對應的物件空間圖如下,與上面圖是對應的,只是從不同角度去看:
--9.其實上面所講的大部分已經是在客戶端ap中執行了,也就是在ap程序中,只是執行邏輯基本是框架程式碼中,還沒有到達我們使用layout.xml佈局的view中來,這裡我先在我們的一個view中onTouchEvent插入一個斷點看一看訊息從WindowManagerService到達Activity.PhoneWindow後執行堆疊情況(我插入的斷點在Launcher2的HandleView中),後面繼續講解:
Thread [<1> main] (Suspended (breakpoint at line 4280 in View))
HandleView(View).onTouchEvent(MotionEvent) line: 4280
HandleView.onTouchEvent(MotionEvent) line: 71
HandleView(View).dispatchTouchEvent(MotionEvent) line: 3766
RelativeLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863
DragLayer(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863
FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863
PhoneWindow$DecorView(ViewGroup).dispatchTouchEvent(MotionEvent) line: 863
PhoneWindow$DecorView.superDispatchTouchEvent(MotionEvent) line: 1671
PhoneWindow.superDispatchTouchEvent(MotionEvent) line: 1107
ForyouLauncher(Activity).dispatchTouchEvent(MotionEvent) line: 2086
PhoneWindow$DecorView.dispatchTouchEvent(MotionEvent) line: 1655
ViewRoot.handleMessage(Message) line: 1785
ViewRoot(Handler).dispatchMessage(Message) line: 99
Looper.loop() line: 123
ActivityThread.main(String[]) line: 4634
三.Activity中View中的Touch事件派發
--1.首先我畫一個Activity中的view層次結構圖:
前面我講過,來自windowManagerService的touch訊息最終會派發到到Decorview,Decorview繼承子FrameLayout,它只有一個子view就是mContentParent,我們寫ap的view全部新增到到mContentParent。
--2.瞭解了Activity中的view的層次結構,那先從DecorView開始看touch事件是如何被派發的,前面講過最終訊息會派發到FrameLayout.dispatchTouchEvent也就是ViewGroup.dispatchTouchEvent(FrameLayout也沒有覆蓋該方法),
同樣mContentParent也是執行ViewGroup.dispatchTouchEvent來派發touch訊息,那我們就詳細看一下ViewGroup.dispatchTouchEvent(若要很好掌握應用程式touch事件處理,這部分要重點看):
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//計算是否禁止touch Intercept
if (action == MotionEvent.ACTION_DOWN) {//按下事件,也就是touch開始
if (mMotionTarget != null) {
mMotionTarget = null;//清除mMotionTarget,也就是說每次touch開始,mMotionTarget要被重新設定
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {//判斷訊息是否需要被viewGroup攔截
// 訊息不被viewGroup攔截,找到相應的子view進行touch事件派發
ev.setAction(MotionEvent.ACTION_DOWN);//重新設定event 為action_down
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;//獲取viewgroup所有的子view
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {//若子view可見或者有動畫在執行的,才能夠接收touch事件
child.getHitRect(frame);//獲取子view的佈局座標區域
if (frame.contains(scrolledXInt, scrolledYInt))
{//若子view 區域包含當前touch點選區域
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {//派發TouchEvent給包含這個touch區域的子view
// 若該子view消費了對應的touch事件
mMotionTarget = child;//設定viewgroup訊息派發的目標子view
return true;//返回true,該touch事件被消費掉
}
}
}
}
}
//若touch事件被攔截,mMotionTarget = null,後面touch訊息不再派發給子view
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||//計算是up或者cancel
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
//target為null,意味著在ACTION_DOWN時沒有找到能消費touch訊息的子view或者在ACTION_DOWN時訊息被攔截了,這個時候
//呼叫父類view的dispatchTouchEvent訊息進行派發,也就是說,此時viewgroup處理touch訊息跟普通view一致。
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
//target!=null,意味在ACTION_DOWN時touch訊息沒有被攔截,而且子view target消費了ACTION_DOWN訊息,需要再判斷訊息是否被攔截
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
//訊息被攔截,而前面ACTION_DOWN時touch訊息沒有被攔截,所以需要傳送ACTION_CANCEL通知子view target
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// 派發訊息ACTION_CANCEL給子view target
}
// mMotionTarget=null,後面訊息不再派發給子view
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
//isUpOrCancel,設定mMotionTarget=null,後面訊息不再派發給子view
mMotionTarget = null;
}
......
//沒有被攔截繼續派發訊息給子view target
return target.dispatchTouchEvent(ev);
}
--3.ViewGroup.dispatchTouchEvent我查看了一下所有子類,只有PhoneWindow.DecorView覆蓋了該方法,該方法前面講DecorView訊息派發時提過,它會找到對應包含這個PhoneWindow.DecorView物件的Activity把訊息交給Activity去處理,其它所有viewGroup的子類均沒有覆蓋dispatchTouchEvent,也就是說所有包含子view的父view對於touch訊息派發均採用上面的邏輯,當然,必要的時候我們可以覆蓋該方法實現自己的touch訊息派發邏輯,如Launcher2中的workspace類就是重新實現的該dispatchTouchEvent方法,從上面的dispatchTouchEvent函式邏輯其實我們也可以總結幾條touch訊息派發邏輯:
(1).onInterceptTouchEvent用來定義是否擷取touch訊息邏輯,若在groupview中想擷取touch訊息,必須覆蓋viewgroup中該方法。
(2).訊息在整個dispatchTouchEvent過程中,若子view.dispatchTouchEvent返回true,父view中將不再處理該訊息,但前提是該訊息沒有被父view擷取,在整個touch訊息處理過程中,若處理函式返回true,我們稱之為消費了該touch事件,並且後面的父view將不再處理該訊息。
(3).在整個touch事件過程中,從action_down到action_up,若父ViewGroup的函式onInterceptTouchEvent一旦返回true,訊息將不再派發給子view,細分可為兩種情況,若是在action_down時onInterceptTouchEvent返回true,不會派發任何訊息給子view,並且後面onInterceptTouchEvent函式將不再會被執行,若是action_down時onInterceptTouchEvent返回false
,而後面touch過程中onInterceptTouchEvent==true,父viewGroup會把action_cancel派發給子view,也之後不再派發訊息給子view,並且onInterceptTouchEvent函式後面將不再被執行。
--4.為了更清楚的理解viewGroup訊息的派發流程,我畫一個流程圖如下:
--5.上面我只是講了父view與子view之間當有touch事件的訊息派發流程,對於view的訊息是怎麼派發的(也包裹viewGroup沒有子view或者有子view但是不消費該touch訊息情況),因為從繼承結構上看viewgroup繼承了view,viewgroup覆蓋了view的dispatchTouchEvent方法,不過從上面流程圖也可以看到當mMotionTarget為Null它會執行父類view.dispatchTouchEvent,其他view的子類都是執行view.dispatchTouchEvent派發touch事件,不過若我們自定義view是可以覆蓋該方法的。下面就仔細研究一下view.dispatchTouchEvent方法的程式碼:
public final boolean dispatchTouchEvent(MotionEvent event) {
//mOnTouchListener是被View.setOnTouchListener設定的,(mViewFlags & ENABLED_MASK)計算view是否可被點選
//當view可被點選並且mOnTouchListener被設定,執行mOnTouchListener.onTouch
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;//若mOnTouchListener.onTouch返回true,函式返回true
}
return onTouchEvent(event);//若mOnTouchListener.onTouch返回false,呼叫onToucheEvent
}
函式邏輯很簡單,前面的viewGroup touch事件流程圖中我已經畫出的,為區別我把它著色成青綠色,總結一句話若mOnTouchListener處理了touch訊息,不執行onTouchEvent,否則交給onTouchEvent進行處理。
不知道是否講清楚的,要清楚掌握估計還得寫些例子測試一下是否是我上面所說的流程,不過我想了解事件的派發流程,對寫應用的事件處理相信很有用,比如我以前碰到一個問題是手指點選螢幕到底是子view執行onclick還是執行父view的view移動,這個時候就需要深入瞭解viewde touch事件派發流程,該響應點選的時候響應子view的點選,該父view移動的時候攔截touch事件交給父view進行處理。