源码阅读#我们所写的View是如何被添加与显示的呢

2,414 阅读9分钟

我们所写的View是如何被添加与显示的呢?接下来我以 Android API 28 为准,Activity 的生命周期为主线,以图文的方式来展现我看源码的思路,内容有点多,如有不当,欢迎交流指正😃

总体思路

Activity的生命周期中我们都说在Activity#onCreate中创建了View,在Activity#onResume之后显示View,那么这个过程是怎么样的?最后View又是如何渲染到屏幕上的呢?我们带着以下问题看源码:

  • Activity#onCreate中调用Activity#setContentView,完成了什么工作?
  • 说是在Activity#onResume之后完成屏幕渲染的,有源码的证据吗?最后又是怎么渲染的?

在onCreate中调用setContentView

我们给Activity编写布局后,习惯在Activity#onCreate中调用Activity#setContentView,这个方法都做了什么工作呢?我们来看看它的具体内容:

private Window mWindow;

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
    
public Window getWindow() {
    return mWindow;
}

这里通过getWindow获取了Window,然后调用了Window#setContentView方法,Window是一个抽象类,setContentView是其中的抽象方法,那这里到底执行了谁的代码?换句话说,mWindow被赋值的对象是谁呢?

Activity的创建代码在ActivityThread#performLaunchActivity中,在利用 反射 创建Activity后,调用了Activity#attach方法,之后又通过Instrumentation调用了Activity#onCreate方法。在Activity#attach方法中,mWindow被赋值:

mWindow = new PhoneWindow(this, window, activityConfigCallback);

说明mWindow所指向的就是PhoneWindow的对象,调用了PhoneWindow的方法。这里我画了一个简单的图,浅色框框代表类,深色框框代表方法:

接下来我们再来看看PhoneWindow#setContentView中都做了什么。

1、PhoneWindow#installDecor

PhoneWindow#setContentView中第一步先调用了PhoneWindow#installDecor,该方法中对mDecormContentParent进行了赋值:

if (mDecor == null) {
    mDecor = generateDecor(-1);
    ......
}
if (mContentParent == null) {
    mContentParent = generateLayout(mDecor);
    ......
}
  • mDecor就是DecorViewPhoneWindow#generateDecor方法中直接 new 了一个DecorView并返回。
  • mContentParent是一个FramenLayout,在PhoneWindow#generateLayout方法中会加载一个 ID 为R.layout.screen_simple的资源,并将生成的View加入mDecor中,再从其中获取 ID 为com.android.internal.R.id.content的View返回:
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

ID的信息在Window.java中:

/**
 * The ID that the main layout in the XML layout file should have.
 */
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

好了,经过PhoneWindow#installDecor之后,我们构建了View树的基本框架:

我们所编写的xml布局的最终会通过Activity#setContentView放入最内层的FrameLayout中,它就是mContentParent,不过现在还没有放入。

2、LayoutInflater#inflate

回到PhoneWindow#setContentView方法,在没有转场动画的情况下,它的第二步是调用LayoutInflater#inflate

mLayoutInflater.inflate(layoutResID, mContentParent);

LayoutInfater的作用是什么?

LayoutInflater的注释中这样说到:

Instantiates a layout XML file into its corresponding {@link android.view.View} objects. It is never used directly. Instead, use {@link android.app.Activity#getLayoutInflater()} or {@link Context#getSystemService} to retrieve a standard LayoutInflater instance that is already hooked up to the current context and correctly configured for the device you are running on.

意思就是LayoutInflater的作用是将XML转换为Java对象的工具,并将转换出来的对象放入对应的View中(当然也可以不放)。一般不直接使用,通过上面所说的方法获取一个标准的实例。它会预先实例化并按照当前设备的要求配置好,并与当前Context关联。

LayoutInflater从哪里来?

PhoneWindow创建的时候,会获取一个LayoutInflater的实例:

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}

这里的LayoutInflater其实是PhoneLayoutInflater,具体的创建过程,大家可以去看我的下一篇文章(如果对你有帮助也请帮忙点个赞😂 ):

源码阅读#LayoutInflater的创建过程 & AsyncLayoutInflater原理

LayoutInflater如何创建View?

上面我们提到,在LayoutInflater#inflate方法中,XML被转化为View的对象并形成View树,该方法中首先将XML转换为了XmlResourceParser对象:

final XmlResourceParser parser = res.getLayout(resource);

