一台设备不够用?那就把附近的都变成‘我的外设’吧!——鸿蒙设备虚拟化与能力共享全栈实战

举报
喵手 发表于 2025/10/31 17:29:00 2025/10/31
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

先把话挑明:所谓 Device Virtualization(设备虚拟化),不是简单的“远程控制屏幕”,而是把周边设备的硬件与系统能力包装成“像本地一样可用”的资源:文件像本地盘、摄像头像本地摄像头、键鼠像本地键鼠、麦克风像本地麦克风……应用开发者以统一 API 使用,系统在幕后完成发现 → 认证 → 虚拟化映射 → 会话传输 → 能力调度的整活儿。
  今天就按“原理 → 文件与输入共享 → 使用场景 → 代码案例 → 性能与工程优化”这条线,把鸿蒙的设备虚拟化与能力共享机制掰开揉碎讲清楚。放心,少点术语脱缰,多点人话、有梗、有实操😉。


目录预告

  • 设备虚拟化到底虚拟了啥:系统分层与能力路由
  • Device Virtualization 技术原理:命名、编址、协议与会话
  • 分布式文件共享:统一命名空间与“像本地一样”的读写
  • 输入设备共享:键鼠/触控的事件时序、坐标与防抖
  • 典型使用场景:超级终端、跨屏协同、边缘采集与本地渲染
  • 开发代码案例:分布式文件读写远端输入转发(ETS + C 伪代码)
  • 性能与可维护性:吞吐/时延/抖动、零拷贝、指标与排障清单
  • 收尾心法:把复杂留给系统,把约束留给自己

一、设备虚拟化到底“虚拟”了啥?

用一句话概括:把“别人的硬件和系统服务”,映射成“我本机可见的资源与接口”。这套映射不是一刀切,而是按能力做细分:

  • 存储能力虚拟化:把远端设备的目录挂载到统一命名空间,表现为分布式文件系统中的路径(示例路径形如 /mnt/hmdfs/<deviceId>/…,具体随版本与设备定制略有差异)。
  • 外设能力虚拟化:远端键盘/鼠标/触控板/手写笔/手柄等通过事件流转发,映射成本机的输入事件源。
  • 多媒体能力虚拟化:远端摄像头/麦克风/扬声器/显示器以“本地设备节点”的方式出现在能力目录里(系统侧做设备抽象与权限闸门)。
  • 系统服务虚拟化:如设备发现、认证、会话、权限与安全域判断,统一走**分布式软总线(DSoftBus)**和相关系统服务。

二、Device Virtualization 技术原理(像搭地铁:统一入口、智能换乘)

从框图看,跑不掉的就是这几件事:

  1. 命名与编址(Bus Center / 设备目录)

    • 每台设备有稳定的设备标识(deviceId)能力目录
    • 能力用统一命名暴露,例如 files://<deviceId>/input://<deviceId>/keyboard0camera://<deviceId>/front
  2. 发现与认证(DeviceAuth/HiChain)

    • 邻近发现后,走双向认证组网授权
    • 认证通过后,产生可信会话密钥,供后续加密传输。
  3. 能力路由与虚拟化层(Device Virtualization Service, DVS)

    • 把远端实体设备映射成本地虚拟节点(文件挂载点、虚拟输入设备、虚拟摄像头等);
    • 提供统一的能力接口(读写、枚举、订阅事件等)。
  4. 传输与调度(Session/Trans on DSoftBus)

    • 建立按类型区分的会话BYTES(控制指令)、STREAM(媒体流)、FILE(大文件);
    • 动态选择链路(Wi-Fi P2P/以太/BT 等),必要时无感切换多路聚合
  5. 权限与沙箱

    • 最小权限:谁申请、申请什么能力、用在哪个进程/页面;
    • 细粒度审计:访问路径/设备节点/时长/带宽等均可被记账审计。

直观比喻:你只管“从统一入口上车”,系统替你“换乘”到最合适的物理线路;你手里拿的不是方向盘,而是一张支持多线路的“通票”。


三、分布式文件与输入设备共享(两条最常用的“虚拟化大动脉”)

3.1 分布式文件共享(Distributed File)

目标:让远端设备的目录/文件对你来说像本地盘open/read/write/stat 一条龙。
关键点

  • 统一命名空间:远端设备在本机表现为一个挂载点(示例 /mnt/hmdfs/<deviceId>/)。
  • 一致的权限模型:按应用沙箱与用户授权,控制读写粒度。
  • 断点/一致性:文件写入支持断点续传,对同一路径有冲突解决策略(通常“最后写入胜出”+ 元数据合并,视版本策略而定)。
  • 冷热感知:按最近访问文件尺寸本地缓存预取

3.2 输入设备共享(Keyboard/Mouse/Touch)

目标:把远端键鼠或触控的输入事件序列当成本机事件源。
关键点

  • 事件标准化:键值、修饰键、鼠标按钮、滚轮、触控点、手势统一编码;
  • 坐标系映射:相对/绝对坐标、dpi/scaling、自适应屏幕旋转;
  • 时序与去抖:携带时间戳,按帧/批对齐;抖动过滤与重复按键折叠;
  • 安全:敏感输入(如密码框)下发限制或本地回退策略。

四、典型使用场景(“超级终端”的落地化拆分)

  • 跨屏协同:笔记本键鼠控制平板/手机,窗口跨屏拖拽;
  • 摄影/会议:用手机的“高级摄像头+麦克风”,在平板/PC 的 App 里作为本地摄像头/麦克使用;
  • 文件漫游:拍照在 A,记账在 B,A 的照片目录在 B 的 App 里就是普通文件夹
  • 边缘采集、本地渲染:摄像头在门口(设备 A),AI 推理在客厅盒子(设备 B),结果出现在电视(设备 C);
  • 课堂/会议投屏:平板作主讲,手机与电子白板作输入/显示协同端。

五、开发案例(可直接参考与改造)

说明:以下代码为 示意性 ArkTS/ETS 与 C 风格伪代码,命名与 API 可能随版本差异,请以实际 SDK 为准。思路与结构可直接复用。

5.1 ArkTS/ETS:分布式文件读写(像本地一样用)

// /feature/file/DistributedFileDemo.ets
// 假设存在 fs 模块暴露 readDir/readFile/writeFile 等接口,路径包含远端 deviceId 挂载点。
// 示例路径仅作演示,实际以设备提供的挂载规则为准。
import fs from '@ohos.file.fs'
import deviceManager from '@ohos.distributedHardware.deviceManager'
import softbus from '@ohos.distributedCommunication.softbus'

const APP_ID = 'com.example.device.vfs.demo'

// 找一个具备 fileShare 能力的邻近设备
async function pickFileDevice(): Promise<{ deviceId: string, name: string }> {
  return new Promise((resolve, reject) => {
    deviceManager.createDeviceManager(APP_ID, (err, dm) => {
      if (err) return reject(err)
      dm.on('deviceFound', async (info) => {
        if (info.capabilitySet?.includes?.('fileShare')) {
          // 认证
          dm.authenticateDevice(info.deviceId, { authType: 'PIN' }, (e) => {
            if (e) return reject(e)
            resolve({ deviceId: info.deviceId, name: info.deviceName })
          })
        }
      })
      dm.startDeviceDiscovery({ publishId: 1201, mode: 'ACTIVE', freq: 'HIGH', capability: 'fileShare' })
    })
  })
}

// 列出远端 “Pictures” 目录并拷一份到本地
export async function copyRemotePictures() {
  const { deviceId, name } = await pickFileDevice()
  const remoteDir = `/mnt/hmdfs/${deviceId}/Pictures` // 示例:远端相册目录
  const localDir = `/data/storage/el2/base/ohos/files/remote-${name}`

  await fs.mkdir(localDir, { recursive: true })

  const entries = await fs.readDir(remoteDir)
  for (const ent of entries) {
    if (ent.isFile && ent.name.endsWith('.jpg')) {
      const src = `${remoteDir}/${ent.name}`
      const dst = `${localDir}/${ent.name}`
      const data = await fs.readFile(src)         // 背后走分布式传输
      await fs.writeFile(dst, data)               // 落本地缓存
      console.info(`Copied ${src} -> ${dst}`)
    }
  }
}

实现要点

  • 把“设备发现/认证”与“文件访问”解耦:找对设备 → 直接当本地路径用
  • 读写 API 与本地一致,系统侧负责分块/断点/重传
  • 你只需要做好异常兜底(网络断连、权限不足、文件冲突)。

5.2 ArkTS/ETS:转发远端输入,做“跨屏键鼠”

思路:订阅远端输入事件 → 归一化(含坐标转换/时间戳)→ 注入本机输入通道。实际产品中,系统已完成输入虚拟化,你更多是“开关能力+订阅状态”。此处展示精简范式。

// /feature/input/RemoteInputDemo.ets
import deviceManager from '@ohos.distributedHardware.deviceManager'
import softbus from '@ohos.distributedCommunication.softbus'

// 订阅远端输入事件(BYTES 通道)-> 注入本地
async function bridgeRemotePointer(remoteId: string) {
  const session = await softbus.createSession({
    sessionName: 'RemoteInputSession',
    type: 'BYTES',
    peerDeviceId: remoteId,
    onBytesReceived: (buf: ArrayBuffer) => {
      const evt = JSON.parse(new TextDecoder().decode(new Uint8Array(buf))) as RemotePointerEvent
      const local = normalize(evt)
      injectLocalPointer(local) // 伪函数:把事件投到本地输入通道
    }
  })
  await session.open()
}

type RemotePointerEvent = {
  t: number,     // epoch 毫秒
  x: number, y: number, // 相对坐标 [0,1]
  dx?: number, dy?: number, // 相对位移
  btn?: 'L'|'R'|'M'
}

function normalize(e: RemotePointerEvent) {
  // 按当前窗口/屏幕大小映射像素坐标,并处理时间戳校正
  const { width, height } = getScreenSize() // 伪函数
  return {
    ts: e.t,
    px: Math.round(e.x * width),
    py: Math.round(e.y * height),
    btn: e.btn
  }
}

function injectLocalPointer(e: { ts: number; px: number; py: number; btn?: string }) {
  // 实际注入依赖系统输入服务;此处仅示意调用
  console.info(`[inject] ts=${e.ts} (${e.px},${e.py}) btn=${e.btn ?? '-'}`)
}

实现要点

  • 输入事件要携带时间戳,本地侧做顺序校正/去抖
  • 相对/绝对坐标统一到本地像素坐标
  • 实机产品会在系统侧完成大部分工作,你只需开关能力与状态订阅(如“是否共享键鼠”“是否跨屏吸附”)。

5.3 C 侧(伪)示例:注册“虚拟设备”与事件回调

下面展示系统侧/服务侧的典型流程:创建虚拟输入设备、处理远端事件并注入内核/系统输入队列。(仅为思路展示,接口名以实际头文件为准)

// /service/input/remote_input_bridge.c
#include "softbus_session.h"
#include "virtual_input_dev.h"  // 伪:内核/系统输入设备抽象
#include <stdio.h>
#include <string.h>

static int g_sessionId = -1;
static vinput_dev_t *g_vdev = NULL;

static int OnSessionOpened(int sid, int result) {
    g_sessionId = sid;
    printf("RemoteInput session opened: %d\n", result);
    return 0;
}

static void OnBytesReceived(int sid, const void *data, unsigned int len) {
    if (!g_vdev) return;
    RemotePointerEvent evt;
    if (parse_event(data, len, &evt) == 0) {
        // 坐标映射与去抖(略)
        vinput_report_abs(g_vdev, ABS_X, evt.px);
        vinput_report_abs(g_vdev, ABS_Y, evt.py);
        if (evt.btn == BTN_LEFT) vinput_report_key(g_vdev, BTN_LEFT, 1);
        vinput_sync(g_vdev);
    }
}

int main() {
    ISessionListener lis = {
        .OnSessionOpened = OnSessionOpened,
        .OnBytesReceived = OnBytesReceived,
    };
    CreateSessionServer("com.example.vinput", "RemoteInputSession", &lis);

    g_vdev = vinput_create("virtual-remote-pointer");
    vinput_enable(g_vdev, EV_ABS);
    vinput_enable(g_vdev, EV_KEY);

    // OpenSession(...) 等待对端连接(略)

    // main loop(略)
    return 0;
}

实现要点

  • 虚拟设备节点创建后,对上层应用就像本地输入一样可见;
  • 会话选 BYTES小包低时延
  • 后台要注意安全校验:只允许受信设备注入输入事件。

