OpenHarmony 统一互联开发指导——富对富文件互传

适用版本:OpenHarmony 4.1 Release 及以上(API 11+),文中接口以 API 12 为准验证环境:两台 RK3568 开发板、OpenHarmony 5.0 Release、DevEco Studio 5.0 Release说明:本文基于应用沙箱的分布式文件目录实现「富设备对富设备」文件互传,属于统一互联底座能力的应用侧落地方式,不依赖厂商私有通道。

1 概述

1.1 场景

统一互联(All Devices, One Connect)面向跨设备、跨厂商的互联互通。富对富文件互传是其中最基础、使用频率最高的场景之一:

  • 手机与平板之间互传照片、文档;

  • 大屏与 PC 之间同步演示材料;

  • 车机与手机之间交换行车记录、离线地图包;

  • 办公场景下多台 OpenHarmony 设备共享工程文件。

典型诉求:

诉求说明
免配网设备已互信时,打开应用即可看到对方,不需要扫码或输密码
大文件支持 GB 级文件,不能一次性读入内存
可恢复断网、切后台、重启后能续传,不从头重来
可校验传输完成后能验证内容一致性,而不是只看「进度 100%」
可清理中间分片自动回收,不长期占用分布式目录

1.2 能力边界

本文方案的能力边界需要先说清楚,避免过度承诺:

  1. 传输通道:使用应用沙箱的分布式文件目录(context.distributedFilesDir),由分布式文件系统在多设备间同步。它不是专门的「点对点 socket 协议」,因此没有连接句柄、没有服务端端口;

  2. 访问前提:两台设备必须已完成互信(同账号或系统设置中确认),并且安装同一 bundleName、同一签名的应用;

  3. 一致性:分布式文件系统保证最终一致,不保证「写完立即可见」。应用层必须用「manifest 提交标记 + 分片大小/哈希校验」判断文件是否真正就绪;

  4. 性能:受 Wi-Fi 带宽与文件系统同步策略影响,单任务串行分片通常比多任务并发更稳定;大文件建议在前台或申请长时任务;

  5. 安全:分布式目录仅在可信设备间同步,不应把敏感明文长期留在该目录,任务完成后要及时清理。

2 技术架构

┌──────────────────────────────────────────────────────┐
│ 业务层:文件选择、进度展示、失败重试、历史记录          │
├──────────────────────────────────────────────────────┤
│ 传输层:任务清单(manifest) + 定长分片 + 完成标记(ack)  │
├──────────────────────────────────────────────────────┤
│ 存储层:distributedFilesDir/transfer/<taskId>/        │
│         ├── chunk_000000.part ...                     │
│         ├── manifest.json   (最后写入,提交标记)      │
│         └── ack.json        (接收端回执)             │
├──────────────────────────────────────────────────────┤
│ 互联底座:分布式文件系统 + 分布式设备管理 + 软总线      │
└──────────────────────────────────────────────────────┘

设计要点:

  • 清单先行、提交后置:先写全部分片,最后写 manifest.json。接收端只在看到 manifest.json 后才开始处理,天然避免读到半包任务;

  • 分片定长:除最后一片外,每片大小固定,便于计算偏移、校验完整性和断点续传;

  • 回执闭环:接收端校验通过后写 ack.json,发送端据此把任务标记为「已完成」,而不是写完就认为成功;

  • 幂等处理:同一 taskId 重复扫描不重复合并,已存在的目标文件按策略覆盖或重命名。

3 前提条件与权限

3.1 权限与配置

module.json5:

