告别“盲调”与“重编”!我写了一个鸿蒙 ArkUI 纯端侧的可视化调试神器,正式开源!

大家好,我是 Jason。

作为一名在客户端摸爬滚打多年的程序员,最近在深度做鸿蒙原生开发时,遇到了一个极其抓狂的痛点:UI 调试太折磨人了!

在 Android 时代,我们可以利用 DoraemonKit (DoKit) 这样的端侧工具,或者直接 Dump View 树来实时修改 UI。在前端,更是有天下无敌的 Chrome F12。 但在鸿蒙 ArkUI 的世界里,由于底层是纯声明式、C++ 渲染引擎,且禁用了反射,导致我们在真机上根本拿不到 DOM 树

每次 UI 设计师坐在我旁边走查,说"把这个字号调大 1px"、"这个间距改到 12vp 看看";又或者测试同学跑来问"如果这个新闻标题超长了会不会截断?"、"当 is_hot 状态变成 true 时,那个火苗图标能不能正常展示?"时,我都只能无奈地:改代码造假数据 -> 重新编译 -> 推送真机 -> 点开页面 -> 查看效果。一套流程下来,不仅效率极低,写代码的灵感也早就没了。

官方的 DevTools 固然强大,但必须插着数据线连着电脑。如果脱离了 PC 环境(比如在开会演示时),我们完全就是"盲人摸象"。

为了彻底解决这个痛点,我花时间撸了一个纯端侧、零反射、即插即用 的可视化 UI 调试引擎 ------ ArkInspector,今天正式把它开源给大家!

🌟 ArkInspector 是什么?

简单来说,ArkInspector 不仅是鸿蒙真机上的审查元素 (F12)面板 ,更是你的端侧状态控制台 (类似 Vue/React DevTools)

它以一个极其轻量的底部透明悬浮窗运行在你的 App 中。这让你不仅能像前端一样"指哪改哪"精调 UI 样式,还能随时随地劫持并修改底层业务数据(Mock 数据、切换状态),真正做到所见即所得!

预览

实时数据修改驱动UI更新

UI检查器修改驱动UI更新

核心能力:

  • 🎯 精准指取(UI Inspector): 开启审查模式后,指尖轻触屏幕任意控件,瞬间唤起红色虚线高亮框(支持精准单选排他),让你在脱离 PC 的真机上拥有纯正的端侧 DOM 审查体验。

  • 🎨 UI 样式实时微调: 底部面板自动拉取选中控件绑定的样式字典(字号、颜色、间距等)。滑动滑块或输入数值,真机 UI 瞬间跟手重绘,彻底告别繁琐的"改代码 -> 重新编译"。

  • 🗄️ 业务数据动态 Mock: 核心管家 (DebugManager) 支持深度劫持并双向绑定任意业务变量。无论是测试超长文本截断、List 数组替换,还是一键切换 is_hot 等 Boolean 状态,只需在悬浮窗内修改源数据,页面立刻响应变化并完成 UI 刷新!

  • 🧰 状态机制全兼容 (V1/V2): 底层调度引擎完美解耦了 ArkUI 的状态管理机制。无论你的老业务使用的是 @State,还是新架构中全面拥抱的 @Local,引擎均可无缝接管并触发页面重绘。

🛠 它是怎么做到的?(双核原理解析)

很多做鸿蒙底层的同行可能会问:ArkTS 彻底禁用了反射,而且状态管理机制非常严格,你是怎么在运行时既能修改控件样式,又能劫持底层业务数据的?

答案是:全面拥抱原生 AttributeModifier 与 ArkUI 响应式状态流转机制!

ArkInspector 没有使用任何黑魔法,它的核心架构分为两大驱动层:

1. UI 样式驱动层(针对视觉微调): 我封装了一套 DynamicModifier 家族(支持 Text, Column, Row, Button, List 等)。业务线只需将静态样式抽离为字典并绑定给修饰器,同时挂载 UIInspector.withInspect 点击拦截器。面板抓取到样式后,一旦发生修改,引用地址改变触发 Modifier 重新解析,执行底层的 .fontSize() 等方法,实现"0 性能损耗"的动态重绘。

2. 状态调度驱动层(针对业务数据 Mock): 引擎底层的 DebugManager 提供了一套全局的数据托管与回调机制。你可以将业务页面的核心状态变量(如 @State 定义的复杂数组、状态标识)注册给管家。当你在悬浮窗修改源数据(如把 is_hot 设为 true,或修改 List 数据源)时,管家会精准触发回调,借助 ArkUI 强大的数据响应系统,让原本绑定了该数据的业务组件(如 If/Else 条件渲染、ForEach 列表)瞬间原地刷新!

