Android View绘制流程详解
在Android开发中,理解View的绘制流程对于优化应用性能和解决UI相关问题至关重要。本文将详细解析从Activity的setContentView()方法开始,到测量(Measure)、布局(Layout)、绘制(Draw)的完整流程。
引言
Android应用的用户界面是由一个个View组成的,从简单的Button、TextView到复杂的自定义View,它们都需要经过一系列的流程才能呈现在用户面前。了解这些流程不仅有助于我们编写高效的UI代码,还能帮助我们解决一些棘手的界面问题。
View的绘制流程主要分为三个阶段:
- 测量阶段(Measure):确定View的大小
- 布局阶段(Layout):确定View的位置
- 绘制阶段(Draw):将View绘制到屏幕上
这三个阶段由ViewRootImpl统一管理,并按照一定的顺序执行。接下来我们将深入探讨每个阶段的具体实现。
1. setContentView()方法执行过程
1.1 Activity中的setContentView()
当我们在Activity中调用setContentView()方法时,实际调用的是Window的setContentView()方法:
            
            
              java
              
              
            
          
          public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}1.2 PhoneWindow中的setContentView()
Window是一个抽象类,其唯一实现类是PhoneWindow。在PhoneWindow中,setContentView()方法主要完成以下工作:
- 创建DecorView
- 根据主题选择合适的布局
- 将我们设置的布局添加到content容器中
            
            
              java
              
              
            
          
          @Override
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 {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}1.3 DecorView的创建
installDecor()方法负责创建DecorView:
            
            
              java
              
              
            
          
          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);
    }
}DecorView继承于FrameLayout,是Activity的根布局。generateLayout()方法会根据主题选择不同的布局,但都会包含一个ID为com.android.internal.R.id.content的容器组件,我们通过setContentView()设置的布局会被添加到这个容器中。
2. View绘制的触发时机
2.1 Activity启动流程
当Activity启动时,会调用ActivityThread的handleResumeActivity()方法:
            
            
              java
              
              
            
          
          public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
        boolean isForward, String reason) {
    
    final Activity a = r.activity;
    // 获取WindowManager
    ViewManager wm = a.getWindowManager();
    
    // 获取DecorView
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    
    // 将DecorView添加到WindowManager中
    wm.addView(decor, l);
}2.2 WindowManagerImpl的addView()
ViewManager的实际实现类是WindowManagerImpl:
            
            
              java
              
              
            
          
          public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}2.3 WindowManagerGlobal的addView()
在WindowManagerGlobal中,会创建ViewRootImpl并调用其setView()方法:
            
            
              java
              
              
            
          
          public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow, int userId) {
    
    ViewRootImpl root;
    View panelParentView = null;
    if (windowlessSession == null) {
        root = new ViewRootImpl(view.getContext(), display);
    } else {
        root = new ViewRootImpl(view.getContext(), display,
                windowlessSession);
    }
    view.setLayoutParams(wparams);
    root.setView(view, wparams, panelParentView, userId);
}3. 测量流程(Measure)
3.1 ViewRootImpl的setView()
在ViewRootImpl的setView()方法中,会调用requestLayout()方法:
            
            
              java
              
              
            
          
          public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
        int userId) {
    requestLayout();
}3.2 requestLayout()和scheduleTraversals()
            
            
              java
              
              
            
          
          public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        // 检查是否是主线程
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        // 通过Choreographer调度绘制任务
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
}3.3 performTraversals()方法
绘制的入口是performTraversals()方法:
            
            
              java
              
              
            
          
          private void performTraversals() {
    // 获取MeasureSpec
    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
            lp.privateFlags);
    
    // 测量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    // 布局
    performLayout(lp, mWidth, mHeight);
    
    // 绘制
    performDraw();
}3.4 performMeasure()方法
            
            
              java
              
              
            
          
          private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}测量过程从根View开始,递归遍历整个View树,计算每个View的尺寸。
