Android 容易遺漏的刷新小細節(jié)

前言

系列文章:
Android Activity創(chuàng)建到View的顯示過程
Android Activity 與View 的互動思考
Android invalidate/postInvalidate/requestLayout-徹底厘清
Android 容易遺漏的刷新小細節(jié)

之前的文章斷斷續(xù)續(xù)有分析過刷新(requestLayout/invalidate)相關的知識,只是那會兒側重點不同锨推,主要是著眼于整體流程甘畅。本篇將著重分析刷新關聯的一些小細節(jié)亲怠。
通過本篇文章脑融,你將了解到:

1宫蛆、Measure/Layout/Draw 三者關聯茸俭。
2全谤、requestLayout/Invalidate 作用肤晓。
3、Measure/Layout/Draw 階段執(zhí)行requestLayout 會發(fā)生什么认然?
4补憾、Measure/Layout/Draw 階段執(zhí)行invalidate 會發(fā)生什么?
5卷员、如何監(jiān)聽 Measure/Layout/Draw 各個流程盈匾?
6、總結

1毕骡、Measure/Layout/Draw 三者關聯

Android 展示頁面簡略過程

以Activity 為例:

1削饵、創(chuàng)建Activity并關聯Window。
2未巫、構建ViewTree(View 布局形成的樹形結構)并關聯Window窿撬。
3、注冊監(jiān)聽屏幕刷新信號叙凡。
4劈伴、當屏幕刷新信號到來時執(zhí)行Measure/Layout/Draw 過程。

顯而易見握爷,我們應該從第4步著手跛璧。

Measure/Layout/Draw 內在聯系

假設頁面布局結構如下:


image.png

當屏幕刷新信號到來時會執(zhí)行ViewRootImpl.doTraversal()方法严里,于是先對整個ViewTree 執(zhí)行Measure過程,也即是:


image.png

上面的流程圖表示調用的時間順序赡模,并不代表有直接調用的關系田炭。
當ViewTree 完成Measure 過程,說明ViewTree里的每個View(ViewGroup) 的寬漓柑、高被確定了教硫。

此時再執(zhí)行Layout 過程,因為寬辆布、高已知瞬矩,因此只需要知道擺放的起點,那么終點也將確定锋玲。
當ViewTree 完成Layout 過程景用,說明ViewTree里的每個View(ViewGroup)的四個頂點值確定了(left、top惭蹂、right伞插、bottom)。

既然位置都確定了盾碗,下面的就交個Draw過程媚污,在確定的位置繪制內容即為Draw的主要工作。

三者關系:

1廷雅、Measure 為了確認布局的寬耗美、高。
2航缀、Layout 在Measure的基礎上確定了布局的起始點商架、終點。
3芥玉、Draw 在Layout 確定的布局邊界里繪制指定的內容蛇摸。

2、requestLayout/invalidate 作用灿巧。

requestLayout 作用

上面探討的是Activity 初次進入時頁面的顯示過程皇型,可以看出必然要經過Measure/Layout/Draw 過程,此時想要更新頁面里的某個元素砸烦,那么該如何操作呢弃鸦?

答案是:requestLayout。
還是以上面的圖為例幢痘,假若已經更改了View3的尺寸唬格,想要其生效只需要調用View3.requestLayout(),而后將會觸發(fā)View3.measure(xx),最后觸發(fā)View3.layout(xx)购岗。
此過程中汰聋,View3.onMeasure(xx)/View3.onLayout(xx) 將會被執(zhí)行。

此時重走了Measure/Layout 過程喊积,因為尺寸發(fā)生了改變烹困,因此將會走Draw過程,最終改變尺寸的View3 將會展示在屏幕上乾吻。

invalidate 作用

requestLayout 僅僅只是針對布局的寬髓梅、高改變、頂點位置發(fā)生變化后的刷新绎签,若是想要內容也要刷新枯饿,則需要借助invalidate。
當調用View3.invalidate()后诡必,最終將會執(zhí)行Draw 過程奢方,重新繪制內容。
此過程中爸舒,View3.onDraw(xx) 將會被執(zhí)行蟋字。

