在这里插入图片描述

每日一句正能量

你此刻觉得过不去的坎,三年后回头看,只是人生坐标上的一个小点。
用未来的时间视角,稀释当下的痛苦浓度。眼前的巨峰,放在生命长卷中,终将变成一个注脚。

一、引言:当文件流转突破设备边界

在移动办公与多屏协同日益普及的今天,"文件传输"早已不是简单的蓝牙发送或社交软件转发。想象一下这样的场景:你在手机上用文档扫描器完成了一份合同扫描,希望立即在平板上进行批注签名;或者在智慧屏上浏览家庭相册时,想将某张精修照片一键发送到手机发朋友圈——这些场景对文件传输提出了更高的要求:跨设备、低延迟、大容量、高安全

HarmonyOS的分布式文件系统(Distributed File System, DFS)正是为解决这一痛点而生。它并非传统意义上的"网盘同步"或"点对点传输",而是基于分布式软总线构建的设备间文件透明访问层。开发者只需将文件放入应用的distributedFilesDir,系统即可自动完成跨设备同步,目标端通过标准fs API即可像读取本地文件一样访问远端文件,无需关心底层网络协议、设备发现或传输细节。

本文将基于HarmonyOS 5+(API 12+)的分布式文件系统接口,从底层架构到工程代码,完整解析跨设备文件传输的全链路实现,并深入探讨大文件分片传输、断点续传、冲突处理与权限控制等生产级议题。


二、技术原理:分布式文件系统架构解析

2.1 整体架构

分布式文件系统的核心设计哲学是"文件放入即同步,远端读取如本地"。其整体架构可抽象为五层模型:

在这里插入图片描述

应用层负责文件选择、业务逻辑与UI展示;文件IO层通过@kit.CoreFileKit提供的fs模块完成标准文件操作;分布式目录层是DFS的核心——distributedFilesDir作为应用专属的分布式共享沙箱,文件放入该目录即触发系统自动同步;安全层采用"同应用+同账号+设备授权"三重校验机制;传输层则深度融合星闪(NearLink)技术,实现峰值160MB/s的高速传输。

与传统跨设备传输方案相比,DFS的最大优势在于透明性:开发者无需编写设备发现代码、无需处理Socket连接、无需关心Wi-Fi/蓝牙/星闪的协议切换。系统底层自动选择最优传输通道,并在网络波动时无缝切换,应用层完全无感知。

2.2 核心运作机制

DFS的运作机制可拆解为以下关键环节:

  1. 分布式组网:设备间通过同一华为账号完成认证,系统基于分布式软总线自动维护可信设备列表。设备发现、能力协商、安全隧道建立均由系统底层完成。

  2. 文件放入分布式目录:源端应用通过fs.copy()fs.write()将文件写入context.distributedFilesDir。该目录在物理上位于本地存储,但逻辑上属于分布式命名空间。

  3. 自动同步触发:文件写入分布式目录后,系统底层自动检测文件变更,通过星闪/Wi-Fi P2P/蓝牙BLE等通道将文件同步至同账号下的其他设备。

  4. 远端透明访问:目标端设备上,同一应用的distributedFilesDir会自动出现源端同步过来的文件。目标端应用通过标准fs.open()fs.read()即可读取,API调用方式与本地文件完全一致。

  5. 连接释放:文件传输完成后,源端应主动调用fs.disconnectDfs()释放分布式文件系统连接,避免资源占用。


三、完整开发流程:从设备发现到文件传输

理解DFS的完整开发流程是避免"文件传输失败"、"远端文件读取为空"等问题的关键。以下时序图展示了从用户触发到文件接收的全流程:

在这里插入图片描述

3.1 前置条件与权限配置

跨设备文件传输依赖三项前置条件:设备登录同一华为账号、开启蓝牙和Wi-Fi功能、应用已安装且版本兼容。同时,应用必须在module.json5中声明分布式数据同步权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "$string:dist_data_sync_reason"
      }
    ]
  }
}

3.2 动态权限申请

