大家好,我是 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");
}
🚀 完整接入示例
我把整个核心引擎打包成了一个黑盒,接入非常简单。
- 初始化窗口管家 (EntryAbility.ets 中)
ts
import { DebugWindowManager } from 'arkinspector';
onWindowStageCreate(windowStage: window.WindowStage): void {
// 注入 WindowStage,剩下的脏活累活管家全包了
DebugWindowManager.init(windowStage, "pages/DebugOverlayPage");
}
- 在业务代码中享用
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 里向我开炮。