兩者區(qū)別:

1、想要重新測量扭勉、確定View 寬愉老、高,可以使用requestLayout剖效。
2、想要頁面內容重新刷新焰盗,可以使用invalidate璧尸。
3、調用 requestLayout 后若是發(fā)現寬熬拒、高發(fā)生變化爷光,那么將會觸發(fā)invalidate。
4澎粟、調用invalidate 則不會觸發(fā)reqeusetLayout(不走Measure/Layout 過程)蛀序。

3、Measure/Layout/Draw 階段執(zhí)行requestLayout 會發(fā)生什么活烙?

一個小例子

public class MyView extends View {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        requestLayout();
        Log.d("fish1", "onMeasure called");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
//        requestLayout();
        Log.d("fish1", "onLayout called");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        requestLayout();
        Log.d("fish1", "onDraw called");
    }
}

MyView 重寫了onMeasure(xx)/onLayout(xx)/onDraw(xx) 方法徐裸,分別在里面調用requestLayout()。

現在有兩個問題:
問題1:當在onMeasure(xx)/onLayout(xx) 里調用requestLayout()后啸盏,onMeasure(xx)/onLayout(xx) 還會被執(zhí)行嗎重贺?

從日志結果反饋,onLayout(xx)只在進入頁面的時候被執(zhí)行一次。


image.png

因此气笙,上面的答案是否定的次企。

問題2:當在onDraw(xx) 里調用requestLayout()后,onMeasure(xx)/onLayout(xx) 還會被執(zhí)行嗎潜圃?
日志如下:

image.png

從日志結果反饋缸棵,答案是肯定的。

刨根問底

先從onMeasure/onLayout 調用requestLayout()說起谭期。

為啥在onMeasure(xx)/onLayout(xx)里執(zhí)行requestLayout 沒效果呢堵第?這得從requestLayout 源碼說起。

#View.java
    public void requestLayout() {
        ...
        //PFLAG_FORCE_LAYOUT 表示需要執(zhí)行Layout 操作
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //如果父布局Layout 請求已經完成崇堵,則可以再次Layout
            mParent.requestLayout();
        }
        ...
    }

最頂級的mParent為ViewRootImpl.java:

#ViewRootImpl.java
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            //標記已經申請Layout
            mLayoutRequested = true;
            //開啟三大流程
            scheduleTraversals();
        }
    }

關鍵之處在于mParent.isLayoutRequested():

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

問題的重點轉為:View 的PFLAG_FORCE_LAYOUT 標記啥時候添加與清除型诚?

View.requestLayout()時會添加PFLAG_FORCE_LAYOUT 標記,而移除的地方在View.layout里:

#View.java
    public void layout(int l, int t, int r, int b) { ;
        ...
        //設置4個頂點的值
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //觸發(fā)子布局Layout
            onLayout(changed, l, t, r, b);
            ...
        }

        final boolean wasLayoutValid = isLayoutValid();

        //清除標記
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        ...
    }

結合添加與移除 標記如下圖:


image.png

簡要概括:

1鸳劳、首次執(zhí)行requestLayout狰贯,將會給ViewTree里的各個View 添加標記,表示需要做測量/布局動作赏廓。
2涵紊、當屏幕刷新信號到來后觸發(fā)三大流程,進行測量/布局 動作幔摸。
3摸柄、而我們此時重寫了onMeasure(xx)/onLayout(xx),在該方法里調用requestLayout既忆,因為Layout 過程未結束驱负,因此標記沒有被清除,最終requestLayout 里判斷mParent.isLayoutRequested()=true患雇,說明上一次的Layout 未完成跃脊,沒必要再次執(zhí)行。

再看onDraw 里調用requestLayout
當執(zhí)行onDraw(xx)時苛吱,說明Layout 過程已經結束酪术,PFLAG_FORCE_LAYOUT 標記已經被清除,表示Layout 過程已經結束翠储,可以重新開始新的一輪Measure/Layout 過程绘雁,因此在onDraw(xx)里執(zhí)行requestLayout 有效果。