DISTRIBUTED_DATASYNC属于危险权限,必须在运行时动态申请:

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[DistributedFileTransfer]';
const DOMAIN = 0xFF00;

export class PermissionManager {
  private static readonly PERMISSION: Permissions = 'ohos.permission.DISTRIBUTED_DATASYNC';

  static async requestDistributedSyncPermission(context: Context): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    try {
      const result = await atManager.requestPermissionsFromUser(context, [this.PERMISSION]);
      hilog.info(DOMAIN, TAG, `Permission request result: ${JSON.stringify(result)}`);

      // 校验所有权限是否均被授予
      const allGranted = result.authResults.every((result: number) => result === 0);
      if (!allGranted) {
        hilog.warn(DOMAIN, TAG, 'User denied distributed data sync permission');
        return false;
      }
      return true;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Permission request failed: ${(err as BusinessError).message}`);
      return false;
    }
  }
}

3.3 设备发现与分布式文件系统建链

获取权限后,源端应用需发现周边可用设备并建立DFS连接:

import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

export interface DeviceInfo {
  networkId: string;
  deviceName: string;
  deviceType: string;
}

export class DistributedFileClient {
  private dmInstance: distributedDeviceManager.DeviceManager | null = null;
  private connectedDevices: Set<string> = new Set();

  async initialize(bundleName: string): Promise<void> {
    this.dmInstance = distributedDeviceManager.createDeviceManager(bundleName);
    hilog.info(DOMAIN, TAG, 'DeviceManager initialized');
  }

  // 获取可用设备列表
  getAvailableDevices(): DeviceInfo[] {
    if (!this.dmInstance) {
      hilog.error(DOMAIN, TAG, 'DeviceManager not initialized');
      return [];
    }

    try {
      const deviceList = this.dmInstance.getAvailableDeviceListSync();
      return deviceList.map(device => ({
        networkId: device.networkId,
        deviceName: device.deviceName,
        deviceType: device.deviceType
      }));
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Failed to get device list: ${(err as BusinessError).message}`);
      return [];
    }
  }

  // 建立分布式文件系统连接
  async connectToDevice(networkId: string): Promise<boolean> {
    if (this.connectedDevices.has(networkId)) {
      hilog.info(DOMAIN, TAG, `Already connected to ${networkId}`);
      return true;
    }

    const listeners: fs.DfsListeners = {
      onStatus: (deviceNetworkId: string, status: number): void => {
        hilog.info(DOMAIN, TAG, `DFS status changed: device=${deviceNetworkId}, status=${status}`);
        // status: 0=DISCONNECTED, 1=CONNECTING, 2=CONNECTED
        if (status === 2) {
          this.connectedDevices.add(deviceNetworkId);
        } else if (status === 0) {
          this.connectedDevices.delete(deviceNetworkId);
        }
      }
    };

    try {
      await fs.connectDfs(networkId, listeners);
      hilog.info(DOMAIN, TAG, `DFS connection established: ${networkId}`);
      return true;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `DFS connection failed: ${(err as BusinessError).message}`);
      return false;
    }
  }

  // 断开分布式文件系统连接
  async disconnectFromDevice(networkId: string): Promise<void> {
    try {
      await fs.disconnectDfs(networkId);
      this.connectedDevices.delete(networkId);
      hilog.info(DOMAIN, TAG, `DFS disconnected: ${networkId}`);
    } catch (err) {
      hilog.error(DOMAIN, TAG, `DFS disconnect failed: ${(err as BusinessError).message}`);
    }
  }
}

关键设计要点

  • 连接状态监听connectDfs()listeners回调必须注册,用于感知连接建立、断开等状态变化。status=2表示连接成功,此时方可进行文件传输。
  • 目标端授权:首次连接时,目标端设备会弹出授权弹窗(“是否允许XXX应用访问文件?”),用户点击"允许"后连接才真正建立。若用户拒绝,连接将失败,应用需引导用户前往设置手动开启。
  • 连接复用:同一networkId的连接可复用,避免重复建链带来的性能开销。

