Android 11 源码分析之View的测绘流程开始时机:如何在onCreate中获取View的宽高?

本文详细解析了Android中Activity的视图绘制流程,包括视图的测量、布局和绘制过程,以及如何在onCreate和onResume方法中正确获取视图的尺寸。

1、开篇

我们在使用Activity的时候,常常会有在onCreate或者onResume方法中获取View的宽高的需求,但是如果我们直接调用View.getWidth()和View.getHeight()获取到的值是0,很明显此时View的测量工作还没有开始。那么问题来了:

  1. View的测量、布局和绘制流程是在什么时候进行的?
  2. 怎么在onCreate或者onResume方法中获取View的正确宽高?

注意:本文分析的是android.app.Activity类的源码,不是androidx.appcompat.app.AppCompatActivity,两者会有一些差异,但总体的流程大致相同。

2、从setContentView开始

既然要开始View的测绘流程,那么首先必须得有View呀。我们使用Activity的时候通常会在onCreate方法中使用setContentView来设置我们的界面布局,那么就从它开始分析,看看我们的布局最终被添加到哪里去了。setContentView有三个重载方法,我们就看最常用的传layoutId的那个:

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

这里很简单,就直接调用了Window的.setContentView方法。Window是一个抽象类,它的实现类是PhoneWindow,这个读者们可以记一下。 来看看PhoneWindow的setContentView

public void setContentView(int layoutResID) {

    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 布局被添加到mContentParent去了
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

可以看到,我们的布局被添加到mContentParent上面去了,那么mContentParent又是什么呢?
注意到方法一开始做了一个判断

if (mContentParent == null) {
    installDecor();
}

那么installDecor方法中必定对mContentParent进行了赋值操作,我们来看看

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        ...
    }
}

这里先对mDecor赋值,然后再通过mDecor给mContentParent赋值。mDecor是DecorView的实例,而DecorView继承自FrameLayout,来看看它的赋值:

protected DecorView generateDecor(int featureId) {
    // System process doesn't have application context and in that case we need to directly use
    // the context we have. Otherwise we want the application context, so we don't cling to the
    // activity.
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, this);
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

这里就是直接new了一个DecorView而已。我们再看mContentParent的赋值

mContentParent = generateLayout(mDecor);

PhoneWindow#generateLayout: 
protected ViewGroup generateLayout(DecorView decor) {
    //根据主题等进行各种设置
    ...

    // 根据不同的条件获取不同的布局
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
        // System.out.println("Title Icons!");
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        ....
    } else ...

    mDecor.startChanging();

    // 把最终获取的布局添加到mDecor
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    // 获取 id 为 android.R.id.content 的View赋值给contentParent,也就是最终赋值给mContentParent
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

    ...

    mDecor.finishChanging();

    return contentParent;
}


Window#findViewById: 
public <T extends View> T findViewById(@IdRes int id) {
    return getDecorView().findViewById(id);
}

这个方法先是做了很多的设置,然后再根据不同的feature获取不同的布局文件添加到mDecor中,这些布局文件有一个共同的特点,就是必定有一个 id 为 android.R.id.content 的FrameLayout,以screen_simple.xml为例

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

至此,我们就明白了,Activity的布局最终设置到了Window中持有的mDecor上去了。而这个mDecor必定有一个id为android.R.id.content的Framelayout,这也是我们为什么可以在Activity中通过findViewById获取这个View来实现类似加水印等之类的功能了。

另外,还有一个地方值得注意的是:

public void setContentView(int layoutResID) {
    ...
    mContentParentExplicitlySet = true;
}

这里设置了一个标记mContentParentExplicitlySet以标记setContentView已经被调用过了。而在requestFeature方法中有这么一段代码:

public boolean requestFeature(int featureId) {
    if (mContentParentExplicitlySet) {
        throw new AndroidRuntimeException("requestFeature() must be called before adding content");
    }
    ...
    return super.requestFeature(featureId);
}

这也就是requestFeature必须在setContentView方法前调用的原因了。至于为什么要这么设计,结合前面对PhoneWindow#generateLayout方法的分析,很容易就明白了。

3、View的测绘流程

