Android Activity 與View 的互動思考

前言

Activity/Fragment/View 系列文章:

Android Activity 與View 的互動思考
Android Activity 生命周期詳解及監(jiān)聽
Android onSaveInstanceState/onRestoreInstanceState 原來要這么理解
Android Fragment 要你何用?
Android Activity/View/Window/Dialog/Fragment 深層次關(guān)聯(lián)(白話解析)

前幾天有個小伙伴問我個問題:當Activity 退到后臺(未銷毀),此時對View 進行requestLayout/invalidate 操作,會有效果嗎?雖然直覺和經(jīng)驗告訴我是沒有效果的,但是還是要以理服人。本篇循著Activity 生命周期,探索View 與其互動的細節(jié)。
通過本篇文章,你將了解到:

1、Activity 創(chuàng)建時如何關(guān)聯(lián)View
2、Activity 銷毀時如何解除關(guān)聯(lián)View
3、Activity 處在其它狀態(tài)時刷新View

1、Activity 創(chuàng)建時如何關(guān)聯(lián)View

Activity 生命周期

image.png

ViewTree 的創(chuàng)建

從一個最簡單的Android Hello World 說起:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

setContentView()指定一個布局文件,表示要在Activity上展示這個布局。
該方法有兩個主要作用:

1、將自定義的布局(View)加入到ViewTree里,而ViewTree的根就是DecorView。
2、將Window(PhoneWindow)和DecorView 關(guān)聯(lián)。

也就是說當Activity 處在"Create"狀態(tài)時,整個ViewTree已經(jīng)被創(chuàng)建了。
這個階段的調(diào)用流程如下:


image.png

其中1、2 表示執(zhí)行的順序,1先于2執(zhí)行。

可以看出,在onCreate調(diào)用之前,Activity 已經(jīng)創(chuàng)建了Window。而在setContentView()時,創(chuàng)建了ViewTree,并將Window與DecorView關(guān)聯(lián)上了。

將ViewTree 添加到Window

我們知道,Activity 處在"Create"狀態(tài)階段,頁面內(nèi)容是看不到的,需要等到"Resume"狀態(tài)才能看到,這是怎么一回事呢?


image.png

其中1、2 表示執(zhí)行的順序,1先于2執(zhí)行。
可以看出,先執(zhí)行了onResume,再執(zhí)行addView()操作。
提取部分代碼如下:

#ActivityThread.java
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                                     String reason) {
        ...
         //最終調(diào)用到onResume
         final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        //通過 r.window 判斷,只有Activity 第一次啟動才會走這
        if (r.window == null && !a.mFinished && willBeVisible) {
            //取出Window賦值
            r.window = r.activity.getWindow();
            //取出DecorView
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //加入到Window里
                    wm.addView(decor, l);
                } else {
                    ...
                }
            }
        } else if (!willBeVisible) {
            ...
        }
        ...
    }

分別將DecorView和WindowManager取出,將兩者進行關(guān)聯(lián),關(guān)聯(lián)的動作是WindowManager.addView()。
而addView()執(zhí)行的結(jié)果是將本次動作(測量、布局、繪制)提交到隊列里,等到有屏幕刷新信號過來時將會執(zhí)行隊列里的動作,最終將會從DecorView開始執(zhí)行VIew的三大流程(測量、布局、繪制),執(zhí)行完畢后我們將會看到頁面展示。

這也就是為什么很多文章經(jīng)常說的:頁面要到onResume()執(zhí)行后才會展示。
那么問題來了:在onResume()里能夠正常獲取布局的寬高嗎?
答案是:不能。因為onResume()和WindowManager.addView()執(zhí)行是在同一個線程里順序執(zhí)行的,此時addView()并沒有執(zhí)行。更進一步說,即使addView()執(zhí)行了,也只是將動作放到隊列里等待執(zhí)行而已。
有幾種方式可以在初次進入Activity時獲取到寬高:

1、在onResume()里post(Runnable),在Runnable里獲取寬高。
2、重寫View的onSizeChanged()方法,在該方法里獲取寬高。
3、監(jiān)聽View.addOnLayoutChangeListener()方法獲取寬高。

至此,隨著Activity從"Create"狀態(tài)到"Resume"狀態(tài),View也從創(chuàng)建到被添加到Window里,并最終展示在屏幕上。

2、Activity 銷毀時如何解除關(guān)聯(lián)View

眾所周知,Activity 銷毀的最后是執(zhí)行了onDestroy(),當Activity 處在"Destroy"狀態(tài)時,View是什么情況呢?


image.png

可以看出,先執(zhí)行了onDestroy(),再移除了View。
提取部分代碼如下:

#ActivityThread.java
    public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,
                                      boolean getNonConfigInstance, String reason) {
        //最終執(zhí)行到onDestroy
        ActivityClientRecord r = performDestroyActivity(token, finishing,
                configChanges, getNonConfigInstance, reason);
        if (r != null) {
            WindowManager wm = r.activity.getWindowManager();
            View v = r.activity.mDecor;
            if (v != null) {
                if (r.activity.mWindowAdded) {
                    if (r.mPreserveWindow) {
                        r.window.clearContentView();
                    } else {
                        //移除View
                        wm.removeViewImmediate(v);
                    }
                }
            }
        }
    }