{
  "module": {
    "name": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "$string:reason_distributed_datasync",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:reason_background_transfer",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
  • DISTRIBUTED_DATASYNC:user_grant,必须在运行时动态申请;

  • KEEP_BACKGROUND_RUNNING:需要配合 backgroundTaskManager 申请长时任务,仅在「传输中切后台」时使用,任务结束必须释放,否则会被系统回收甚至限制。

3.2 设备与账号

条件检查方式
同一局域网两台设备连接同一 SSID、同一网段
已互信系统「设备互联」中双向可见,应用侧 getTrustedDeviceListSync() 能查到
同一应用两端 bundleName、签名证书一致
系统版本大版本尽量一致,避免能力位差异

设备发现与互信判断的完整实现见配套文章《OpenHarmony 统一互联设备发现不到对端设备问题分析报告》,本文直接复用其中的 UnifiedDeviceService。

4 端到端流程

发送端                         接收端
  │                              │
  ├─1. 发现设备 / 校验可信 ──────┤
  │                              │
  ├─2. 生成 taskId、创建目录 ────┤
  │                              │
  ├─3. 写入 chunk_*.part ────────┼─→ 分布式文件系统同步
  │                              │
  ├─4. 写入 manifest.json ───────┼─→ 5. 轮询发现 manifest
  │                              │
  │                              ├─6. 校验分片完整性/大小
  │                              │
  │                              ├─7. 合并为目标文件 + SHA-256 校验
  │                              │
  │                              ├─8. 写入 ack.json ──┐
  │                              │                     │
  ├─9. 轮询 ack.json ←───────────┼─────────────────────┘
  │                              │
  ├─10. 清理 outbox 分片 ────────┼─→ 11. 清理已处理任务

时序上的两个「等待」必须用轮询实现,而不是回调:

  1. 发送端等 ack.json:分布式文件系统没有跨设备文件变更回调;

  2. 接收端等分片齐全:manifest 可见时,部分分片可能尚未同步完成。

轮询间隔建议 2~3 秒,单任务超时 10 分钟,超时后进入可重试状态。

5 关键实现

5.1 任务模型与路径

import { common } from '@kit.AbilityKit';
​
export const CHUNK_SIZE: number = 4 * 1024 * 1024;
​
export class TransferManifest {
  taskId: string = '';
  fileName: string = '';
  fileSize: number = 0;
  chunkSize: number = CHUNK_SIZE;
  chunkCount: number = 0;
  sha256: string = '';
  createdAt: number = 0;
  senderName: string = '';
}
​
export class TransferAck {
  taskId: string = '';
  success: boolean = false;
  message: string = '';
  finishedAt: number = 0;
}
​
export class TransferPaths {
  static root(context: common.UIAbilityContext): string {
    return `${context.distributedFilesDir}/transfer`;
  }
​
  static taskDir(context: common.UIAbilityContext, taskId: string): string {
    return `${TransferPaths.root(context)}/${taskId}`;
  }
​
  static chunkPath(taskDir: string, index: number): string {
    return `${taskDir}/chunk_${index.toString().padStart(6, '0')}.part`;
  }
​
  static manifestPath(taskDir: string): string {
    return `${taskDir}/manifest.json`;
  }
​
  static ackPath(taskDir: string): string {
    return `${taskDir}/ack.json`;
  }
}

说明:

  • context.distributedFilesDir 是应用沙箱内的分布式文件目录,只有同一应用(同 bundleName + 同签名)在可信设备上可访问;

  • 任务目录按 taskId 隔离,避免多任务互相干扰;

  • 分片文件名使用 6 位定长序号,保证字典序与数值序一致,便于排查。

5.2 发送端:分片写入与提交

import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { util } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
​
const TAG = 'UnifiedTransfer';
​
export class FileSender {
  private context: common.UIAbilityContext;
​
  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }
​
  async send(srcPath: string): Promise<TransferManifest> {
    const stat = await fs.stat(srcPath);
    if (stat.size <= 0) {
      throw new Error('empty file');
    }
​
    const fileName = srcPath.substring(srcPath.lastIndexOf('/') + 1);
    const taskId = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
    const taskDir = TransferPaths.taskDir(this.context, taskId);
    await fs.mkdir(taskDir, true);
​
    const manifest = new TransferManifest();
    manifest.taskId = taskId;
    manifest.fileName = fileName;
    manifest.fileSize = stat.size;
    manifest.chunkSize = CHUNK_SIZE;
    manifest.chunkCount = Math.ceil(stat.size / CHUNK_SIZE);
    manifest.sha256 = (await hash.hash(srcPath, 'sha256')).toLowerCase();
    manifest.createdAt = Date.now();
    manifest.senderName = 'local';
​
    const src = await fs.open(srcPath, fs.OpenMode.READ_ONLY);
    try {
      for (let i = 0; i < manifest.chunkCount; i++) {
        const offset = i * manifest.chunkSize;
        const length = Math.min(manifest.chunkSize, manifest.fileSize - offset);
        const buffer = new ArrayBuffer(length);
        await fs.read(src.fd, buffer, { offset: offset, length: length });
​
        const dst = await fs.open(
          TransferPaths.chunkPath(taskDir, i),
          fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC
        );
        try {
          await fs.write(dst.fd, buffer);
        } finally {
          await fs.close(dst);
        }
      }
    } finally {
      await fs.close(src);
    }
​
    // 最后写入 manifest,作为「任务提交」标记
    await this.writeText(TransferPaths.manifestPath(taskDir), JSON.stringify(manifest));
    hilog.info(0x0000, TAG, 'submit task %{public}s, chunks=%{public}d', taskId, manifest.chunkCount);
    return manifest;
  }
​
  async waitAck(manifest: TransferManifest, timeoutMs: number = 10 * 60 * 1000): Promise<boolean> {
    const taskDir = TransferPaths.taskDir(this.context, manifest.taskId);
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
      try {
        const text = await this.readText(TransferPaths.ackPath(taskDir));
        const ack = JSON.parse(text) as TransferAck;
        if (ack.taskId === manifest.taskId) {
          return ack.success;
        }
      } catch (err) {
        // ack 尚未同步过来,继续等待
      }
      await this.sleep(2000);
    }
    return false;
  }
​
  private async writeText(path: string, text: string): Promise<void> {
    const file = await fs.open(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
    try {
      await fs.write(file.fd, text);
    } finally {
      await fs.close(file);
    }
  }
​
  private async readText(path: string): Promise<string> {
    const stat = await fs.stat(path);
    const buffer = new ArrayBuffer(stat.size);
    const file = await fs.open(path, fs.OpenMode.READ_ONLY);
    try {
      await fs.read(file.fd, buffer, { offset: 0, length: stat.size });
    } finally {
      await fs.close(file);
    }
    return util.TextDecoder.create('utf-8').decodeToString(new Uint8Array(buffer));
  }
​
  private sleep(ms: number): Promise<void> {
    return new Promise<void>((resolve: () => void) => {
      setTimeout(resolve, ms);
    });
  }
}

实现要点:

  1. 只读源文件一次:fs.open 后按偏移读取,避免反复打开;

  2. 逐片写入,不并发:并发写多个分片会加剧分布式文件系统同步压力,实际测试中串行更稳定;

  3. TRUNC 不可省:重试写同一分片时,若不加 TRUNC,旧内容尾部会残留,导致大小校验失败;

  4. 错误要向上抛:send 失败时由业务层展示重试入口,不要静默吞掉 BusinessError;

  5. waitAck 用轮询:分布式目录没有文件变更监听,轮询是必要成本。

5.3 接收端:扫描、校验、合并

import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
​
export class FileReceiver {
  private context: common.UIAbilityContext;
  private timer: number = -1;
  private handling: Set<string> = new Set<string>();
​
  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }
​
  start(onTaskDone: (manifest: TransferManifest, path: string) => void): void {
    this.timer = setInterval(() => {
      this.scan(onTaskDone);
    }, 3000);
  }
​
  stop(): void {
    if (this.timer >= 0) {
      clearInterval(this.timer);
      this.timer = -1;
    }
  }
​
  private async scan(onTaskDone: (manifest: TransferManifest, path: string) => void): Promise<void> {
    const root = TransferPaths.root(this.context);
    let taskIds: string[] = [];
    try {
      taskIds = await fs.listFile(root);
    } catch (err) {
      // 首次运行目录尚未创建,属于正常情况
      return;
    }
​
    for (const taskId of taskIds) {
      if (this.handling.has(taskId)) {
        continue;
      }
      const taskDir = `${root}/${taskId}`;
      try {
        const ackStat = await fs.stat(TransferPaths.ackPath(taskDir));
        if (ackStat.size > 0) {
          continue; // 已处理
        }
      } catch (err) {
        // 无 ack,继续处理
      }
​
      try {
        const manifestText = await this.readText(TransferPaths.manifestPath(taskDir));
        const manifest = JSON.parse(manifestText) as TransferManifest;
        this.handling.add(taskId);
        const target = await this.merge(manifest, taskDir);
        await this.writeAck(taskDir, manifest.taskId, true, 'ok');
        onTaskDone(manifest, target);
        hilog.info(0x0000, 'UnifiedTransfer', 'merged %{public}s -> %{public}s', taskId, target);
      } catch (err) {
        // 分片未同步完成或校验失败:本轮跳过,下一轮重试
        hilog.warn(0x0000, 'UnifiedTransfer', 'task %{public}s not ready', taskId);
      } finally {
        this.handling.delete(taskId);
      }
    }
  }
​
  private async merge(manifest: TransferManifest, taskDir: string): Promise<string> {
    const targetDir = `${this.context.filesDir}/recv`;
    await fs.mkdir(targetDir, true);
    const targetPath = `${targetDir}/${manifest.fileName}`;
​
    const dst = await fs.open(targetPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
    try {
      for (let i = 0; i < manifest.chunkCount; i++) {
        const chunkPath = TransferPaths.chunkPath(taskDir, i);
        const stat = await fs.stat(chunkPath);
        const expected = this.expectedSize(manifest, i);
        if (stat.size !== expected) {
          throw new Error(`chunk ${i} size mismatch: ${stat.size}/${expected}`);
        }
​
        const buffer = new ArrayBuffer(expected);
        const src = await fs.open(chunkPath, fs.OpenMode.READ_ONLY);
        try {
          await fs.read(src.fd, buffer, { offset: 0, length: expected });
        } finally {
          await fs.close(src);
        }
        await fs.write(dst.fd, buffer);
      }
    } finally {
      await fs.close(dst);
    }
​
    const digest = (await hash.hash(targetPath, 'sha256')).toLowerCase();
    if (digest !== manifest.sha256) {
      await fs.unlink(targetPath);
      throw new Error('sha256 mismatch');
    }
    return targetPath;
  }
​
  private expectedSize(manifest: TransferManifest, index: number): number {
    if (index < manifest.chunkCount - 1) {
      return manifest.chunkSize;
    }
    return manifest.fileSize - manifest.chunkSize * (manifest.chunkCount - 1);
  }
​
  private async writeAck(taskDir: string, taskId: string, success: boolean, message: string): Promise<void> {
    const ack = new TransferAck();
    ack.taskId = taskId;
    ack.success = success;
    ack.message = message;
    ack.finishedAt = Date.now();
​
    const file = await fs.open(TransferPaths.ackPath(taskDir), fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
    try {
      await fs.write(file.fd, JSON.stringify(ack));
    } finally {
      await fs.close(file);
    }
  }
​
  private async readText(path: string): Promise<string> {
    const stat = await fs.stat(path);
    const buffer = new ArrayBuffer(stat.size);
    const file = await fs.open(path, fs.OpenMode.READ_ONLY);
    try {
      await fs.read(file.fd, buffer, { offset: 0, length: stat.size });
    } finally {
      await fs.close(file);
    }
    return util.TextDecoder.create('utf-8').decodeToString(new Uint8Array(buffer));
  }
}

关键设计:

  1. ack.json 作为幂等标记:已存在则跳过,避免重复合并且产生同名覆盖;

  2. handling 集合防重入:轮询间隔短于合并耗时时,同一任务不会被重复处理;

  3. 大小校验代替「分片数量够」:分布式文件系统先创建元数据再写内容,只判断文件存在会读到空片;

  4. 哈希不匹配就删除目标文件:避免把损坏文件交付给业务层;

  5. 失败不写 ack:保留重试机会,直到超时由业务层提示用户。

5.4 断点续传

上面的实现已经具备「任务级重试」:分片未同步齐时整轮跳过,下一轮继续。若要支持「进程重启后续传」,需要额外落盘校验状态:

export class TransferState {
  taskId: string = '';
  verifiedChunkCount: number = 0;
  updatedAt: number = 0;
}

实现思路:

  1. 接收端每校验通过一个分片,就把 verifiedChunkCount 写入 state.json;

  2. 重启后从 verifiedChunkCount 继续合并,已校验分片不重复读取;

  3. 发送端如支持中断续传,可按分片索引跳过已成功写入且大小正确的分片;

  4. state.json 与分片在同一任务目录,随任务清理一起删除。

注意:断点续传的前提是源文件未变化。发送端应在 manifest 中记录文件大小、修改时间与哈希,续传前重新计算哈希,不一致则重新建任务。

5.5 进度上报

发送端可在每个分片写完后回调业务层:

export type ProgressCallback = (taskId: string, finished: number, total: number) => void;

界面侧按 1 秒节流刷新,避免每个分片都触发一次完整 UI 重绘:

private lastUpdate: number = 0;
​
private onProgress(taskId: string, finished: number, total: number): void {
  const now = Date.now();
  if (now - this.lastUpdate < 1000 && finished < total) {
    return;
  }
  this.lastUpdate = now;
  this.progress = Math.floor(finished * 100 / total);
}

进度只反映「已写入分片数 / 总分片数」,不代表对端已接收。界面应区分两个状态:

  • 发送中:本端分片写入进度;

  • 已完成:收到 ack.json 且 success === true。

5.6 后台传输与资源释放

大文件传输耗时较长,切后台时容易被冻结:

import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent, WantAgent } from '@kit.AbilityKit';
import { common } from '@kit.AbilityKit';
​
export class TransferKeepAlive {
  private context: common.UIAbilityContext;
  private running: boolean = false;
​
  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }
​
  async start(): Promise<void> {
    if (this.running) {
      return;
    }
    const info: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: this.context.abilityInfo.bundleName,
          abilityName: this.context.abilityInfo.name
        }
      ],
      actionType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent: WantAgent = await wantAgent.getWantAgent(info);
    await backgroundTaskManager.startBackgroundRunning(
      this.context,
      backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
      agent
    );
    this.running = true;
  }
