引言

在HarmonyOS应用开发中,PDF文档处理是一个常见且重要的功能需求。无论是办公应用中的文档批注、教育应用中的课件标注,还是企业应用中的合同签署,都需要对PDF文档进行编辑操作。华为提供的PdfView组件为开发者提供了强大的PDF预览和编辑能力,但在实际开发中,许多开发者会遇到一个棘手的问题:编辑PDF文件后,重新进入页面时,PdfView组件仍然展示编辑前的效果

本文将深入剖析这一问题的根源,并提供完整的解决方案。通过详细的代码示例和最佳实践,帮助开发者掌握PdfView组件的编辑保存机制,实现PDF文档的实时更新显示。

问题分析

常见场景与痛点

假设我们正在开发一个PDF阅读器应用,用户可以对PDF文档进行以下操作:

  • 添加文本批注和标记

  • 绘制图形和签名

  • 高亮重要内容

  • 添加页面注释

开发者在实现这些功能时,通常会遇到以下问题:

  1. 编辑操作无法持久化:用户在页面上进行的编辑操作只在当前会话中有效,关闭应用后编辑内容丢失。

  2. 重新加载显示旧内容:即使调用了保存方法,重新进入页面时PdfView仍然显示原始文档。

  3. 内存管理问题:频繁编辑和保存可能导致内存泄漏或性能下降。

问题根源

通过分析华为官方文档,问题的核心在于:

  1. 编辑与保存分离PdfView组件主要负责文档的预览和显示,而编辑操作需要通过pdfServicepdfViewManager的控制器来实现。

  2. 文件读写冲突:PDF文档不能同时进行读写操作,直接覆盖原文件可能导致数据损坏。

  3. 缓存机制PdfView可能会缓存已加载的文档内容,导致重新加载时显示旧数据。

解决方案:正确的编辑保存流程

核心思路

要解决编辑后显示旧内容的问题,需要遵循以下流程:

  1. 使用正确的API:通过pdfViewManager.PdfControllersaveDocument方法保存编辑内容。

  2. 避免文件冲突:创建临时文件作为过渡,确保数据完整性。

  3. 强制刷新显示:保存后重新加载文档,确保PdfView显示最新内容。

关键技术点

根据官方文档,saveDocument方法的使用要点:

// saveDocument方法签名
saveDocument(path: string, onProgress?: Callback<number>): Promise<number>
  • path: 文档的沙箱路径

  • onProgress: 保存进度回调函数(可选)

  • 返回值: Promise对象,返回1表示成功,0表示失败

完整实现代码

基础示例:PDF编辑与保存

以下是一个完整的PDF编辑保存示例,展示了如何正确使用saveDocument方法:

