动图魔方技术拆解 17:清除虚拟数据后,如何用真实素材验证 GIF 工具
SEO 信息
- SEO 标题:动图魔方技术拆解 17:清除虚拟数据后,如何用真实素材验证 GIF 工具
- SEO 摘要:基于 HarmonyOS NEXT / ArkTS 项目“动图魔方”,拆解一个 GIF 工具在移除虚拟作品、虚拟素材和演示文案之后,如何通过
TestAssetService.ets、MediaService.ets与Index.ets建立真实可复验的素材验证闭环:内置测试视频 / 图片 / GIF 复制到沙箱、编辑页直接加载真实文件、导出结果回写作品列表、分享与空状态都围绕真实素材路径验收。 - 关键词:HarmonyOS, ArkTS, TestAssetService, MediaService, GIF 工具, 真实素材, 回归测试, PhotoViewPicker, DocumentViewPicker, 本地验证
- 文章封面:
doc/csdn-series/covers/cover-17-test-asset-real-validation.jpg - 投稿方向:普通技术拆解 / HarmonyOS 本地工具 App 的真实素材回归链路
- 项目环境:HarmonyOS SDK
6.1.0(23)、ArkTS、DevEco Studio、GIFRubiksCube - 验证时间:
2026-06-27 - 验证对象:
entry/src/main/ets/common/services/TestAssetService.ets、entry/src/main/ets/common/services/MediaService.ets、entry/src/main/ets/products/main/Index.ets
第 03 篇解决的是“怎么接系统素材入口”,第 16 篇解决的是“编辑页和作品页怎么保持同一套移动端版式”。到了第 17 篇,问题开始变成另一个工程层面的硬要求:如果页面里还残留虚拟作品、默认样图、示意文案,那么 GIF 工具的抽帧、预览、导出、作品列表和分享按钮,都无法证明自己在真实文件路径上真正跑通过。所以这篇不再讲权限,也不再讲布局,而是专门讲“清除演示假数据之后,怎么用真实素材做稳定回归”。
一、真实工程问题背景
很多工具类 App 在开发中后期都会遇到同一个误区:页面已经做出来了,按钮也能点,空状态也有文案,于是继续往前加功能;但到了真正验收时,才发现页面上大半证据都来自“演示数据”。这类现象表面上只是“页面先跑起来”,本质上却会导致验收口径失真、回归判断失真和用户路径判断失真。
“动图魔方”在清理假数据之前,风险主要集中在四类地方。这里我刻意把现象、原因、影响、验证路径都写清楚:
- 现象:作品页如果默认塞几条示例导出记录,看起来像“已有闭环”。 原因:页面在没有真实导出结果时仍然允许靠内置数组占位。 影响:这会直接导致用户、测试和截图都无法判断“作品条目”到底来自真实导出,还是默认样例。 验证路径:把
DEFAULT_WORKS清空,再看作品页是否只能通过真实导出生成条目。 - 现象:编辑页如果默认带一张演示图或者虚拟素材 URI,预览区能亮起来。 原因:预览层没有把素材入口和真实文件路径强绑定。 影响:这会导致
MediaService、TestAssetService和导出链路之间出现不一致,看上去能预览,实际上不一定能导出。 验证路径:要求测试素材也必须回写sourceUris,并触发同一条预览刷新逻辑。 - 现象:分享按钮如果只挂在 UI 上,即便页面能点,也不代表作品列表里的条目真的对应一个存在的 GIF 文件。 原因:作品元数据和真实文件路径可能脱钩。 影响:这会直接导致用户在“看起来能分享”的页面里点到一个实际已失效的结果,风险会在发布后暴露。 验证路径:导出后必须进入作品页,再验证作品条目、分享按钮和删除入口同时存在。
- 现象:空状态如果文案写成“暂无内容”,却没有明确告诉用户必须导入真实素材。 原因:页面没有把“禁止假数据兜底”写进产品口径。 影响:开发、验收和截图都很容易继续沿用演示假数据,导致真实回归长期缺席。 验证路径:把空状态改成“这里不再内置虚拟作品,请导入真实素材后导出”,让回归口径直接外显。
这个问题在 GIF 工具里尤其明显,因为它不是纯展示页,而是一条典型的文件处理链路:
- 选择真实视频、图片或 GIF。
- 把素材路径写进编辑器状态。
- 生成可播放预览。
- 触发导出并把结果写回作品列表。
- 从作品列表继续编辑、分享或删除。
只要其中任意一步还依赖虚拟数据,前后链路就断了。最终你能证明的,只是“页面搭起来了”,而不是“真实素材闭环跑通了”。
第 17 篇就是为了解决这个问题:让“测试素材”不再是摆拍,而是真正参与编辑、导出、回看和分享的工程输入。
二、目标与边界
这次的目标很明确:
- 移除作品页和编辑页对虚拟素材、虚拟作品的依赖。
- 保留一条开发期可复现的“内置真实测试素材”路径,避免每次验收都手动到系统相册里找文件。
- 让测试素材和系统选择器最终都回到同一个
sourceUris状态模型。 - 确保“素材导入 -> 预览 -> 导出 -> 作品页回看 -> 分享”能基于真实文件路径完成。
- 让截图、布局树、源码行号和本地质检脚本能够相互印证。
边界同样要讲清楚:
- 本文不重复展开第 03 篇已经写过的权限和 URI 访问原则,只关注“如何拿真实样例做回归”。
- 本文不重讲第 11、12 篇的导出线程与进度状态机,只把它们当成第 17 篇验收闭环的下游。
- 本文里的“测试素材”不是演示假数据,而是打包进应用 rawfile 的真实视频、图片序列和 GIF 文件。
- 本文不把内置测试素材当最终用户能力,而是把它视为开发、截图和回归验证的稳定入口。
三、源码对象与证据文件
本篇实际对应的源码对象和证据文件如下:
entry/src/main/ets/common/services/TestAssetService.etsentry/src/main/ets/common/services/MediaService.etsentry/src/main/ets/products/main/Index.etsentry/src/main/ets/entryability/EntryAbility.etsentry/src/main/resources/base/profile/main_pages.jsondoc/screenshots_current/gifrubik_test_asset_button.jpegdoc/screenshots_current/gifrubik_test_asset_loaded.jpegdoc/screenshots_current/gifrubik_test_asset_current.jpegdoc/screenshots_current/gifrubik_test_asset_exported.jpegdoc/screenshots_current/layout_test_asset_loaded.jsondoc/screenshots_current/layout_test_asset_exported.json
这几组对象分别承担不同职责:
| 对象 | 责任 | 为什么它是第 17 篇主角 |
|---|---|---|
TestAssetService.ets |
把 rawfile 里的测试视频 / 图片 / GIF 复制到沙箱缓存目录 | 决定“测试素材”是否真的是可导出的真实文件 |
MediaService.ets |
系统素材入口统一返回 MediaPickResult |
决定真实用户路径和测试路径能否收口到同一接口 |
Index.ets |
编辑页、作品页、空状态、测试素材按钮和状态文案 | 决定真假素材链路是否最终落到同一 UI 状态机 |
gifrubik_test_asset_*.jpeg |
编辑页与作品页的真实截图证据 | 证明测试素材不是停留在代码层,而是真的进入了页面与作品列表 |
layout_test_asset_*.json |
布局树快照 | 证明测试素材文案、按钮、作品条目在真实 UI 结构中存在 |
我这次定位关键实现的位置,实际用了下面这几条命令:
rg -n "const VIDEO_ASSETS|const IMAGE_ASSETS|const GIF_ASSETS|static async prepare|copyAssets\(|copyAsset\(" entry/src/main/ets/common/services/TestAssetService.ets
rg -n "private async pickSource|private async useTestAssets|未选择素材,请重新选择真实素材|测试素材加载失败,请重新构建应用|这里不再内置虚拟作品|EditorActionButton\('测试素材'" entry/src/main/ets/products/main/Index.ets
Get-ChildItem doc/screenshots_current -File |
Where-Object { $_.Name -match "test_asset|picker_open|share_button" } |
Select-Object Name, Length
命中的关键结果如下:
entry/src/main/ets/common/services/TestAssetService.ets:15:const VIDEO_ASSETS: RawAssetEntry[] = [
entry/src/main/ets/common/services/TestAssetService.ets:20:const IMAGE_ASSETS: RawAssetEntry[] = [
entry/src/main/ets/common/services/TestAssetService.ets:26:const GIF_ASSETS: RawAssetEntry[] = [
entry/src/main/ets/common/services/TestAssetService.ets:31: static async prepare(context: common.UIAbilityContext): Promise<TestAssetBundle> {
entry/src/main/ets/products/main/Index.ets:445: private async pickSource(): Promise<void> {
entry/src/main/ets/products/main/Index.ets:466: private async useTestAssets(type: string): Promise<void> {
entry/src/main/ets/products/main/Index.ets:931: this.EditorActionButton('测试素材', '#23A6D5', true, () => this.useTestAssets(this.editorType))
entry/src/main/ets/products/main/Index.ets:1205: this.EmptyState('作品列表为空', '这里不再内置虚拟作品,请导入真实素材后导出', '开始创作', 'video')
这些行号说明第 17 篇不是泛泛在聊“测试”,而是可以明确落到:素材定义、测试素材准备、真实素材入口、按钮接入和空状态收口五个可复核点。
和这篇实现直接相关的官方依据也建议一起对照看:
四、先把“测试素材”变成真实文件,而不是数组占位
第 17 篇最关键的一步,是把“测试素材”从概念改成真实文件。
TestAssetService.ets 先明确列出三类 rawfile 素材:
const VIDEO_ASSETS: RawAssetEntry[] = [
{ rawPath: 'test_assets/flower_cc0.mp4', fileName: 'flower_cc0.mp4' },
{ rawPath: 'test_assets/flower_cc0.webm', fileName: 'flower_cc0.webm' }
];
const IMAGE_ASSETS: RawAssetEntry[] = [
{ rawPath: 'test_assets/image_sequence_01.jpg', fileName: 'image_sequence_01.jpg' },
{ rawPath: 'test_assets/image_sequence_02.jpg', fileName: 'image_sequence_02.jpg' },
{ rawPath: 'test_assets/image_sequence_03.jpg', fileName: 'image_sequence_03.jpg' }
];
const GIF_ASSETS: RawAssetEntry[] = [
{ rawPath: 'test_assets/rotating_earth.gif', fileName: 'rotating_earth.gif' }
];
这一步的价值不在于“文件名写在常量里”,而在于三件事:
- 测试视频、图片序列和 GIF 被明确拆成三条验证分支,而不是一个笼统的“示例素材包”。
- 每一类素材都能映射到真实编辑器类型:
video、image、gif。 - 当截图、日志或作品列表出现问题时,可以直接回溯到哪一类素材没有真正准备好。
真正让它们成为“真实素材”的,是 prepare() 和 copyAsset() 这两层:
static async prepare(context: common.UIAbilityContext): Promise<TestAssetBundle> {
const baseDir = `${context.cacheDir}/test_assets`;
try {
fs.mkdirSync(baseDir, true);
} catch (err) {
}
return {
videoUris: await TestAssetService.copyAssets(context, baseDir, VIDEO_ASSETS),
imageUris: await TestAssetService.copyAssets(context, baseDir, IMAGE_ASSETS),
gifUris: await TestAssetService.copyAssets(context, baseDir, GIF_ASSETS)
};
}
private static async copyAsset(context: common.UIAbilityContext, baseDir: string, asset: RawAssetEntry): Promise<string> {
const outputPath = `${baseDir}/${asset.fileName}`;
const content = await context.resourceManager.getRawFileContent(asset.rawPath);
const start = content.byteOffset;
const end = content.byteOffset + content.byteLength;
const buffer = content.buffer.slice(start, end);
const file = fs.openSync(outputPath, fs.OpenMode.CREATE | fs.OpenMode.TRUNC | fs.OpenMode.READ_WRITE);
fs.writeSync(file.fd, buffer);
fs.closeSync(file);
return outputPath;
}
这里的工程意义非常明确:
- rawfile 里的测试资源不会直接拿来当页面展示占位,而是先复制到
cacheDir/test_assets。 - 页面后续拿到的是可读、可导出、可分享的真实沙箱文件路径。
- 这意味着测试素材和系统选择器最终都会落到“文件路径 / URI”这一层,而不是停留在某个演示对象。
如果没有这一步,你最多只能证明“按钮能把一张内置图显示出来”;有了这一步,才能证明内置素材与真实导出链路是同一种输入。
五、测试路径和用户真实路径必须回到同一个状态模型
光有 TestAssetService 还不够。如果内置测试素材走一套状态逻辑、系统选择器走另一套逻辑,那它依然只是“开发专用分支”,无法证明主链路稳定。
MediaService.ets 的作用,就是把用户真实素材入口统一成 MediaPickResult:
static async pickVideo(): Promise<MediaPickResult> {
const pickerView = new photoAccessHelper.PhotoViewPicker();
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
options.maxSelectNumber = 1;
const result = await pickerView.select(options);
return {
uris: result.photoUris,
message: result.photoUris.length > 0 ? '已选择视频素材' : '未选择视频'
};
}
static async pickDocument(context: common.UIAbilityContext): Promise<MediaPickResult> {
const documentPicker = new picker.DocumentViewPicker(context);
const options = new picker.DocumentSelectOptions();
const uris = await documentPicker.select(options);
return {
uris: uris,
message: uris.length > 0 ? `已选择 ${uris.length} 个文件` : '未选择文件'
};
}
页面层接用户真实路径时,落点是 pickSource():
private async pickSource(): Promise<void> {
try {
this.clearLivePreview();
let result: MediaPickResult;
if (this.editorType === 'video') {
result = await MediaService.pickVideo();
} else if (this.editorType === 'image' || this.editorType === 'depth' || this.editorType === 'threeD') {
result = await MediaService.pickImages();
} else {
result = await MediaService.pickDocument(this.ctx());
}
this.sourceUris = result.uris.slice();
this.statusText = result.message;
this.schedulePreviewRefresh();
} catch (err) {
this.sourceUris = [];
this.clearLivePreview();
this.statusText = '未选择素材,请重新选择真实素材';
}
}
而测试路径接入时,落点是 useTestAssets():
private async useTestAssets(type: string): Promise<void> {
try {
const assets = await TestAssetService.prepare(this.ctx());
this.clearLivePreview();
this.editorType = type;
this.page = 'editor';
if (type === 'image' || type === 'depth' || type === 'threeD') {
this.sourceUris = assets.imageUris.slice();
this.statusText = `已载入 ${this.sourceUris.length} 张内置测试图片`;
} else if (type === 'gif') {
this.sourceUris = assets.gifUris.slice();
this.statusText = '已载入内置测试 GIF';
} else {
this.sourceUris = assets.videoUris.slice(0, 1);
this.statusText = '已载入内置测试视频';
}
this.schedulePreviewRefresh();
} catch (err) {
this.sourceUris = [];
this.clearLivePreview();
this.statusText = '测试素材加载失败,请重新构建应用';
}
}
这两条路径最重要的共同点是:
- 最终都写回
this.sourceUris。 - 最终都会刷新预览。
- 失败时都会清空状态,避免伪成功。
也就是说,第 17 篇真正建立的是一个判断标准:测试路径不是特权路径,而是主链路的另一个稳定入口。
六、清掉虚拟作品,才能让作品页证明导出真的发生过
如果作品页里默认放着几条演示数据,任何“导出成功”的截图都不够可信。因为你无法证明当前看到的条目,到底是刚导出的,还是默认内置的。
这次代码里最关键的收口之一,是把默认作品清成空数组:
const DEFAULT_WORKS: WorkEntry[] = [];
然后在作品页直接把产品口径写死:
if (this.works.length === 0) {
this.EmptyState('作品列表为空', '这里不再内置虚拟作品,请导入真实素材后导出', '开始创作', 'video')
}
这句文案看起来只是 UI 提示,实际上非常重要:
- 它明确告诉开发和验收者,这个页面不再接受“默认演示作品”作为完成态。
- 它让所有后续截图都必须基于一次真实导出。
- 它为第 17 篇建立了一个可复验前提:作品页里一旦出现条目,就代表至少发生过一次真实导出。
这也是为什么第 17 篇能和第 03 篇区分开。第 03 篇证明“能拿到素材”;第 17 篇证明“没有虚拟数据兜底后,真实素材闭环依然成立”。
七、截图和布局树要证明“测试素材”真的进入了 UI 与作品闭环
第 17 篇最不能接受的写法,就是只贴源码,不贴真实界面。因为这里讨论的是“真实素材是否真的穿过页面和导出链路”。所以截图这一节必须显式回答两件事:需要证明的规则是什么,以及它的对应源码位置在哪里。
| 截图 | 需要证明的规则 | 对应源码位置 |
|---|---|---|
gifrubik_test_asset_button.jpeg |
编辑页存在显式测试素材入口,不靠隐藏调试命令 | Index.ets:931 |
gifrubik_test_asset_loaded.jpeg |
测试素材载入后状态文案切换到真实文件语义,并保留导出入口 | Index.ets:466-486 |
gifrubik_test_asset_current.jpeg |
测试素材最终写回 sourceUris 并可重新选择素材 |
Index.ets:445-462、466-486 |
gifrubik_test_asset_exported.jpeg |
测试素材导出后会进入作品页,作品条目不再依赖虚拟默认值 | Index.ets:1205 + 导出链路回写作品列表 |
7.1 编辑页上必须能看到“测试素材”入口

这张图对应 Index.ets:931 的按钮接入:
this.EditorActionButton('测试素材', '#23A6D5', true, () => this.useTestAssets(this.editorType))
它证明“测试素材”不是后台接口或隐藏调试命令,而是一个面向真实回归流程的显式入口。
7.2 测试素材载入后,状态文案必须切到真实文件语义

这张图能验证两件事:
- 顶部状态文案已经变成“已载入内置测试视频”,说明不是默认预览图。
- 编辑页下方仍然保留真实导出入口,证明测试素材不是死图展示,而是接着往下走导出。
布局树快照也给出了同样的证据:
layout_test_asset_loaded.json
- Text: 视频转GIF
- Text: 已载入内置测试视频
- Button: 导出
- Button: 测试素材
7.3 当前素材状态必须能回到“真实 URI 已进入编辑器”

这张图对应的布局树中,有一条很关键的文本:
Text: 已选择 1 个素材 URI
Button: 重新选择素材
Button: 测试素材
这说明测试素材路径并不是单独塞进预览区,而是已经进入和用户真实选择素材相同的状态模型:sourceUris。
7.4 作品页出现条目后,才算真正跑通

这张图是第 17 篇最关键的闭环证据,因为它证明:
- 测试素材不是只用于预览。
- 导出结果已经变成作品页可见条目。
- 作品条目上继续存在“编辑 / 分享 / 删除”操作。
对应布局树里也能看到:
Text: 视频转GIF_1
Text: 视频转GIF · 16:9 · 5s · 15fps · 高清
Text: 已导出
Text: 分享
这组证据说明作品页已经不再是演示容器,而是导出结果的真实回看页。
八、回归命令、风险点与本地质检
第 17 篇不能只靠截图,还要有可重复的检查口径。我这次的回归命令主要有四类。
第一类,定位测试素材服务和编辑页接入点:
rg -n "TestAssetService|useTestAssets|测试素材|这里不再内置虚拟作品" entry/src/main/ets -S
第二类,确认截图证据已经齐全:
Get-ChildItem doc/screenshots_current -File |
Where-Object { $_.Name -match "test_asset|picker_open|share_button" } |
Select-Object Name, Length
第三类,检查作品页与编辑页布局树里是否真的出现关键文本:
Get-Content doc/screenshots_current/layout_test_asset_loaded.json -TotalCount 120
Get-Content doc/screenshots_current/layout_test_asset_exported.json -TotalCount 120
第四类,用本地文章质检脚本做发布前结构预检:
node tools/check_csdn_article_quality.js 17
这一篇的高风险回归点主要有五类:
- 测试素材仍然停留在内存数据,没有复制成真实沙箱文件。
useTestAssets()单独维护一套预览逻辑,没有回写sourceUris。- 作品页默认又被塞回虚拟条目,导致导出证据失真。
- 分享按钮只保留在 UI 上,但作品文件实际不存在。
- 空状态文案回退成模糊描述,导致验收者看不出是否仍在用假数据。
九、工程验收清单
| 验收项 | 结果 | 证据 |
|---|---|---|
| 内置测试视频、图片序列、GIF 已拆成三类真实 rawfile 素材 | 通过 | TestAssetService.ets:15-28 |
测试素材会先复制到 cacheDir/test_assets,不是直接拿占位对象预览 |
通过 | TestAssetService.ets:31-41、53-61 |
用户真实素材入口与测试素材入口最终都回写 sourceUris |
通过 | Index.ets:445-462、466-486 |
| 编辑页存在显式“测试素材”按钮 | 通过 | Index.ets:931 + gifrubik_test_asset_button.jpeg |
| 测试素材载入后状态文案切换为真实文件语义 | 通过 | gifrubik_test_asset_loaded.jpeg + layout_test_asset_loaded.json |
| 作品页不再内置虚拟作品 | 通过 | Index.ets:1205 |
| 导出后的测试素材结果能进入作品页 | 通过 | gifrubik_test_asset_exported.jpeg + layout_test_asset_exported.json |
| 作品条目继续具备编辑 / 分享 / 删除操作 | 通过 | layout_test_asset_exported.json 中 分享 / 删除 文本 |
| 本篇与第 03 篇不重复,主问题转向“清除假数据后的真实回归闭环” | 通过 | 本文结构与源码对象已区分 |
十、小结
第 17 篇真正解决的,不是“怎么给 App 加一个测试素材按钮”,而是如何把工具类 App 从演示态拉回工程态。
对于 GIF 工具来说,真实素材验证至少要满足三件事:
- 素材本身是真实文件,不是内存占位或虚拟数组。
- 测试路径和用户路径最终落在同一套编辑器状态模型上。
- 导出结果必须能回到作品页,继续接受分享、删除和二次编辑的真实回归。
一旦做到这三点,后面的抽帧、编码、保存、分享、主题、布局和 3D 扩展才有稳定地基。否则再多的页面 polish,也只是给假数据做包装。
十一、下一篇衔接
第 18 篇继续拆 《动图魔方技术拆解 18:ArkGraphics3D 预览、能力检测与 3DGS 降级路线》,重点会转向:
- 为什么 GIF 工具里的“3D 转动图”不能直接等同于真实 3D 重建。
CapabilityService、Model3DView和能力文案如何区分已可用、实验中和预留路线。- 当设备不支持目标能力时,如何把 3DGS / 渲染 / 单图浅 3D 三条路线做成可验收的降级方案。
更多推荐



所有评论(0)