​
  async stop(): Promise<void> {
    if (!this.running) {
      return;
    }
    await backgroundTaskManager.stopBackgroundRunning(this.context);
    this.running = false;
  }
}

注意事项:

  • 长时任务必须在传输开始后申请、传输结束或失败后立即释放;

  • 未释放会被系统判定为常驻后台,影响应用后续调度;

  • KEEP_BACKGROUND_RUNNING 被拒时,降级为前台传输,并提示用户保持页面在前台。

5.7 清理策略

对象时机动作
发送端分片收到 ack 且成功删除整个任务目录
发送端分片超时/失败保留 24 小时,供重试后清理
接收端 ack发送端确认后随任务目录一起删除
接收端 state.json任务完成一并删除
目标文件哈希校验失败立即删除,避免污染业务目录
历史任务应用启动时扫描超过 7 天的任务目录,统一回收

清理必须在传输完成之后执行,不能在写 manifest 后立即删除分片,否则接收端读不到内容。

6 性能与可靠性

6.1 分片大小

分片大小优点缺点建议场景
1 MB首次可见快、失败重传成本低文件数量多,元数据开销大小文件、弱网
4 MB平衡较好——默认推荐
8 MB大文件吞吐高单次内存占用大、重传成本高局域网稳定、GB 级文件