这个补充太有必要了!作为一篇面向开发者的技术推文,光吹牛皮不放代码是没用的, "Show me the code" 才是王道。把核心 API 亮出来,能让读者立刻感受到这个库的接入有多么丝滑。

我为你单独编写了**「重点 API 极简使用指南」**这一节,重点展示了初始化、UI 审查机制以及数据状态劫持的三个核心 API,你可以直接把这一段插在文章的"核心原理解析"和"开源地址"之间:


🔌 重点 API 极简使用指南

ArkInspector 主打一个"低侵入、即插即用",只需调用以下三个核心 API,即可全面接管你的页面:

1. DebugWindowManager:全局窗口管家

负责底层透明悬浮窗的生命周期管理,只需在项目入口处喂给它 WindowStage 即可。

ts 复制代码
import { DebugWindowManager } from 'arkinspector';

// 在 EntryAbility.ets 中初始化
onWindowStageCreate(windowStage: window.WindowStage): void {
  // 注入 stage 与宿主调试面板路径
  DebugWindowManager.init(windowStage, "pages/DebugOverlayPage");
}

// 在任意页面随时呼出/隐藏面板
Button("呼出面板").onClick(() => DebugWindowManager.showDebugger())

2. UIInspector & DynamicModifier:UI 审查与重绘套装

这是精准拾取和修改样式的核心。将样式抽离为字典,绑定修饰器,并用 UIInspector 包装原来的点击事件。

ts 复制代码
import { UIInspector, DynamicTextModifier } from 'arkinspector';

@State titleStyle: Record<string, Object> = { 'fontSize': 18, 'fontColor': '#333333' };

// 业务组件中的调用:
Text("鸿蒙端侧调试神器")
  // 1. 绑定动态样式修饰器
  .attributeModifier(new DynamicTextModifier(this.titleStyle, 'title_1'))
  // 2. 绑定探针:点击时将样式字典推送到悬浮窗,并接收修改后的回调
  .onClick(UIInspector.withInspect('title_1', this.titleStyle, (newStyle: ESObject) => {
    this.titleStyle = newStyle as Record<string, Object>;
  }))

3. DebugManager.register:业务数据劫持核心 🔥

这是实现数据 Mock 和状态切换的"大杀器"。你可以将原本写死的假数据或需要频繁切换状态的变量注册给管家,即可在面板中直接修改源数据!

ts 复制代码
import { DebugManager } from 'arkinspector';

@Local newsList: NewsItem[] = [ ... ]; // 复杂的业务列表数据

aboutToAppear() {
  // 向管家注册业务数据:起个别名,传入原数据,提供更新回调
  DebugManager.register("listData", this.newsList, (newVal: ESObject) => {
    // 当在悬浮窗中修改了数据,这里会被触发,利用 ArkUI 机制瞬间刷新页面
    this.newsList = newVal as NewsItem[];
  });
}

aboutToDisappear() {
  // 页面销毁时解绑
  DebugManager.unregister("listData");
}

🚀 完整接入示例

我把整个核心引擎打包成了一个黑盒,接入非常简单。

  1. 初始化窗口管家 (EntryAbility.ets 中)
ts 复制代码
import { DebugWindowManager } from 'arkinspector';

onWindowStageCreate(windowStage: window.WindowStage): void {
  // 注入 WindowStage,剩下的脏活累活管家全包了
  DebugWindowManager.init(windowStage, "pages/DebugOverlayPage");
}
  1. 在业务代码中享用
ts 复制代码
import {
  DebugManager,
  DebugWindowManager,
  UIInspector,
  DynamicTextModifier,
  DynamicColumnModifier
} from 'arkinspector'; // 💥 从你的独立库中引入
import { NewsItem } from './NewsItemModel';
import { JSON } from '@kit.ArkTS';

@Entry
@ComponentV2
struct RealWorldListPage {
  @Local newsList: NewsItem[] = [];

  @Local titleStyle: Record<string, Object> = {
    'fontSize': 18,
    'fontColor': '#333333',
    'fontWeight': 'bold',
    'padding': { 'top': 10, 'bottom': 10 } as Record<string, number>,
    'layoutWeight': 1
  };

  @Local cardStyle: Record<string, Object> = {
    'width': '100%',
    'backgroundColor': '#FFFFFF',
    'borderRadius': 12,
    'padding': 16,
    'margin': { 'top': 10 } as Record<string, number>
  };

