面試被掛,回來後埋頭研究才發現requestLayout竟然涉及到這麼多知識點

1. 背景

最近有個讀者跟我提了一個很有深度的問題。

鎖屏後,調用View.requestLayout()方法後會不會postSyncBarrier?

乍一看有點超綱了。細細一想,沒超綱。我把這個問題拆分成了兩個問題,本文我將緊緊圍繞這兩個問題,講解requestLayout背後的故事。

其一:鎖屏後,調用View.requestLayout(),會往上層層調用requestLayout()嗎?

其二:鎖屏後,調用View.requestLayout(),會觸發View的測量和佈局操作嗎?

postSyncBarrier我知道,Handler的同步屏障機制嘛,但是鎖屏之後爲什麼還要調用requestLayout()呢?於是我腦補了一個場景。

假設在Activity onResume()中每隔一秒調用View.requestLayout(),但是在onStop()方法中沒有停止調用該方法。當用戶鎖屏或者按Home鍵時。

我腦補的這個場景,用羅翔老師的話來講是 “法律允許,但是不提倡”。當Activity不在前臺的時候,就應該把requestLayout()方法停掉嘛,「我們知道的,這個方法會從調用的View一層一層往上調用直到ViewRootImpl.requestLayout()方法,然後會從上往下觸發View的測量和佈局甚至繪製方法。非常之浪費嘛!錯誤非常之低級!但是果真如此嗎?(偷偷告訴大家,其實一直調用也沒關係,Google大神已經考慮到了,不信且看後文)」

電競主播蕪湖大司馬,有一句網絡流行語你以爲我在第一層,其實我在第十層。下面我將用層級來表示對requestLayout方法的瞭解程度,層級越高,表示瞭解越深刻。

我喜歡用樹形圖來分析Android View源碼。上圖:

2. 第一層(往上,層層遍歷)

「假設調用I.requestLayout(),會觸發哪些View的requestLayout方法?」

答:會依次觸發I.requestLayout() -> C.requestLayout() -> A.requestLayout() -> ...省略一些View -> ViewRootImpl.requestLayout()

//View.java
public void requestLayout() {
    // 1. 清除測量記錄
    if (mMeasureCache != null) mMeasureCache.clear();

    // 2. 增加PFLAG_FORCE_LAYOUT給mPrivateFlags
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    // 3\. 如果mParent沒有調用過requestLayout,則調用之。換句話說,如果調用過,則不會繼續調用
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
}

該方法作用如下:

  1. 清除測量記錄
  2. 增加PFLAG_FORCE_LAYOUT給mPrivateFlags
  3. 如果mParent沒有調用過requestLayout,則調用之。換句話說,如果調用過,則不會繼續調用

重點看下mParent.isLayoutRequested()方法,它在View.java中有具體實現

//View.java
 public boolean isLayoutRequested() {
    return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}

如果mPrivateFlags增加了PFLAG_FORCE_LAYOUT標誌位,則認爲View已經請求過佈局。由前文可知,在requestLayout的第二步會增加該標誌位。熟悉位操作的朋友就會知道,有增加操作就會有對應的清除操作。 經過一番搜索,找到:

//View.java
public void layout(int l, int t, int r, int b) { 
  // ... 省略代碼
  //在View調用完layout方法,會將PFLAG_FORCE_LAYOUT標誌位清除掉
  mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
  mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
  // ... 省略代碼
}

在View調用完layout方法,會將PFLAG_FORCE_LAYOUT標誌位清除掉。當View下次再調用requestLayout方法時,依舊能往上層層調用。但是如果當layout()方法沒有執行時,下次再調用requestLayout方法時,就不會往上層層調用了。

回答文章中的第一個問題:

其一:鎖屏後,調用View.requestLayout(),會往上層層調用requestLayout()嗎?

答:鎖屏後,除了第一次調用會往上層層調用,其它的都不會

爲什麼,只有第一次調用會呢?那必定是因爲之後的layout方法沒有得到執行,導致PFLAG_FORCE_LAYOUT無法被清除。欲探究竟,接着往下看

如果你知道requestLayout調用是一個層級調用,那麼恭喜你,你已經處於認知的第一層了。送你一張二層入場券。

3. 第二層(ViewRootImpl.requestLayout)

我們來看看第一層講到的ViewRootImpl.requestLayout()

//ViewRootImpl.java
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //1. 往主線程的Handler對應的MessageQueue發送一個同步屏障消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //2. 將mTraversalRunnable保存到Choreographer中
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

該方法主要作用如下:

  1. 往主線程的Handler對應的MessageQueue發送一個同步屏障消息
  2. 將mTraversalRunnable保存到Choreographer中

此處有三個特別重要的知識點:

  1. mTraversalRunnable
  2. MessageQueue的同步屏障
  3. Choreographer機制

mTraversalRunnable相對比較簡單,它的作用就是從ViewRootImpl 從上往下執行performMeasure、performLayout、performDraw。

