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