3.4 文件传输实战:从本地到分布式目录

连接建立后,源端将文件拷贝至分布式目录,系统自动触发同步:

import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

export class FileTransferEngine {
  private context: common.UIAbilityContext;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  // 将本地文件传输至目标设备
  async transferFile(localFilePath: string, fileName: string): Promise<boolean> {
    try {
      // 1. 校验本地文件存在性
      const fileStat = await fs.stat(localFilePath);
      if (!fileStat) {
        hilog.error(DOMAIN, TAG, `Source file not found: ${localFilePath}`);
        return false;
      }

      hilog.info(DOMAIN, TAG, `File size: ${fileStat.size} bytes, preparing for transfer`);

      // 2. 构建分布式目录目标路径
      const distributedDir = this.context.distributedFilesDir;
      const destPath = `${distributedDir}/${fileName}`;

      // 3. 将文件从本地沙箱拷贝至分布式目录
      const srcUri = fileUri.getUriFromPath(localFilePath);
      const destUri = fileUri.getUriFromPath(destPath);

      await fs.copy(srcUri, destUri);
      hilog.info(DOMAIN, TAG, `File copied to distributed directory: ${destPath}`);

      // 4. 可选:设置文件安全等级标签(控制跨设备访问范围)
      await this.setSecurityLabel(destPath, 's0');

      return true;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `File transfer failed: ${(err as BusinessError).message}`);
      return false;
    }
  }

  // 批量传输多个文件
  async batchTransfer(files: Array<{ path: string; name: string }>): Promise<{
    success: string[];
    failed: string[];
  }> {
    const result = { success: [] as string[], failed: [] as string[] };

    for (const file of files) {
      const ok = await this.transferFile(file.path, file.name);
      if (ok) {
        result.success.push(file.name);
      } else {
        result.failed.push(file.name);
      }
    }

    hilog.info(DOMAIN, TAG, `Batch transfer complete: ${result.success.length} success, ${result.failed.length} failed`);
    return result;
  }

  // 设置文件安全等级标签
  private async setSecurityLabel(filePath: string, level: string): Promise<void> {
    try {
      const securityLabel = await import('@ohos.file.securityLabel');
      await securityLabel.setSecurityLabel(filePath, level);
      hilog.info(DOMAIN, TAG, `Security label set: ${level} for ${filePath}`);
    } catch (err) {
      hilog.warn(DOMAIN, TAG, `Failed to set security label: ${(err as BusinessError).message}`);
    }
  }
}

3.5 目标端文件接收与读取

目标端应用无需额外建链,只需在distributedFilesDir中读取同步过来的文件:

import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';

