跳转至

Android Activity 与View 的互动思考

前言

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

1、Activity 创建时如何关联View

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

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

1、Activity 创建时如何关联View

Activity 生命周期

Activity 生命周期

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已经被创建了。

这个阶段的调用流程如下:

流程

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

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

将ViewTree 添加到Window

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

activity

其中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是什么情况呢?

act

可以看出,先执行了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();
        }
    }
此处就提交了刷新操作到队列里,等待屏幕刷新信号的到来。

从实验的结果来看,可以肯定的是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();
    }
}

当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分析。

本站说明

一起在知识的海洋里呛水吧。广告内容与本站无关。如果喜欢本站内容,欢迎投喂作者,谢谢支持服务器。如有疑问和建议,欢迎在下方评论~

📖AndroidTutorial 📚AndroidTutorial 🙋反馈问题 🔥最近更新 🍪投喂作者

Ads