View相关知识总结

所有涉及的源码基于Android 10, api 29

Window添加

image.png

Window是抽象的概念,实际显示的内容是View, ViewManager是管理View的基本接口, WindowManger继承了ViewManager接口,WindowManagerImplWindowManger的实现类, 内部的实际工作交给WindowManagerGlobal处理。

image.png

WindowManagerGlobaladdView时, 创建了一个ViewRootImpl对象并调用其setView方法,setView主要包含图上的两个过程:requestLayoutaddToDisplay。其中addToDisplay是IPC调用,通过Session最终调用WMS的方法。resquestLayout的相关代码如下:

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

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 消息屏障, 只处理异步消息
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
// mTraversalRunnable的定义
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        // 移除消息屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        // 进入三大流程
        performTraversals();
    }
}
复制代码

通过源码可知最终会执行performTraversals进入View的三大流程。

setContentView

最终调用PhoneWindowsetContentView

public void setContentView(View view, ViewGroup.LayoutParams params) {
    // mContentParent就是放置我们定义的XML视图的位置
    if (mContentParent == null) {
        // 创建DecorView, 并初始化mContentParent
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    // Activity切换动画
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        view.setLayoutParams(params);
        final Scene newScene = new Scene(mContentParent, view);
        transitionTo(newScene);
    } else {
        mContentParent.addView(view, params);
    }
    // 省略
}
// 省略缩减的代码
private void installDecor() {
    if (mDecor == null) {
        // 创建DecorView
        mDecor = generateDecor(-1);
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        // 初始化mContentParent
        mContentParent = generateLayout(mDecor);
    }
}

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

ViewGroup generateLayout(DecorView decor) {
    // 省略
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    // 省略
    return contentParent
}
// 从DecorView中查找id为com.android.internal.R.id.content的ViewGroup
public <T extends View> T findViewById(@IdRes int id) {
    return getDecorView().findViewById(id);
}

复制代码

由源码可以看出setContentView只是创建出了DecorView并将我们定义的视图放到id为com.android.internal.R.id.content的ViewGroup中, 还没有通过WindowManager来真正添加视图到窗口上。

Activity onCreate和onResume执行的时机

  1. onCreateActivityTheard#performLaunchActivity方法中利用Instrumentation#callActivityOnCreate调用ActivityonCreate方法
  2. onResume的调用和onCreate基本一样, 在performResumeActivity方法中, 最终也是通过Instrumentation调用了onResume方法

DecorView何时添加

ActivityTheardhandleResumeActivity方法中, 首先调用了performResumeActivity方法,之后再用WindowManageraddView方法将DecorView添加到Window中。由上面Window添加的过程可以看出最终通过ViewRootImpl完成setView操作, 并触发View的三大流程以及和WMS的IPC通信。

View#post为什么可以获取到视图的宽高

  • 为什么onCreate和onResume获取不到

    通常我们编写代码时在Activity的onCreate或者onResume方法中无法获取到View的宽高, 而通过View#post则可以获取到。说明onCreateonResume方法的执行在View测量之前。从上面onCreateonResume的执行时机以及DecorView被添加的时机可以直到View的测量发生在onCreate和onResume之后, 因此在这些方法中无法获取View的宽高。

  • 为什么View#post可以获取到
    public boolean post(Runnable action) {
        // 获取AttachInfo
        final AttachInfo attachInfo = mAttachInfo;
        // AttachInfo不为null, 之间通过Handler放到MessageQueue中
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        // 消息暂时存到HandlerActionQueue中
        getRunQueue().post(action);
        return true;
    }
    
    // 简略代码
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        // mAttachInfo赋值
        mAttachInfo = info;
        if (mRunQueue != null) {
            // 将暂存的消息发送到MessageQueue等待执行
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
    }
    
    public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }
            mActions = null;
            mCount = 0;
        }
    }
    复制代码

    View#post的源码可以看到mAttachInfo为null的时候会暂存消息, 搜索源码发现mAttachInfodispatchAttachedToWindow方法中被复制, 而该方法在performTraversals中被调用,因此当该方法被调用时,View的三大流程已经开始了,等到Looper轮询到我们post的消息的时候,View的宽高以及被测量了。

View三大流程

ViewRootImplperformTraversals方法中分别调用了performMeasureperformLayoutperformDraw方法而进入View的三大流程

Measure

performMeasure直接调用View#measure, 最终会回调具体ViewGroup实现的onMeasure方法, 对于ViewGroup需要先对其所属的子View进行测量。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    // 子View的LayoutParam参数
    final LayoutParams lp = child.getLayoutParams();
    // 根据父View的MeasureSpec和View自己的LayoutParam的到View自己的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    // 调用子View的measure方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// View#onMeasure的默认实现, 有具体的View或者ViewGroup负责重写
// 参数的两个MeasureSpec都是上面 getChildMeasureSpec 方法计算到的View自己的 MeasureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码

注意:onMeasure方法中的MeasureSpec参数是当前View自身的MeasureSpec

  • MeasureSpec

    View类的一个静态内部类, 用一个32的int数据来表示View的测量模式, 以及对应的测量数值。
    高2位表示测量模式, 低30表示具体数值。
    测量模式有UNSPECIFIED,EXACTLY,AT_MOST三种

  • MeasureSpec的计算

    上面提到通过getChildMeasureSpec来计算View的MeasureSpec

    // childDimension:LayoutParams中的width或者height, 就是XML中写的
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 父View对应的Mode和Size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
    
        int size = Math.max(0, specSize - padding); // 去掉padding
    
        int resultSize = 0;
        int resultMode = 0;
        
        // ViewGroup.LayoutParams中MATCH_PARENT定义为-1, WRAP_CONTENT定义为-2
    
        switch (specMode) {
        // Parent has imposed an exact size on us 父View的Mode是EXACTLY
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) { // XML中写的具体dp数值
                resultSize = childDimension; 
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it. 和父View一样大
                resultSize = size; 
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.  最大不超过父View
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent has imposed a maximum size on us  父View指定了一个最大值
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) { // 
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    复制代码

Layout

performLayout直接调用View#layout方法, 最终调用了具体ViewGroup的onLayout进行布局操作, 计算中子View在布局中的具体位置后, 再调用具体View的layout来设置View的left、top、right、bottom相关参数。

Draw

performDraw方法,最终会调用具体View的onDraw方法绘制View自身

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享