MeasureSpec介绍
在测量过程中,Android使用MeasureSpec类来封装测量规格。MeasureSpec包含两个信息:
- 测量模式(Mode):UNSPECIFIED、EXACTLY、AT_MOST
- 测量大小(Size):具体的尺寸值
- EXACTLY:父容器已确定子View的精确大小,对应match_parent和具体数值
- AT_MOST:子View的大小不能超过父容器指定的大小,对应wrap_content
- UNSPECIFIED:子View想多大就多大,一般用于系统内部
测量过程通过measure()方法传递MeasureSpec,每个View根据自己的LayoutParams和父容器的MeasureSpec来决定自己的测量规格。
4. 布局流程(Layout)
4.1 performLayout()方法
            
            
              java
              
              
            
          
          private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}4.2 View的layout()方法
            
            
              java
              
              
            
          
          public void layout(int l, int t, int r, int b) {
    // 设置View的位置
    setFrame(l, t, r, b);
    
    // 调用onLayout方法
    onLayout(changed, l, t, r, b);
}布局过程确定每个View在屏幕上的位置。
Layout流程详解
布局流程主要分为两个步骤:
- setFrame():设置View本身的四个顶点坐标(left, top, right, bottom)
- onLayout(): ViewGroup特有的方法,用于确定子View的位置
在ViewGroup的onLayout()方法中,会遍历所有子View并调用它们的layout()方法,从而完成整个View树的布局过程。与测量流程类似,布局流程也是从根View开始,逐级向下传递的。
对于LinearLayout、RelativeLayout等不同的ViewGroup,它们的onLayout()实现也不同,这决定了子View的排列方式。
5. 绘制流程(Draw)
5.1 performDraw()方法
            
            
              java
              
              
            
          
          private void performDraw() {
    draw(fullRedrawNeeded);
}5.2 View的draw()方法
绘制过程分为几个步骤:
- 绘制背景
- 绘制内容
- 绘制子View
- 绘制装饰(如滚动条)
            
            
              java
              
              
            
          
          public void draw(Canvas canvas) {
    // Step 1: 绘制背景
    drawBackground(canvas);
    
    // Step 2: 保存画布状态
    saveCount = canvas.getSaveCount();
    
    // Step 3: 绘制内容
    onDraw(canvas);
    
    // Step 4: 绘制子View
    dispatchDraw(canvas);
    
    // Step 5: 绘制装饰
    onDrawForeground(canvas);
    
    // Step 6: 恢复画布状态
    canvas.restoreToCount(saveCount);
}Draw流程详解
绘制流程是View显示到屏幕上的最后一步,它决定了View的最终外观:
- 绘制背景 (drawBackground):绘制View的背景色或背景图片
- 保存画布状态:保存当前Canvas的状态,便于后续恢复
- 绘制内容 (onDraw):这是开发者最常重写的方法,用于绘制View的具体内容
- 绘制子View (dispatchDraw):对于ViewGroup,需要遍历绘制所有子View
- 绘制装饰 (onDrawForeground):绘制滚动条、前景等装饰元素
- 恢复画布状态:恢复之前保存的Canvas状态
需要注意的是,绘制顺序遵循"后绘制的覆盖先绘制的"原则,因此子View会覆盖父View的内容。
6. 总结
Android View的绘制流程可以概括为以下步骤:
- setContentView阶段:Activity通过PhoneWindow创建DecorView,并将布局添加到content容器中
- 添加到WindowManager阶段:Activity启动时,通过WindowManager将DecorView添加到系统窗口管理器中
- 触发绘制阶段:通过ViewRootImpl触发测量、布局、绘制流程
- 测量阶段:从根View开始递归计算每个View的尺寸
- 布局阶段:确定每个View在屏幕上的位置
- 绘制阶段:将View内容绘制到屏幕上
理解这个流程有助于我们:
- 优化布局性能,减少不必要的measure/layout
- 自定义View时正确实现measure、layout、draw方法
- 解决UI相关的问题,如布局错乱、绘制异常等
通过深入理解View的绘制机制,开发者可以更好地进行性能调优和自定义控件开发,从而构建更流畅、用户体验更好的Android应用。