Android View绘制流程详解(一)

Android View绘制流程详解

在Android开发中,理解View的绘制流程对于优化应用性能和解决UI相关问题至关重要。本文将详细解析从Activity的setContentView()方法开始,到测量(Measure)、布局(Layout)、绘制(Draw)的完整流程。

引言

Android应用的用户界面是由一个个View组成的,从简单的Button、TextView到复杂的自定义View,它们都需要经过一系列的流程才能呈现在用户面前。了解这些流程不仅有助于我们编写高效的UI代码,还能帮助我们解决一些棘手的界面问题。

View的绘制流程主要分为三个阶段:

  1. 测量阶段(Measure):确定View的大小
  2. 布局阶段(Layout):确定View的位置
  3. 绘制阶段(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()方法主要完成以下工作:

  1. 创建DecorView
  2. 根据主题选择合适的布局
  3. 将我们设置的布局添加到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包含两个信息:

  1. 测量模式(Mode):UNSPECIFIED、EXACTLY、AT_MOST
  2. 测量大小(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流程详解

布局流程主要分为两个步骤:

  1. setFrame():设置View本身的四个顶点坐标(left, top, right, bottom)
  2. 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()方法

绘制过程分为几个步骤:

  1. 绘制背景
  2. 绘制内容
  3. 绘制子View
  4. 绘制装饰(如滚动条)
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的最终外观:

  1. 绘制背景 (drawBackground):绘制View的背景色或背景图片
  2. 保存画布状态:保存当前Canvas的状态,便于后续恢复
  3. 绘制内容 (onDraw):这是开发者最常重写的方法,用于绘制View的具体内容
  4. 绘制子View (dispatchDraw):对于ViewGroup,需要遍历绘制所有子View
  5. 绘制装饰 (onDrawForeground):绘制滚动条、前景等装饰元素
  6. 恢复画布状态:恢复之前保存的Canvas状态

需要注意的是,绘制顺序遵循"后绘制的覆盖先绘制的"原则,因此子View会覆盖父View的内容。

6. 总结

Android View的绘制流程可以概括为以下步骤:

  1. setContentView阶段:Activity通过PhoneWindow创建DecorView,并将布局添加到content容器中
  2. 添加到WindowManager阶段:Activity启动时,通过WindowManager将DecorView添加到系统窗口管理器中
  3. 触发绘制阶段:通过ViewRootImpl触发测量、布局、绘制流程
  4. 测量阶段:从根View开始递归计算每个View的尺寸
  5. 布局阶段:确定每个View在屏幕上的位置
  6. 绘制阶段:将View内容绘制到屏幕上

理解这个流程有助于我们:

  • 优化布局性能,减少不必要的measure/layout
  • 自定义View时正确实现measure、layout、draw方法
  • 解决UI相关的问题,如布局错乱、绘制异常等

通过深入理解View的绘制机制,开发者可以更好地进行性能调优和自定义控件开发,从而构建更流畅、用户体验更好的Android应用。

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android