Android Activity 与View 的互动思考

前言

Activity/Fragment/View 系列文章:

Android Activity 与View 的互动思考
Android Activity 生命周期详解及监听
Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解
Android Fragment 要你何用?
Android Activity/View/Window/Dialog/Fragment 深层次关联(白话解析)

前几天有个小伙伴问我个问题:当Activity 退到后台(未销毁),此时对View 进行requestLayout/invalidate 操作,会有效果吗?虽然直觉和经验告诉我是没有效果的,但是还是要以理服人。本篇循着Activity 生命周期,探索View 与其互动的细节。
通过本篇文章,你将了解到:

1、Activity 创建时如何关联View
2、Activity 销毁时如何解除关联View
3、Activity 处在其它状态时刷新View

1、Activity 创建时如何关联View

Activity 生命周期

image.png

ViewTree 的创建

从一个最简单的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 关联。

也就是说当Activity 处在"Create"状态时,整个ViewTree已经被创建了。
这个阶段的调用流程如下:


image.png

其中1、2 表示执行的顺序,1先于2执行。

可以看出,在onCreate调用之前,Activity 已经创建了Window。而在setContentView()时,创建了ViewTree,并将Window与DecorView关联上了。

将ViewTree 添加到Window

我们知道,Activity 处在"Create"状态阶段,页面内容是看不到的,需要等到"Resume"状态才能看到,这是怎么一回事呢?


image.png

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

#ActivityThread.java
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                                     String reason) {
        ...
         //最终调用到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取出,将两者进行关联,关联的动作是WindowManager.addView()。
而addView()执行的结果是将本次动作(测量、布局、绘制)提交到队列里,等到有屏幕刷新信号过来时将会执行队列里的动作,最终将会从DecorView开始执行VIew的三大流程(测量、布局、绘制),执行完毕后我们将会看到页面展示。

这也就是为什么很多文章经常说的:页面要到onResume()执行后才会展示。
那么问题来了:在onResume()里能够正常获取布局的宽高吗?
答案是:不能。因为onResume()和WindowManager.addView()执行是在同一个线程里顺序执行的,此时addView()并没有执行。更进一步说,即使addView()执行了,也只是将动作放到队列里等待执行而已。
有几种方式可以在初次进入Activity时获取到宽高:

1、在onResume()里post(Runnable),在Runnable里获取宽高。
2、重写View的onSizeChanged()方法,在该方法里获取宽高。
3、监听View.addOnLayoutChangeListener()方法获取宽高。

至此,随着Activity从"Create"状态到"Resume"状态,View也从创建到被添加到Window里,并最终展示在屏幕上。

2、Activity 销毁时如何解除关联View

众所周知,Activity 销毁的最后是执行了onDestroy(),当Activity 处在"Destroy"状态时,View是什么情况呢?


image.png

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

#ActivityThread.java
    public void handleDestroyActivity(IBinder token, boolean finishing, int configChanges,
                                      boolean getNonConfigInstance, String reason) {
        //最终执行到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 流转到"Destroy"状态,View也被移除出了Window,此时页面已经不可见。

3、Activity 处在其它状态时刷新View

上两节阐述了Activity 创建与销毁对应的View的操作,接下来分析创建与销毁状态的中间状态是如何表现的。
分两种情况:

1、Activity 处在"Resume"状态时,对View进行刷新操作。
2、Activity 处在"Stop"状态时,对View进行刷新操作。

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

Resume 状态下刷新View

要判断刷新是否生效,只需要监听View的onMeasure()、onLayout()、onDraw()方法即可,它们若是被调用了,说明刷新操作成功了。
举个简单例子:

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() 方法,并添加打印。

然后测试刷新操作,看打印结果:

    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"状态下刷新View,当调用requestLayout()时,onMeasure()、onLayout()被执行了;当调用invalidate()时,onDraw()被执行了。
因此,页面的刷新操作是成功的。

Stop 状态下刷新View

改造测试Demo:

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

不断地延迟调用:

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

在Activity 处在"Resume"状态时,"onMeasure called"一直在打印。
此时,回到桌面,Activity 处在"Stop"状态,"onMeasure called" 打印没有了。
这说明:

当Activity 处在"Stop"状态时,此时对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()) {
            //若是父布局没有layout,则会再次进行
            //mParent 为父布局
            mParent.requestLayout();
        }
    }

