多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Android DecorView 全解:视图树根节点的创建、显示与职责剖析(YCBlogs 源码级解读)

Android DecorView 全解:视图树根节点的创建、显示与职责剖析(YCBlogs 源码级解读) 教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载导读DecorView 是 Android 界面体系的根节点视图Activity 中setContentView()设置的布局最终都挂载在它内部的 content 容器之下。本文以 YCBlogs 仓库中 10.DecorView介绍.md 为核心骨架结合 17.视图管理者Window.md、18.窗口管理服务WMS.md、16.视图的载体View.md 等仓库文档逐层拆解 DecorView 的定义、内部结构、创建过程、显示时机与职责边界。读完本文你将能回答DecorView 与 ViewRootImpl、PhoneWindow、WindowManager 之间到底如何协作为什么setContentView()之后界面要等到onResume()之后才可见View 的测量/布局/绘制三大流程由谁驱动01. 什么是 DecorViewDecorView 是FrameLayout的子类可以把它理解为Android 视图树的根节点视图top-level view。整个 Activity 的界面无论你在 XML 里写了多少层布局最终都嵌套在唯一的一个 DecorView 之下。关键点在于 DecorView 的内部结构并非固定它由当前窗口的主题Theme决定一般情况下DecorView 内部是一个竖直方向的 LinearLayout这个 LinearLayout 从下到上通常包含三个区域ViewStub延迟加载视图一般是action_mode_bar_stub用于 ActionMode上下文操作栏的延迟加载是否出现由 Theme 决定标题栏TitleBarandroid:id/title对应的 TextView同样由 Theme 决定部分主题没有标题栏内容栏Contentandroid:id/content对应的 FrameLayoutsetContentView()设置的布局就添加在这里。注具体结构随 Android 版本与主题差异而不同上述布局只是其中一个典型示例。以下是一份典型的 DecorView 内部布局源自仓库原文档LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:fitsSystemWindowstrue android:orientationvertical !-- Popout bar for action modes -- ViewStub android:idid/action_mode_bar_stub android:layout_widthmatch_parent android:layout_heightwrap_content android:inflatedIdid/action_mode_bar android:layoutlayout/action_mode_bar android:theme?attr/actionBarTheme / FrameLayout style?android:attr/windowTitleBackgroundStyle android:layout_widthmatch_parent android:layout_height?android:attr/windowTitleSize TextView android:idandroid:id/title style?android:attr/windowTitleStyle android:layout_widthmatch_parent android:layout_heightmatch_parent android:backgroundnull android:fadingEdgehorizontal android:gravitycenter_vertical / /FrameLayout FrameLayout android:idandroid:id/content android:layout_widthmatch_parent android:layout_height0dip android:layout_weight1 android:foreground?android:attr/windowContentOverlay android:foregroundGravityfill_horizontal|top / /LinearLayout1.1 setContentView 的布局最终去哪了在 Activity 中通过setContentView()设置的布局文件其实就是被添加到上述布局中id 为content的 FrameLayout内容栏之中成为它的唯一子 View。因此在代码里可以这样拿到自己设置的布局ViewGroup content (ViewGroup) findViewById(android.R.id.content); ViewGroup rootView (ViewGroup) content.getChildAt(0);android.R.id.content指向的就是 DecorView 内部内容栏 FrameLayout 的 id对应源码中的ID_ANDROID_CONTENT com.android.internal.R.id.content见 17.视图管理者Window.md。1.2 根视图视角View 的生命周期与 DecorView从仓库文档 16.视图的载体View.md 的实测日志可以看到onAttachedToWindow()发生在onResume()之后而onMeasure() → onLayout() → onDraw()的完整绘制流程则发生在窗口真正附加之后——这正是 DecorView 被添加到 Window 之后的连锁反应也是理解为什么onCreate()里拿不到 View 宽高的根本原因详见第 04 节。02. 什么是 ViewRootViewRootImplViewRoot 这个名字可能比较陌生但它的作用非常重大所有 View 的绘制以及事件分发等交互都是通过它来执行或传递的。ViewRoot 对应ViewRootImpl 类它是连接WindowManagerServiceWMS和DecorView的纽带View 的三大流程——测量measure、布局layout、绘制draw——均通过 ViewRootImpl 来完成ViewRootImpl 并不属于 View 树的一份子从源码实现上看它既非 View 的子类也非 View 的父类但它实现了ViewParent接口因此可以充当 View 的名义上的父视图ViewRootImpl 内部持有 Handler可以接收事件并分发。Android 的所有触屏事件、按键事件、界面刷新等事件都是通过 ViewRootImpl 进行分发的。2.1 四者之间的关系整个链路可以概括为Activity控制器 ↓ 持有 Window / PhoneWindow承载器 ↓ 持有 DecorView顶层视图 / 根节点 ↑ 通过 ViewRootImpl 关联 ViewRootImpl连接器与 WMS 通信Activity 就像控制器不负责具体的视图部分Window 像个承载器装着内部视图DecorView 是顶层视图是所有 View 的最外层布局ViewRootImpl 像个连接器负责沟通通过硬件的感知来通知视图进行用户之间的交互详见第 05 节。2.2 ViewRootImpl 如何与 DecorView 建立联系Activity 对象在 ActivityThread 中创建完毕后会将 DecorView 添加到 Window 中同时会创建 ViewRootImpl并调用其setView()方法将 ViewRootImpl 与 DecorView 建立关联root new ViewRootImpl(view.getContext(), display); root.setView(view, wparams, panelParentView);之所以必须建立这种关联是因为DecorView 等 View 的三大流程measure/layout/draw需要通过 ViewRootImpl 来完成。关于setView()内部的详细逻辑requestLayout()mWindowSession.addToDisplay()的 IPC 过程见 18.窗口管理服务WMS.md 的Window 的添加流程一节。03. DecorView 的创建这一部分回答的核心问题是DecorView 是怎么一层层嵌套在 Activity、PhoneWindow 中的以及 DecorView 如何加载内部布局。3.1 从 setContentView() 开始首先从 Activity 的setContentView()开始这个入口我们已经很熟悉了但它具体是怎么把布局加载到 Window 上的呢public void setContentView(LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }可以看到实际是交给 Window 去装载视图。那么 Activity 是怎么获得 Window 对象的答案在attach()方法中——Activity 被创建后ActivityThread 会调用attach()完成初始化其中就创建了 PhoneWindowfinal void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window) { .......................................................... mWindow new PhoneWindow(this, window); // 创建一个 Window 对象 mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); // 设置回调向 Activity 分发点击或状态改变等事件 mWindow.setOnWindowDismissedCallback(this); ......................................................... mWindow.setWindowManager( (WindowManager) context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags ActivityInfo.FLAG_HARDWARE_ACCELERATED) ! 0); // 给 Window 设置 WindowManager 对象 ............................................................ }依据仓库文档 17.视图管理者Window.mdWindow 是一个抽象类它的唯一实现类是 PhoneWindowPhoneWindow 内部持有DecorView mDecor、ViewGroup mContentParent、左右图标、进度条、转场动画相关对象等。既然有了 Window 对象就可以设置 DecorView 给 Window 对象了。PhoneWindow 的setContentView()逻辑如下public void setContentView(int layoutResID) { if (mContentParent null) { // mContentParent 为空创建一个 DecorView installDecor(); } else { mContentParent.removeAllViews(); // mContentParent 不为空删除其中的 View } mLayoutInflater.inflate(layoutResID, mContentParent); // 为 mContentParent 添加子 View即 Activity 中设置的布局文件 final Callback cb getCallback(); if (cb ! null !isDestroyed()) { cb.onContentChanged(); // 回调通知内容改变 } }对照仓库文档 17.视图管理者Window.md 中更完整的 PhoneWindow.setContentView 实现还可以看到两点补充细节在FEATURE_CONTENT_TRANSITIONS特性开启时走的是Scene.getSceneForLayout()transitionTo(newScene)的转场分支而不是直接 inflate加载完成后还会调用mContentParent.requestApplyInsets()随后回调Window.Callback.onContentChanged()——由于 Activity 实现了Window.Callback接口见 17.视图管理者Window.md 的窗口回调一节该回调最终会进入 Activity 的onContentChanged()。3.2 mContentParent 到底是什么看完上面的流程可能会有一个疑惑mContentParent 到底是什么答案mContentParent 就是前面布局中android:id/content所对应的 FrameLayout内容栏。通过上面的流程可以大致了解先在 PhoneWindow 中创建了一个 DecorView创建过程中可能根据 Theme 不同加载不同的布局格式例如有没有 Title、有没有 ActionBar 等然后再向 mContentParent 中加入子 View即 Activity 中设置的布局。到此视图就一层层嵌套添加上了。下面具体看看installDecor()方法——它是怎么创建 DecorView 并设置其整体布局的private void installDecor() { if (mDecor null) { mDecor generateDecor(); // 生成 DecorView mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted mInvalidatePanelMenuFeatures ! 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } if (mContentParent null) { mContentParent generateLayout(mDecor); // 为 DecorView 设置布局格式并返回 mContentParent ... } }再来看generateDecor()它非常简单就是创建了一个 DecorViewprotected DecorView generateDecor() { return new DecorView(getContext(), -1); }而generateLayout(DecorView decor)则是整个创建流程的重头戏——根据主题样式选择并加载不同的窗口布局protected ViewGroup generateLayout(DecorView decor) { // 从主题文件中获取样式信息 TypedArray a getWindowStyle(); ................... if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { requestFeature(FEATURE_NO_TITLE); } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { // Dont allow an action bar if there is no title. requestFeature(FEATURE_ACTION_BAR); } ................ // 根据主题样式加载窗口布局 int layoutResource; int features getLocalFeatures(); // System.out.println(Features: 0x Integer.toHexString(features)); if ((features (1 FEATURE_SWIPE_TO_DISMISS)) ! 0) { layoutResource R.layout.screen_swipe_dismiss; } else if(...){ ... } View in mLayoutInflater.inflate(layoutResource, null); // 加载 layoutResource // 往 DecorView 中添加子 View即文章开头介绍 DecorView 时提到的布局格式 // 那只是一个例子根据主题样式不同加载不同的布局 decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot (ViewGroup) in; ViewGroup contentParent (ViewGroup) findViewById(ID_ANDROID_CONTENT); // 这里获取的就是 mContentParent if (contentParent null) { throw new RuntimeException(Window couldnt find content container view); } if ((features (1 FEATURE_INDETERMINATE_PROGRESS)) ! 0) { ProgressBar progress getCircularProgressBar(false); if (progress ! null) { progress.setIndeterminate(true); } } if ((features (1 FEATURE_SWIPE_TO_DISMISS)) ! 0) { registerSwipeCallbacks(); } // Remaining setup -- of background and title -- that only applies // to top-level windows. ... return contentParent; }虽然这段代码比较长但逻辑还是很清楚的先从主题Theme中获取样式信息如windowNoTitle、windowActionBar等根据样式与窗口 features 加载对应的布局资源如screen_swipe_dismiss等将加载出来的布局以MATCH_PARENT的方式 add 到 DecorView 中从该布局中通过findViewById(ID_ANDROID_CONTENT)找到内容栏作为mContentParent返回。拿到mContentParent之后回到前面的setContentView()为 mContentParent 添加 Activity 中的布局即可。DecorView 的创建过程到installDecor()就已经介绍完了后面的代码只是其中的具体逻辑。补充从源码结构看generateLayout()内部还会处理窗口背景、标题、FEATURE_INDETERMINATE_PROGRESS圆形/水平进度条、FEATURE_SWIPE_TO_DISMISS滑动关闭等窗口特性这与 17.视图管理者Window.md 中窗口实现一节列举的 PhoneWindow 内部成员mLeftIconView、mRightIconView、mCircularProgressBar、mHorizontalProgressBar等一一对应可以看出这些界面元素都是在installDecor()/generateLayout()阶段被创建出来的。04. DecorView 的显示通过setContentView()设置的界面为什么在onResume()之后才对用户可见这就要从 ActivityThread 说起。4.1 handleLaunchActivity创建但不可见private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) { // 就是在这里调用了 Activity.attach()接着调用了 Activity.onCreate() 和 Activity.onStart() 生命周期。 // 但是由于只是初始化了 mDecor、添加了布局文件还没有把 mDecor 添加到负责 UI 显示的 // PhoneWindow 中所以这时候对用户来说是不可见的。 Activity a performLaunchActivity(r, customIntent); ...... if (a ! null) { // 这里面执行了 Activity.onResume() handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished !r.startsNotResumed); if (!r.activity.mFinished r.startsNotResumed) { try { r.activity.mCalled false; // 执行 Activity.onPause() mInstrumentation.callActivityOnPause(r.activity); } } } }关键在于此时只是把布局 inflate 到了 DecorView 内部的 mContentParent 中DecorView 还没有真正被 WindowManager 添加到 Window 上因此它无法接收用户的输入信息和焦点事件——界面尚不可见。4.2 handleResumeActivityDecorView 开始登场重点看handleResumeActivity()在这其中 DecorView 将会显示出来同时重要的角色 ViewRoot 也将登场final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { // 这个时候Activity.onResume() 已经调用了但是现在界面还是不可见的 ActivityClientRecord r performResumeActivity(token, clearHide); if (r ! null) { final Activity a r.activity; if (r.window null !a.mFinished willBeVisible) { r.window r.activity.getWindow(); View decor r.window.getDecorView(); // decor 对用户不可见 decor.setVisibility(View.INVISIBLE); ViewManager wm a.getWindowManager(); WindowManager.LayoutParams l r.window.getAttributes(); a.mDecor decor; l.type WindowManager.LayoutParams.TYPE_BASE_APPLICATION; if (a.mVisibleFromClient) { a.mWindowAdded true; // 被添加进 WindowManager 了但是这个时候还是不可见的 wm.addView(decor, l); } if (!r.activity.mFinished willBeVisible r.activity.mDecor ! null !r.hideForNow) { // 在这里执行了重要的操作使得 DecorView 可见 if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } } } } }执行Activity.makeVisible()之后界面才对用户可见void makeVisible() { if (!mWindowAdded) { ViewManager wm getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); // 将 DecorView 添加到 WindowManager mWindowAdded true; } mDecor.setVisibility(View.VISIBLE); // DecorView 可见 }到此 DecorView 便可见、显示在屏幕中。但在这其中wm.addView(mDecor, getWindow().getAttributes());起到了重要的作用因为其内部创建了一个 ViewRootImpl 对象负责绘制显示各个子 View。4.3 addView 的层层调用WindowManagerImpl → WindowManagerGlobal → ViewRootImpladdView()方法中因为 WindowManager 是个接口具体是交给 WindowManagerImpl 来实现的public final class WindowManagerImpl implements WindowManager { private final WindowManagerGlobal mGlobal WindowManagerGlobal.getInstance(); ... Override public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } }接着交给 WindowManagerGlobal 的addView()方法去实现public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { final WindowManager.LayoutParams wparams (WindowManager.LayoutParams) params; ...... synchronized (mLock) { ViewRootImpl root; // 实例化一个 ViewRootImpl 对象 root new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); // mViews 存储所有 Window 对应的 View 对象 mRoots.add(root); // mRoots 存储所有 Window 对应的 ViewRootImpl 对象 mParams.add(wparams); // mParams 存储所有 Window 对应的 LayoutParams 对象 } ...... try { // 将 DecorView 交给 ViewRootImpl root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { } }佐证细节源自 18.窗口管理服务WMS.mdWindowManagerGlobal内部维护了mViews、mRoots、mParams三个列表分别保存所有 Window 对应的 View、ViewRootImpl 和 LayoutParamsViewRootImpl.setView()内部做了两件关键的事① 调用requestLayout()完成界面异步绘制的请求最终会走到scheduleTraversals()→TraversalRunnable→performTraversals()② 通过mWindowSession.addToDisplay()请求 WindowManagerService 完成 Window 的添加这是一个 IPC 过程performTraversals()的流程是获取 Surface →performMeasure()测量视图树 →performLayout()计算位置 →performDraw()绘制视图树。View 的三大流程measure/layout/draw正是在这里被驱动起来的。setView()方法经过一系列折腾最终调用了performTraversals()方法然后依照测量 → 布局 → 绘制的流程层层调用完成绘制最终界面才显示出来。这也与仓库文档 16.视图的载体View.md 中的实测日志吻合onAttachedToWindow()之后依次触发onMeasure() → onSizeChanged() → onLayout() → onDraw()界面真正绘制完成。4.4 事件传递链硬件 → ViewRootImpl → DecorView → PhoneWindow → Activity要知道当用户点击屏幕产生一个触摸行为这个触摸行为通过底层硬件来传递捕获然后交给 ViewRootImpl接着将事件传递给 DecorView而 DecorView 再交给 PhoneWindowPhoneWindow 再交给 Activity然后接下来就是我们常见的 View 事件分发了硬件 → ViewRootImpl → DecorView → PhoneWindow → Activity由此可见ViewRootImpl 的重要性它是一个连接器负责 WindowManagerService 与 DecorView 之间的通信。仓库文档 03.View事件机制源码分析.md 也从另一个角度印证了这条链路的终点行为事件最先传到 Activity 的dispatchTouchEvent()而 Activity 内部是getWindow().superDispatchTouchEvent(ev)PhoneWindow 的实现则是mDecor.superDispatchTouchEvent(event)——即Window 把事件下发给 DecorView随后才进入 ViewGroup/View 的分发流程ActivityWindow→ ViewGroup → View。DecorView 在这里扮演了事件进入视图树的入口这一角色。05. DecorView 的职责总结以上通过源码形式介绍了 Window、Activity、DecorView 以及 ViewRoot 之间的错综关系以及如何创建并显示 DecorView。通过以上了解可以知道角色定位职责Activity控制器不负责视图部分持有 Window实现 Window.Callback 接收事件回调WindowPhoneWindow承载器装着内部视图是 Window 抽象类的唯一实现类DecorView顶层视图所有 View 的最外层布局FrameLayout 子类视图树的根节点ViewRootImpl连接器负责沟通驱动 measure/layout/draw 三大流程连接 WMS 与 DecorView它们之间的完整协作关系可以总结为创建阶段ActivityThread 调用Activity.attach()创建 PhoneWindow →setContentView()触发installDecor()→generateDecor()创建 DecorView、generateLayout()按 Theme 加载窗口布局并返回 mContentParent → 业务布局 inflate 进 content 容器显示阶段handleResumeActivity()中wm.addView(decor, l)走 WindowManagerImpl → WindowManagerGlobal → 创建 ViewRootImpl 并setView()→requestLayout()performTraversals()完成测量/布局/绘制 →makeVisible()后 DecorView 可见交互阶段触摸事件沿硬件 → ViewRootImpl → DecorView → PhoneWindow → Activity到达再按 View 事件分发机制向下传递处理。06. 关联阅读与延伸本文内容与仓库中以下文档相互印证、可以对照阅读17.视图管理者Window.mdWindow 类型应用 Window / 子 Window / 系统 Window、WindowManager.LayoutParams 窗口参数与 flags、窗口回调 Callback 接口、PhoneWindow 内部成员与 setContentView 完整实现18.窗口管理服务WMS.mdWindowManagerGlobal 的 addView/removeView/updateViewLayout 三流程、ViewRootImpl.setView() 的 IPC 细节、WindowSession 与 WindowManagerService 的跨进程通信机制16.视图的载体View.mdView 的生命周期回调与 Activity 生命周期的实测日志验证 DecorView 可见时机03.View事件机制源码分析.mdActivity → Window → ViewGroup → View 的事件分发顺序与源码分析10.DecorView介绍.md本文的原始骨架文档包含 DecorView 内部布局示例与全部核心源码片段。赞分享教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载相关推荐YCBlogs Android Framework 源码级剖析ANR 触发流程与 Service 超时机制全解析YCBlogs Android Framework 源码级剖析ANR 触发流程与 Service 超时机制全解析 本篇以 YCBlogs 仓库中 ANR 基础教程技术博客文档YCBlogs 源码精读Activity 布局创建全链路解析——从 handleLaunchActivity 到 setContentViewYCBlogs 源码精读Activity 布局创建全链路解析——从 handleLaunchActivity 到 setContentView 本文基于 YC教程技术博客文档Android View 刷新机制源码级剖析invalidate、postInvalidate 与 requestLayout 全解析YCBlogs 实战笔记Android View 刷新机制源码级剖析invalidate、postInvalidate 与 requestLayout 全解析YCBlogs 实战笔教程技术博客文档上一篇如何用Winhance中文版让你的Windows系统焕然一新下一篇终极指南如何在PC上免费运行Switch游戏 - Ryujinx开源模拟器完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表