[重點:敲黑板]:它的執行時機是當VSync信號來到時,會往主線程的Handler對應的MessageQueue中發送一條異步消息,由於在scheduleTraversals()中給MessageQueue中發送過一條同步屏障消息,那麼當執行到同步屏障消息時,會將異步消息取出執行

4. 第三層(TraversalRunnable)

當VSync信號量到達時,Choreographer會發送一個異步消息。當異步消息執行時,會觸發ViewRootImpl.mTraversalRunnable回調。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

它的作用:

  1. 移除同步屏障
  2. 執行performTraversals方法

performTraversals()方法特別複雜,給出僞代碼如下

private void performTraversals() {
    if (!mStopped || mReportNextDraw) {
      performMeasure()
    }

   final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
      performLayout(lp, mWidth, mHeight);
    }

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
        performDraw();
    }
}

該方法的作用:

  1. 滿足條件的情況下調用performMeasure()
  2. 滿足條件的情況下調用performLayout()
  3. 滿足條件的情況下調用performDraw()

mStopped表示Activity是否處於stopped狀態。如果Activity調用了onStop方法,performLayout方法是不會調用的。

//ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
// ... 省略代碼
 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());  
// ... 省略代碼
}

回答文章中第二個問題:

其二:鎖屏後,調用View.requestLayout(),會觸發View的測量和佈局操作嗎?

答:不會,因爲當前Activity處於stopped狀態了

至此第一層裏面留下的小懸念也得以解開,因爲不會執行View.layout()方法,所以PFLAG_FORCE_LAYOUT不會被清除,導致接下來的requestLayout方法不會層層往上調用。

至此本文的兩個問題都已經得到了答案。

當我把問題提交給一位大佬上時,大佬又給我提了一個問題。

鴻洋大佬:既然Activity的onStop會導致requestLayout layout方法得不到執行,那麼onResume方法會不會讓上一次的requestLayout沒有執行的layout方法執行一次呢?

於是我寫了個demo來驗證,鎖屏後延時一秒亮屏。

//MyDemoActivity.kt
override fun onStop() {
    super.onStop()
    root.postDelayed(object : Runnable {
        override fun run() {
            root.requestLayout()
            println("ChoreographerActivity  reqeustLayout")
        }
    }, 1000)
}

在自定義佈局的onLayout方法中打印日誌

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    System.out.println("ChoreographerActivity onLayout");
    super.onLayout(changed, left, top, right, bottom);
}

鎖屏,日誌沒有打印。亮屏,日誌打印了。

所以

大佬:既然Activity的onStop會導致requestLayout layout方法得不到執行,那麼onResume方法會不會讓上一次的requestLayout沒有執行的layout方法執行一次呢?

我:經過demo驗證,會。原因且聽我道來

有了demo找原因就很簡單了。正面不好攻破,那就祭出調試大法唄。但是斷點放在哪好呢?思考了一番。我覺得斷點放在發送同步屏障的地方比較好,ViewRootImpl.scheduleTraversals()。爲什麼斷點放這裏?因爲這裏必經之路。

那你有可能會問:必經之路不應該是onLayout方法麼?(那你就得了解同步屏障和VSync刷新機制了,之後有時間會講)

亮屏後,發現斷點執行了。從堆棧中可以看出Activity的performRestart()方法執行了ViewRootImpl的scheduleTraversals方法。

雖然,亮屏的時候沒有執行View.requestLayout方法,由於鎖屏後1s執行了View.requestLayout方法,所以PFLAG_FORCE_LAYOUT標記位還是有的。亮屏調用了performTraversals方法時,會執行Measure、Layout、Draw等操作。

至此,完美回答了讀者和大佬的問題

5. 第四層(Handler同步屏障)

Handler原理是面試必問的問題。涉及到很多知識點。線程、Looper、MessageQueue、ThreadLocal、鏈表、底層等技術。本文我就不展開講了。即使對Handler不是很瞭解,也不影響本層次的學習。

A同學:同步屏障。感覺好高大上的樣子?能給我講講嗎?

我:乍一看,是挺高大上的。讓人望而生畏。但是細細一想,也不是那麼難,說白了就是將Message分成三種不同類型

A同學:此話怎講,願聞其詳~

我:如下代碼應該看得懂吧?

class Message{
 int mType;
//同步屏障消息
 public static final int SYNC_BARRIER = 0;
//普通消息
 public static final int NORMAL = 1;
//異步消息
 public static final int ASYNCHRONOUS = 2;
}

A同學:這很簡單呀,平時開發中經常用不同的值表示不同的類型,但是android中的Message類並沒有這幾個不同的值呀?

我:Android Message 類確實沒有用不同的值來表示不同類型的Message。它是通過target和isAsynchronous()組合出三種不同類型的Message。