  aboutToAppear() {
    let news1 = new NewsItem();
    news1.newsTitle = "鸿蒙 SDUI 框架原理解析";
    news1.praiseCount = 100;

    let news2 = new NewsItem();
    news2.newsTitle = "富士 X-T5 摄影技巧分享";
    news2.praiseCount = 888;
    news2.isHot = true;

    this.newsList = [news1, news2];
    
    //🎯数据绑定注册
    DebugManager.register("listData", this.newsList, (newVal: ESObject) => {
      this.newsList = newVal as NewsItem[];
    });
  }

  aboutToDisappear(): void {
    //🎯数据绑定取消注册
    DebugManager.unregister("listData");
  }

  build() {
    Column() {
      List({ space: 16 }) {
        ForEach(this.newsList, (item: NewsItem, index: number) => {
          ListItem() {
            Column({ space: 10 }) {
              Row() {
                Text(`[${index}] `).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E75D8')

                // --- 标题组件 ---
                Text(item.newsTitle)
                  .attributeModifier(new DynamicTextModifier(this.titleStyle, `news_title_${index}`))
                  .onClick(UIInspector.withInspect(`news_title_${index}`, this.titleStyle, (newStyle: ESObject) => {
                    this.titleStyle = newStyle as Record<string, Object>;
                  }))

                if (item.isHot) {
                  Text("🔥 热搜")
                    .fontSize(12).fontColor('#FF0000').padding(4)
                    .backgroundColor('#FFE5E5').borderRadius(4)
                }
              }.width('100%')

              Row({ space: 20 }) {
                Text(`👍 点赞: ${item.praiseCount}`).fontSize(15)
              }.width('100%')
            }
            // --- 卡片(Column)组件 ---
            .attributeModifier(new DynamicColumnModifier(this.cardStyle, `card_style_${index}`))
            .onClick(UIInspector.withInspect(`card_style_${index}`, this.cardStyle, (newStyle: ESObject) => {
              // 这里现在绑定的是卡片自己的 cardStyle
              this.cardStyle = newStyle as Record<string, Object>;
            }))
          }
        },
          // 💥 关键修复:把 inspectTick 拼接到 Key 里,这样每次属性改变,ForEach 都会乖乖重绘!
          (item: NewsItem, index: number) => JSON.stringify(item))
      }
      .width('100%').padding(16).backgroundColor('#F5F6F8').layoutWeight(1)

      Button("🐞 呼出全局调试面板")
        .onClick(() => {
          DebugWindowManager.showDebugger();
        })
    }
    .height('100%')
    .width('100%')
  }
}

🤝 开源地址与共建邀请

目前项目已经基于 MIT 协议完全开源,你可以随便拿去集成到你们公司的内部工具箱中。

👉 GitHub 仓库地址: [github.com/xiangfly11/...]

目前我实现了最常用的几个基础组件 Modifier,但鸿蒙的 UI 组件库非常庞大。如果你觉得这个工具切中了你的痛点,欢迎来给个 Star ⭐️ 支持一下! 更欢迎提交 PR,一起来补充更多组件的动态修饰器,把这个鸿蒙开发者自己的端侧 DevTools 做大做强!

如果在接入过程中遇到任何问题,欢迎在评论区或者 GitHub Issues 里向我开炮。

相关推荐
盐焗西兰花2 小时前
鸿蒙学习实战之路-Share Kit系列(7/17)-自定义分享面板操作区
linux·学习·harmonyos
xym4 小时前
Taskpool简单使用2
harmonyos
不爱吃糖的程序媛5 小时前
鸿蒙 Flutter 多引擎场景开发指导
flutter·华为·harmonyos
小雨青年5 小时前
鸿蒙 HarmonyOS 6 | 多媒体(05)全局播控 AVSession 接入与后台控制
华为·harmonyos
Keya5 小时前
鸿蒙平台实现高斯模糊的渐变色
harmonyos
大雷神7 小时前
HarmonyOS APP<玩转React>开源教程四:状态管理基础
react.js·开源·harmonyos
前端不太难7 小时前
90% 的鸿蒙 App,没有真正的依赖管理
华为·状态模式·harmonyos
左手厨刀右手茼蒿11 小时前
Flutter 三方库 build_modules 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、模块化的 Dart 代码编译策略与构建流水线系统
flutter·harmonyos·鸿蒙·openharmony·build_modules
MardaWang11 小时前
鸿蒙App内存排查与监控全链路实战(工具+方案)
华为·面试·harmonyos·鸿蒙