ARTICLE DETAIL

资讯详情

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

用HierarchyViewer定位布局性能瓶颈,优化Android页面卡顿

用HierarchyViewer定位布局性能瓶颈,优化Android页面卡顿 前段时间在帮一个老项目做性能优化页面滑动的时候总是有点掉帧。同事上来就怀疑是内存泄漏我说别急先把HierarchyViewer打开看一眼布局树。这个工具名字听起来像上个时代的产物但在分析布局这件事上它能给你的信息量和直接程度很多新工具反而比不了。如果你是做Android开发尤其还在维护旧项目或者想搞明白“为什么我的页面层级一深就卡”那这篇文章就是给你写的。HierarchyViewer是Android SDK Tools里自带的视图层级分析器。它的核心能力是两个第一以树状结构把当前界面的View层级完整拉出来第二统计每个View在measure、layout、draw三个阶段分别花了多少时间。这两个信息一旦到手布局里哪个节点藏了性能黑洞基本就藏不住了。我第一次用它是从一个12层嵌套的订单页里找出罪魁祸首整个过程比想象中顺利得多。后面我会把这个过程完整还原出来文章也会把环境准备、界面读法、实战优化思路和常见坑都过一遍。1. 为什么要用HierarchyViewer分析布局1.1 先搞清楚它是什么HierarchyViewer是Android早期推出的一把性能分析“手术刀”。它通过ADB和手机里的View Server服务通信把正在运行的应用的View Hierarchy抓取出来并且在不打断运行的情况下逐个节点标记测量和绘制耗时。很多新人容易把这个工具和Android Studio里的Layout Inspector搞混。Layout Inspector更偏“结构查看器”侧重看层级、属性、样式方便你查一个控件为什么长这样而HierarchyViewer除了能看到同样的层级结构还额外提供了每个节点的性能指标。这一项是当年的独门绝技尤其是三个指标measure测量、layout布局、draw绘制分别对应一个视图从计算尺寸、确定位置到真正画到屏幕上的三个阶段。任何一个阶段耗时过高都会直接影响帧率和流畅度。它之所以在分析布局时特别好用是因为它把“布局到底有多复杂”这个问题量化了。不是用肉眼看代码估计而是直接显示“这个RelativeLayout拖了多少毫秒”。有了数据你优化起来就有依据。1.2 什么场景下最适合用它在我的实际使用里HierarchyViewer主要在三类场景下发挥价值。第一类是页面启动慢或滑动卡顿怀疑是布局嵌套过深、测量耗时过高。这种时候用HierarchyViewer拉一遍视图树很快能看到哪个容器节点是红色的耗时明显高于其他节点。第二类是排查布局重叠、控件遮挡之类的问题。这类问题在真机上肉眼很难看出来尤其是多个控件坐标有细微的交错。HierarchyViewer的屏幕截图上可以点击节点高亮对应的控件区域一眼就能发现问题。第三类是做优化前后的对比验收。你改了一个布局把三层嵌套改成了ConstraintLayout性能到底提升了多少不是靠感觉的而是用HierarchyViewer分别抓优化前后的数据对比同一个节点的耗时结论一目了然。1.3 它和你熟悉的Layout Inspector是什么关系刚才提到Layout Inspector这里展开说下关系。Android Studio从3.x开始逐步用Layout Inspector替代了HierarchyViewer在IDE里的位置。但Layout Inspector有一个遗憾它虽然能看层级和属性却不直接展示每个View的measure/layout/draw耗时。如果你要拿一个老工具完成“性能定量分析”这件事还需要依赖Profile GPU Rendering或其他工具。所以在我看来HierarchyViewer并没有完全退休它更适合那些在本地SDK里还留着旧工具链、或者需要在命令行环境下快速导出一份视图树性能快照的开发者。理解它的原理和操作也能帮你更顺畅地理解Android视图渲染的底层过程。2. 把HierarchyViewer跑起来2.1 环境准备模拟器、真机与Root先说结论想少踩坑优先用模拟器。HierarchyViewer需要访问系统级的View Server数据在普通发布的手机上这个服务默认是不开放的。即使你把开发者选项里的USB调试打开也不一定能看到应用进程。究其原因是系统出于安全考虑不会让普通应用读取其他应用的视图层信息。所以真机测试通常需要满足两个条件之一手机是工程师手机或工程固件或者应用是可调试版本debuggable。如果你手上只有一台量产版安卓手机建议直接绕道用模拟器尤其是Android 7.0及以下版本的模拟器镜像。Android 8.0以上系统已经取消了对View Server的支持这个后面第5章还会详细说。在模拟器上跑流程相当顺滑先启动模拟器确认adb devices能看到设备然后找到SDK目录下的tools目录准备启动工具。2.2 启动工具与连接设备具体操作路径很简单。假设你已经配置好Android SDK环境变量打开命令行工具进入SDK安装目录Windows系统下进入 tools 目录双击 hierarchyviewer.bat或者命令行执行 hierarchyviewer.bat。macOS/Linux系统下在 tools 目录执行./hierarchyviewer。如果你是老一点的Android Studio版本也可以通过菜单Tools Android Android Device Monitor打开在Device Monitor面板的窗口菜单里能找到 HierarchyViewer 入口。启动后工具窗口左边是设备列表和当前运行的进程列表。如果这里看不到任何设备先检查adb连接问题执行adb devices。正常情况下第一行会显示设备序列号后面跟着device状态。如果显示unauthorized就去手机上确认允许USB调试弹窗。如果显示offline拔插一下数据线重启adb服务adb kill-server再adb start-server。注意如果JAVA_HOME环境变量没有正确配置hierarchyviewer.bat会启动失败直接弹一个“Failed to create the Java Virtual Machine”的报错。这个问题很隐蔽查了半天还以为是SDK问题。2.3 选择进程与加载视图树工具界面右侧是“Hierarchy Viewer”的区域。选择你的设备后中间会列出当前设备上所有可调试应用的包名。注意不是所有应用都会出现只有标签页里显示为可调试的应用才会出现在列表中。所以如果你自己的应用没显示确认一下AndroidManifest里是否设置了android:debuggabletrue或者当前构建的是不是debug版本。找到你的包名后双击或者在选中状态下点击“Load View Hierarchy”按钮。这一步工具会连接设备拉取当前界面的视图树数据。稍等一两秒屏幕下方会出现手机当前界面的截图左侧出现一棵树状结构每个节点代表一个View。到了这一步工具就算正式跑起来了。很多第一次用的人会不知道从哪看起其实主要看两个窗口Tree View和Layout View。下一章我详细拆解界面上的每一个信息点。3. 读懂界面上的每一个信号3.1 视图树窗口Tree ViewTree View就是左侧那棵树。树的根节点通常是PhoneWindow.DecorView往下展开是内容根布局、ActionBar、内容区域一直到每个Button、TextView、ImageView。看这棵树要关注两点一是树的深度。从根到最深的叶子节点经过了几层Android界面渲染时View树的深度直接影响Measure和Layout的遍历开销。深度越深一次遍历需要执行的递归调用就越多。业界一般建议单页面的视图树深度控制在10层以内超过15层就要高度警惕。如果看到一条分支又深又长基本上就是性能瓶颈所在。二是兄弟节点的数量。某个父容器下面直接挂了10几个子View这也会增加单帧的测量成本因为父容器需要遍历子View来确认尺寸。子树分支多不见得比深度浅安全。3.2 Layout View与屏幕叠层主界面下面的屏幕截图就是Layout View。它最大的价值在于能把抽象树和具体视觉元素对应起来。你在Tree View里点中一个节点屏幕截图里对应的矩形区域就会高亮你还可以切换到底部的“屏幕截图”窗口按住鼠标在截图上移动右侧会显示当前光标所在的View。这种联动方式在处理“布局重叠”时特别好用。比如有个痛点两个控件明明视觉上重叠了但在代码里只看layout_margin很难定位到具体是哪个View。这时候你直接点击截图上重叠的位置工具会自动把位于该坐标的View在树里选中你再看树上的父子关系遮挡问题的原因立刻清楚了。此外Layout View支持网格选项“Enable grid”可以帮你看控件间的间距是否符合对齐规范。虽然这个功能主要用于辅助视觉设计但在排查某些奇怪的偏移时也有用。3.3 三色圆点和三个指标measure / layout / draw每个View节点旁边会出现三颗彩色圆点。它们的含义如下圆点对应阶段绿色黄色红色第一个点measure测量耗时正常中等耗时过高第二个点layout布局耗时正常中等耗时过高第三个点draw绘制耗时正常中等耗时过高这个彩色标记是HierarchyViewer最经典的视觉设计。看到红色第一反应不是马上改而是先点中节点在下方“View Properties”表单里找到具体的耗时数值。红色代表它在当前设备环境下的耗时超过了正常范围但并不一定绝对意味着它有问题。要结合节点类型、大小、复杂度综合判断。比如一个ImageView里加载了一张超大分辨率Bitmap那么它的draw耗时很高是正常的优化方向应该是压缩图片而不是改布局层级。反之如果一个不起眼的LinearLayout测量耗时亮红灯就要重点看它是不是嵌套了太多子View或者在layout_weight中使用不当导致了二次测量。提示只看颜色不够。我抓数据时习惯把每个关键节点测得的数值记录下来在优化后再抓一次做对比。单次数据的绝对值其实意义不大优化前后的差值才是真正有价值的参考。4. 实战一个嵌套过深的页面是怎么被揪出来的4.1 优化前的布局代码与View树为了更能说明问题这里把当年分析订单详情页的过程还原一遍。先看核心布局长什么样以下是简化后的XMLLinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal RelativeLayout android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical TextView android:idid/tv_price android:layout_widthwrap_content android:layout_heightwrap_content android:text199 / TextView android:idid/tv_title android:layout_widthwrap_content android:layout_heightwrap_content android:text商品名称 / /LinearLayout /RelativeLayout /LinearLayout /LinearLayout注意这段XML只是组装结构的一小部分真正的页面比这复杂得多嵌套了多层LinearLayout和RelativeLayout整体视图树深度达到了12层。当时用HierarchyViewer拉出来的树形结构里几乎每个父容器都是LinearLayout而且不少LinearLayout只有一个子View这纯粹是开发时图省事一路套下去的典型。4.2 HierarchyViewer里的“显形”过程加载完视图树后我首先在Tree View里找红色节点。第一个醒目的红色出现在一个嵌套很深的LinearLayout上它的measure耗时显示为5.6ms。这个LinearLayout本身不起眼里面只有两个TextView但它的父容器也是一个LinearLayout再往上还是一个LinearLayout一层包一层。为什么一个简单的TextView会拖慢速度因为当某个父容器使用layout_weight后系统在测量阶段可能需要执行两次甚至多次measure以保证正确分配剩余空间。HierarchyViewer里能看到的是从根节点往下几条链路上多个容器在measure阶段的耗时都偏高。单独看每个节点只是零点几毫秒叠加起来就变成了肉眼可见的卡顿。这个过程是分布式的不是某一个View写了性能很差的onMeasure代码而是层层嵌套放大了整棵树的测量成本。HierarchyViewer把这种“隐形开销”变成了一条条可观测的数据。为了后续对比我当时记录了优化前几个关键节点的数据节点measure耗时layout耗时draw耗时根LinearLayout3.2ms1.8ms0.4ms二级LinearLayout2.1ms1.1ms0.3ms商品信息RelativeLayout1.9ms0.9ms0.2ms内层LinearLayout1.5ms0.7ms0.2msTextView价格0.8ms0.4ms0.8ms这套数据是当时在模拟器上测的不同设备数值会不同但不影响优化方向的判断。根容器和二级容器的测量耗时明显偏高叶子节点反而不高。4.3 用merge、include和ConstraintLayout做减负定位到问题后我没有直接无脑套ConstraintLayout而是针对那个订单详情页做了结构化调整。第一步去掉只“包皮”的中间层。比如某个RelativeLayout里面只有一个LinearLayout而LinearLayout里面才有真实内容这个RelativeLayout是多余的直接去掉。第二步把可以用ConstraintLayout直接表达的相对关系改写掉。原来的多个嵌套LinearLayout核心是左侧价格、右侧标题两个元素的位置关系用ConstraintLayout一步表达即可。第三步对于多个页面共用同一段头部结构的情况用include标签抽取出来。如果include进来的布局根节点只是单纯的容器并且外层也用不到它特有的布局属性时可以改成merge标签这样include的时候不会多包一层节点。优化后的核心代码可以简化为androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content TextView android:idid/tv_price android:layout_widthwrap_content android:layout_heightwrap_content android:text199 app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent / TextView android:idid/tv_title android:layout_width0dp android:layout_heightwrap_content android:text商品名称 app:layout_constraintStart_toEndOfid/tv_price app:layout_constraintEnd_toEndOfparent app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent android:layout_marginStart8dp / /androidx.constraintlayout.widget.ConstraintLayout这只是局部示例。实际把整页深度从12层压到6层左右同时还把几个重复的结构块抽成了复用组件。这里也要注意不要为了追求层级少而把代码可读性牺牲掉。ConstraintLayout好用但也不是所有地方都适合能用LinearLayout解决的简单横向排列没必要非换。经验优化布局时优先删“怎么删都不影响功能”的中间容器其次才考虑换布局类型。一个只有一个子View的FrameLayout或LinearLayout是优先清洗对象。4.4 优化后的对比与结论改完之后重新在同一个模拟器上抓取HierarchyViewer数据对比优化前后的关键指标指标优化前优化后提升幅度视图树最大深度12层6层50%根容器measure耗时3.2ms1.1ms65%关键区域整体measure耗时8.4ms3.1ms63%页面滑动掉帧次数明显基本消失-优化成果非常直观。虽然HierarchyViewer给出的绝对时间会随设备浮动但同一环境下前后两次抓取的对比仍然很能说明问题。布局深度减少之后渲染工作量明显下降页面滑动的跟手度也有所改善。所以我的结论是布局优化不是“感觉有用”而是要用工具证明它有用。5. 使用过程中最常见的几个坑5.1 真机连不上 / 列表为空很多人第一次用HierarchyViewer时在电脑上打开工具选设备列表里却空空如也。先别怀疑工具坏了按下面几个方向排查USB调试是否开启adb devices是否正常识别到设备。应用是否设置了android:debuggabletrue不是所有应用都能被HierarchyViewer看到。手机厂商是否限制了ADB接口。部分品牌手机需要额外打开“仅充电模式下允许ADB调试”的选项。尽量不要用Android 8.0以上的手机跑HierarchyViewer系统层面已经不支持View Server。如果只是想在真机上快速看层级推荐改用Android Studio的Layout Inspector能解决90%的层级查看需求。但如果你一定要看耗时指标那还是老老实实开模拟器。5.2 新版本Android怎么看不了这一点前面反复提过Android 8.0API 26之后系统移除了HierarchyViewer依赖的View Server机制。所以你拿一台Android 10的模拟器或者Android 11的真机打开HierarchyViewer多半会卡在进程列表或直接报错。新版本机器想看布局性能怎么办我目前的替代方案是工具/方式用途说明Layout Inspector查看层级与属性Android Studio自带支持高版本Profile GPU Rendering查看渲染耗时在开发者选项中开启能够看到柱状图Systrace / Perfetto分析渲染主线程专业级可定位到具体方法耗时adb shell dumpsys activity top查看视图层级命令行方案只输出文本结构从长期来看到HierarchyViewer更像一个历史工具但对于理解Android布局的测量/布局/绘制过程非常直观。学会了它的分析逻辑再上手其他工具思路是一样的。5.3 数值忽大忽小怎么判断HierarchyViewer抓到的耗时不是完全稳定的设备负载、当前动画、CPU频率波动都会影响数值。如果你拿第一次抓到的红色节点就着手优化很可能会误判。我的建议是每次抓取前先把页面切走再切回来等待界面处于静止状态等2秒再点击Load按钮。同一个页面连续抓3次记录每个关键节点的数值取中间值。对比优化前后时一定要做到同一台设备、同一个页面、同一个操作路径数据才具备可比性。还有一点HierarchyViewer的数据是在它抓取那一刻的快照不会实时刷新。你想观察点击按钮后某控件的变化就要在点击前后分别抓两次。不要指望它像Profile工具那样连续滚动输出。5.4 命令行替代方案与后续工具如果手头真的没有GUI环境或者想把这个分析过程脚本化可以试一下命令行方案。adb shell uiautomator dump /sdcard/ui.xml导出当前界面的UI层级XML能看到content-desc、text、bounds等属性适合做基础的层级检查。adb shell dumpsys activity top | grep -A 100 View Hierarchy部分系统会输出视图层级信息可视情况使用。第三方开源项目如facebook的stetho也提供过布局层级查看能力但项目已停止维护不再推荐。现代Android开发更推荐直接使用Android Studio的Profiler、Layout Inspector这两个组合。Systrace在分析启动帧和卡顿时也非常有用。学习HierarchyViewer带来的最大收获是你知道了从“布局树”和“节点耗时”两个维度去分析问题这个思路在后继工具里都完全适用。6. 写在最后工具会过时思路不会6.1 从HierarchyViewer开始的布局优化习惯整理这篇文章时我一直在想为什么这么老的工具现在看来仍然值得写。也许是因为它教会了很多人一个核心习惯不要靠猜去优化布局先量化再动手。当年用HierarchyViewer优化完那个订单页之后我养成了一个习惯凡是新页面开发完都会拉一次视图树看一眼深度有没有超过10层有没有奇怪的单个父容器拽着一大堆子View的情况。这个方法比代码审查更直接因为它反映的是运行时的真实层级而不是你以为的样子。后来即使不再频繁打开HierarchyViewer我也会用Layout Inspector做类似的事。工具界面一直在变核心逻辑始终没变布局越扁平View树越浅渲染阶段越轻松。6.2 我的几点实战心得第一不要一听到卡顿就去找内存泄漏。先把布局这个基础层排干净。很多卡顿其实不是内存问题而是measure阶段在几次嵌套里面反复消耗CPU时间用布局工具一抓就现原形。第二优化前必须存“证据”。截图、记录每次抓取的耗时数据、写出视图树层级深度都是证据。改完后如果没有对比基本等于白优化因为你自己也说不清到底提升在哪里。第三布局文件不是越短越好。有时候你把布局写得极短但为了达到同样的视觉结构加了大量margin和约束效果反而比多一层LinearLayout更差。用HierarchyViewer看数据别光看代码行数要看实际渲染的开销。第四如果遇到布局重叠不要只盯着布局XML里那几个View。先把HierarchyViewer的屏幕截图联动打开点一下重叠区域看看那个坐标下到底有几个View叠在那里。这样的排查方式比一行行找margin可靠得多。我在实际使用中最深的体会是性能优化这件事数据先行永远是对的。HierarchyViewer或许已经不适合在新系统上使用但它给我的那套“先用数据找问题再有针对性优化”的方法在之后用所有工具时都管用。如果你也正在为一个卡顿页面头疼不妨先把这套思路捡起来换任何一个现代工具重新走一遍相信会有收获。
返回列表