六、性能与可维护性:让“像本地一样”真的像本地

  1. 会话类型对齐业务

    • 文件:FILE(分片、断点、校验);
    • 输入:BYTES(时延优先,小包);
    • 媒体:STREAM(丢包优先于阻塞,Jitter Buffer)。
  2. 零拷贝与内存池

    • 传输层尽量减少拷贝;应用层复用 Buffer;
    • 大文件用顺序读写 + 预读/回写队列
  3. 链路与 MTU

    • Wi-Fi P2P:高带宽/大 MTU;BLE:低功耗/兜底;
    • 按会话协商 MTU,降低分片与重组压力。
  4. 拥塞/重传策略

    • 文件走“强一致”重传;
    • 流媒体“能丢就丢”但要平滑,防止卡顿雪崩;
    • 输入事件需去重与压缩(只保留最新位置与按钮状态)。
  5. 批处理与节流

    • 批量 dispatch,在 UI 侧做帧间合并
    • 日志与落盘节流,避免 IO 放大。
  6. 可观测性(强烈建议上监控)

    • 指标:时延 p50/p95、吞吐、丢包、公差(Jitter)、重建会话次数、失败原因分布;
    • 文件:平均块大小、重传率、断点恢复次数;
    • 输入:事件队列长度、压缩比、注入失败率。
  7. 故障与回退

    • 断链自动重连与最终一致保证;
    • 安全策略触发(高风险输入场景)→ 本地优先提示用户
    • 写失败回滚/重试窗口可配置。

七、端到端 MVP 自测配方(团队演示可直接照抄)

  • 目标:A 设备拍 50 张大图,B 设备秒级可浏览;A 的触控可在 B 上操控相册 UI。

  • 步骤

    1. A↔B 认证;
    2. B 挂载 A 的 /Pictures,列表首屏用分页 + LazyForEach
    3. 选中大图时后台预取下一张
    4. 开启远端输入共享,把 A 的触控作为 B 的虚拟输入源;
    5. 采集指标:列表首屏时间、单图切换 p95 时延、输入到 UI 响应的 p95。
  • 通过标准

    • 首屏 < 1200ms;
    • 大图切换 p95 < 80ms;
    • 输入到 UI 响应 p95 < 30ms;
    • 断网 3s 内自动恢复浏览,输入共享自动回退到本地。

八、常见“坑位”清单(踩过就会记一辈子)

  • 把所有东西都当文件传:媒体与输入各有最优通道,别一把梭。
  • 未做权限分级:开发期一切顺利,上线被权限拦下;先画清权限矩阵
  • 列表不用分页/LazyForEach:分布式目录一大,首屏就卡。
  • 输入事件不带时间戳:跨屏 jitter 让人抓狂。
  • 不可变数据没做好:UI 层 diff 失真,要么不刷新,要么全量刷新。
  • 监控缺席:线上说“卡”,你只能靠玄学;埋点先行

结语:把复杂留给系统,把约束留给自己

设备虚拟化与能力共享的“爽点”,是让多设备像一台设备一样好用;它的“难点”,是你必须尊重时延/带宽/安全/一致性的铁律。把能力抽象、会话类型、权限边界和观测体系一次搭好,后面的业务场景就像搭积木一样——稳当、顺滑、能扩展。
  最后抛个反问给你:“既然能把邻居的设备变成‘我的外设’,你的应用还要把用户困在一台设备里吗?” 🚀


附:超简“可跑通”的功能片段(便于团队 Demo 拼装)

// A. 列出某远端目录并分页显示(懒加载)
async function listRemoteDirPaged(base: string, page: number, size: number) {
  const all = await fs.readDir(base)
  const files = all.filter(e => e.isFile).sort((a,b) => a.name.localeCompare(b.name))
  const start = page * size
  return files.slice(start, start + size)
}

// B. 预取下一张图(简单版本)
let preloadHandle: Promise<ArrayBuffer> | null = null
async function showImageWithPrefetch(currPath: string, nextPath?: string) {
  const data = await fs.readFile(currPath)
  renderImage(data) // 伪:展示图像
  if (nextPath) preloadHandle = fs.readFile(nextPath)
}

// C. 输入事件压缩(只保留最近 N ms 的最后一个指针位置)
class PointerCompressor {
  private last?: { ts: number; x: number; y: number }
  flushWindowMs = 12
  push(e) { this.last = e }
  popIfDue(now: number) {
    if (this.last && now - this.last.ts >= this.flushWindowMs) {
      const v = this.last; this.last = undefined; return v
    }
  }
}

小抄(放进团队 Wiki 就能用)

  • 设备发现/认证先行,能力目录与命名空间统一管理;
  • 文件/输入/媒体各走其最优通道(FILE / BYTES / STREAM);
  • 分页 + LazyForEach + 预取让分布式目录也能“首屏秒开”;
  • 输入事件必须带时间戳与坐标归一化;
  • 权限矩阵监控指标在立项阶段就定稿;
  • 断点/重连/回退是体验保底线;
  • 不可变数据 + 局部快照稳住 UI 重绘面。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。