由于我们在onResume中仍然无法获取View的宽高,那么View的测绘流程应该在onResume之后。ActivityThread源码分析中说过,ActivityThread负责Activity等四大组件的生命周期调度。onResume生命周期的调度在ActivityThread#handleResumeActivity方法:

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    ...

    // 调度Activity的onResume生命周期
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    
    ...

    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        // 虽然这里使用ViewManager接收返回值,但是从Activity#getWindowManager()方法看,返回值类型是子类WindowManager 
        // WindowManager是一个接口,实现类是WindowManagerImpl
        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;

                // 关键步骤,调用WindowManager的addView方法
                wm.addView(decor, l);
            } else ...
        }

    } else ...

    
    if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
        ...

        ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
        WindowManager.LayoutParams l = impl != null
                ? impl.mWindowAttributes : r.window.getAttributes();
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                != forwardBit) {
            l.softInputMode = (l.softInputMode
                    & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                    | forwardBit;
            if (r.activity.mVisibleFromClient) {
                ViewManager wm = a.getWindowManager();
                View decor = r.window.getDecorView();
                wm.updateViewLayout(decor, l);
            }
        }

        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) {
            // 这里调用了mDecor.setVisibility(View.VISIBLE)恢复它的可见
            r.activity.makeVisible();
        }
    }

    r.nextIdle = mNewActivities;
    mNewActivities = r;
    
    Looper.myQueue().addIdleHandler(new Idler());
}

前文提到,我们的View被添加到了DecorView中,所以这里我们只关心对DecorView的操作,排除其他代码的干扰。
可以看到,这里首先设置了DecorView的可见性为INVISIBLE,在最后在调用Activity#makeVisible()重新设置为VISIBLE。这是为了避免在这个过程中软键盘的弹起。
此外,处理DecorView有两个关键地方:

// 把DecorView交给WindowManager管理
wm.addView(decor, l);

// 这里应该是根据软键盘设置来更新布局参数
wm.updateViewLayout(decor, l);

先看WindowManager的addView方法,它的实现在WindowManagerImpl里

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    // 交给了WindowManagerGlobal#addView来处理
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}

看WindowManagerGlobal#addView:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
    if (display == null) {
        throw new IllegalArgumentException("display must not be null");
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    ....

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        ...

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try {
            root.setView(view, wparams, panelParentView, userId);
        } catch (RuntimeException e) {
            ...
        }
    }
}

这里做了一些异常判断,其中“Params must be WindowManager.LayoutParams”我们可能比较熟悉。

这个方法里创建了一个ViewRootImpl的实例对象,从名字可以看出来,ViewRootImpl应该是View树的根结点的实现类,不过它没有对应的抽象类ViewRoot。ViewRootImpl实现了ViewParent接口,可以作为View的Parent,但它不是ViewGroup或者View的子类。ViewRootImpl的类头注释如下:

The top of a view hierarchy, implementing the needed protocol between View and the WindowManager. This is for the most part an internal implementation detail of WindowManagerGlobal

既然这里的root是View树的根结点,那么root.setView(view, wparams, panelParentView, userId)应该就是把我们的页面布局挂在到View树上了。

