Shark Explorer在桌面上读懂 Android 堆转储的内存去向——LeakCanary 生态的支配树可视化桌面工具【免费下载链接】leakcanaryA memory leak detection library for Android.项目地址: https://gitcode.com/gh_mirrors/le/leakcanaryShark Explorer 是 LeakCanary 仓库中与 LeakCanary 互补的一款桌面应用LeakCanary 回答哪些对象本应被垃圾回收而 Shark Explorer 回答紧随其后的那个问题——这个 App 用了 200 MB到底都是些什么、是什么在持有它们。本文以 docs/shark-explorer.md 为主线结合仓库源码shark/shark-explorer讲解它的安装方式、堆转储打开流程、支配树dominator tree地图的读法、shark://深链、Markdown 笔记与日志系统并深入其底层实现原理。读完你将掌握如何用 Shark Explorer 分析任意可调试 App 的.hprof如何读懂、导航、链接与记录堆内存中的每一个对象以及它背后的支配树算法与渲染取舍。定位LeakCanary 问谁泄漏了Shark Explorer 问内存是什么Shark Explorer 是一个桌面应用打开一个 Android 堆转储heap dump并以可视化的方式展示是什么在持有它的内存。它运行在 Shark 之上——Shark 是 LeakCanary 2 内置的堆分析库分多层发布Shark Hprof读写 hprof 记录、Shark Graph对象图导航、Shark生成分析报告、Shark Android平台启发式、Shark CLI命令行分析、Shark Explorer桌面可视化即本主题而 LeakCanary 构建于其上见 docs/shark.md。关键的一点你不需要给 App 加任何依赖。Shark Explorer 直接消费.hprof文件——任何可调试debuggableApp 产出的堆转储都可以打开。它画的是堆转储的支配树dominator tree以矩形树图treemap或圆环rings呈现每个对象都被画在持有它的对象内部因此一个色块的面积就代表如果它外面的色块放手会释放出的内存量而嵌套关系就是责任的传递链。需要提醒的是Shark Explorer 仍处于alpha 阶段它与 LeakCanary 分开发布使用独立的版本线迄今为止的每个发布都被标记为预发布版prerelease。其模块结构与 LeakCanary 正式版在仓库中也是独立的三个模块shark-explorer-app桌面 UI、shark-explorer-core分析核心、shark-explorer-jdwp通过 JDWP 与设备交互均在 settings.gradle 中注册。安装随附 JVM按平台下载即用Shark Explorer 的安装产物按平台分发下载入口为项目的 Releases 页面可按shark-explorer过滤查找平台下载文件签名macOSApple SiliconShark-Explorer-version-macos-arm64.dmg是macOSIntelShark-Explorer-version-macos-x64.dmg是WindowsShark-Explorer-version-windows-x64.msi否LinuxShark-Explorer-version-linux-x64.deb否macOS的构建由 Block 签名并公证notarized打开方式与普通应用无异。Windows / Linux的安装包未签名因此会弹出警告Windows 的 SmartScreen 会提示发布者未知需要从更多信息 → 仍要运行More info → Run anyway继续安装。安装后无需额外安装任何运行时应用自带所需的 Java 运行时。应用启动时会检查是否有更新版本实现见 shark-explorer-app/.../UpdateCheck.kt它只检查并提示、不自动下载有新版本时会在窗口上方显示一条提示栏。打开一个堆转储本地文件或从连接的设备直接抓取窗口顶部的工具栏提供两种打开方式UI 实现见 shark-explorer-app/.../Main.kt 中的HeapDumpBarOpen heap dump…打开堆转储…通过系统文件对话框选择任意 Android.hprof文件对话框会按.hprof后缀过滤。Take heap dump…抓取堆转储…通过 Android SDK 中的adb从已连接设备上现场抓取——先选设备再选进程。只有以 debuggable 方式构建的 App 才能被抓取除非设备整体是可调试构建ro.debuggable1userdebug模拟器镜像即属此类此时设备上的每个进程都可以抓取。每个堆转储都在独立的窗口中打开因此两个转储可以并排对照。打开一个大转储需要几秒钟——从源码可见其打开流程分为多个可观测步骤shark-explorer-core/.../HeapExplorer.kt索引 hprof → 计算谁拥有什么OwnerReferences.computeFor→ 计算可达性HeapReachability.computeFor→ 计算支配关系HeapDominatorTree.buildForbuildNodes。界面会逐步显示当前正在执行的步骤。值得注意的是Shark Explorer 使用HprofRecordTag.rootTags全部 GC 根类型而非 Shark 的默认defaultIndexedGcRootTags()来索引默认实现为了查找泄漏而丢弃了大量根类型真实转储中 188K 个根里有约 180K 会被丢弃而一个解释每个对象在哪里被持有的工具不能丢弃任何根。读支配树地图一个矩形的面积 它独占保留的内存打开后的主视图是一张可导航的内存地图理解它的核心规则如下一个矩形就是一个对象它的面积代表该对象保留retain的内存即它自身的字节加上只有它才能存活的全部内容。画在另一个矩形内部的矩形由外层矩形保留。鼠标悬停时窗口会描述该对象点击它标签页会跳转过去并把该对象的全部内容重绘到整个视图。所以读图就是一次鼠标扫过树顶部一个只有一像素宽的矩形往下点两次就能看到全貌。左侧面板回答什么持有它显示从某个 GC 根到该对象的最短引用链一行一个对象并标出持有下一个对象的字段名。每一行都可点击这也是返回的路。凡是命名对象的元素都是入口同样的三种点击方式处处通用单击在当前标签页跳转中键或 ⌘/Ctrl 单击在后台新标签页打开右键弹出菜单可二选一也可复制该对象的链接。顶部按钮总是打开新标签页每个标签页都可关闭——包括最后一个关闭后堆转储仍保持打开、按钮随时可用。← 和 → 在该标签页自己的历史中前进/后退走丢的标签页一键可回。右键点击任一个箭头可看到它通向的所有位置列表想回到第 4 个位置点一下即可而不是连点四下。三个面板均可调整大小也可各自折叠成一个按钮一条 30 步的引用链或一个有 40 个字段的详情面板有时值得让它们占据整个窗口。**Shape形状**在矩形与圆环之间切换。圆环能容纳的子项更少会更早地合并小对象更利于观察树形但不利于精确比较大小。Colour颜色按对象被持有的牢固程度引用强度切分转储。取消勾选某个强度会把对应对象置灰而不是隐藏——这正是让其余那点东西显眼出来的方法。Bitmap 会按原图绘制当转储中包含像素时。Android 在 API 26 到 34 之间把位图像素放在 Java 堆之外对这些转储应用会提议从转储来源设备上把像素抓取回来。**Object list对象列表**把整个转储变成可搜索的列表**Starred星标**保存你想回头再看一遍的对象。每个位置都可以添加一条note笔记用 Markdown 书写跨运行保留——详见下文做笔记一节。其中矩形面积 保留大小的语义正是支配树的数学性质在支配树中父节点是子节点的必经点immediate dominator任何到子对象的路径都必须经过它。所以一个矩形代表的是这个对象单独保住的那部分字节。链接到任意标签页shark://深链右键点击并选择 Copy link复制链接即可得到指向当前位置的shark://URL。把它粘贴到任何链接可点击的地方——聊天消息、issue、给自己的便签——点击后就会把 Shark Explorer 带到前台并在新标签页中打开那个位置。凡是提供在新标签页打开的地方旁边都会提供复制链接标签页、顶部按钮、地图上的矩形、对象列表或泄漏列表的一行、引用链的一步、详情面板的一个字段、一个星标对象。窗口能把你带去的任何地方都会同时把链接递给你。示例shark://vugs93jp/object?id0x7f2a4b18 shark://vugs93jp/objects?queryBitmapexacttrue shark://vugs93jp/leaks标签页能到的任何位置都是链接某个对象、带好搜索和过滤条件的对象列表、保持相同分组展开状态的泄漏列表、星标对象。看这里因此是一个 URL而不是一段指路说明——这也是一个读完了你堆转储的工具或 Agent 可以直接把你指向它发现的问题的方式。链接语义上有几个重要细节实现见 shark-explorer-core/.../DeepLink.ktshark://之后的部分是窗口window而非堆转储heap dump同一个转储打开两次是两个窗口链接指向它被复制时的那个窗口。这意味着链接只在那个窗口打开期间有效窗口关闭或应用重启后链接失效跟随一个失效链接会打开一个说明此事的空窗口。链接从不替换你正在读的内容它总是打开自己的新标签页。链接只能到达安装版应用——是安装程序告诉操作系统shark://属于这个应用。从源码直接运行见下文的实例之间可以互相链接但操作系统不会为链接自动启动它。其实现分为两部分DeepLinkScheme.registerWithTheOs()/takeUrisFromTheOs负责与操作系统对接DeepLinkPeers负责在当前运行的实例之间投递链接见 Main.kt。链接中的窗口 ID 是 8 个随机小写字符字母表刻意去掉了l、1、o、0保证从屏幕上读下来再敲回去不会出错。随机而非递增是有意设计递增 ID 会在两次运行间重复让昨天的链接碰巧打开今天的某个窗口静默地指向错误位置。做笔记绑定位置的 Markdown 笔记标题下方有✎ Add Note添加笔记按钮用于为当前所在位置写一条 Markdown 笔记——可以是某个对象、对象列表、泄漏列表、星标对象也可以是窗口打开时默认标签页所代表的整个堆转储。在输入框中输入按Save保存笔记就显示在输入框原来的位置Cancel取消则丢弃输入。下次再到这个位置时笔记还在标签条也会在带笔记的标签页上显示 ✎ 标记。笔记的关键语义实现见 shark-explorer-core/.../Note.kt 与 NoteFile.kt笔记属于位置而不是标签页同一个位置开两个标签页只有一条笔记同一个转储开两个窗口也一样。所以第二次写到同一个对象时是追加而非另起一份上周留的笔记也仍然在那。删除方式打开笔记、清空文字、点 Save——空笔记即无笔记标签页上的 ✎ 也随之消失。笔记显示在标题行之下、面板之上因为它是关于整个标签页所展示内容的。没人写过的地方什么都不显示——只有按钮一旦有了笔记按钮就消失因为笔记自带✎ Edit编辑。存储位置~/.shark-explorer/notes每个堆转储一个子目录、每个位置一个.md文件。因此笔记可以直接用编辑器打开、粘进 issue或被 Agent 不经此应用直接读取。子目录按转储文件名-所在目录哈希命名如large-dump.hprof-1f3a9c0b避免同名转储的笔记互相覆盖源码 NoteDirectory。你输入的是纯 Markdown——输入过程中不会被改写——保存后凡是这个堆转储能识别的名字都会变成回到窗口的入口你写的内容它被识别为点击它的效果com.example.MyApp$CacheMyApp$Cache在新标签页打开该类0x7f2a4b18Cache instance (0x7f2a4b18)在新标签页打开该对象shark://vugs93jp/leaksLeaks跟随该链接与在其他任何地方点击一样某个 web 链接对应短标题在浏览器中打开转储中没有对应物的名字或地址保持原样这个堆转储从未见过的类只是你写下的一个类名而不是一个坏链接。这正是笔记在应用之外保持可读的方式——磁盘上的内容从不被改写只在屏幕上呈现。笔记支持的 Markdown 子集刻意保持克制一行就是一个块像 GitHub 评论框那样读——标题、列表、引用、code、粗体、斜体、围栏代码块和[链接](https://example.com)都可用行与行之间不需要空行。围栏代码块内部的内容不会被链接化或缩短。一个位置指的是你在哪里而不是界面如何排列在对象列表中搜索、展开一个泄漏或调整窗口大小都不会另起一条笔记。由于shark://链接命名的是窗口写进笔记的链接在窗口关闭后就失效了见上文。所以在写某个标签页的时候为它复制一条链接只要那个窗口还开着链接就一直有效。报告问题每轮运行一份日志Bug 报告请提交到 LeakCanary 的 issue 跟踪器。每次运行都会在~/.shark-explorer/logs写一份日志文件一次运行一份只保留最近 20 份——请附上出错那次运行的日志。日志里包含 JVM、操作系统、应用分配的堆上限、打开堆转储的每一步及耗时以及之后对转储的每一次读取。实现上shark-explorer-core/.../SessionLog.kt每行日志都带时间戳与线程名且每次写入都会 flush——因为最值得读的往往正是崩溃前的那次运行缓冲的尾巴恰恰是解释原因的部分。从源码运行仓库自带 Gradle Wrapper可以直接运行桌面应用git clone 本仓库地址 cd leakcanary ./gradlew :shark:shark-explorer:shark-explorer-app:run --argspath/to/dump.hprof需要JDK 17。--args是可选的不带路径时窗口会打开并只显示Open heap dump…按钮。命令行还支持两个附加能力解析见 shark-explorer-app/.../ExplorerArguments.kt--title窗口标题前缀给本次运行的所有窗口标题加前缀方便在同时打开多个实例时区分。直接传shark://...链接作为参数Windows 和 Linux 投递深链的方式就是为每条链接启动一个进程手动跟随链接也一样。原理纵深支配树、引用强度与三种视图形状支配树是怎么算的精确的 Lengauer–TarjanShark Explorer 使用精确的 Lengauer–Tarjan 算法HeapDominatorTree而不是近似实现。仓库笔记 shark/shark-explorer/notes/dominator-tree.md 记录了为什么要精确BFS 近似法在跨边cross edge被处理时父节点的支配者可能仍是过期的且之后不会重新访问导致保留大小被低估。引用强度的处理上有一个关键规则决定一条弱化边是否被跟随的是目标对象的强度而不是引用本身的类型。一个WeakReference的 referent 若同时被强引用持有跟随这条边只会给已在树中的对象加第二条路径让字节数上移到两条路径的共同祖先、树重新洗牌看不出任何东西。因此WeakeningAwareReferenceReader会询问HeapReachability目标对象如何被持有只在没有强引用到达时跟随弱化边。每个弱可达对象于是都挂在唯一持有它的引用之下其余不动。HeapReachability把转储中的每个对象分类为STRONG、CACHE、THREAD_LOCAL、LOCAL、SOFT、WEAK、FINALIZER、PHANTOM或UNREACHABLE九档使对象数与字节数恰好构成对转储的划分各强度求和 总量这是 UI 能为每种强度显示一个数字的前提。其中CACHE档介于STRONG与SOFT之间缓存引用在没有更强引用持有目标时才被保留这样一张被界面同时持有的图片归界面所有只有无人持有的缓存图片才以CACHE强度画在树中源码 HeapReachability。与之配套的OwnerReferences规则让视图归属其父ViewGroup、Activity 归属运行它的ActivityThread使界面层级在树中保持完整OwnerReferences.kt。打开一个转储要多久笔记中给出了在一个 82 MB、约 102 万对象的 Android OOM 转储上的实测分解索引 hprof 约 0.44 s所有权计算约 0.04 s可达性计算约 2.41 s支配树构建约 2.48 s首次顶层拆分与按类分组约 0.29 s到第一个矩形上屏约 5.7 s。这些数字来自 dominator-tree.md 的实测记录具体环境不同会有差异但可作为量级参考。三种视图形状同一棵树的不同切法地图支持三种形状实现见 treemap-rendering.md 以及TreemapView/RadialView/StackViewTreemap矩形树图比较面积是精确大小的最佳读法。深度由面积驱动而非固定层级——只细分大到值得分的矩形约 12×12 dp 以上才细分、3×3 dp 以下不绘制并以上限约 5000 个矩形为预算、最大者优先分配。Radial圆环比较环中线上对应弧长。一个圆环能容纳的子项远少于矩形——约 50 个等大小子项就超出单环的逐项展示能力——因此它更早分组更利于读树的形状。Stack堆叠行一行一层像 profiler 的 icicle chart。它的独特价值是每一层都不花面积22 层引用链里的最后一个对象也能以与其堆占比相称的宽度绘制并且在每一层都能标注名字和大小。颜色方案有三种Daisy默认每个顶层块一个色相、随深度变浅类似 DaisyDisk 的磁盘着色、Reachability每个引用强度一个色相、按深度着色最能说明 GC 与堆的关系、Slate仅蓝灰当颜色妨碍观察形状时使用。灰色在整套配色里有唯一含义被关掉的引用强度——这也正是把强者置灰让弱者显形的设计见 CellColors.kt。小结Shark Explorer 是 LeakCanary 生态中回答内存为什么这么大的那一半答案无需改动 App就能把任意.hprof变成一张可导航、可链接、可批注的支配树地图。配合shark://深链与纯文本 Markdown 笔记它既可以作为单人排查工具也能作为工具或 Agent 指向问题所在的共享界面。若想进一步了解其算法决策仓库中的 notes/dominator-tree.md、notes/treemap-rendering.md 与 notes/decisions.md 记录了完整的设计取舍与实测数据。【免费下载链接】leakcanaryA memory leak detection library for Android.项目地址: https://gitcode.com/gh_mirrors/le/leakcanary创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考