Android知識筆記:記錄 2 個 “容易誤解” 的Android 知識點


Android知識筆記:記錄 2 個 “容易誤解” 的Android 知識點

今天分享兩個之前我們可能都搞錯的Android知識點,我們還是要追求極致,把不懂的問題搞懂的~

1. 事件到底是先到DecorView還是先到Window的?

有天早上看到事件分發的一個討論:

Android知識筆記:記錄 2 個 “容易誤解” 的Android 知識點

那麼事件到底是先到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層

<code>//InputEventReceiver.dispachInputEvent()
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
}/<code>

ViewRootImpl.WindowInputEventReceiver

Native層通過JNI執行Framework層的InputEventReceiver.dispachInputEvent(),而真正調用的是繼承了InputEventReceiver的ViewRootImpl.WindowInputEventReceiver。

所以這裡執行的WindowInputEventReceiver的dispachInputEvent():

<code>final class WindowInputEventReceiver extends InputEventReceiver {
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
...
}/<code>

ViewRootImpl

<code> 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);
}

/<code>

ViewRootImpl.ViewPostImeInputStage

前面事件會派發到ViewRootImpl.ViewPostImeInputStage中處理,它的父類InputStage.deliver()方法會調用apply()來處理Touch事件:

<code>@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);
...
}/<code>

DecorView

如果你熟悉安卓的Window,Activity和Dialog對應的ViewRootImpl成員mView就是DecorView,View的dispatchPointerEvent()代碼如下:

<code>//View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//分發Touch事件
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}/<code>

因為DecorView繼承FrameLayout,上面所以會調用DecorView的dispatchTouchEvent():

<code>@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);
}/<code>

上面Window.Callback都被Activity和Dialog實現,所以變量cb可能就是Activity和Dialog。

Activity

當上面cb是Activity時,執行Activity的dispatchTouchEvent():

<code>public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {//關鍵點:getWindow().superDispatchTouchEvent(ev)
return true;
}
return onTouchEvent(ev);
}/<code>

如果你熟悉安卓的Window,Activity的getWindow()拿到的就是PhoneWindow,下面是PhoneWindow的代碼:

<code>//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//調用DecorView的superDispatchTouchEvent
return mDecor.superDispatchTouchEvent(event);
}/<code>

下面是DecorView.superDispatchTouchEvent()代碼:

<code>//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
//調用ViewGroup的dispatchTouchEvent()開始我們常見的分發Touch事件
return super.dispatchTouchEvent(event);
}/<code>

流程圖

Android知識筆記:記錄 2 個 “容易誤解” 的Android 知識點

答:為什麼要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知識筆記:記錄 2 個 “容易誤解” 的Android 知識點

Android知識筆記:記錄 2 個 “容易誤解” 的Android 知識點

【Android進階學習視頻】、【全套Android面試秘籍PDF】、【Android開發核心知識點筆記】可以 私信我【安卓】免費獲取!

當程序員容易,當一個優秀的程序員是需要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。


分享到:


相關文章: