OPPO Watch X3 Mini 表盘魔改过程记录

我很喜欢 OPPO 表盘市场的「明晰组件」这个表盘,唯一的美中不足是,日期、星期和秒数都是灰色。暗光下还能看清,但在户外强光下对比度太低,几乎要凑近才能辨认。想把这几个元素改成白色。真正动手后才发现,它不是普通的 APK 表盘,而是 OPPO 的 engine 表盘包,而且主屏和抬腕/AOD 是两套完全不同的渲染资源。

本文记录一次在自家设备上的修改过程,不涉及绕过付费或分发修改后的表盘;如果你也想改自己买的表盘,可以参考思路。

一、它不是 APK

设备是 OPPO Watch X3 Mini(OWW263,Android 11)。「明晰组件」在 pm list packages 里找不到,dumpsys package 里也没有对应的 WatchFaceService,说明它不是独立安装的 APK。

它的真实身份是一个 engine 表盘 zip 包:

1
2
包名:com.heytap.wearable.enginewf.O20250310114637290
引擎:com.heytap.wearable.uiengine

包内大致是:

1
2
3
4
5
images/          主屏用的 WebP 图层和数字精灵
manifest.json 主屏显示列表:坐标、颜色、字体、数据绑定
infos.json 表盘元数据:包名、版本、组件槽位
project.json 设计工程数据
mcu/bin/ 低功耗 MCU 用的预编译资源

关键在于它有两条渲染路径:

场景渲染方使用的资源
解锁后的主屏主 SoC 上的 uienginemanifest.json + images/
抬腕亮屏 / AOD低功耗 MCUmcu/bin/style0.bin、mcu/bin/aod/style0.bin 等

所以只改 manifest.json 会出现一个很典型的现象:解锁后是白色,抬腕还是灰色。两套资源都要改。

二、把原始包弄出来

手表上的资源在 Launcher 的私有目录里,普通 adb 读不到:

1
/data/user_de/0/com.heytap.wearable.launcher/files/enginewf/<包名>/

手机 OPPO 健康下发前,会先把 zip 写到自己的临时目录:

1
/data/data/com.heytap.health/files/WatchFace/Temp/Pkg/<包名>.zip

这个文件同步完成后立即删除,所以要实时轮询复制。手机需要 root:

1
2
3
4
5
6
7
8
9
10
11
12
13
adb shell su -c '
mkdir -p /data/local/tmp/wfcap
touch /data/local/tmp/wfcap/marker
while true; do
find /data/data/com.heytap.health \
-type f -newer /data/local/tmp/wfcap/marker -size +5k |
while read f; do
cp "$f" /data/local/tmp/wfcap/
done
touch /data/local/tmp/wfcap/marker
sleep 0.2
done
'

然后在手机 OPPO 健康里删除并重新下载表盘。同步完成后从 /data/local/tmp/wfcap/ 里把 <包名>.zip 拷出来:

1
2
adb shell su -c 'cp /data/local/tmp/wfcap/<包名>.zip /sdcard/Download/'
adb pull /sdcard/Download/<包名>.zip wf_orig.zip

同目录下还会出现一个 .wtheme 文件,是商店的加密主题包,改色用不到,忽略即可。

三、包结构

文件/目录作用
images/wf_pic_no_*.webp背景、时分秒数字、天气/电量等图层,按编号被 manifest.json 引用
images/preview.png / preview_small.png表盘选择器里的预览图
manifest.json主屏显示列表:每个图层的类型、坐标、颜色、字体、数据源
infos.json表盘元数据:packageName、versionCode、可编辑组件槽位
project.json设计工具生成的工程数据,运行时基本不用
mcu/bin/style0.binMCU 正常模式的显示列表(编译后)
mcu/bin/aod/style0.binMCU 的 AOD 版本显示列表
mcu/bin/preview.bin12 字节头(宽、高、类型、长度)+ PNG 预览图
mcu/bin/font_*.binMCU 位图字体,魔数 UFL$
mcu/bin/setting.bin / widget.binMCU 配置头和组件定义