接着继续调用LayoutInflater#inflate的重载方法,在LayoutInflater#inflate的最终重载方法里有重要的三步:

  • 首先通过LayoutInflater中的createViewFromTag方法创建了根布局(XML最外层View)。
  • 其次通过rInflateChildren方法构建子布局(这里面使用了 循环+递归 的方式实现了深度优先遍历XML元素,调用createViewFromTag方法创建各个View,并会使用ViewGroup#addView方关联View,构建出View树)。
  • 最后调用了ViewGroup#addView方法将生成的View加入一开始传入的父布局中。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ......
    // Temp is the root view that was found in the xml
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    ......
    // Inflate all children under temp against its context.
    rInflateChildren(parser, temp, attrs, true);
    ......
    if (root != null && attachToRoot) {
        root.addView(temp, params);
    }
    ......
}

createViewFromTag中会首先使用各个工厂类(FactoryFactory2对象)创建View,如果没创建,则最终调用LayoutInflater#createView方法通过 反射 创建各个View

最后通过ViewGruop#addView方法将View关联(关联的本质就是给View中的mParent赋值,它是一个ViewParent接口变量,而ViewGroupViewRootImp实现了ViewParent接口)

到此为止我们就获取了整个View树(这些只是对象,并没有显示到屏幕上),经历了以下方法:

到此为止,我们的Activity#onCreate阶段就结束了。我们来回答第一个问题:在Activity#onCreate中调用Activity#setContentView,完成了创建View、形成View树的工作


但是,但是,注意了,这里会有一个疑问,“inflate”的过程调用了ViewGruop#addView,为什么没有显示到界面上?addView可是会触发整个绘制过程的呀。嗯,我们接着看下一个问题。

调用了inflate方法,触发了addView,为什么没有显示到屏幕上?

这里的关键就是View中的mParent变量。

PhoneWindow在调用LayoutInflater#inflate方法时传入了mContentParent,并且通过重载方法后attachToRoottrue

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);// 这里root不为空
}

所以,在LayoutInflater#inflate的最终重载方法里会调用ViewGroup#addView方法:

if (root != null && attachToRoot) {
    root.addView(temp, params);
}
public void addView(View child, int index, LayoutParams params) {
    ......
    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

这其中有三个方法:

  • requestLayout:其中利用了mParent对象调用了ViewParent#requestLayout
if (mParent != null && !mParent.isLayoutRequested()) {
    mParent.requestLayout();
}
  • invalidate:其中利用mParent对向调用了ViewParent#invalidateChild
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
    ......
    p.invalidateChild(this, damage);
}
  • addViewInner:其中会将子ViewmParent赋值为thisViewGroup对象本身。

requestLayout中,如果mParent不为空,这里会一直调用父ViewrequestLayout,最终调用ViewRootImplrequestLayout(下面会讲到原因)。但是此时(onCreate阶段)mParent为空,所以以上两个方法并没有做多少实质上的工作。所以,在Activity#onCreate阶段进行addView,只会对mParent进行赋值,并没有触发能把View渲染到屏幕上的方法

😑为什么mParent是空的呢?

因为mParent只有在两个地方被赋值,一个就是上面所说的addViewInner方法,它在ViewGroup中,另一个就是在ViewRootImpl中的setView方法中。

ViewRootImpl#setView会调用View#assignParentassignParent方法会给ViewmParent赋值):

view.assignParent(this);

这里的this就是ViewRootImpl对象本身。

ViewRootImpl#setView又是在哪里被调用的呢?在Activity#handleResumeActivity中,先调用了Activity#performResumeActivity,然后调用了WindowManager#addView,然后在WindwoManagerGlobal中调用了ViewRootImpl#setView

这里的WindowManager#addView传入的是DecorView,所以,不同于其他ViewDecorViewmParentViewRootImpl,所以ViewGroup#requestLayout中的mParent.requestLayout向上调用的终点是ViewRootImpl#requestLayout

可以看到,WindowManager#addView过程是在Activity#onResume之后触发的,所以,前面ViewGroup#addView之中的前两个方法执行时(addViewInner在这两个方法之后调用),mParent还是空的。

😏所以,在Activity#onCreate中,只是构建了View树,却没有进行绘制~~


在onResume之后调用WindowManager的addView