可以看出,一直调用父布局的requestLayout,调用的终点是:

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

此处就提交了刷新操作到队列里,等待屏幕刷新信号的到来。


image.png

从实验的结果来看,可以肯定的是requestLayout 请求没有分发到ViewRootImpl,甚至大胆猜测TextView.reqeustLayout()请求没有交给父布局。
而此处判断的依据是:

mParent.isLayoutRequested()

该方法实现为:

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

显而易见,其实就是判断标记位:PFLAG_FORCE_LAYOUT。
接着来寻找该标记位在哪里改变了。此处直接说结论,更详细的分析请移步:
Android 自定义View之Measure过程

1、添加该标记的时机在View.requestLayout时。
2、清除该标记的时机是View.layout()时。

当View.layout 执行后,说明View的摆放位置已经确定,因此标记可以清空了。
添加标记和清除标记是成对出现的,requestLayout 没有提交给父布局,说明PFLAG_FORCE_LAYOUT 只是添加了,没有被清除,也就是说父布局的layout操作没有执行,当然它的measure操作也没执行

问题就转到了:为什么父布局没有执行measure/layout?
寻根溯流,三大流程的发起是在ViewRootImpl实现的,重点方法:performTraversals()
而该方法里分别执行了performMeasure、performLayout、performDraw。最终这些方法执行到onMeasure、onLayout、onDraw 里。
执行performMeasure 前提条件是:

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

执行performLayout 前提条件是:

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

我们注意到了mStopped 变量,当mStopped=false的时候才会执行performMeasure、performLayout。

只需要找到mStopped什么时候变为true,答案就找到了。

#ViewRootImpl.java
    void setWindowStopped(boolean stopped) {
        checkThread();
        //不一致才会执行,此处会执行两次
        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"状态时,AMS 发出指令给ActivityThread,最终将会执行到ViewRootImpl. setWindowStopped(boolean stopped),将成员变量mStopped置为false。
2、当要执行View的三大流程时,发现mStopped==false,表示当前Activity 已经处在"Stop"状态了,因此不会执行刷新操作了。

以上解释了:

当Activity处在"Stop"状态时,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);
                //调用父布局
                p.invalidateChild(this, damage);
            }
        }
    }

也是通过层层调用,最终到ViewRootImpl.java里的:

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

当Activity 处在"Stop"状态时,因为View的PFLAG_DRAWN标记没有被添加,所以在invalidateInternal()方法里就不会再执行p.invalidateChild(this, damage);

而PFLAG_DRAWN 标记是执行了View.draw(x1,x2,x3)方法时添加的,表示这一次的绘制动作已经完成。
与requestLayout 一样,因为draw过程没有被执行,因此看看执行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"状态时,AMS 发出视图可见性更改的命令,最终会执行到ViewRootImp.handleAppVisibility(),此时mAppVisible==false,表示App已经不可见。
而执行perfromDraw()前置条件是App可见。

当Activity处在"Pause"、"Stop"状态时,View.invalidate()是没有效果的原因。

注意:此处的"Pause" 状态应该排除其上层有透明非全屏的Activity,此种场景下是不会调用ViewRootImp.handleAppVisibility()

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

从上面的分析可知,当Activity 变为Stop状态时,显示有关的Surface、Render都已经被销毁。当从Stop状态回到Resume状态时,这些又是怎么触发的呢?

从ViewRootImpl.setWindowStopped()与ViewRootImpl.handleAppVisibility() 方法的实现可知:

1、在可见时ViewRootImpl.setWindowStopped()会调用scheduleTraversals()。
2、ViewRootImpl.handleAppVisibility() 则是每次调用都会触发scheduleTraversals() 调用。
而scheduleTraversals()会触发三大流程(Measure/Layout/Draw),这样当我们App从后台退到前台时,界面就完成了渲染并展示了。

本文基于Android 10.0
Demo 地址:测试刷新

接下来将重点分析Activity/Fragment的深层次关联,以及整个生命周期的联动,最后自然而然就会进入Jetpack分析。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列

你可能感兴趣的:(Android Activity 与View 的互动思考)