style0.bin 不是文本配置,是编译后的二进制显示列表,魔数 78 56 34 12;颜色字段可以直接找出来改,但版式(坐标、字号、文本拆分)没有公开的编译器可用,这一点后面再说。

四、改颜色

4.1 主屏

日期和星期在 manifest.json 里是普通的 Text 节点,颜色为灰色 #FF7D7D7D,共 4 处(主样式 + AOD 样式各一套)。直接替换成白色:

1
2
3
4
p = 'orig/manifest.json'
s = open(p, encoding='utf-8').read()
s = s.replace('#FF7D7D7D', '#FFFFFFFF')
open(p, 'w', encoding='utf-8').write(s)

秒数不是文字,而是 WebP 数字精灵,像素本身是灰度 (125,125,125)。把这些图的非透明像素重绘成白色,保留 alpha:

1
2
3
4
5
6
7
8
9
10
11
12
from PIL import Image

for h in ['1f', '20', '21', '22', '23', '24', '2b', '2c', '2d', '2e']:
f = f'orig/images/wf_pic_no_000000{h}.webp'
im = Image.open(f).convert('RGBA')
px = im.load()
for y in range(im.height):
for x in range(im.width):
r, g, b, a = px[x, y]
if a > 0:
px[x, y] = (255, 255, 255, a)
im.save(f, format='WEBP', lossless=True)

背景图 wf_pic_no_00000000.webp 叠到黑底后已经是 #000000,不用动。截图里日期上方那块 #262626 深灰圆角底是引擎绘制的组件底板,包内没有对应配置,保持原样。

4.2 MCU / AOD

MCU 资源里同样有两处日期/星期的灰色 RGBA 值,分别在 mcu/bin/style0.bin 和 mcu/bin/aod/style0.bin:

1
2
3
4
for f in ['orig/mcu/bin/style0.bin', 'orig/mcu/bin/aod/style0.bin']:
d = open(f, 'rb').read()
d = d.replace(b'\x7d\x7d\x7d\xff', b'\xff\xff\xff\xff')
open(f, 'wb').write(d)

mcu/bin/preview.bin 是「12 字节头 + PNG」,把 PNG 里的灰色像素重绘成白色后,还要更新头里的长度字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import io, struct
from PIL import Image

def recolor(data):
im = Image.open(io.BytesIO(data)).convert('RGBA')
px = im.load()
for y in range(im.height):
for x in range(im.width):
r, g, b, a = px[x, y]
if (r, g, b) == (125, 125, 125):
px[x, y] = (255, 255, 255, a)
out = io.BytesIO()
im.save(out, format='PNG')
return out.getvalue()

f = 'orig/mcu/bin/preview.bin'
d = open(f, 'rb').read()
w, h, t, ln = struct.unpack('<HHII', d[:12])
new = recolor(d[12:12 + ln])
open(f, 'wb').write(struct.pack('<HHII', w, h, t, len(new)) + new)

选择器里的 images/preview.png 和 preview_small.png 也按同样方式重绘一下,保持预览一致。

五、装回手表

5.1 为什么不能直接 adb 安装

手表的 Launcher 有一个 EngineWfProvider(authority com.heytap.wearable.provider.watchfaces),安装表盘时需要向它写入 zip。但它的写入口做了校验:调用方必须是 OPPO 表盘商店(包名 com.heytap.wearable.themestore),并且证书 SHA-256 匹配。

adb shell 能通过 content call --method install_watchface 拿到安装 URI,但 content write 会被拒绝;普通第三方 App 连调用都返回 null。这条路走不通,正确做法是在手机同步给手表之前把 zip 换掉。

5.2 在同步瞬间替换