export class FileReceiver {
  private context: common.UIAbilityContext;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  // 读取远端同步过来的文件内容
  async readRemoteFile(fileName: string): Promise<string | null> {
    const filePath = `${this.context.distributedFilesDir}/${fileName}`;

    try {
      // 1. 打开文件(标准fs API,与本地文件完全一致)
      const file = await fs.open(filePath, fs.OpenMode.READ_ONLY);

      // 2. 读取文件内容
      const fileStat = await fs.stat(filePath);
      const arrayBuffer = new ArrayBuffer(fileStat.size);
      const readResult = await fs.read(file.fd, arrayBuffer);

      // 3. 转换为字符串
      const buf = buffer.from(arrayBuffer, 0, readResult.bytesRead);
      const content = buf.toString();

      // 4. 关闭文件描述符
      await fs.close(file);

      hilog.info(DOMAIN, TAG, `Remote file read success: ${fileName}, size=${readResult.bytesRead}`);
      return content;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Read remote file failed: ${(err as BusinessError).message}`);
      return null;
    }
  }

  // 将远端文件拷贝到本地沙箱(持久化存储)
  async persistRemoteFile(fileName: string, localName?: string): Promise<string | null> {
    const remotePath = `${this.context.distributedFilesDir}/${fileName}`;
    const localPath = `${this.context.filesDir}/${localName ?? fileName}`;

    try {
      const srcUri = fileUri.getUriFromPath(remotePath);
      const destUri = fileUri.getUriFromPath(localPath);
      await fs.copy(srcUri, destUri);

      hilog.info(DOMAIN, TAG, `Remote file persisted to local: ${localPath}`);
      return localPath;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Persist remote file failed: ${(err as BusinessError).message}`);
      return null;
    }
  }

  // 扫描分布式目录中的所有文件(含冲突重命名文件)
  async scanDistributedFiles(): Promise<Array<{ name: string; size: number; isConflict: boolean }>> {
    const files: Array<{ name: string; size: number; isConflict: boolean }> = [];

    try {
      const distributedDir = this.context.distributedFilesDir;
      const dir = await fs.opendir(distributedDir);

      let entry: fs.Dirent | null = null;
      while ((entry = await dir.read()) !== null) {
        const isConflict = entry.name.includes('_conflict_dev');
        const stat = await fs.stat(`${distributedDir}/${entry.name}`);
        files.push({
          name: entry.name,
          size: stat.size,
          isConflict
        });
      }

      await dir.close();
      hilog.info(DOMAIN, TAG, `Scanned ${files.length} files in distributed directory`);
      return files;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Scan distributed files failed: ${(err as BusinessError).message}`);
      return [];
    }
  }
}

四、大文件分片传输与断点续传

对于超过100MB的大型文件(如4K视频、设计源文件),直接全量传输存在内存溢出风险,且网络波动时容易前功尽弃。HarmonyOS 6+深度融合星闪技术,支持大文件分片传输与断点续传。

在这里插入图片描述

4.1 分片传输引擎实现

import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { fileIo as fs } from '@kit.CoreFileKit';

interface FileChunk {
  index: number;
  offset: number;
  size: number;
  hash: string;
}

interface TransferState {
  fileId: string;
  fileName: string;
  totalSize: number;
  chunkSize: number;
  totalChunks: number;
  completedChunks: Set<number>;
  fingerprint: string;
}

export class ChunkedFileTransfer {
  private static readonly CHUNK_SIZE = 1024 * 1024; // 1MB分片,适配星闪MTU
  private transferStates: Map<string, TransferState> = new Map();

  // 计算文件SHA-256指纹
  async calculateFingerprint(filePath: string): Promise<string> {
    try {
      const md = cryptoFramework.createMd('SHA256');
      const file = await fs.open(filePath, fs.OpenMode.READ_ONLY);
      const stat = await fs.stat(filePath);

      let offset = 0;
      while (offset < stat.size) {
        const chunkSize = Math.min(this.CHUNK_SIZE, stat.size - offset);
        const buf = new ArrayBuffer(chunkSize);
        await fs.read(file.fd, buf, { offset, length: chunkSize });
        await md.update({ data: new Uint8Array(buf) });
        offset += chunkSize;
      }

      await fs.close(file);
      const result = await md.digest();
      return result.data.toString();
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Fingerprint calculation failed: ${(err as BusinessError).message}`);
      return '';
    }
  }

  // 初始化分片传输任务
  async initTransfer(filePath: string, fileName: string): Promise<TransferState | null> {
    try {
      const stat = await fs.stat(filePath);
      const fingerprint = await this.calculateFingerprint(filePath);
      const totalChunks = Math.ceil(stat.size / this.CHUNK_SIZE);

      const state: TransferState = {
        fileId: `file_${Date.now()}_${Math.random().toString(36).slice(2)}`,
        fileName,
        totalSize: stat.size,
        chunkSize: this.CHUNK_SIZE,
        totalChunks,
        completedChunks: new Set(),
        fingerprint
      };

      this.transferStates.set(state.fileId, state);
      hilog.info(DOMAIN, TAG, `Transfer initialized: ${totalChunks} chunks, fingerprint=${fingerprint.slice(0, 16)}...`);
      return state;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Init transfer failed: ${(err as BusinessError).message}`);
      return null;
    }
  }

  // 分片并行传输(简化版,实际需配合分布式对象协调进度)
  async transferChunk(filePath: string, state: TransferState, chunkIndex: number): Promise<boolean> {
    if (state.completedChunks.has(chunkIndex)) {
      return true;
    }

    try {
      const offset = chunkIndex * this.CHUNK_SIZE;
      const chunkSize = Math.min(this.CHUNK_SIZE, state.totalSize - offset);

      const file = await fs.open(filePath, fs.OpenMode.READ_ONLY);
      const buf = new ArrayBuffer(chunkSize);
      await fs.read(file.fd, buf, { offset, length: chunkSize });
      await fs.close(file);

      // 将分片写入分布式目录(实际工程中需设计分片命名协议)
      const chunkFileName = `${state.fileName}.part${chunkIndex}`;
      const destPath = `${this.context.distributedFilesDir}/${chunkFileName}`;

      const destFile = await fs.open(destPath, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE);
      await fs.write(destFile.fd, buf);
      await fs.close(destFile);

      state.completedChunks.add(chunkIndex);
      hilog.info(DOMAIN, TAG, `Chunk ${chunkIndex}/${state.totalChunks} transferred`);
      return true;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Chunk ${chunkIndex} transfer failed: ${(err as BusinessError).message}`);
      return false;
    }
  }

  // 断点续传:检测已传输分片,仅补传缺失部分
  async resumeTransfer(filePath: string, state: TransferState): Promise<boolean> {
    const missingChunks: number[] = [];
    for (let i = 0; i < state.totalChunks; i++) {
      if (!state.completedChunks.has(i)) {
        missingChunks.push(i);
      }
    }

    if (missingChunks.length === 0) {
      hilog.info(DOMAIN, TAG, 'All chunks already transferred');
      return true;
    }

    hilog.info(DOMAIN, TAG, `Resuming transfer: ${missingChunks.length} chunks missing`);

    for (const chunkIndex of missingChunks) {
      const ok = await this.transferChunk(filePath, state, chunkIndex);
      if (!ok) {
        hilog.error(DOMAIN, TAG, `Resume failed at chunk ${chunkIndex}`);
        return false;
      }
    }

    return true;
  }
}