消息類型 target isAsynchronous()
同步屏障消息 null 無所謂
異步消息 不爲null 返回true
普通消息 不爲null 返回false

A同學:理解了,那麼它們有什麼區別呢?

我:世界上本來只有普通消息,但是因爲事情有輕重緩急,所以誕生了同步屏障消息和異步消息。它們兩是配套使用的。當消息隊列中同時存在這三種消息時,如果碰到了同步屏障消息,那麼會優先執行異步消息。

A同學:有點暈~

我:別急,且看如下圖解

  1. 綠色表示普通消息,很守規矩,按照入隊順序依次出隊。
  2. 紅色表示異步消息,意味着它比較着急,有優先執行的權利。
  3. 黃色表示同步屏障消息,它的作用就是警示,後續只會讓異步消息出隊,如果沒有異步消息,則會一直等待。

上圖,消息隊列中全是普通消息。那麼它們會按照順序,從隊首依次出隊列。msg1->msg2->msg3

上圖,三種類型消息全部存在,msg1是同步屏障消息。同步屏障消息並不會真正執行,它也不會主動出隊列,需要調用MessageQueue的removeSyncBarrier()方法。它的作用就是"警示",後續優先讓紅色的消息出隊列。

1. msg3出隊列

2. msg5出隊列

3. 此刻msg2並不會出隊列,隊列中已經沒有了紅色消息,但是存在黃色消息,所以會一直等紅色消息,綠色消息得不到執行機會

4. 調用removeSyncBarrier()方法,將msg1出隊列

5. 綠色消息按順序出隊

postSyncBarrier()和removeSyncBarrier()必須成對出現,否則會導致消息隊列出現假死情況。

同步屏障就介紹到這,如果意猶未盡的話,歡迎關注公衆號,留言探討。

6. 第五層(Choreographer VSync機制)

B同學:VSync機制感覺好高大上的樣子?能給我講講嗎

我:這個東西比較底層了,理解難度比較大,但是有一個比較取巧的理解方式。

B同學:說來聽聽。

我:可以從觀察者模式角度來理解,VSync信號是由底層發出的。APP層會監聽VSync的信號,當接收到信號時,就會通過Choreographer向消息隊列發送異步消息,這個消息的作用之一就是通知ViewRootImpl去執行測量,佈局,繪製操作。

//Choreographer.java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

        //...省略其他代碼
        long now = System.nanoTime();
        if (timestampNanos > now) {
            Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                    + " ms in the future!  Check that graphics HAL is generating vsync "
                    + "timestamps using the correct timebase.");
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
            Log.w(TAG, "Already have a pending vsync event.  There should only be "
                    + "one at a time.");
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

7. 第六層(繪製機制)

ViewRootImpl和Choreographer是繪製機制的兩大主角。他們負責功能如下。具體的源代碼就不貼了,總結如下圖。

大廠面試前的複習準備

接下來分享的系統學習資源以詳解各大互聯網公司的 Android 常見面試題爲主線,從面試的角度帶你介紹必備知識點,以及該知識點在項目中的實際應用

幫你在現在的基礎上,重新梳理和建立 Android 開發的知識體系。無論是你短期內想提升 Android 內功實力,突破自己工作中的能力瓶頸,還是準備參加 Android 面試,都會在這份資料中有所一些收穫。

從架構基礎開始,分了8個模塊來逐步從基礎進階到架構師的環節:

多餘的話就不講了,接下來將分享面試的一個複習路線,如果你也在準備面試但是不知道怎麼高效複習,可以參考一下我的複習路線,有任何問題也歡迎一起互相交流,加油吧!

首先是超級詳細得不能再詳細的Android開發學習思維導圖,因爲圖片實在是太大了,所以我就只把二級目錄的內容放出來,更加詳細的你們可以點擊這裏

接下來就需要梳理知識,提升儲備了!(Android移動架構師七大專題學習資源)

  • 架構師築基必備技能:深入Java泛型+註解深入淺出+併發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO

  • Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化

  • 360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化

  • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack

  • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發

  • 微信小程序:小程序介紹+UI開發+API操作+微信對接

  • Hybrid 開發與Flutter:Html5項目實戰+Flutter進階

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。

然後再是通過源碼來系統性地學習

只要是程序員,不管是Java還是Android,如果不去閱讀源碼,只看API文檔,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀源碼,不僅限於閱讀各大系統源碼,還包括各種優秀的開源庫。

刷大廠面試題備戰,增加大廠通過率

歷時半年,整理了這份市面上最全面的安卓面試題解析大全。

1.可以通過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

以上這些內容均免費分享給大家,需要完整版的朋友,點這裏可以看到全部內容。或者點擊 【這裏】 查看獲取方式。

最後還有耗時一年多整理的一系列Android學習資源:Android源碼解析、Android第三方庫源碼筆記、Android進階架構師七大專題學習、歷年BAT面試題解析包、Android大佬學習筆記等等,這些內容均免費分享給大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章