在上面的叙述中有说到如何调用ViewRootImpl#setView,它是在Activity#onResume之后被触发的,这里先回答第二个问题的一部分:屏幕渲染是在Activity#onResume之后,它就是在ViewRootImpl#requestLayout之后触发的。

首先ViewRootImpl#requestLayout会调用到ViewRootImpl#scheduleTraversals方法:

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

然后scheduleTraversals会设置同步屏障,并调用postCallback,传入一个Runnable的任务(mTraversalScheduled用于控制每帧只绘制一次,mTraversalRunnable最终会被执行):

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ......
    }
}

Choreographer#postCallback继续调用Choreographer#postCallbackDelayed,在Choreographer#postCallbackDelayedInternal方法中,delayMillis0

private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

继续调用了scheduleFrameLocked方法:

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            // If running on the Looper thread, then schedule the vsync immediately,
            // otherwise post a message to schedule the vsync from the UI thread
            // as soon as possible.
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            final long nextFrameTime = Math.max(mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

这里一般会走到scheduleVsyncLocked方法中:

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}

然后调用DisplayEventReceiver中的scheduleVsync

/**
 * Schedules a single vertical sync pulse to be delivered when the next
 * display frame begins.
 */
public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}

最终调用native方法nativeScheduleVsync,前面通过mHandler发送的异步消息,最终都会走到这个方法(这里用了同步屏障,保证了这些消息可以优先被处理)。

最终,nativeScheduleVsync会向SurfaceFlinger注册VSync信号的监听,VSync信号由SurfaceFlinger实现并定时发送,当Vsync信号来的时候就会回调FrameDisplayEventReceiver#onVsync,这个方法给发送一个带时间戳Runnable消息,这个Runnable消息的run()现就是FrameDisplayEventReceiver#run, 接着就会执行Choreographer#doFramedoFrame会执行doCallbacks方法就是上面代码中mCallbackQueues中的callback,就是前面提到的mTraversalRunnable

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

doTraversal会调用到performTraversals,这个方法中就会调用到我们常说的三大方法:performMeasure,performLayout,performDraw

ViewRootImpl创建的时候,会创建一个Surface:

// These can be accessed by any thread, must be protected with a lock.
// Surface can never be reassigned or cleared (use Surface.clear()).
public final Surface mSurface = new Surface();

performDraw最终会触发ViewRootImpl#drawSoftware,这个SurfaceViewRootImpl#drawSoftware方法中会被加上数据:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    ......
    // 获取Surface中的画布
    canvas = mSurface.lockCanvas(dirty);
    ......
    // 绘制
    mView.draw(canvas);
    ......
    // 发送数据
    surface.unlockCanvasAndPost(canvas);
    ......
}

最终,unlockCanvasAndPost会使用native方法发送绘制数据(如果hwuiContext不为空会使用hwui):

public void unlockCanvasAndPost(Canvas canvas) {
    synchronized (mLock) {
        checkNotReleasedLocked();
        if (mHwuiContext != null) {
            mHwuiContext.unlockAndPost(canvas);
        } else {
            unlockSwCanvasAndPost(canvas);
        }
    }
}

此时数据会被加载到一个缓冲区中,然后当SurfaceFlinger发送VSync信号后,便会最终显示到屏幕上啦(这里用到了双缓冲的机制,或者是三缓冲)。

总结一下,这里有两个信号监听:

  1. nativeScheduleVsync会注册一个VSync信号的监听,会回调FrameDisplayEventReceiver#onVsync,从而开始创建绘制数据(此时在CPU中)。
  2. drawSoftware会将数据放入缓冲区,等待VSync信号,然后将数据(从CPU)传给GPU。

值得一提的是,在SurfaceFlinger的实现里,通常一个Surface有两块Buffer, 一块用于绘画,一块用于显示,两个Buffer按照固定的频率进行交换,从而实现Window的动态刷新:

到这里,我们所写的XML文件就真正绘制出来啦~ 💪💪💪

好了,第二个问题,源码证据,如上所述,最后是怎么渲染的,就是native层的工作了,大家可以去了解下双缓冲机制。

关于后半部分的参考文章(几片文章内容上可能有重叠,但是都有自己理解很到位的地方):

  1. 深入浅出Android屏幕刷新原理
  2. Android Vsync 原理浅析
  3. Android性能优化典范之Understanding VSYNC
  4. Android卡顿原理分析和SurfaceFlinger,Surface概念简述

写的有点长,有不妥的地方的话希望能指出,谢谢。