import { pdfService, pdfViewManager, PdfView } from '@kit.PDFKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct PdfEditorPage {
  // PdfView控制器
  private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController();
  
  // 应用上下文
  private context = getContext(this) as common.UIAbilityContext;
  
  // 文件路径
  private originalFilePath: string = '';
  private tempFilePath: string = '';
  
  // 编辑状态
  @State isEdited: boolean = false;
  @State saveProgress: number = 0;
  @State loadResult: pdfService.ParseResult = pdfService.ParseResult.PARSE_ERROR_FORMAT;

  aboutToAppear(): void {
    // 初始化文件路径
    this.originalFilePath = this.context.filesDir + '/document.pdf';
    this.tempFilePath = this.context.tempDir + `/temp_${Date.now()}.pdf`;
    
    // 加载PDF文档
    this.loadPdfDocument();
  }

  // 加载PDF文档
  async loadPdfDocument(): Promise<void> {
    try {
      // 检查文件是否存在
      let fileExists = fileIo.accessSync(this.originalFilePath);
      
      if (!fileExists) {
        // 从资源文件复制到沙箱
        await this.copyResourceToSandbox();
      }
      
      // 加载文档
      this.loadResult = await this.controller.loadDocument(this.originalFilePath);
      
      if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
        hilog.info(0x0000, 'PdfEditorPage', 'PDF文档加载成功');
      } else {
        hilog.error(0x0000, 'PdfEditorPage', 'PDF文档加载失败: %{public}d', this.loadResult);
      }
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorPage', '加载PDF失败: %{public}s', JSON.stringify(error));
    }
  }

  // 从资源文件复制到沙箱
  async copyResourceToSandbox(): Promise<void> {
    try {
      // 获取资源文件内容
      let content: Uint8Array = this.context.resourceManager.getRawFileContentSync('rawfile/document.pdf');
      
      // 写入沙箱
      let fd = fileIo.openSync(this.originalFilePath, 
        fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
      fileIo.writeSync(fd, content.buffer);
      fileIo.closeSync(fd);
      
      hilog.info(0x0000, 'PdfEditorPage', '资源文件复制到沙箱成功');
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorPage', '复制资源文件失败: %{public}s', JSON.stringify(error));
    }
  }

  // 添加文本批注
  async addTextAnnotation(): Promise<void> {
    if (this.loadResult !== pdfService.ParseResult.PARSE_SUCCESS) {
      hilog.error(0x0000, 'PdfEditorPage', '文档未加载成功,无法添加批注');
      return;
    }

    try {
      // 获取第一页
      const pageIndex = 0;
      
      // 添加文本批注
      await this.controller.updateMarkupAnnotation(pageIndex, 0, 0xAA000000);
      
      // 标记为已编辑
      this.isEdited = true;
      
      hilog.info(0x0000, 'PdfEditorPage', '文本批注添加成功');
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorPage', '添加批注失败: %{public}s', JSON.stringify(error));
    }
  }

  // 保存文档(核心方法)
  async saveDocument(): Promise<void> {
    if (!this.isEdited) {
      hilog.info(0x0000, 'PdfEditorPage', '文档未编辑,无需保存');
      return;
    }

    try {
      // 创建临时文件作为过渡
      fileIo.copyFileSync(this.originalFilePath, this.tempFilePath);
      
      // 使用临时文件路径保存
      const saveResult = await this.controller.saveDocument(this.tempFilePath, (progress: number) => {
        this.saveProgress = progress;
        hilog.info(0x0000, 'PdfEditorPage', '保存进度: %{public}d%', progress);
      });
      
      if (saveResult === 1) {
        // 保存成功,用临时文件覆盖原文件
        fileIo.copyFileSync(this.tempFilePath, this.originalFilePath);
        
        // 重新加载文档以更新显示
        await this.reloadDocument();
        
        this.isEdited = false;
        this.saveProgress = 0;
        
        hilog.info(0x0000, 'PdfEditorPage', '文档保存并重新加载成功');
      } else {
        hilog.error(0x0000, 'PdfEditorPage', '文档保存失败');
      }
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorPage', '保存过程出错: %{public}s', JSON.stringify(error));
    }
  }

  // 重新加载文档(关键步骤)
  async reloadDocument(): Promise<void> {
    try {
      // 释放当前文档
      this.controller.releaseDocument();
      
      // 重新加载
      this.loadResult = await this.controller.loadDocument(this.originalFilePath);
      
      if (this.loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
        hilog.info(0x0000, 'PdfEditorPage', '文档重新加载成功');
      }
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorPage', '重新加载文档失败: %{public}s', JSON.stringify(error));
    }
  }

  // 另存为功能
  async saveAsDocument(): Promise<void> {
    try {
      const saveAsPath = this.context.filesDir + `/document_${Date.now()}.pdf`;
      
      const saveResult = await this.controller.saveDocument(saveAsPath, (progress: number) => {
        this.saveProgress = progress;
        hilog.info(0x0000, 'PdfEditorPage', '另存为进度: %{public}d%', progress);
      });
      
      if (saveResult === 1) {
        hilog.info(0x0000, 'PdfEditorPage', '文档另存为成功: %{public}s', saveAsPath);
      } else {
        hilog.error(0x0000, 'PdfEditorPage', '文档另存为失败');
      }
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorPage', '另存为过程出错: %{public}s', JSON.stringify(error));
    }
  }

  build() {
    Column({ space: 20 }) {
      // PDF预览区域
      PdfView({
        controller: this.controller,
        pageFit: pdfService.PageFit.FIT_WIDTH
      })
      .width('100%')
      .height('60%')
      .backgroundColor(Color.White)
      .border({ width: 1, color: Color.Gray })
      
      // 编辑操作区域
      Row({ space: 10 }) {
        Button('添加文本批注')
          .width('40%')
          .height(40)
          .fontSize(14)
          .backgroundColor('#007DFF')
          .onClick(() => {
            this.addTextAnnotation();
          })
        
        Button('保存文档')
          .width('40%')
          .height(40)
          .fontSize(14)
          .backgroundColor(this.isEdited ? '#34C759' : '#CCCCCC')
          .enabled(this.isEdited)
          .onClick(() => {
            this.saveDocument();
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding(10)
      
      // 另存为按钮
      Button('另存为')
        .width('80%')
        .height(40)
        .fontSize(14)
        .backgroundColor('#FF9500')
        .onClick(() => {
          this.saveAsDocument();
        })
      
      // 保存进度显示
      if (this.saveProgress > 0) {
        Text(`保存进度: ${this.saveProgress}%`)
          .fontSize(12)
          .fontColor('#666666')
          .margin({ top: 10 })
        
        Progress({ value: this.saveProgress, total: 100 })
          .width('80%')
          .height(4)
          .color('#007DFF')
      }
      
      // 编辑状态提示
      Text(this.isEdited ? '文档已编辑,请保存' : '文档未编辑')
        .fontSize(12)
        .fontColor(this.isEdited ? '#FF3B30' : '#666666')
        .margin({ top: 10 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5')
  }
}

进阶功能实现

1. 拆页与合页功能

// 拆页:将PDF拆分为多个文件
async splitPdfDocument(): Promise<void> {
  try {
    // 获取总页数
    const pageCount = await this.controller.getPageCount();
    
    // 创建新的PDF文档用于保存拆分后的页面
    const pdfDocument = new pdfService.PdfDocument();
    
    // 假设拆分为每5页一个文件
    const pagesPerFile = 5;
    
    for (let i = 0; i < pageCount; i += pagesPerFile) {
      const endPage = Math.min(i + pagesPerFile, pageCount);
      
      // 创建新文档
      const newDocument = new pdfService.PdfDocument();
      
      // 复制页面到新文档
      for (let j = i; j < endPage; j++) {
        const page = await this.controller.getPage(j);
        newDocument.insertPageFromDocument(this.controller, j, 1, newDocument.getPageCount());
      }
      
      // 保存拆分后的文档
      const splitFilePath = `${this.context.filesDir}/split_${i / pagesPerFile + 1}.pdf`;
      const saveResult = newDocument.saveDocument(splitFilePath);
      
      if (saveResult) {
        hilog.info(0x0000, 'PdfEditorPage', '拆分文件保存成功: %{public}s', splitFilePath);
      }
      
      // 释放资源
      newDocument.releaseDocument();
    }
    
    // 释放原始文档
    pdfDocument.releaseDocument();
    
  } catch (error) {
    hilog.error(0x0000, 'PdfEditorPage', '拆页失败: %{public}s', JSON.stringify(error));
  }
}

// 合页:合并多个PDF文件
async mergePdfDocuments(filePaths: string[]): Promise<void> {
  try {
    const mergedDocument = new pdfService.PdfDocument();
    
    for (const filePath of filePaths) {
      const tempDocument = new pdfService.PdfDocument();
      const loadResult = tempDocument.loadDocument(filePath);
      
      if (loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
        const pageCount = tempDocument.getPageCount();
        
        // 将页面插入到合并文档
        mergedDocument.insertPageFromDocument(
          tempDocument, 
          0, 
          pageCount, 
          mergedDocument.getPageCount()
        );
      }
      
      tempDocument.releaseDocument();
    }
    
    // 保存合并后的文档
    const mergedFilePath = `${this.context.filesDir}/merged_${Date.now()}.pdf`;
    const saveResult = mergedDocument.saveDocument(mergedFilePath);
    
    if (saveResult) {
      hilog.info(0x0000, 'PdfEditorPage', '合并文件保存成功: %{public}s', mergedFilePath);
      
      // 重新加载合并后的文档
      this.controller.releaseDocument();
      await this.controller.loadDocument(mergedFilePath);
    }
    
    mergedDocument.releaseDocument();
    
  } catch (error) {
    hilog.error(0x0000, 'PdfEditorPage', '合页失败: %{public}s', JSON.stringify(error));
  }
}

2. 页面旋转功能

// 旋转指定页面
async rotatePage(pageIndex: number, rotation: pdfService.Rotation): Promise<void> {
  try {
    // 获取页面对象
    const page = await this.controller.getPage(pageIndex);
    
    // 设置旋转角度
    page.setRotation(rotation);
    
    // 标记为已编辑
    this.isEdited = true;
    
    hilog.info(0x0000, 'PdfEditorPage', '页面旋转成功: 页面%{public}d, 角度%{public}d', 
      pageIndex, rotation);
      
  } catch (error) {
    hilog.error(0x0000, 'PdfEditorPage', '页面旋转失败: %{public}s', JSON.stringify(error));
  }
}

// 旋转所有页面
async rotateAllPages(rotation: pdfService.Rotation): Promise<void> {
  try {
    const pageCount = await this.controller.getPageCount();
    
    for (let i = 0; i < pageCount; i++) {
      const page = await this.controller.getPage(i);
      page.setRotation(rotation);
    }
    
    this.isEdited = true;
    hilog.info(0x0000, 'PdfEditorPage', '所有页面旋转成功');
    
  } catch (error) {
    hilog.error(0x0000, 'PdfEditorPage', '旋转所有页面失败: %{public}s', JSON.stringify(error));
  }
}

3. 缩放与创建功能

// 设置PDF显示缩放
setPdfZoom(zoomLevel: number): void {
  // 通过PdfView的pageFit属性控制缩放
  this.controller.setPageFit(pdfService.PageFit.FIT_NONE);
  this.controller.setZoom(zoomLevel);
}

// 创建新的PDF文档
async createNewDocument(width: number, height: number): Promise<void> {
  try {
    // 释放当前文档
    this.controller.releaseDocument();
    
    // 创建新的PDF文档
    const newDocument = new pdfService.PdfDocument();
    newDocument.createDocument(width, height);
    
    // 添加默认页面
    const defaultPage = newDocument.getPage(0);
    
    // 添加默认文本
    const fontInfo = new pdfService.FontInfo();
    fontInfo.fontPath = this.getSystemFontPath();
    fontInfo.fontName = 'HarmonyOS Sans';
    
    const textStyle: pdfService.TextStyle = {
      textColor: 0x000000,
      textSize: 24,
      fontInfo: fontInfo
    };
    
    defaultPage.addTextObject('新建PDF文档', 50, 50, textStyle);
    
    // 保存新文档
    const newFilePath = `${this.context.filesDir}/new_document_${Date.now()}.pdf`;
    const saveResult = newDocument.saveDocument(newFilePath);
    
    if (saveResult) {
      // 加载新文档
      await this.controller.loadDocument(newFilePath);
      hilog.info(0x0000, 'PdfEditorPage', '新文档创建成功: %{public}s', newFilePath);
    }
    
    newDocument.releaseDocument();
    
  } catch (error) {
    hilog.error(0x0000, 'PdfEditorPage', '创建新文档失败: %{public}s', JSON.stringify(error));
  }
}

// 获取系统字体路径
private getSystemFontPath(): string {
  // 这里需要根据实际系统字体路径进行调整
  return '/system/fonts/HarmonyOS_Sans.ttf';
}

最佳实践与注意事项

1. 文件路径管理

// 统一的文件路径管理类
class PdfFileManager {
  private context: common.UIAbilityContext;
  
  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }
  
  // 获取原始文件路径
  getOriginalFilePath(filename: string): string {
    return `${this.context.filesDir}/${filename}`;
  }
  
  // 获取临时文件路径
  getTempFilePath(prefix: string = 'temp'): string {
    return `${this.context.tempDir}/${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.pdf`;
  }
  
  // 获取备份文件路径
  getBackupFilePath(filename: string): string {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    return `${this.context.filesDir}/backup/${filename}_${timestamp}.pdf`;
  }
  
  // 清理临时文件
  cleanupTempFiles(maxAge: number = 24 * 60 * 60 * 1000): void {
    try {
      const tempDir = this.context.tempDir;
      const files = fileIo.listDirSync(tempDir);
      
      const now = Date.now();
      for (const file of files) {
        const filePath = `${tempDir}/${file}`;
        const stat = fileIo.statSync(filePath);
        
        if (now - stat.mtime > maxAge) {
          fileIo.unlinkSync(filePath);
          hilog.info(0x0000, 'PdfFileManager', '清理临时文件: %{public}s', filePath);
        }
      }
    } catch (error) {
      hilog.error(0x0000, 'PdfFileManager', '清理临时文件失败: %{public}s', JSON.stringify(error));
    }
  }
}

2. 错误处理与恢复

// 增强的错误处理机制
class PdfEditorWithRecovery {
  private controller: pdfViewManager.PdfController;
  private fileManager: PdfFileManager;
  private backupEnabled: boolean = true;
  
  // 带恢复功能的保存
  async saveWithRecovery(filePath: string): Promise<boolean> {
    const backupPath = this.backupEnabled ? 
      this.fileManager.getBackupFilePath('recovery') : null;
    
    try {
      // 1. 创建备份(如果启用)
      if (backupPath && fileIo.accessSync(filePath)) {
        fileIo.copyFileSync(filePath, backupPath);
      }
      
      // 2. 保存到临时文件
      const tempPath = this.fileManager.getTempFilePath();
      const saveResult = await this.controller.saveDocument(tempPath);
      
      if (saveResult !== 1) {
        throw new Error('保存到临时文件失败');
      }
      
      // 3. 验证临时文件
      if (!this.validatePdfFile(tempPath)) {
        throw new Error('临时文件验证失败');
      }
      
      // 4. 覆盖原文件
      fileIo.copyFileSync(tempPath, filePath);
      
      // 5. 清理临时文件
      fileIo.unlinkSync(tempPath);
      
      hilog.info(0x0000, 'PdfEditorWithRecovery', '文档保存成功');
      return true;
      
    } catch (error) {
      hilog.error(0x0000, 'PdfEditorWithRecovery', '保存失败: %{public}s', error.message);
      
      // 恢复备份
      if (backupPath && fileIo.accessSync(backupPath)) {
        try {
          fileIo.copyFileSync(backupPath, filePath);
          hilog.info(0x0000, 'PdfEditorWithRecovery', '已从备份恢复文件');
        } catch (recoveryError) {
          hilog.error(0x0000, 'PdfEditorWithRecovery', '恢复备份失败: %{public}s', recoveryError.message);
        }
      }
      
      return false;
    }
  }
  
  // 验证PDF文件完整性
  private validatePdfFile(filePath: string): boolean {
    try {
      const tempDoc = new pdfService.PdfDocument();
      const result = tempDoc.loadDocument(filePath);
      tempDoc.releaseDocument();
      
      return result === pdfService.ParseResult.PARSE_SUCCESS;
    } catch {
      return false;
    }
  }
}

3. 性能优化建议

// 性能优化策略
class OptimizedPdfEditor {
  private controller: pdfViewManager.PdfController;
  private cacheEnabled: boolean = true;
  private documentCache: Map<string, Uint8Array> = new Map();
  
  // 带缓存的加载
  async loadWithCache(filePath: string): Promise<pdfService.ParseResult> {
    // 检查缓存
    if (this.cacheEnabled && this.documentCache.has(filePath)) {
      hilog.info(0x0000, 'OptimizedPdfEditor', '从缓存加载文档');
      // 这里需要实现从缓存加载的逻辑
    }
    
    // 正常加载
    const result = await this.controller.loadDocument(filePath);
    
    // 更新缓存
    if (this.cacheEnabled && result === pdfService.ParseResult.PARSE_SUCCESS) {
      await this.updateCache(filePath);
    }
    
    return result;
  }
  
  // 更新缓存
  private async updateCache(filePath: string): Promise<void> {
    try {
      const content = fileIo.readSync(filePath);
      this.documentCache.set(filePath, content);
      
      // 限制缓存大小
      if (this.documentCache.size > 10) {
        const firstKey = this.documentCache.keys().next().value;
        this.documentCache.delete(firstKey);
      }
    } catch (error) {
      hilog.error(0x0000, 'OptimizedPdfEditor', '更新缓存失败: %{public}s', JSON.stringify(error));
    }
  }
  
  // 批量操作优化
  async batchOperations(operations: Array<() => Promise<void>>): Promise<void> {
    // 开始批量操作
    this.controller.beginBatchOperation();
    
    try {
      for (const operation of operations) {
        await operation();
      }
      
      // 提交批量操作
      this.controller.commitBatchOperation();
      
    } catch (error) {
      // 回滚批量操作
      this.controller.rollbackBatchOperation();
      throw error;
    }
  }
}

常见问题与解决方案

Q1: 保存后重新进入页面仍然显示旧内容

问题原因PdfView组件可能缓存了文档内容,或者保存后没有正确重新加载。

解决方案

// 正确的保存和重新加载流程
async saveAndReload(): Promise<void> {
  // 1. 保存文档
  const saveResult = await this.controller.saveDocument(this.tempFilePath);
  
  if (saveResult === 1) {
    // 2. 释放当前文档
    this.controller.releaseDocument();
    
    // 3. 重新加载文档
    await this.controller.loadDocument(this.filePath);
    
    // 4. 强制刷新UI
    this.forceUpdate();
  }
}

Q2: 大文件保存时应用卡顿

问题原因:保存操作在主线程执行,阻塞了UI更新。

解决方案

// 使用异步保存和进度提示
async saveLargeDocument(): Promise<void> {
  // 显示保存进度
  this.showProgress = true;
  
  // 在后台线程执行保存
  await asyncTask.execute(async () => {
    await this.controller.saveDocument(this.filePath, (progress: number) => {
      // 更新进度(需要在UI线程执行)
      getContext(this).runOnUIThread(() => {
        this.saveProgress = progress;
      });
    });
  });
  
  this.showProgress = false;
}

Q3: 编辑内容在保存后丢失

问题原因:编辑操作没有正确应用到文档对象。

解决方案

// 确保编辑操作被应用
async applyEditAndSave(): Promise<void> {
  // 1. 获取页面对象
  const page = await this.controller.getPage(0);
  
  // 2. 应用编辑操作
  page.addTextObject('编辑内容', 100, 100, textStyle);
  
  // 3. 标记页面为已修改
  page.setModified(true);
  
  // 4. 保存文档
  await this.controller.saveDocument(this.filePath);
}

总结

通过本文的详细讲解,我们深入探讨了HarmonyOS 6中PdfView组件的编辑保存机制。关键要点总结如下:

  1. 正确使用saveDocument方法:编辑PDF后必须调用控制器的saveDocument方法保存更改。

  2. 避免文件读写冲突:通过临时文件过渡,确保数据完整性。

  3. 及时重新加载:保存后需要释放并重新加载文档,确保PdfView显示最新内容。

  4. 完整的编辑功能:除了基本的文本批注,还支持拆页、合页、旋转、缩放等高级功能。

  5. 健壮的错误处理:实现备份恢复机制,防止数据丢失。

  6. 性能优化:通过缓存、批量操作等技术提升用户体验。

在实际开发中,建议开发者:

  • 始终在try-catch块中执行PDF操作

  • 实现自动保存和版本管理功能

  • 为用户提供清晰的保存状态反馈

  • 定期清理临时文件,避免存储空间浪费

掌握这些技术要点,开发者可以构建出功能完善、性能优异、用户体验良好的PDF编辑应用,满足各种业务场景的需求。

Logo

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

更多推荐