用 strace 观察手机的同步过程,顺序是:

  1. 下载进程用 O_WRONLY|O_CREAT|O_TRUNC 打开 Temp/Pkg/<包名>.zip 并写入
  2. 其他进程随后按路径重新 open(O_RDONLY) 读取
  3. 最后 unlink 删除

所以只要在文件刚被创建时,把我们的改色包原子 rename 覆盖到该路径即可。写进程手里的 fd 仍指向被 unlink 的旧 inode,它继续写它的;后面按路径打开的进程拿到的就是我们的文件。

有两个细节:

  • 等长:手机/手表会 stat 文件大小,最好让改色包和原包字节数完全一致。用最高压缩级别重打,再用 zip comment 补齐到原大小(本例 341427 字节)。
  • 同文件系统:临时文件必须放在目标目录内再 ATOMIC_MOVE。放在 /data/local/tmp 会报 Cross-device link。

等长重打包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os, zipfile

root, out, target = 'orig', 'wf_patch.zip', 341427
if os.path.exists(out):
os.remove(out)

with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as z:
for dirpath, _, filenames in os.walk(root):
for name in filenames:
p = os.path.join(dirpath, name)
z.write(p, os.path.relpath(p, root).replace(os.sep, '/'))

if os.path.getsize(out) <= target:
with zipfile.ZipFile(out, 'a') as z:
z.comment = b'P' * (target - os.path.getsize(out))

5.3 一个 1ms 轮询的 watcher

用一个很小的 Java 程序跑在手机 root 下(dalvikvm),每 1ms 检查目标文件是否存在;出现时,把源包复制到目标目录内的 .wfrepl.tmp,再 Files.move(..., ATOMIC_MOVE) 覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Path dir = Paths.get("/data/data/com.heytap.health/files/WatchFace/Temp/Pkg");
Path target = dir.resolve(PKG);
Path src = Paths.get("/data/local/tmp/wf_mod_same.zip");
Path tmp = dir.resolve(".wfrepl.tmp");

while (true) {
if (Files.exists(target)) {
Files.copy(src, tmp, StandardCopyOption.REPLACE_EXISTING);
Files.move(tmp, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
while (Files.exists(target)) Thread.sleep(50);
// 删除源包、dex、日志,然后退出
return;
}
Thread.sleep(1);
}

同步完成后,App 会把包用掉并删除,watcher 看到文件消失(日志 cleared)就自动清理源包、dex、日志并退出,不留后台进程。

部署大致是:

1
2
3
adb push wf_patch.zip /data/local/tmp/wf_mod_same.zip
adb push classes.dex /data/local/tmp/wfwatch/classes.dex
adb shell su -c 'nohup dalvikvm -cp /data/local/tmp/wfwatch/classes.dex com.imaegoo.Watcher >/dev/null 2>&1 &'

然后在 OPPO 健康里点一次「同步/应用到手表」。watcher 日志出现:

1
2
replaced size=341427
cleared

就说明替换发生在读取之前,手表拿到的是改色包。

六、结果

改色前后对比:

主屏和抬腕/AOD 的日期、星期、秒数都变成了白色,背景保持纯黑,版式和原来一致。

七、限制

主屏的版式(比如去掉 24H、把星期移到秒上方、日期改成上下排列、字号加大)在 manifest.json 里改起来很简单。但抬腕/AOD 用的是 MCU 的 style0.bin,这是编译后的显示列表:

  • 没有公开的编译器/反编译器
  • 不能从 manifest.json 重新生成
  • 手工改颜色可以,改坐标/字号/文本结构成本很高

如果只改主屏,就会出现主屏和 AOD 版式不一致。所以这次只做了颜色,版式保持原样,两边看起来一致。对「只想看清时间」来说,已经够了。

OPPO Watch X3 Mini 表盘魔改过程记录

https://www.imaegoo.com/2026/oppo-watch-x3-mini-watchface-patch/

作者

iMaeGoo

发布于

2026-10-21

更新于

2026-10-21

许可协议

CC BY 4.0

评论