4援所、Measure/Layout/Draw 階段執(zhí)行invalidate 會發(fā)生什么庐舟?

一個小例子

public class MyView extends View {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        requestLayout();
        invalidate();
        Log.d("fish1", "onMeasure called");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
//        requestLayout();
        invalidate();
        Log.d("fish1", "onLayout called");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
//        requestLayout();
        invalidate();
        Log.d("fish1", "onDraw called");
    }
}

直接說結論,從日志反饋分析可知:

1住拭、onMeasure(xx)/onLayout(xx) 里執(zhí)行invalidate 不會觸發(fā)Draw 過程继阻。
2耻涛、在onDraw(xx) 里執(zhí)行invalidate 會觸發(fā)Draw 過程(這也是實現動畫的關鍵)。

尋本溯源

從View.invalidate()開始分析:

#View.java
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                            boolean fullInvalidate) {
        ...
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            ...
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                //清空標記
                mPrivateFlags &= ~PFLAG_DRAWN;
            }
            if (p != null && ai != null && l < r && t < b) {
                ...
                //往上調用瘟檩,直至ViewRootImpl
                p.invalidateChild(this, damage);
            }
        }
        //不滿足抹缕,則不會開啟Draw 過程
    }

重點查看PFLAG_DRAWN 標記,此處的判斷:若是該View 已經繪制過墨辛,那么先清除PFLAG_DRAWN標記卓研,然后再往上傳遞刷新意圖,最后執(zhí)行Draw過程睹簇。
若是判斷沒有PFLAG_DRAWN 標記奏赘,說明上一次的Draw 過程沒有結束,則無需再次刷新太惠。
問題重點轉到PFLAG_DRAWN 標記的清除與添加磨淌,其中清除過程已經明了,剩下的看標記啥時候添加上的凿渊。
Draw 執(zhí)行過程:


image.png

在View.draw(x1,x2,x3)里清空標記梁只。

結合添加與移除標記,如下圖:


image.png

簡要概括:

1埃脏、在onMeasure(xx)/onLayout(xx)里調用invalidate搪锣,因為此時還沒走Draw 過程,因此PFLAG_DRAWN 標記沒被添加彩掐,在invalidate()內部判斷的時候不會再向上傳遞動作构舟,因此最終沒有執(zhí)行Draw過程。
2堵幽、在onDraw(xx)里執(zhí)行invalidate狗超,因為在View.draw(x1,x2,x3)里已經將PFLAG_DRAWN 標記添加(而后會執(zhí)行onDraw(xx)),因此在invalidate()內部判斷通過朴下,最終將會觸發(fā)Draw 過程努咐。

5、如何監(jiān)聽 Measure/Layout/Draw 各個流程桐猬?

以上是我們通過重寫onMeasure(xx)/onLayout(xx)/onDraw(xx)來實現View三大流程監(jiān)聽,在很多時候我們并不需要重寫前兩者刽肠、甚至第三者溃肪,或者說我們只關注ViewTree 當前所處的過程而不需要知道具體某個View 所處的過程,因此需要一個外部的機制來監(jiān)聽音五。
ViewTreeObserver 提供了一系列的接口用來監(jiān)聽各個過程惫撰。


image.png

只需要添加對應的Listener 到ViewTreeObserver里,當ViewTree 走到對應的流程時將會回調給Listener躺涝,外界可以據此做一些操作厨钻。
比如,想要監(jiān)聽Layout 過程:

   textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //可以拿到ViewTree 里所有布局的寬度、起點夯膀、終點
                //invalidate()
                //requestLayout()
            }
        });

大家有興趣可以猜猜此時執(zhí)行invalidiate()诗充、requestLayout() 會重走三大流程嗎?
如果你答對了诱建,說明對刷新的細節(jié)之處已經明了蝴蜓。

6、總結

可以看出Android 在設計刷新機制時是做了一些限制的俺猿,通過成對的標記來限制一些無意義的頻繁調用invalidate/requestLayout茎匠。