单次读取会分配同等大小的 ArrayBuffer,因此分片不宜超过 8 MB,否则在 4GB 内存设备上容易出现内存压力。

6.2 可靠性设计清单

  • manifest.json 最后写入,作为提交标记;

  • 每个分片校验大小,不只判断存在;

  • 合并后校验整文件 SHA-256,不匹配立即删除;

  • ack.json 幂等,重复扫描不重复合并;

  • 任务级超时(建议 10 分钟)+ 有限重试(建议 3 次);

  • 长时任务及时释放;

  • 任务目录定期清理,避免分布式目录膨胀;

  • 传输前校验目标设备可信,避免写入了却读不到。

6.3 并发策略

  • 同一时刻只跑 1~2 个传输任务,避免多个大文件争抢带宽与文件系统同步队列;

  • 多文件场景建议排队:小文件优先,大文件串行;

  • 需要并行时,按设备维度限流,不要按文件维度无限并发。

7 调试验证

7.1 常用命令

# 查看应用分布式目录(沙箱路径需替换为实际路径)
hdc shell "ls -l /data/app/el2/100/base/com.example.unifiedlink/haps/entry/files"
​
# 观察传输相关日志
hdc shell "hilog | grep -iE 'UnifiedTransfer|distributed_file|hmdfs'"
​
# 确认设备互信状态(系统设置中查看,或应用内打印可信列表)
hdc shell "ps -ef | grep -i distributed"
​
# 抓取传输期间网络流量(确认是否走 Wi-Fi)
hdc shell ifconfig wlan0