至此,隨著Activity 流轉(zhuǎn)到"Destroy"狀態(tài),View也被移除出了Window,此時頁面已經(jīng)不可見。

3、Activity 處在其它狀態(tài)時刷新View

上兩節(jié)闡述了Activity 創(chuàng)建與銷毀對應的View的操作,接下來分析創(chuàng)建與銷毀狀態(tài)的中間狀態(tài)是如何表現(xiàn)的。
分兩種情況:

1、Activity 處在"Resume"狀態(tài)時,對View進行刷新操作。
2、Activity 處在"Stop"狀態(tài)時,對View進行刷新操作。

注:此處的刷新指的是View.requestLayout()、View.invalidate()。

Resume 狀態(tài)下刷新View

要判斷刷新是否生效,只需要監(jiān)聽View的onMeasure()、onLayout()、onDraw()方法即可,它們?nèi)羰潜徽{(diào)用了,說明刷新操作成功了。
舉個簡單例子:

public class MyTextView extends AppCompatTextView {
    public MyTextView(Context context) {
        super(context);
    }

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

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

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

聲明一個類,繼承自AppCompatTextView,重寫onMeasure()/onLayout()/onDraw() 方法,并添加打印。

然后測試刷新操作,看打印結(jié)果:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flush_ui);

        TextView textView = findViewById(R.id.tv);

        findViewById(R.id.btn_request).setOnClickListener((v)->{
            textView.requestLayout();
        });

        findViewById(R.id.btn_invalidate).setOnClickListener((v)->{
            textView.invalidate();
        });
    }

毫無疑問,在"Resume"狀態(tài)下刷新View,當調(diào)用requestLayout()時,onMeasure()、onLayout()被執(zhí)行了;當調(diào)用invalidate()時,onDraw()被執(zhí)行了。
因此,頁面的刷新操作是成功的。

Stop 狀態(tài)下刷新View

改造測試Demo:

    private Runnable requestRunnable = new Runnable() {
        @Override
        public void run() {
            Log.d("fish", "request layout call");
            textView.requestLayout();
            textView.postDelayed(this, 1000);
        }
    };

不斷地延遲調(diào)用:

        findViewById(R.id.btn_request).setOnClickListener((v)->{
            textView.postDelayed(requestRunnable, 1000);
        });

在Activity 處在"Resume"狀態(tài)時,"onMeasure called"一直在打印。
此時,回到桌面,Activity 處在"Stop"狀態(tài),"onMeasure called" 打印沒有了。
這說明:

當Activity 處在"Stop"狀態(tài)時,此時對View的刷新是無效的。

以上是針對requestLayout()的操作,實際上對于invalidate()效果亦是如此,就不重復演示了,可在文末的Demo鏈接里查看。

View 的刷新原理

View.requestLayout()

從實踐中驗證了猜想,接下來探究其原理。
之前在 Android invalidate/postInvalidate/requestLayout-徹底厘清 有分析過刷新原理,本次再來簡單回顧一下。

#View.java
    public void requestLayout() {
        ...
        //添加標記
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            //若是父布局沒有l(wèi)ayout,則會再次進行
            //mParent 為父布局
            mParent.requestLayout();
        }
    }

可以看出,一直調(diào)用父布局的requestLayout,調(diào)用的終點是:

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

此處就提交了刷新操作到隊列里,等待屏幕刷新信號的到來。


image.png

從實驗的結(jié)果來看,可以肯定的是requestLayout 請求沒有分發(fā)到ViewRootImpl,甚至大膽猜測TextView.reqeustLayout()請求沒有交給父布局。
而此處判斷的依據(jù)是:

mParent.isLayoutRequested()

該方法實現(xiàn)為:

    public boolean isLayoutRequested() {
        return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }

顯而易見,其實就是判斷標記位:PFLAG_FORCE_LAYOUT。
接著來尋找該標記位在哪里改變了。此處直接說結(jié)論,更詳細的分析請移步:
Android 自定義View之Measure過程

1、添加該標記的時機在View.requestLayout時。
2、清除該標記的時機是View.layout()時。

當View.layout 執(zhí)行后,說明View的擺放位置已經(jīng)確定,因此標記可以清空了。
添加標記和清除標記是成對出現(xiàn)的,requestLayout 沒有提交給父布局,說明PFLAG_FORCE_LAYOUT 只是添加了,沒有被清除,也就是說父布局的layout操作沒有執(zhí)行,當然它的measure操作也沒執(zhí)行

問題就轉(zhuǎn)到了:為什么父布局沒有執(zhí)行measure/layout?
尋根溯流,三大流程的發(fā)起是在ViewRootImpl實現(xiàn)的,重點方法:performTraversals()
而該方法里分別執(zhí)行了performMeasure、performLayout、performDraw。最終這些方法執(zhí)行到onMeasure、onLayout、onDraw 里。
執(zhí)行performMeasure 前提條件是:

boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);

執(zhí)行performLayout 前提條件是:

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);

我們注意到了mStopped 變量,當mStopped=false的時候才會執(zhí)行performMeasure、performLayout。

只需要找到mStopped什么時候變?yōu)閠rue,答案就找到了。

#ViewRootImpl.java
    void setWindowStopped(boolean stopped) {
        checkThread();
        //不一致才會執(zhí)行,此處會執(zhí)行兩次
        if (mStopped != stopped) {
            //修改mStopped
            mStopped = stopped;
            final ThreadedRenderer renderer = mAttachInfo.mThreadedRenderer;
            if (renderer != null) {
                renderer.setStopped(mStopped);
            }
            if (!mStopped) {
                //如果不是停止,那么就是開始
                mNewSurfaceNeeded = true;
                //重新提交刷新動作到隊列里。
                scheduleTraversals();
            } else {
                //釋放資源
                if (renderer != null) {
                    renderer.destroyHardwareResources(mView);
                }
            }
            ...
            if (mStopped) {
                if (mSurfaceHolder != null && mSurface.isValid()) {
                    notifySurfaceDestroyed();
                }
                //銷毀surface
                destroySurface();
            }
        }
    }

1、當Activity 處在"Stop"狀態(tài)時,AMS 發(fā)出指令給ActivityThread,最終將會執(zhí)行到ViewRootImpl. setWindowStopped(boolean stopped),將成員變量mStopped置為false。
2、當要執(zhí)行View的三大流程時,發(fā)現(xiàn)mStopped==false,表示當前Activity 已經(jīng)處在"Stop"狀態(tài)了,因此不會執(zhí)行刷新操作了。

以上解釋了:

當Activity處在"Stop"狀態(tài)時,View.requestLayout()是沒有效果的原因。

View.invalidate()

與View.requestLayout 類似:

#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;
            }
            ...
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                //調(diào)用父布局
                p.invalidateChild(this, damage);
            }
        }
    }

也是通過層層調(diào)用,最終到ViewRootImpl.java里的:

#ViewRootImpl.java
    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            //提交到刷新隊列,等待屏幕信號的到來
            scheduleTraversals();
        }
    }
image.png

當Activity 處在"Stop"狀態(tài)時,因為View的PFLAG_DRAWN標記沒有被添加,所以在invalidateInternal()方法里就不會再執(zhí)行p.invalidateChild(this, damage);

而PFLAG_DRAWN 標記是執(zhí)行了View.draw(x1,x2,x3)方法時添加的,表示這一次的繪制動作已經(jīng)完成。
與requestLayout 一樣,因為draw過程沒有被執(zhí)行,因此看看執(zhí)行draw過程的前置條件:

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

而決定isViewVisible的因素是:mAppVisible。
該變量被賦值的地方:

#ViewRootImpl.java
    void handleAppVisibility(boolean visible) {
        if (mAppVisible != visible) {
            mAppVisible = visible;
            mAppVisibilityChanged = true;
           //提交刷新動作
            scheduleTraversals();
            if (!mAppVisible) {
                WindowManagerGlobal.trimForeground();
            }
        }
    }

當Activity 處在"Pause"狀態(tài)時,AMS 發(fā)出視圖可見性更改的命令,最終會執(zhí)行到ViewRootImp.handleAppVisibility(),此時mAppVisible==false,表示App已經(jīng)不可見。
而執(zhí)行perfromDraw()前置條件是App可見。

當Activity處在"Pause"、"Stop"狀態(tài)時,View.invalidate()是沒有效果的原因。

注意:此處的"Pause" 狀態(tài)應該排除其上層有透明非全屏的Activity,此種場景下是不會調(diào)用ViewRootImp.handleAppVisibility()

從Stop到Start/Resume View 是如何刷新的

從上面的分析可知,當Activity 變?yōu)镾top狀態(tài)時,顯示有關(guān)的Surface、Render都已經(jīng)被銷毀。當從Stop狀態(tài)回到Resume狀態(tài)時,這些又是怎么觸發(fā)的呢?

從ViewRootImpl.setWindowStopped()與ViewRootImpl.handleAppVisibility() 方法的實現(xiàn)可知:

1、在可見時ViewRootImpl.setWindowStopped()會調(diào)用scheduleTraversals()。
2、ViewRootImpl.handleAppVisibility() 則是每次調(diào)用都會觸發(fā)scheduleTraversals() 調(diào)用。
而scheduleTraversals()會觸發(fā)三大流程(Measure/Layout/Draw),這樣當我們App從后臺退到前臺時,界面就完成了渲染并展示了。

本文基于Android 10.0
Demo 地址:測試刷新

接下來將重點分析Activity/Fragment的深層次關(guān)聯(lián),以及整個生命周期的聯(lián)動,最后自然而然就會進入Jetpack分析。

您若喜歡,請點贊、關(guān)注,您的鼓勵是我前進的動力

持續(xù)更新中,和我一起步步為營系統(tǒng)、深入學習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事件驅(qū)動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 線程池系列

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容