ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

第 25 章:View 系统与输入事件分发

第 25 章:View 系统与输入事件分发 Android View 系统是客户端侧渲染与事件处理框架,将声明式 XML(或代码动态构建)转换为屏幕上可交互的像素画面。它运行在应用进程中,与第 23 章介绍的服务端WindowManagerService不同,通过桥接对象ViewRootImpl与服务端通信。每一次按钮点击、滚动手势、键盘导航事件、无障碍查询,都会流经 View 层级树。每一个动画帧都由Choreographer调度,经由ViewRootImpl的performTraversals()流转,最终由 HWUI 在独立渲染线程完成光栅化绘制。本章从源码层面对 View 系统进行完整解析。我们将追踪从 XML 布局解析膨胀到硬件加速渲染的完整生命周期,细致剖析触摸事件分发算法,研究焦点、无障碍、窗口 Insets 以及自定义 View 背后的实现机制。25.1 View 层级:View、ViewGroup、ViewRootImpl25.1.1 基础核心类Android View 系统建立在三大基础组件之上:View— 原子基础单元。屏幕上每一个可见元素(Button、TextView、ImageView、自定义控件)都是 View 的子类。View.java代码量超过 35000 行,是 Android 框架中体量最大的文件之一,负责测量、布局、绘制、触摸事件、焦点、无障碍、动画等全部逻辑。ViewGroup— 组合容器。ViewGroup 继承自 View,可以持有一组有序的子 View 对象。LinearLayout、FrameLayout、ConstraintLayout、RecyclerView等布局全部为 ViewGroup 子类。ViewRootImpl— 桥接器。每个窗口的 View 层级树顶端存在唯一的ViewRootImpl实例,将应用的 View 树与system_server中的WindowManagerService建立连接。它持有 Surface,管理Choreographer回调,通过performTraversals()驱动完整的测量‑布局‑绘制循环。源码路径:frameworks/base/core/java/android/view/ViewGroup.java(第 142 行)public abstract class ViewGroup extends View implements ViewParent, ViewManager { ... private View mFocused; private View mDefaultFocus; View mFocusedInCluster; ... }25.1.2 类层级关系25.1.3 窗口‑View 的对应关系Android 中每一个窗口严格对应一个ViewRootImpl实例。调用WindowManagerImpl.addView()(例如 Activity 的DecorView首次显示)时,执行如下调用链:WindowManagerImpl.addView(decorView, layoutParams)→WindowManagerGlobal.addView()→ 新建ViewRootImpl(context, display)→viewRootImpl.setView(decorView, layoutParams, panelParent)ViewRootImpl.setView()内部(第 1649 行):源码路径:frameworks/base/core/java/android/view/ViewRootImpl.javapublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) { checkThreadCompat(); synchronized (this) { if (mView == null) { mView = view; ... // 在添加到窗口管理器之前调度第一次布局, // 确保在收到系统其他事件前完成relayout。 requestLayout(); InputChannel inputChannel = null; if ((mWindowAttributes.inputFeatures WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { inputChannel = new InputChannel(); } ...本段代码关键点:ViewRootImpl将根视图保存在mView。在窗口向服务端注册之前调用requestLayout(),保证在任何输入事件到来前完成首轮测量布局。创建InputChannel,用于接收系统服务端InputDispatcher下发的输入事件。25.1.4 AttachInfo 数据结构当 View 附加到窗口时,会获得一个AttachInfo对象,保存该窗口下整棵 View 树共享的窗口状态:层级树中每一个 View 都持有同一个AttachInfo引用,通过它可以获取消息投递 Handler、硬件加速渲染器、窗口可见性、布局变更回调的树观察者。25.1.5 View 标识与 View 树每一个 View 拥有数字 ID(XML 中android:id或代码setId()设置)以及可选的临时名称。findViewById()对 View 树执行深度优先搜索,根据 ID 查找目标 View。每一个 ViewGroup 内部以数组存储子视图:源码路径:frameworks/base/core/java/android/view/ViewGroup.javaprivate View[] mChildren; private int mChildrenCount;子视图按照数组顺序绘制(索引 0 最先绘制,位于底层;后续索引绘制在上层),也可以通过getChildDrawingOrder()自定义绘制顺序。25.1.6 私有标志位:内部状态机View 通过一组私有位掩码标志mPrivateFlags、mPrivateFlags2、mPrivateFlags3、mPrivateFlags4维护自身状态,几乎控制 View 生命周期的全部行为。标志字段十六进制值含义PFLAG_WANTS_FOCUSmPrivateFlags0x00000001布局阶段 View 申请焦点PFLAG_FOCUSEDmPrivateFlags0x00000002View 当前持有焦点PFLAG_SELECTEDmPrivateFlags0x00000004View 处于选中状态PFLAG_HAS_BOUNDSmPrivateFlags0x00000010View 已经分配边界坐标PFLAG_DRAWNmPrivateFlags0x00000020View 至少完成一次绘制PFLAG_DRAW_ANIMATIONmPrivateFlags0x00000040View 正在执行动画PFLAG_SKIP_DRAWmPrivateFlags0x00000080View 无绘制内容,跳过 drawPFLAG_REQUEST_TRANSPARENT_REGIONSmPrivateFlags0x00000200申请透明区域计算PFLAG_DRAWABLE_STATE_DIRTYmPrivateFlags0x00000400Drawable 状态需要刷新PFLAG_MEASURED_DIMENSION_SETmPrivateFlags0x00000800已经调用 setMeasuredDimension ()PFLAG_FORCE_LAYOUTmPrivateFlags0x00001000强制执行下一次测量‑布局PFLAG_LAYOUT_REQUIREDmPrivateFlags0x00002000测量完成后需要执行布局PFLAG_PRESSEDmPrivateFlags0x00004000View 被按下(触摸 DOWN)PFLAG_DRAWING_CACHE_VALIDmPrivateFlags0x00008000绘制缓存有效PFLAG_DIRTYmPrivateFlags0x00200000View 需要重绘PFLAG_INVALIDATEDmPrivateFlags0x80000000View 执行过 invalidatePFLAG_PREPRESSEDmPrivateFlags0x02000000预按下状态(点击延时)单帧内标志位生命周期流转:invalidate→ 标记 InvalidatedrequestLayout→ 标记 ForceLayout 执行 measure → 设置 MeasuredSet 设置setMeasuredDimension→ 设置 PFLAG_LAYOUT_REQUIRED 执行 layout → 设置 PFLAG3_IS_LAID_OUT 执行 draw → 设置 PFLAG_DRAWN、PFLAG_DRAWING_CACHE_VALID 帧完成理解这些标志位对调试至关重要 —— 当 View 无法正常绘制时,标志位可以定位它具体卡在流水线哪一个环节。View.toString()调试模式会输出这些标志位的简写形式:// View.toString()调试输出示例: // V.E..... ........ 0,0‑1080,1920 #7f080001 android:id/content // | | flags: V=VISIBLE, E=ENABLED, F=FOCUSED, etc.25.1.7 View 坐标系统View 存在多套容易混淆的坐标体系:坐标系统原点使用场景屏幕坐标物理显示屏左上角getLocationOnScreen()、无障碍模块窗口坐标窗口 Surface 左上角getLocationInWindow()、触摸事件父容器坐标View 在父容器中的位置mLeft/mTop、getX()、getY()本地坐标View 内容区域左上角onDraw()、onTouchEvent()滚动坐标叠加滚动偏移后的坐标onDraw()内部 CanvasgetX()与getY()返回叠加 translation 之后的视觉位置:getX() = mLeft + getTranslationX()。对做动画的 View 非常关键:getLeft()返回布局原始位置,getX()返回屏幕上实际视觉位置。25.1.8 ViewTreeObserver每一棵 View 树都具备ViewTreeObserver,提供全局布局事件回调:回调触发时机OnGlobalLayoutListener布局流程执行完成之后OnPreDrawListener绘制即将开始;可取消本次绘制OnDrawListener每一次绘制流程执行期间OnScrollChangedListener任意 View 发生滚动时OnGlobalFocusChangeListener焦点在 View 之间切换时OnWindowAttachListenerView 树附加 / 脱离窗口时OnWindowFocusChangeListener窗口获得 / 失去焦点时OnTouchModeChangeListener触摸模式发生切换时OnGlobalLayoutListener最常用,用于布局完成后获取 View 真实宽高,布局完成前getWidth()/getHeight()返回 0。view.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // View已经完成测量和布局 int width = view.getWidth(); int height = view.getHeight(); // 移除监听器避免重复回调 view.getViewTreeObserver() .removeOnGlobalLayoutListener(this); } });25.2 测量‑布局‑绘制循环Android View 系统核心渲染循环就是测量‑布局‑绘制,由ViewRootImpl.performTraversals()驱动。UI 需要更新时,该方法会在下一次 VSYNC 信号到来时被Choreographer调用,每帧执行一次。25.2.1 三大阶段25.2.2 MeasureSpec:约束协议测量系统通过MeasureSpec向子 View 传递约束,它是一个 32 位整型,同时编码模式与尺寸。源码路径:frameworks/base/core/java/android/view/View.java(第 31989 行)public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 MODE_SHIFT; public static final int UNSPECIFIED = 0 MODE_SHIFT; // 0x00000000 public static final int EXACTLY = 1 MODE_SHIFT; // 0x40000000 public static final int AT_MOST = 2 MODE_SHIFT; // 0x80000000 public static int makeMeasureSpec(int size, int mode) { return (size ~MODE_MASK) | (mode MODE_MASK); } public static int getMode(int measureSpec) { return (measureSpec MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec ~MODE_MASK); } }三种模式含义:模式数值含义触发来源EXACTLY0x40000000子 View 必须使用该精确尺寸match_parent、明确 dp/px 赋值AT_MOST0x80000000子 View 最大不能超过该尺寸wrap_contentUNSPECIFIED0x00000000无约束,子 View 自行决定尺寸ScrollView 测量子控件高 2 位存储模式,低 30 位存储尺寸,最大可测量像素为2^30‑1 = 1073741823像素。25.2.3 View.measure () — 入口函数View.measure()被声明为 final,子类不能重写;子类重写onMeasure()。measure()内部逻辑:测量缓存:LongSparseLongArray 缓存,key 为宽高 MeasureSpec 拼接值。如果约束未改变、且没有强制布局标记,直接复用缓存尺寸,不调用onMeasure()。强制布局标记:PFLAG_FORCE_LAYOUT标记会强制重新测量,忽略缓存。光学边界:处理带有光学内边距的 View(例如.9patch 阴影)。源码路径:frameworks/base/core/java/android/view/View.java(第 28787 行)public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... long key = (long) widthMeasureSpec 32 | (long) heightMeasureSpec 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); final boolean forceLayout = (mPrivateFlags PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; ... if (forceLayout || needsLayout) { mPrivateFlags = ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = ...; if (cacheIndex 0) { onMeasure(widthMeasureSpec, heightMeasureSpec); } else { long value = mMeasureCache.valueAt(cacheIndex); setMeasuredDimensionRaw((int)(value 32), (int)value); } // 校验是否调用setMeasuredDimension() if ((mPrivateFlags PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException( getClass().getName() + "#onMeasure() did not set the " + "measured dimension by calling setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } ... }契约约束:重写onMeasure()必须调用setMeasuredDimension(),否则抛出IllegalStateException异常。25.2.4 默认的 onMeasure () 实现基类 View 的onMeasure()简单取背景尺寸与最小尺寸的较大值:源码路径:frameworks/base/core/java/android/view/View.java(第 28917 行)protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }getDefaultSize():EXACTLY/AT_MOST 模式直接使用 spec 传入尺寸;UNSPECIFIED 使用建议最小尺寸。 这就是未重写onMeasure()的自定义 View 使用 wrap_content 仍然会填满父容器的原因。25.2.5 ViewGroup 测量逻辑ViewGroup 是抽象类,并不直接重写onMeasure(),而是提供辅助工具方法给子类使用:measureChild(child, parentWidthSpec, parentHeightSpec):父 spec 减去 padding 得到子 View 的 spec,调用子 View 的measure()。measureChildWithMargins(child, parentWidthSpec, widthUsed, parentHeightSpec, heightUsed):与 measureChild 类似,同时计入子 Viewmargin 以及已经被其他子 View 占用的空间。getChildMeasureSpec(parentSpec, padding, childDimension):核心算法,将父容器约束与子 View LayoutParams 尺寸合并,生成子 View 的 MeasureSpec。Spec 组合对照表:父容器模式子 View LayoutParams输出模式输出尺寸EXACTLY精确 dpEXACTLY子 View 指定尺寸EXACTLYmatch_parentEXACTLY父尺寸‑paddingEXACTLYwrap_contentAT_MOST父尺寸‑paddingAT_MOST精确 dpEXACTLY子 View 指定尺寸AT_MOSTmatch_parentAT_MOST父尺寸‑paddingAT_MOSTwrap_contentAT_MOST父尺寸‑paddingUNSPECIFIED精确 dpEXACTLY子 View 指定尺寸UNSPECIFIEDmatch_parentUNSPECIFIED0UNSPECIFIEDwrap_contentUNSPECIFIED025.2.6 View.layout () — 位置确定测量完成后,performLayout()将根 View 放置在坐标(0,0):源码路径:frameworks/base/core/java/android/view/ViewRootImpl.java(第 5554 行)host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());View.layout()(第 26040 行)保存坐标位置并调用onLayout():源码路径:frameworks/base/core/java/android/view/View.javapublic void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 = ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); ... // 通知OnLayoutChangeListeners if (li != null li.mOnLayoutChangeListeners != null) { for (OnLayoutChangeListener listener : listenersCopy) { listener.onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags = ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }注意重测量保护逻辑:如果 View 使用测量缓存得到尺寸(标记PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT),在 layout 执行前会再次执行onMeasure()保证测量结果是最新的。25.2.7 View.draw () — 七个绘制步骤View.draw()(第 25493 行)严格按照固定顺序执行绘制流程:源码路径:frameworks/base/core/java/android/view/View.javapublic void draw(@NonNull Canvas canvas) { /* * draw绘制流程执行多个绘制步骤,必须严格按顺序执行: * * 1. 绘制背景 * 2. 如果需要,保存canvas图层,为淡出效果做准备 * 3. 绘制View自身内容 * 4. 绘制子View * 5. 如果需要,绘制淡出边缘并恢复图层 * 6. 绘制装饰(例如滚动条) * 7. 如果需要,绘制默认焦点高亮 */ drawBackground(canvas); // 步骤1 ... onDraw(canvas); // 步骤3 dispatchDraw(canvas); // 步骤4 ... onDrawForeground(canvas); // 步骤6 drawDefaultFocusHighlight(canvas); // 步骤7 }对于 ViewGroup,dispatchDraw()遍历全部子 View,调用drawChild(),内部再调用子 View 的draw(canvas, this, ...)。25.2.8 PFLAG_SKIP_DRAW 优化很多 ViewGroup 子类(LinearLayout、FrameLayout)本身没有自定义绘制逻辑,只负责承载子 View。当 ViewGroup 无背景、无前景、无自定义绘制内容,框架设置PFLAG_SKIP_DRAW标志;updateDisplayListIfDirty()会直接跳过draw(),直接调用dispatchDraw()。源码路径:frameworks/base/core/java/android/view/View.java(第 24771 行)if ((mPrivateFlags PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { dispatchDraw(canvas); drawAutofilledHighlight(canvas); if (mOverlay != null !mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } } else { draw(canvas); }该优化收益明显:在深度嵌套的 View 树中,大量中间 ViewGroup 节点可以跳过大部分绘制流水线。25.2.9 requestLayout () 与 invalidate ()两个方法都会触发重渲染,但职责完全不同。requestLayout():代表 View 尺寸或者位置可能发生变化。向上遍历父链,给每一个祖先设置PFLAG_FORCE_LAYOUT标记,直到抵达ViewRootImpl,调用scheduleTraversals(),触发完整测量‑布局‑绘制整套流程。源码路径:frameworks/base/core/java/android/view/View.java(第 28723 行)public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); ... // 如果布局流程正在执行,同时View发起requestLayout, // 请求转发给ViewRootImpl做延迟处理。 ... mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null !mParent.isLayoutRequested()) { mParent.requestLayout(); } }调用链路:View.requestLayout()→ parent.requestLayout () → 逐级向上祖先 →ViewRootImpl.requestLayout()→scheduleTraversals()→ MEASURE + LAYOUT + DRAWinvalidate():代表 View 外观发生变化,但尺寸、位置不变。向上传递脏矩形区域到ViewRootImpl,仅触发绘制流程,不执行测量、布局。源码路径:frameworks/base/core/java/android/view/View.java(第 21431、21453 行)public void invalidate() { invalidate(true); } void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { ... mPrivateFlags |= PFLAG_DIRTY; if (invalidateCache) { mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags = ~PFLAG_DRAWING_CACHE_VALID; } // 将脏矩形向上传递给父View final ViewParent p = mParent; if (p != null ai != null l r t b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b); p.invalidateChild(this, damage); } }调用链路:View.invalidate()→ parent.invalidateChild (this, dirty) → 逐级向上祖先 →ViewRootImpl.invalidateChildInParent()→scheduleTraversals()→仅执行 DRAW 绘制25.2.10 performTraversals () — 总调度器ViewRootImpl.performTraversals()(第 3924 行)是 View 系统体量最大的方法,数百行代码,调度完整渲染流水线。Android17 版本接收帧 VSYNC 时间戳performTraversals(long frameTimeNanos),动画、Choreographer、帧率投票全部绑定同一个统一帧时间。源码路径:frameworks/base/core/java/android/view/ViewRootImpl.javaprivate void performTraversals(long frameTimeNanos) { final View host = mView; if (host == null || !mAdded) return; ... mIsInTraversal = true; mWillDrawSoon = true; ... // 阶段0:计算期望窗口尺寸 if (mFirst) { desiredWindowWidth = ...; // 来自显示屏或者窗口框架 desiredWindowHeight = ...; host.dispatchAttachedToWindow(mAttachInfo, 0); dispatchApplyInsets(host); } ... // 阶段1:MEASURE测量 if (layoutRequested) { windowSizeMayChange |= measureHierarchy(host, lp, resources, desiredWindowWidth, desiredWindowHeight, ...); } ... // 阶段1.5:如果窗口尺寸发生变化,重新relayout窗口 if (windowShouldResize || ...) { relayoutResult = relayoutWindow(params, ...); ... } ... // 阶段2:LAYOUT布局 if (didLayout) { performLayout(lp, desiredWindowWidth, desiredWindowHeight); } ... // 阶段3:DRAW绘制
返回列表