Android View绘制流程

Window创建过程中了解到:
在WM创建了ViewRootImpl之后,ViewRootImpl执行了requestLayout操作
这个requestLayout就是整个绘制的起点

ViewRootImpl

子View调用requestLayout最终会调用ViewRootImpl.requestLayout进行绘制

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
        
    // 创建ViewRootImpl时的线程
    final Thread mThread;
    
    // 是否正在进行布局,即正在执行
    boolean mHandlingLayoutInLayoutRequest = false;
    
    // 是否已经请求了布局
    boolean mLayoutRequested;
    
    // 是否有待处理的布局请求
    public boolean mTraversalScheduled;
    
    // 是否正在进行布局传递 layout()
    private boolean mInLayout = false;
    
    // handle消息屏障标识
    int mTraversalBarrier;
    
    // VSync垂直信号
    final Choreographer mChoreographer;
    
    final View.AttachInfo mAttachInfo;
    
    public ViewRootImpl(Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
            
            ...
        mThread = Thread.currentThread();
        
        // 创建mAttachInfo
        mAttachInfo = new View.AttachInfo(.., this, mHandler, ..);
    
    }
    
   // 将Window添加到窗口管理器
   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
            
       mView = view;
       // 屏幕输入事件
       mInputChannel = new InputChannel();
       ...
       
       // 在添加到窗口管理器之前请求一次布局
       // 以确保我们从系统接收任何其他事件之前进行重新布局。
       requestLayout();
       
       // Window的添加等操作
       mWindowSession.addToDisplayAsUser(mWindow, ...., mInputChannel, ...);
       
       ...
       
       // 将ViewRootImpl设置为DecorView的parent (继承ViewParent)
       view.assignParent(this);
       
       ...
   }  
        
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }  
    
    // 这边检测了当前线程是否与创建ViewRootImpl的线程一致
    // 不一致,则抛异常
    // 子线程创建ViewRootImpl(在子线程调用WindowManager.addView),则在当前线程就能更新ui?
    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
    
    // 利用同步消息屏障 + Vsync信号机制 
    // 在下一个Vsync信号来之前阻止同步消息的执行
    // 在下一个Vsync信号来时执行刷新操作
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            //此字段保证同时间多次更改只会刷新一次,例如TextView连续两次setText(),也只会走一次绘制流程
            mTraversalScheduled = true;
            
            // 发送同步消息屏障,只有处理异步消息能够得到处理
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            
            // mChoreographer内部发送了一个异步消息
            // 在下一个Vsync信号来时,会执行异步消息执行
            // 在异步消息执行时,执行了mTraversalRunnable
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }
    
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            // 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            
            // 执行测量/布局/绘制
            performTraversals();
            
        }
    }
    
    private void performTraversals() {
        // DecorView
        final View host = mView;
        ....
        // 第一次调用
            if (mFirst) {
             // mAttachInfo 初始化
           ...
           host.dispatchAttachedToWindow(mAttachInfo, 0);
        }
        
        getRunQueue().executeActions(mAttachInfo.mHandler);
        
        // 当前是否有布局请求的副本
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
      
        if (layoutRequested) {
            // 清除状态
            mLayoutRequested = false;
        }
        
        boolean windowShouldResize = layoutRequested && ...
        
        if (mFirst || windowShouldResize ...) {
                
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                        // 测量
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
            ...
            
            // 如果有设置权重,比如LinearLayout设置了weight,需要测量两次
            if (measureAgain) {
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
            ... 
                
        }
        
        // 布局
        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        
        if (didLayout) {
        
            performLayout(lp, mWidth, mHeight);
            
            ...
        }
        
        // 绘制
        performDraw();
    }
    
    
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
         
         mInLayout = true;
         
         final View host = mView;
         
         // 执行第一次布局
         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
         
         mInLayout = false;
         
         // 布局完成后,检查是否有待处理的请求,有则处理(主要是布局期间调用requestLayout的操作)
         int numViewsRequestingLayout = mLayoutRequesters.size();
         if (numViewsRequestingLayout > 0) {
                
             mHandlingLayoutInLayoutRequest = true;
             
             // 这边主要是校验,请求requestLayout的View是否扔有效
             // 有几个条件:mParent不为null,设置了强制刷新标识PFLAG_FORCE_LAYOUT
             ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false);
             
             
             int numValidRequests = validLayoutRequesters.size();
             for (int i = 0; i < numValidRequests; ++i) {
             
                  final View view = validLayoutRequesters.get(i);
                       
                  view.requestLayout();
            }
            
            mInLayout = true;
            
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                    
            mHandlingLayoutInLayoutRequest = false;
         }
         
         mInLayout = false;
    }
}
public class View implements ... {