7.2 建议用例

用例步骤期望
单文件小文件传 100 KB 文本3 秒内完成,哈希一致
单文件大文件传 1 GB 视频进度单调递增,最终哈希一致
断网恢复传输中关闭 Wi-Fi,10 秒后恢复任务重试后完成,不产生损坏文件
切后台传输中按 Home 键有长时任务时继续,无长时任务时暂停不损坏
重复点击连续点击发送同一文件生成不同 taskId,互不覆盖
对端未安装目标设备无同签名应用发现阶段即不可见,不产生脏数据
磁盘不足目标分区剩余空间小于文件大小明确报错,清理半成品
进程重启传输中杀进程后重启已校验分片可续传,未完成分片重传

7.3 验收标准

  1. 100 次连续传输(混合大小文件)无损坏、无重复落盘;

  2. 断网/切后台/重启三类异常场景均可恢复或明确失败;

  3. 传输完成后 SHA-256 与源文件一致率 100%;

  4. 任务目录在完成后 1 分钟内清理干净;

  5. 传输期间 UI 帧率不低于 50 FPS,无明显卡顿。

8 常见问题

现象可能原因处理方式
发送端写入成功,接收端一直无任务两端非同一 bundleName/签名,或未互信校验签名与可信列表
manifest 可见但合并失败分片尚未同步完成保留重试,校验分片大小
合并后哈希不一致分片被覆盖写入、未加 TRUNC、源文件变化重建任务,写分片加 TRUNC
同名文件被覆盖目标目录直接使用原文件名加时间戳或冲突重命名策略
传输中途卡死分片过大导致内存压力,或后台被冻结降分片大小,申请长时任务
ack 一直等不到接收端校验失败后未写 ack失败也写 ack(success=false) 便于定位
目录越来越大任务完成后未清理启动时扫描并回收历史任务
换设备后完全不可见设备未互信或网络 AP 隔离参考设备发现排查流程

9 最佳实践

  1. 先互通,再传文件:把设备发现、可信校验封装成独立服务,传输层只接收「可信 deviceId」,不要在传输代码里夹杂发现逻辑;

  2. 用清单协议而不是裸文件:manifest + chunk + ack 三件套是可恢复、可校验、可幂等的基础;

  3. 所有判断都要可证伪:分片看大小、整文件看哈希、完成看 ack,不要用「进度 100%」代替成功;

  4. 异常路径优先设计:断网、切后台、进程被杀、磁盘满,这四类场景在富对富传输中出现频率远高于预期;

  5. 及时清理:分布式目录是共享资源,残留分片既占空间,也会干扰下一轮扫描;

  6. 统一日志规范:所有日志带 taskId,便于双端对齐时间线;建议同时打印分片索引与大小;

  7. 版本兼容:manifest 增加 version 字段,后续协议升级时老版本能明确拒绝而不是解析出错。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