Android开发

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

本文主要是介绍源码阅读#我们所写的View是如何被添加与显示的呢?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

我们所写的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从哪里来?

LayoutInflater的创建过程很有意思,这里想多说一下,如果嫌太长可以跳过这一节。

我们带着一个问题往下看:为什么ContextLayoutInflater是一对一的关系?我们从PhoneWindow的构造方法看起,这里获取了LayoutInflater实例:

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}
复制代码

接着看LayoutInflater#from:

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}
复制代码

我们可以看到其中调用了Context#getSystemService,这里传入的是ActivityContext,而从ActivityThread#performLaunchActivity中我们知道,这里的Context引用的实际上就是ContextImpl对象,我们继续看ContextImpl#getSystemService方法:

@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}
复制代码

这里调用了SystemServiceRegistry#getSystemService:

private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new HashMap<>();
            
public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
}
复制代码

敲黑板了哈🧐,记住上面方法中使用的fetcher.getService(ctx),还有SYSTEM_SERVICE_FETCHERS,下面要考。

下面简述下SystemServiceRegistry是如何是如何获取各个“Service”的:

1. 通过静态代码块注册各“Service”的ServiceFetcher

比如:

static {
    ......
    registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
            new CachedServiceFetcher<LayoutInflater>() {
        @Override
        public LayoutInflater createService(ContextImpl ctx) {
            return new PhoneLayoutInflater(ctx.getOuterContext());
        }});
    ......
}

private static <T> void registerService(String serviceName, Class<T> serviceClass,
        ServiceFetcher<T> serviceFetcher) {
    ......
    SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
复制代码

可以看到Fecther被加到了SYSTEM_SERVICE_FETCHERS中。这里的“Service”还有很多,包括ActivityManagerBluetoothManagerBatteryManager等等。

2. 创建CachedServiceFetcher并记录数目

每创建一个CachedServiceFetcherSystemServiceRegistry中的一个静态变量都会+1:

private static int sServiceCacheSize;
复制代码

CachedServiceFetcher继承了Fetcher,实现了Fetcher#getService,对“Service”进行了懒创建,即首次调用才创建,又声明了createService抽象方法,该方法用于在在静态代码块中实现特定的创建功能,参考第1点中的代码。因为是静态代码块中创建的,所以一种“Service”对应一个具体的CachedServiceFetcher对象,并且CachedServiceFetcher会记录当前值作为下标,这个下标有什么用呢,看下一点。

3. 调用fetcher.getService(ctx)获取“Service”

在调用fetcher.getService(ctx)的时候,CachedServiceFetcher会通过ContextImp中的一个非静态成员来获取“Service”的对象:

// The system service cache for the system services that are cached per-ContextImpl.
final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
复制代码

SystemServiceRegistry#createServiceCache是利用了SystemServiceRegistry的静态成员sServiceCacheSize进行初始化(就是上面那个会+1的变量):

/**
 * Creates an array which is used to cache per-Context service instances.
 */
public static Object[] createServiceCache() {
    return new Object[sServiceCacheSize];
}
复制代码

这下明白了:静态代码块创建了多个“Service”,与此同时静态的sServiceCacheSize记录了“Service”的数量,CachedServiceFetcher记录了“Service”的下标,在每个ContextImpl被创建的时候,非静态的mServiceCache就根据sServiceCacheSize进行初始化,在我们调用Context#getSystemService的时候,如果当前ContextmServiceCache没有对应的“Service”,就创建一个,放到CachedServiceFetcher事先记录好的下标下。

所以,一个ContextImpl对象会持有一套“Service”,保存在mServiceCache中。当然,如果没有“get”过这个“Service”,它就不会被创建(所以我从来都是只用ApplicationContext来“getService”,这样各种“Service”就只有一个实例,减少对象数目😂)。总结一下:

LayoutInflater如何创建View?

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

final XmlResourceParser parser = res.getLayout(resource);
复制代码

接着继续调用LayoutInflater#inflate的重载方法,这里面使用了 循环+递归 的方式实现了深度遍历XML元素,调用了LayoutInflater#createViewFromTag方法创建各个View,其中会首先使用各个工厂类(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的最终重载方法里有重要的三步:

  • 首先通过createViewFromTag创建了根布局(XML最外层View)。
  • 其次通过rInflateChildren方法构建子布局(子布局会使用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);
    }
    ......
}
复制代码

以上过程会调用到ViewGroup#addView方法:

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方法中发送了消息,并将任务加入队列:

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);
        }
    }
}
复制代码

这里实现了每隔一段固定时间进行一次绘制的逻辑,最终需要绘制时会发送异步消息。这里的消息是MSG_DO_SCHEDULE_CALLBACKmHandlerFrameHandler,收到消息后执行Choreographer#doScheduleCallback方法,进过一系列方法后调用到native方法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);
        }
    }
}
复制代码

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

好了,第二个问题,源码证据,如上所述,最后是怎么渲染的,就是native层的工作了,大家可以去了解下双缓冲机制,可以去看 深入浅出Android屏幕刷新原理,这篇写的不错。

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

这篇关于源码阅读#我们所写的View是如何被添加与显示的呢?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!