性能基准:基于星闪技术,4GB高清视频文件跨设备流转耗时约25秒,传输时延低至8毫秒。采用分片传输后,1GB大型文档的传输时间可缩短约40%。


五、冲突处理与权限控制

5.1 三重权限校验机制

DFS采用严格的安全模型,确保文件仅在授权范围内流转:

在这里插入图片描述

层级 校验内容 失败后果
同应用校验 访问设备与被访问设备是否安装同一BundleName应用 拒绝访问,防止恶意应用越权
同账号认证 是否登录同一华为账号 账号隔离,确保数据边界
设备授权 目标端用户主动点击"允许" 弹窗拒绝后连接失败,可引导用户至设置开启

5.2 自动冲突处理策略

当多设备同时操作同一文件时,DFS提供自动冲突解决机制:

export class ConflictResolver {
  // 扫描并处理冲突文件
  async resolveConflicts(distributedDir: string): Promise<{
    resolved: string[];
    manualReview: string[];
  }> {
    const result = { resolved: [] as string[], manualReview: [] as string[] };

    try {
      const dir = await fs.opendir(distributedDir);
      let entry: fs.Dirent | null = null;

      while ((entry = await dir.read()) !== null) {
        // 检测_conflict_dev后缀文件
        if (entry.name.includes('_conflict_dev')) {
          const baseName = entry.name.split('_conflict_dev')[0];
          const localFile = `${distributedDir}/${baseName}`;
          const conflictFile = `${distributedDir}/${entry.name}`;

          // 策略1:若本地文件较新,保留本地,备份冲突版本
          const localStat = await fs.stat(localFile).catch(() => null);
          const conflictStat = await fs.stat(conflictFile).catch(() => null);

          if (localStat && conflictStat) {
            if (localStat.mtime > conflictStat.mtime) {
              // 本地更新,将冲突文件移至备份目录
              const backupPath = `${this.context.filesDir}/conflict_backup/${entry.name}`;
              await fs.mkdir(`${this.context.filesDir}/conflict_backup`);
              await fs.copy(
                fileUri.getUriFromPath(conflictFile),
                fileUri.getUriFromPath(backupPath)
              );
              await fs.unlink(conflictFile);
              result.resolved.push(entry.name);
            } else {
              // 冲突版本更新,需人工审查
              result.manualReview.push(entry.name);
            }
          }
        }
      }

      await dir.close();
      return result;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `Conflict resolution failed: ${(err as BusinessError).message}`);
      return result;
    }
  }
}

冲突处理规则

  • 同名文件冲突:远端同步文件自动重命名为filename_conflict_dev{N}.ext,本地文件保持不变。
  • 多设备并发修改:以最后写入时间戳为准,旧版本自动备份为filename_backup_YYYYMMDD.ext
  • 离线后重新同步:按设备ID大小依次重命名冲突文件,确保不丢失任何一方的修改。

六、工程化最佳实践与避坑指南

6.1 避坑清单

坑点 现象 解决方案
未申请动态权限 connectDfs直接抛出权限异常 必须在运行时调用requestPermissionsFromUser申请DISTRIBUTED_DATASYNC
目标端未授权 连接状态始终为CONNECTING,无法建立 引导用户检查目标端弹窗,或在设置中手动开启分布式服务
文件路径错误 分布式目录文件读取为空 使用context.distributedFilesDir而非硬编码路径,注意区分filesDirdistributedFilesDir
大文件内存溢出 传输超过100MB文件时应用崩溃 采用分片传输,单分片控制在1MB以内
未关闭文件描述符 文件操作后无法删除或重命名 严格遵循open → read/write → close流程,使用try-finally确保关闭
未释放DFS连接 应用后台运行后系统资源紧张 传输完成后主动调用disconnectDfs()
忽略冲突文件 用户看到重复文件,体验混乱 定期扫描_conflict_dev*后缀文件,提供合并/选择UI

6.2 性能优化建议

  1. 传输优先级管控:基于DistributedDataManager传输优先级API,将实时编辑内容设为高优先级,缩略图/历史版本设为低优先级,弱网环境下仅传输高优先级数据。

  2. 文件压缩预处理:图片/视频传输前进行动态压缩。根据目标设备类型(手机/平板/PC)调整压缩质量,减少传输耗时。

  3. 增量同步策略:对于文本类文件,采用Diff算法仅传输变更内容,而非全量覆盖。可结合分布式数据对象实现更细粒度的增量同步。

  4. 传输进度可视化:大文件传输时提供进度条与预估剩余时间,提升用户体验。通过分片传输的completedChunks数量计算实时进度。


七、总结

跨设备文件传输是HarmonyOS"超级终端"生态中不可或缺的基础设施能力。通过本文的深入剖析,我们系统掌握了DFS的完整技术链路:

  • 架构层面:理解了分布式软总线如何屏蔽底层通信差异,星闪技术如何实现160MB/s的峰值传输速率;
  • API层面:掌握了从权限申请、设备发现、connectDfs建链、文件拷贝到disconnectDfs释放的全套接口调用规范;
  • 工程层面:获得了大文件分片传输、断点续传、冲突处理、权限控制等生产级方案;
  • 优化层面:学习了传输优先级管控、文件压缩预处理、增量同步等性能优化策略。

分布式文件系统的本质,是将"设备存储边界"从用户感知中抹除。当开发者能够熟练驾驭这套能力时,所构建的应用便不再受限于单设备的存储容量与传输瓶颈,用户可以在手机、平板、PC乃至智慧屏之间自由流转任意大小的文件——这正是鸿蒙生态"一生万物,万物归一"的技术哲学在存储层的最佳诠释。


转载自:https://blog.csdn.net/u014727709/article/details/164126032
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