Android知識筆記:記錄 2 個 「容易誤解」 的Android 知識點

android高級架構師 發佈 2020-03-23T14:15:03+00:00

mSeqMap.put, seq);}ViewRootImpl.WindowInputEventReceiverNative層通過JNI執行Framework層的InputEventReceiver.dispachInputEvent:final class WindowI


今天分享兩個之前我們可能都搞錯的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開發核心知識點筆記】可以 私信我【安卓】免費獲取!

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

關鍵字: