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应用。

相关推荐
2501_915909067 小时前
iOS 26 性能监控工具有哪些?多工具协同打造全方位性能分析体系
android·macos·ios·小程序·uni-app·cocoa·iphone
美狐美颜SDK开放平台8 小时前
美颜SDK跨平台适配实战解析:让AI美颜功能在iOS与Android都丝滑运行
android·人工智能·ios·美颜sdk·直播美颜sdk·第三方美颜sdk·美颜api
这个昵称也不能用吗?9 小时前
【安卓 - 小组件】图片的渲染
android
2501_915918419 小时前
uni-app 上架 iOS 应用全流程 从云打包到开心上架(Appuploader)免 Mac 上传发布指南
android·macos·ios·小程序·uni-app·iphone·webview
2501_938791229 小时前
PHP Laravel 10 框架:使用队列处理异步任务(邮件发送 / 数据导出)
android·php·laravel
2501_9159214310 小时前
iOS 抓包工具有哪些,开发者的选型与实战指南
android·ios·小程序·https·uni-app·iphone·webview
东哥很忙XH10 小时前
flutter开发的音乐搜索app
android·javascript·flutter
I'm Jie10 小时前
Gradle 的项目结构与源码集(Source Sets)详解(Kotlin DSL)
android·java·开发语言·spring boot·spring·kotlin·gradle
Android-Flutter10 小时前
kotlin - 显示heic图片
android·kotlin