看完上面的內容,有些同學可能會說:"了解了這有啥用押袍?看了就容易忘~"诵冒。
確實,上面的細節(jié)分析以前我也接觸過一些谊惭,后面確實忘了汽馋。
后面寫了如下代碼:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 500) {
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            layoutParams.width = 1000;
            setLayoutParams(layoutParams);
            Log.d("fish1", "onDraw called");            
        }
    }

本意是想在寬度>500時,將寬度提高一倍午笛,最終沒能如愿惭蟋。如果當時知道了這些細節(jié),就會明白setLayoutParams(xx)里調用了requestLayout()药磺,而此時調用requestLayout()是不會觸發(fā)Measure/Layout 過程的告组。
原因是:

View.Layout--->View.onSizeChanged(xx)--->View.onLayout(xx)--->OnLayoutChangeListener.onLayoutChange(xx)--->清除PFLAG_FORCE_LAYOUT 標記--->ViewTree的Layout 執(zhí)行完成后---> OnGlobalLayoutListener. onGlobalLayout()。

以上是各個流程調用的時序癌佩。

解決方法:當然是放到標記清除之后的步驟木缝,比如在OnGlobalLayoutListener. onGlobalLayout() 處理重設寬高的邏輯。

其實想表達的意思是:

雖然是一些不起眼的小細節(jié)围辙,若是提前知道了可能就會避坑我碟。

本文基于Android 10.0。

刷新演示Demo

您若喜歡姚建,請點贊矫俺、關注,您的鼓勵是我前進的動力

持續(xù)更新中掸冤,和我一起步步為營系統厘托、深入學習Android

1、Android各種Context的前世今生
2稿湿、Android DecorView 必知必會
3铅匹、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5饺藤、Android事件分發(fā)全套服務
6包斑、Android invalidate/postInvalidate/requestLayout 徹底厘清
7流礁、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅動Handler-Message-Looper解析
9罗丰、Android 鍵盤一招搞定
10神帅、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12丸卷、Android Activity創(chuàng)建到View的顯示過
13枕稀、Android IPC 系列
14、Android 存儲系列
15谜嫉、Java 并發(fā)系列不再疑惑
16萎坷、Java 線程池系列
17、Android Jetpack 前置基礎系列
18沐兰、Android Jetpack 易懂易學系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末哆档,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子住闯,更是在濱河造成了極大的恐慌瓜浸,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件比原,死亡現場離奇詭異插佛,居然都是意外死亡,警方通過查閱死者的電腦和手機量窘,發(fā)現死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門雇寇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蚌铜,你說我怎么就攤上這事锨侯。” “怎么了冬殃?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵囚痴,是天一觀的道長。 經常有香客問我审葬,道長深滚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任涣觉,我火速辦了婚禮痴荐,結果婚禮上,老公的妹妹穿的比我還像新娘旨枯。我一直安慰自己蹬昌,他們只是感情好混驰,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布攀隔。 她就那樣靜靜地躺著皂贩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昆汹。 梳的紋絲不亂的頭發(fā)上明刷,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天,我揣著相機與錄音满粗,去河邊找鬼辈末。 笑死,一個胖子當著我的面吹牛映皆,可吹牛的內容都是我干的挤聘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捅彻,長吁一口氣:“原來是場噩夢啊……” “哼组去!你這毒婦竟也來了?” 一聲冷哼從身側響起步淹,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤从隆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后缭裆,有當地人在樹林里發(fā)現了一具尸體键闺,經...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年澈驼,在試婚紗的時候發(fā)現自己被綠了辛燥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡盅藻,死狀恐怖购桑,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情氏淑,我是刑警寧澤勃蜘,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站假残,受9級特大地震影響缭贡,放射性物質發(fā)生泄漏。R本人自食惡果不足惜辉懒,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一阳惹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧眶俩,春花似錦莹汤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抹竹。三九已至,卻和暖如春止潮,著一層夾襖步出監(jiān)牢的瞬間窃判,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人婶恼。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像唆樊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子刻蟹,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內容