今天分享兩個之前我們可能都搞錯的Android知識點,我們還是要追求極致,把不懂的問題搞懂的~
1. 事件到底是先到DecorView還是先到Window的?
有天早上看到事件分發的一個討論:
那麼事件到底是先到DecorView還是先到Window(Activity,Dialog)的呢,引發出兩個問題:
1. touch相關事件在DecorView,PhoneWindow,Activity/Dialog之間傳遞的順序是什麼樣子的?
2. 為什麼要按照1這麼設計?
答:事件先到DecorView
Input系統
當用戶觸摸螢幕或者按鍵操作,首次觸發的是硬體驅動,驅動收到事件後,將該相應事件寫入到輸入設備節點,這便產生了最原生態的內核事件。
接著,輸入系統取出原生態的事件,經過層層封裝後成為KeyEvent或者MotionEvent ;
最後,交付給相應的目標窗口(Window)來消費該輸入事件。
1、當螢幕被觸摸,Linux內核會將硬體產生的觸摸事件包裝為Event存到/dev/input/event[x]目錄下。
2、Input系統—InputReader線程:loop起來讓EventHub調用getEvent()不斷的從/dev/input/文件夾下讀取輸入事件。然後轉換成EventEntry事件加入到InputDispatcher的mInboundQueue。
3、Input系統—InputDispatcher線程:從mInboundQueue隊列取出事件,轉換成DispatchEntry事件加入到connection的outboundQueue隊列。再然後開始處理分發事件 (比如分發到ViewRootImpl的WindowInputEventReceiver中),取出outbound隊列,放入waitQueue.
4、Input系統—UI線程:創建socket pair,分別位於」InputDispatcher」線程和focused窗口所在進程的UI主線程,可相互通信。
這裡只說大概,詳情請看gityuan的這篇文章Input系統—事件處理全過程,文章3.3.3小節講的是input系統事件從Native層分發Framework層的InputEventReceiver.dispachInputEvent()。
http://gityuan.com/2016/12/31/input-ipc/
Framework層
//InputEventReceiver.dispachInputEvent()
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
}
ViewRootImpl.WindowInputEventReceiver
Native層通過JNI執行Framework層的InputEventReceiver.dispachInputEvent(),而真正調用的是繼承了InputEventReceiver的ViewRootImpl.WindowInputEventReceiver。
所以這裡執行的WindowInputEventReceiver的dispachInputEvent():
final class WindowInputEventReceiver extends InputEventReceiver {
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
...
}
ViewRootImpl
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
...
if (processImmediately) {
//關鍵點:執行Input事件
doProcessInputEvents();
} else {
//走一遍Handler延遲處理事件
scheduleProcessInputEvents();
}
}
void doProcessInputEvents() {
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
long eventTime = q.mEvent.getEventTimeNano();
long oldestEventTime = eventTime;
if (q.mEvent instanceof MotionEvent) {
MotionEvent me = (MotionEvent)q.mEvent;
if (me.getHistorySize() > 0) {
oldestEventTime = me.getHistoricalEventTimeNano(0);
}
}
mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
//關鍵點:進一步派發事件處理
deliverInputEvent(q);
}
...
}
private void deliverInputEvent(QueuedInputEvent q) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
q.mEvent.getSequenceNumber());
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
}
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (stage != null) {
//關鍵點:上面決定將事件派發到那個InputStage中處理
stage.deliver(q);
} else {
finishInputEvent(q);
}
ViewRootImpl.ViewPostImeInputStage
前面事件會派發到ViewRootImpl.ViewPostImeInputStage中處理,它的父類InputStage.deliver()方法會調用apply()來處理Touch事件:
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
//關鍵點:執行分發touch事件
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
...
//關鍵點:mView分發Touch事件,mView就是DecorView
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
...
}
DecorView
如果你熟悉安卓的Window,Activity和Dialog對應的ViewRootImpl成員mView就是DecorView,View的dispatchPointerEvent()代碼如下:
//View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//分發Touch事件
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
因為DecorView繼承FrameLayout,上面所以會調用DecorView的dispatchTouchEvent():
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
上面Window.Callback都被Activity和Dialog實現,所以變量cb可能就是Activity和Dialog。
Activity
當上面cb是Activity時,執行Activity的dispatchTouchEvent():
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {//關鍵點:getWindow().superDispatchTouchEvent(ev)
return true;
}
return onTouchEvent(ev);
}
如果你熟悉安卓的Window,Activity的getWindow()拿到的就是PhoneWindow,下面是PhoneWindow的代碼:
//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//調用DecorView的superDispatchTouchEvent
return mDecor.superDispatchTouchEvent(event);
}
下面是DecorView.superDispatchTouchEvent()代碼:
//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
//調用ViewGroup的dispatchTouchEvent()開始我們常見的分發Touch事件
return super.dispatchTouchEvent(event);
}
流程圖
答:為什麼要DecorView -> Activity -> PhoneWindow -> DecorView傳遞事件?
解耦!
ViewRootImpl並不知道有Activity這種東西存在!它只是持有了DecorView。
所以,不能直接把觸摸事件送到Activity.dispatchTouchEvent();
那麼,既然觸摸事件已經到了Activity.dispatchTouchEvent()中了,為什麼不直接分發給DecorView,而是要通過PhoneWindow來間接發送呢?
因為Activity不知道有DecorView!但是,Activity持有PhoneWindow ,而PhoneWindow當然知道自己的窗口裡有些什麼了,所以能夠把事件派發給DecorView。
在Android中,Activity並不知道自己的Window中有些什麼,這樣耦合性就很低了。
我們換一個Window試試?
不管Window裡面的內容如何,只要Window仍然符合Activity制定的標準,那麼它就能在Activity中很好的工作。當然,這就是解耦所帶來的擴展性的好處。
以上回答感謝:蔡徐坤打籃球。
2. RecyclerView卡片中持有的資源,到底該什麼時候釋放?
之前我們討論過 View的onAttachedToWindow ,onDetachedFromWindow 調用時機 。
這個機制在RecyclerView卡片中還適用嗎?
例如我們在RecyclerView的Item的onBindViewHolder時,利用一個CountDownTimer去做一個倒計時顯示 / 或者是有一個屬性動畫效果?
到底在什麼時候可以cancel掉這個倒計時/ 動畫,而不影響功能了(滑動到用戶可見範圍內,倒計時/動畫 運作正常)?
有什麼方法可以和onBindViewHolder 對應嗎?就像onAttachedToWindow ,onDetachedFromWindow這樣 。
答:
onAttachedToWindow和onDetachedFromWindow在RecyclerView中還適用嗎?
在RecyclerView中,Item的這兩個方法分別會在【首次出現】和【完全滑出螢幕】(即在螢幕中完全不可見)時回調(在Adapter中也可以重寫同名方法,用來監聽ViewHolder的出現和消失)。
至於說適不適用,還是看具體需求,比如列表中的視頻播放,在onDetachedFromWindow回調時暫停/停止還是合理的。
但是像題目說的倒計時和屬性動畫效果,就不合適了,為什麼呢?
我們先粗略地溫習一下RecyclerView的回收機制:
RecyclerView在布局(自然滑動其實也是反覆布局子View)時,會回收一些符合條件的ViewHolder,它會根據ViewHolder的狀態來決定臨時存放在哪個地方,且把這些臨時存放ViewHolder的集合看作兩種:
不需要經過onBindViewHolder能直接重用的(mAttachedScrap、mCachedViews);
需要經過onBindViewHolder重新綁定數據的(mRecyclerPool.mScrap);
mAttachedScrap,正常情況下,它會在RecyclerView每次布局時都用到:在布局子View時,會把全部子View所屬的Holder,都臨時放裡面,計算好了每個子View的新位置後,會一個個從mAttachedScrap中取出來,當然了不一定是全部都會取出來的,因為可能本次布局,一些舊Item已經完全滑出螢幕了。
那麼,這些留在mAttachedScrap中沒有被取出來的ViewHolder會怎麼樣呢?
正常情況下,它們會被扔到mCachedViews裡面去(注意從mCachedViews中取出來時也是不用重新綁定數據的,即不會經過onBindViewHolder方法)。
剛剛說過,當Item被完全滑出螢幕時,Adapter的onDetachedFromWindow和該Item的onDetachedFromWindow會被回調,也就是說,當onDetachedFromWindow被回調時,ViewHolder並沒有真正被回收!如果這時候把倒計時/動畫取消掉了,那麼在它們再次出現在螢幕中的時候,就不會動了,因為是直接重用,不會重新綁定數據的。
那應該在什麼時候取消?
Adapter中有個onViewRecycled方法,看名字就知道是當Item被回收後回調的。。。
沒錯了,這個方法回調時,表示這個Holder已經被扔進mRecyclerPool.mScrap里了,也就是再次取出的時候會經過onBindViewHolder方法重新綁定數據。
倒計時/動畫在這裡取消的話,是完全沒問題的(但記得保存當前進度,以便下次恢復)。
所以與onBindViewHolder對應的方法,就是這個onViewRecycled了。
最後,以上的闡述沒辦法保證一定是非常嚴謹的,所以請抱著學習以及批判的態度學習,有問題就指出,爭取把一個個技術點儘可能搞清楚,大家一起進步。
最後
最後我想說:對於程式設計師來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!
這裡附上上述的技術體系圖相關的幾十套騰訊、頭條、阿里、美團等公司19年的面試題,把技術點整理成了視頻和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節,由於篇幅有限,這裡以圖片的形式給大家展示一部分。
相信它會給大家帶來很多收穫:
【Android進階學習視頻】、【全套Android面試秘籍PDF】、【Android開發核心知識點筆記】可以 私信我【安卓】免費獲取!
當程式設計師容易,當一個優秀的程式設計師是需要不斷學習的,從初級程式設計師到高級程式設計師,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。