/**
* We have one child
* ViewRootImpl作为ViewParent只有一个子节点
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
        int userId) {
    synchronized (this) {
        if (mView == null) {
            mView = view;

            ...

            // Schedule the first layout -before- adding to the window
            // manager, to make sure we do the relayout before receiving
            // any other events from the system.
            // 上面的注释很明显了,接收系统事件前,调度首次布局流程
            requestLayout();

            // 注册到WMS,初始化事件接收
            InputChannel inputChannel = null;
            if ((mWindowAttributes.inputFeatures
                    & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                inputChannel = new InputChannel();
            }
            mForceDecorViewVisibility = (mWindowAttributes.privateFlags
                    & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                adjustLayoutParamsForCompatibility(mWindowAttributes);
                // mWindowSession是与WMS通信的IBinder接口,接收
                res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mDisplayCutout, inputChannel,
                        mTempInsets, mTempControls);
                setFrame(mTmpFrame);
            } catch (RemoteException e) {
                ...
            } finally {
                ...
            }

            ...

            if (inputChannel != null) {
                if (mInputQueueCallback != null) {
                    mInputQueue = new InputQueue();
                    mInputQueueCallback.onInputQueueCreated(mInputQueue);
                }
                mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                        Looper.myLooper());
            }

            // 把view的parent设置为此ViewRootImpl对象
            view.assignParent(this);
            
            ...

            // 初始化输入时间处理流水线,责任链模式
            CharSequence counterSuffix = attrs.getTitle();
            mSyntheticInputStage = new SyntheticInputStage();
            InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
            InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                    "aq:native-post-ime:" + counterSuffix);
            InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
            InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                    "aq:ime:" + counterSuffix);
            InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
            InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                    "aq:native-pre-ime:" + counterSuffix);

            mFirstInputStage = nativePreImeStage;
            mFirstPostImeInputStage = earlyPostImeStage;
            mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;

            ...
        }
    }
}

就是这里了!requestLayout中开始View树的测绘流程。值得一提的是,向WMS注册和接收WMS事件分发的设置也在这里。
跟进一下requestLayout方法:

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

这里的逻辑很简单,如果当前正在处理布局流程,直接返回;做一个线程检查,然后遍历View树进程测绘。

看一下线程检查:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

这里如果当前线程不是mThread,那么就会抛出一个著名的异常了。这就是为什么说“不能在子线程更新UI”的原因了。但是这个线程检查在ViewRootImpl,我们回想一下,ViewRootImpl对象是在WindowManagerGlobal#addView方法里面创建的,也就是在Activity的onResume之后,那么在此之前如果我们通过子线程更新UI是不是可以成功呢?是的,只要更新UI的代码先于onResume执行的话,子线程更新UI是可以成功的。不过这里我们不深入讨论,有兴趣的读者可以自行测试一下。

回到我们的主流程来,看一下scheduleTraversals方法

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

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

这里通过mChoreographer设置了一个回调,回调里执行了doTraversal方法。

Choreographer是接收系统垂直同步信号(vsync)的类,后续文章在深入分析它。

doTraversal方法:

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        ...

        performTraversals();

        ...
    }
}

performTraversals方法:

private void performTraversals() {
    final View host = mView;
    ...

    host.dispatchAttachedToWindow(mAttachInfo, 0);

    ...

    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    ...

    performLayout(lp, mWidth, mHeight);

    ...

    mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();

    ...

    performDraw();
}

performTraversals方法十分庞大,不过我们只关注View的测绘流程的三大步骤和一个关键的View#dispatchAttachedToWindow就好了。至此,我们已经明白了三大流程的执行时机。

4、onCreate和onResume中正确获取View宽高

通过上面的分析我们已经明白了三大流程的执行时机,但是还不足以解决我们开篇提出的问题。问题的答案在View#dispatchAttachedToWindow方法中:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    
    ...

    if (mFloatingTreeObserver != null) {
        // 关键!
        info.mTreeObserver.merge(mFloatingTreeObserver);
        mFloatingTreeObserver = null;
    }

    ...

    if (mRunQueue != null) {
        // 执行消息队列中的操作
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    ...
}

这个方法里有两个与我们的问题紧密相关的地方:

  1. 一是把mFloatingTreeObserver合并到了info.mTreeObserver,有经验的同学可能会知道,View类中有一个getViewTreeObserver方法

    public ViewTreeObserver getViewTreeObserver() {
        if (mAttachInfo != null) {
            return mAttachInfo.mTreeObserver;
        }
        if (mFloatingTreeObserver == null) {
            mFloatingTreeObserver = new ViewTreeObserver(mContext);
        }
        return mFloatingTreeObserver;
    }
    

    也就是说,getViewTreeObserver方法在dispatchAttachedToWindow前返回的值是mFloatingTreeObserver,dispatchAttachedToWindow后返回的值mAttachInfo.mTreeObserver。而ViewRootImpl#performTraversals方法中调用了mAttachInfo.mTreeObserver.dispatchOnGlobalLayout()!第一个答案就出来了,可以通过以下代码获取View的宽高

    view.viewTreeObserver.addOnDrawListener {
        val width = view.width
        val height = view.height
    }
    
  2. 第二个关键的地方就是mRunQueue.executeActions(info.mHandler)了,看一下mRunQueue的注释:

    /**
     * Queue of pending runnables. Used to postpone calls to post() until this
     * view is attached and has a handler.
     */
    private HandlerActionQueue mRunQueue;
    

    很明显,它是用于在dispatchAttachedToWindow前缓存View#post方法传递的Runnable对象,以在dispatchAttachedToWindow后执行。所以获取View宽高的第二个方法也很容易得出了:

    view.post {
        val width = view.width
        val height = view.height
    }
    

    或许有同学可能会有疑问,ViewRootImpl#performTraversals方法中dispatchAttachedToWindow比performMeasure先执行,

    private void performTraversals() {
        final View host = mView;
        ...
    
        host.dispatchAttachedToWindow(mAttachInfo, 0);
    
        ...
    
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
        ...
    }
    

    这样还能在这些Runnabe中获取View的宽高吗?其实是可以的。由于Handler的消息循环机制,dispatchAttachedToWindow方法中只是把这些Runnable加入了执行队列,真正执行起码会在performTraversals方法执行之后。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值