        protected ViewParent mParent;
    
    AttachInfo mAttachInfo;
    
    private HandlerActionQueue mRunQueue;
    
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    
        mAttachInfo = info;
        ...
        // 如果调用post时,mAttachInfo为null,会将消息延迟到dispatchAttachedToWindow执行
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        ...
    }
        
    // 如果ViewRootImpl正在执行布局传递时,再次调用requestLayout()
    // 1.请求会延迟到当前布局请求完成后再次执行布局
    // 2.或者当前帧绘制完成后,下一次布局请求发起时执行
    @CallSuper
    public void requestLayout() {
    
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            ViewRootImpl viewRoot = getViewRootImpl();
            
            // 正在执行布局传递
            if (viewRoot != null && viewRoot.isInLayout()) {
                    // 如果正在执行延迟的布局请求,则当前请求再次延迟到下一次布局请求发起时执行
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
        
        // 设置强制刷新标识,ViewTree重新测量布局(measure、layout)的时候
        // 会过滤没有设置这个标识的View
        // 相关代码在measure、layout方法中,这边就不贴了
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        
                // 递归调用mParent的requestLayout,最终调用到ViewRootImpl
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
       
    }
    
    void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        }
    }
    
}

1.requestLayout最终传递到ViewRootImpl,并且传递的时候,给路过的View设置强制刷新标识
2.ViewRootImpl使用同步屏障+Choreographer+异步消息,在下一个VSync信号执行刷新
3.ViewRootImpl会依次递归执行测量、布局、绘制
4.未设置强制刷新标识的View,不会执行测量、布局
5.在layout()递归期间,再次调用requestLayout,不会再次发起请求,会延迟到当前布局请求完成后再次执行布局;或者当前帧绘制完成后,下一次布局请求发起时执行

View.post

View.post为何能拿到View的宽高信息
1.拿到 View 内部的 mAttachInfo,拿到 mAttachInfo.handler,调用post将runnable插入消息队列
2.又下可知,mAttachInfo 在 ViewRootImpl 执行测量时初始化
3.此时,刚插入消息队列的message肯定会在测量完成后才会执行
5.所以,View.post 能拿到View的宽高信息

public class View implements ... {

    // 当视图附加到其父窗口时提供给视图的一组信息。
    AttachInfo mAttachInfo;
    
    private HandlerActionQueue mRunQueue;
    
    // 由于DecorView是一个ViewGroup,所以会将dispatchAttachedToWindow传递给每一个子View
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    
        mAttachInfo = info;
        ...
        // 如果调用post时,mAttachInfo为null,会将消息延迟到dispatchAttachedToWindow执行
        if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        ...
    }
    
    // 由上述 ViewRootImpl 可知,
    // mAttachInfo 在 ViewRootImpl 执行测量时,调用View.dispatchAttachedToWindow 传进来
    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        
        // 如果 mAttachInfo 还未初始化,延迟到dispatchAttachedToWindow执行
        getRunQueue().post(action);
        return true;
     }
    
}

invalidate

最终调用的是ViewRootImp的scheduleTraversals()
由于mLayoutRequested为false,所以不会执行测量和布局
硬件绘制和软件绘制具有不一样的调用流程

绘制

前面经过View的测量、布局,收集到了绘制需要的信息之后,需要将其绘制到屏幕上,这里需要区分软件绘制和硬件绘制

这里就不细讲了,主要参考:
屏幕刷新机制
关于UI渲染,你需要了解什么
RenderThread:异步渲染动画

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

推荐阅读更多精彩内容