View工作原理之MeasureSpec、ViewImpl、DecorView

初识DecorView和ViewRootImpl/ViewRoot

很多书上都提过这两个类,我一度以为他们俩谁是谁的实现类,查了一下发现ViewRoot是Android2.2以前的,2.2之后就被ViewRootImpl替代了。所以叫ViewRootImpl就好了

DecorView和ViewRootImpl加载的源码

DecorView

  • Activity#setContentView
  • Activity#attach
    mWindow = new PhoneWindow(this);
  • PhoneWindow#setContentView()
public void setContentView(int layoutResID) {
    //mContentParent是mDecor本身或者是mDecor的一个子元素
    if (mContentParent == null) { 
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ......
  • PhoneWindow#installDecor()
  • PhoneWindow#generateDecor()
protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}
  • PhoneWindow#generateLayout
    设置DecorView,是否有titlebar,为DecorView添加子view,即上面提到的mContentParent

ViewRootImpl

DecorView加入Window时序图

ViewRootImpl开启测量、布局和绘制流程

ViewRootImpl是连接WindowManager和DecorView的纽带,它负责开启了视图树的测量 布局 绘制流程,这一过程在它的performTaversals()方法中。

首先看一下performTraversals()方法是从哪里开始被调用的,在ViewRootImpl中有一个setView()方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     synchronized (this) {
         if (mView == null) {
             mView = view;
             ……
             requestLayout();;
         }
     }
 }

requsetLayout方法中调用了scheduleTraversals()方法

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

看到下面post了mTraversalRunnable

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

TraversalRunnable是一个线程

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

开启线程执行doTraversals()方法

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

在doTraversal()找到了唯一调用perform的入口。

由performTraversals()出现的大致流程可以知道,ViewRootImpl的setView()方法,将DecorView和将要加进去的view绑定,通过:

  • requestLayout()--->
  • scheduleTraversals()--->
  • 开启TraversalRunnable线程调用doTraversal()--->
  • performTraversals()

performTraversals()

过程可以大致分为三部分

1.计算出控件树为显示其内容所需的尺寸,即期望的窗口尺寸

开始执行的条件:

//这个mView是通过setView方法传进来的,也就是Activity的根布局DecorView,使用final修饰,以防在遍历过程中被修改
final View host = mView;
 //mAdded在setView方法修改为true,表明Decorview已经添加到了PhoneWindow
if (host == null || !mAdded)
    return;

在往下就是计算当前窗口的宽度和高度,大致可以分为两个部分,第一次执行遍历和不是第一次

//Activity窗口的宽度和高度
int desiredWindowWidth;
int desiredWindowHeight;
...
//用来保存窗口宽度和高度,来自于全局变量mWinFrame,这个mWinFrame保存了窗口最新尺寸
Rect frame = mWinFrame;
//构造方法里mFirst赋值为true,意思是第一次执行遍历吗    
if (mFirst) {
    //是否需要重绘
    mFullRedrawNeeded = true;
    //是否需要重新确定Layout
    mLayoutRequested = true;
    
    这里又包含两种情况:是否包括状态栏
    
    //判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
    if (shouldUseDisplaySize(lp)) {
        // NOTE -- system code, won't try to do compat mode.
        Point size = new Point();
        mDisplay.getRealSize(size);
        desiredWindowWidth = size.x;
        desiredWindowHeight = size.y;
    } else {
        //宽度和高度为整个屏幕的值
        Configuration config = mContext.getResources().getConfiguration();
        desiredWindowWidth = dipToPx(config.screenWidthDp);
        desiredWindowHeight = dipToPx(config.screenHeightDp);
    }
    ...
    else{
    
        这是window的长和宽改变了的情况,需要对改变的进行数据记录
    
        //如果不是第一次进来这个方法,它的当前宽度和高度就从之前的mWinFrame获取
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        /**
         * mWidth和mHeight是由WindowManagerService服务计算出的窗口大小,
         * 如果这次测量的窗口大小与这两个值不同,说明WMS单方面改变了窗口的尺寸
         */
        if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
            //需要进行完整的重绘以适应新的窗口尺寸
            mFullRedrawNeeded = true;
            //需要对控件树进行重新布局
            mLayoutRequested = true;
            //window窗口大小改变
            windowSizeMayChange = true;
        }
    }
    
    ...
    
    //进行预测量
    if (layoutRequested){
        ...
        if (mFirst) {
            // 视图窗口当前是否处于触摸模式。
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            //确保这个Window的触摸模式已经被设置
            ensureTouchModeLocally(mAddedTouchMode);
        } else {
            //六个if语句,判断insects值和上一次比有什么变化,不同的话就改变insetsChanged
            //insects值包括了一些屏幕需要预留的区域、记录一些被遮挡的区域等信息
            if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = true;
            }
            ...
            
            这里有一种情况,我们在写dialog时,会手动添加布局,当设定宽高为Wrap_content时,会把屏幕的宽高进行赋值,给出尽量长的宽度
            
            /**
             * 如果当前窗口的根布局的width或height被指定为WRAP_CONTENT时,
             * 比如Dialog,那我们还是给它尽量大的长宽,这里是将屏幕长宽赋值给它
             */
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = true;
                //判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
                if (shouldUseDisplaySize(lp)) {
                    // NOTE -- system code, won't try to do compat mode.
                    Point size = new Point();
                    mDisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                } else {
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }
    }
}
 // 进行预测量窗口大小,以达到更好的显示大小
windowSizeMayChange |= measureHierarchy(host, lp, res,
        desiredWindowWidth, desiredWindowHeight);

但是当我们的内容很少时,不需要那么多的宽高,measureHierarchy()方法就会缩小宽高来找一个匹配当前内容的样式。很人性化,这就是源码中的亮点地方吧

performMeasure

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

调用了view.measure(),measure()方法并没有做什么,而是将测量交给了它内部的onMeasure()方法

2.第二阶段

* 开始进入三大阶段的第一个阶段了 6个条件只要满足一个就进入
* mFirst true 说明是第一次执行测量布局绘制操作
* windowShouldResize true 即Activity窗口大小需要改变
* insetsChanged true 说明此次窗口overscan等一些边衬区域发生了改变,与上次不一样
* viewVisibilityChanged 说明View的可见性发生了变化
* params 即窗口属性发生了变化,指向了mWindowAttributes
* mForceNextWindowRelayout true 即ViewRootHandler接收到消息MSG_RESIZED_REPORT,即size改变了

//请求WMS计算Activity窗口大小及边衬区域大小
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);

这一部分还是在计算测量

3.layout和draw

//根据这三个值判断需不需要重新布局
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
        || mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
    performLayout(lp, mWidth, mHeight);
    ...
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    //没有取消绘制 也没有重新创建Surface
    if (!cancelDraw && !newSurface) {
        //执行动画效果
        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }
        //开始绘制
        performDraw();
    }

MeasureSpec

LayoutParams

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容