requestLayout()
如何启动绘制流程?当调用 View.requestLayout()
时,源码逻辑如下(基于 Android 13 源码):
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear(); // 清除测量缓存
mLayoutRequested = true; // 标记布局需要更新
// 向上遍历父容器,直到找到 ViewRootImpl(窗口顶级容器)
ViewParent parent = getParent();
if (parent != null && !parent.isLayoutRequested(this)) {
parent.requestLayout(); // 递归父容器的 requestLayout
}
}
checkThread()
确保在主线程(否则抛 CalledFromWrongThreadException
),然后调用 scheduleTraversals()
: private void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 通过 Choreographer 安排下一帧的绘制任务(重点!)
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// 触发 vsync 信号等待(60Hz 时约 16ms 一次)
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput(); // 处理输入事件
}
}
}
requestLayout()
只会标记布局需要更新,真正的绘制流程由 Choreographer 驱动,结合 vsync 信号(60Hz 对应 16ms 间隔)在下一帧执行。requestLayout()
,会因 ViewRootImpl.checkThread()
校验失败而崩溃(除非通过 Looper.getMainLooper()
切换到主线程)。Android 的屏幕刷新率通常为 60Hz(即每秒 60 帧),每帧理想耗时 16ms(1000ms/60 ≈ 16.6ms
)。绘制流程由 Choreographer 协调,其核心逻辑在 Choreographer.java:
CALLBACK_TRAVERSAL
),执行 ViewRootImpl
的 performTraversals()
方法,该方法依次调用: performMeasure() → performLayout() → performDraw()
面试官常问:“如果自定义 View 的 onMeasure
或 onLayout
耗时过长,会发生什么?”
→ 答:超过 16ms 会导致掉帧,若主线程阻塞超过 5s(如后台耗时操作未切线程),则触发 ANR。
触摸事件由 MotionEvent 封装,其坐标数据与 屏幕采样率(getDisplay().getRefreshRate()
)和 触控传感器采样率有关:
ACTION_MOVE
)包含 当前坐标(getX()
/getY()
),但多次 ACTION_MOVE
的间隔由采样率决定(如 120Hz 屏幕可能每 8ms 产生一次移动事件)。getPointerCount()
获取触点数量,每个触点有独立坐标(getX(int pointerIndex)
)。源码证据:
在 MotionEvent.java 中,坐标存储在数组 mX
和 mY
中,支持最多 MAX_POINTERS
(通常为 10)个触点。每次触摸事件的坐标数量等于当前活动的触点数,而非 “移动前和移动后” 两个坐标(用户之前回答错误)。
面试官追问:“如何优化滑动卡顿?”
→ 答:减少 onTouchEvent
耗时,避免在事件处理中执行复杂计算;使用 VelocityTracker
计算滑动速度;确保 View
层级简洁,减少 measure/layout/draw
耗时。
MeasureSpec
控制),调用 onMeasure()
。
onMeasure()
,并通过 setMeasuredDimension()
设置最终尺寸,否则抛 IllegalStateException
。left/top/right/bottom
),调用 onLayout()
(ViewGroup 实现,View 默认为 0,0 固定大小)。background → content → children → decoration
,调用 onDraw()
(View 实现,ViewGroup 通常不重绘,除非设置 willNotDraw=false
)。关键类:
mLayoutRequested
, mDirty
)判断是否需要执行某一步骤。layout()
,是布局嵌套的核心逻辑。adb shell systrace -t 10 -o trace.html gfx input view
,分析 performTraversals
耗时是否超过 16ms。onMeasure/onLayout/onDraw
中添加耗时统计,如: long start = System.currentTimeMillis();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d("ViewDebug", "onMeasure耗时:" + (System.currentTimeMillis() - start) + "ms");
Choreographer.FrameCallback
监听每帧耗时,超过 16ms 则记录警告。requestLayout()
→ ViewRootImpl.scheduleTraversals()
→ Choreographer 绑定 vsync 信号,16ms 一周期。performTraversals()
串联 measure/layout/draw
,自定义 View 需正确实现三大方法,避免耗时操作。MotionEvent
支持多点触控,坐标数量由触点数决定,采样率影响事件频率(非 “两个坐标”)。“请详细说明 Android View 的绘制流程,涉及哪些关键类和方法?自定义 View 时需要重写哪些方法?”
Android View 绘制分为 measure(测量)、layout(布局)、draw(绘制) 三大流程,由 ViewRootImpl
统一协调
measure 阶段(确定宽高)
View.measure(int, int)
→ View.onMeasure(int, int)
MeasureSpec
控制测量规则(EXACTLY
/AT_MOST
/UNSPECIFIED
),自定义 View 需重写 onMeasure
并调用 setMeasuredDimension(width, height)
,否则抛 IllegalStateException
。// ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if (child.getVisibility() != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec); // 递归测量子View
}
}
}
layout 阶段(确定位置)
View.layout(int, int, int, int)
→ View.onLayout(boolean, int, int, int, int)
(仅 ViewGroup 需重写,View 默认位置为 (0,0))。setChildFrame(child, left, top, right, bottom)
确定子 View 坐标,如 RecyclerView
的 LayoutManager
在此阶段计算子 View 布局。draw 阶段(像素渲染)
View.draw(Canvas)
,分 6 步(背景→内容→子 View→修饰),自定义 View 需重写 onDraw(Canvas)
绘制内容(如绘制文本、路径)。// View.java
public void draw(Canvas canvas) {
int saveCount;
// 1. 绘制背景
drawBackground(canvas);
// 2. 绘制主体内容(自定义View重写此步)
onDraw(canvas);
// 3. 绘制子View(仅ViewGroup执行)
dispatchDraw(canvas);
// 4. 绘制前景(边框、滚动条等)
onDrawForeground(canvas);
}
ViewRootImpl.performTraversals()
是三大流程的总入口,通过 mLayoutRequested
/mDirty
标志位判断是否执行对应流程。onDraw
;onMeasure
并正确处理 MeasureSpec
;onLayout
并遍历子 View 调用 layout()
。“requestLayout()
、invalidate()
、postInvalidate()
有什么区别?为什么不能在子线程调用前两者?”
方法 | 触发流程 | 线程限制 | 使用场景 |
---|---|---|---|
requestLayout() |
触发 measure + layout |
必须主线程 | View 尺寸 / 布局参数变化(如setLayoutParams ) |
invalidate() |
触发 draw (可能先触发requestLayout ) |
必须主线程 | View 内容变化(如文字 / 颜色更新) |
postInvalidate() |
内部通过Handler 切主线程 |
子线程可用 | 子线程中触发重绘(等价invalidate() 的线程安全版) |
requestLayout()
线程校验:
// ViewRootImpl.java
public void requestLayout() {
checkThread(); // 校验是否为主线程,否则抛异常
mLayoutRequested = true;
scheduleTraversals();
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
子线程调用会因 checkThread()
失败崩溃,需通过 view.post(() -> requestLayout())
间接调用。
invalidate()
触发条件:
LayoutParams
修改),invalidate()
会先调用 requestLayout()
触发测量布局,再执行绘制。setText()
),直接标记 mDirty
区域,跳过测量布局,仅重绘。“为什么说 Android 的理想布局周期是 16ms?超过会怎样?如何通过源码定位掉帧?”
16ms 的由来(VSYNC 机制):
1000ms/60 ≈ 16.6ms
,由 Choreographer
(源码Choreographer.java
)监听硬件 VSYNC 信号(垂直同步),确保绘制与屏幕刷新同步。Choreographer
通过 FrameDisplayEventReceiver
接收 VSYNC 事件,触发 ViewRootImpl
的 performTraversals()
执行绘制(约 16ms 一次)。掉帧与卡顿:
measure+layout+draw
总耗时超过 16ms,会导致 掉帧(该帧被丢弃),连续掉帧即感知为卡顿;Input dispatching timed out
。源码级性能分析:
adb shell systrace -t 10 -o trace.html gfx input view
,查看 performTraversals
各阶段耗时(绿色为 measure,蓝色为 layout,红色为 draw)。Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
long frameDuration = frameTimeNanos - mLastFrameTimeNanos;
if (frameDuration > 16_666_667) { // 超过16.6ms即掉帧
Log.w("ViewRender", "Frame dropped: " + frameDuration);
}
mLastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
“MotionEvent
中的坐标如何获取?多点触控时如何区分不同触点?移动事件的坐标数量由什么决定?”
坐标类型:
getX()
/getY()
(相对于屏幕左上角);getRawX()
/getRawY()
(相对于设备左上角,含系统栏高度)。多点触控支持:
getPointerCount()
获取触点数量,每个触点有唯一 pointerId
(通过 getPointerId(int index)
获取);getX(int pointerIndex)
/getY(int pointerIndex)
,如双指缩放时需同时获取两个触点的坐标。采样率影响:
Display.getRefreshRate()
)和 触控传感器采样率 相关,高刷新率屏幕(如 120Hz)会更频繁触发 ACTION_MOVE
(约 8ms 一次),每次事件携带当前触点坐标(非 “移动前 / 后两个坐标”)。MotionEvent
内部通过数组存储多触点坐标(mX[]
, mY[]
),支持最多 MAX_POINTERS
(通常 10 个),示例:
// 处理多点触控
for (int i = 0; i < event.getPointerCount(); i++) {
float x = event.getX(i);
float y = event.getY(i);
int pointerId = event.getPointerId(i);
// 根据pointerId区分不同手指
}
“自定义 View 时容易出现哪些性能问题?如何避免过度绘制和布局嵌套?”
测量尺寸未正确处理:
onMeasure
中调用 setMeasuredDimension()
,导致运行时崩溃;@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int width = measureDimension(DEFAULT_WIDTH, widthSpec);
int height = measureDimension(DEFAULT_HEIGHT, heightSpec);
setMeasuredDimension(width, height);
}
private int measureDimension(int defaultSize, int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
return specMode == MeasureSpec.EXACTLY ? specSize : defaultSize;
}
过度绘制优化:
android:background="@null"
或 setWillNotDraw(true)
(ViewGroup 默认不绘制,减少不必要的draw
调用)。布局嵌套优化:
ConstraintLayout
替代多层 LinearLayout/RelativeLayout
,减少 measure/layout
次数;ViewStub
延迟加载非必要视图,避免启动时全量渲染。