从 WebP 元数据的不完整返回说起

在直播封面转码前检查的工程中,遇到一个看似简单但很容易被误解的现象:调用 readImageMetadataByType() 读取 WebP 元数据时,canvasWidthcanvasHeightdelayTimeunclampedDelayTimeloopCount 这些字段常常返回 undefined——不是 0,不是空字符串,就是 undefined
在这里插入图片描述

最初的反应往往是"某个字段没有值,我补一个默认值吧"。但在实际工程中发现,这样做会带来更复杂的问题:一旦应用侧把 undefined 转换为 0 或其他默认值,后续的业务规则、数据验证和问题追溯都会变得模糊。

技术事实:HarmonyOS 6.1.1 的 ImageSource API 在返回 WebP 元数据时,保留了"字段未提供"的状态,而非使用平台统一的哨兵值(sentinel value)。这个设计决策改变了应用在处理元数据时的选择空间。


元数据未提供的三个业务含义

第一层:区分"0 值"和"不存在"

在直播封面检查中,这个区分变得至关重要。以 delayTime(帧延迟)为例:

// 转码前检查的元数据处理
private async loadSelectedSample(): Promise<void> {
  const metadata = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA]);
  const webp = metadata.webPMetadata;
  
  this.delayTime = this.valueText(webp?.delayTime);
  this.unclampedDelayTime = this.valueText(webp?.unclampedDelayTime);
  this.loopCount = this.valueText(webp?.loopCount);
}

// 将 undefined 保留为"未提供",而非补 0
private valueText(value?: number): string {
  return value === undefined ? '未提供' : `${value}`;
}

对于 WebP 动画:

  • delayTime = undefined 意味着这个 WebP 不包含延迟字段的定义,可能根本不是动画
  • delayTime = 0 意味着 WebP 中明确标注了延迟为 0 毫秒,这是特意的设计,需要特殊处理

如果应用侧把 undefined 补成 0,就把"不是动画"和"是 0 延迟的动画"混为一谈,造成后续转码配置错误。

第二层:数据完整性的可观察状态

应用可以通过元数据返回的"未提供"字段数量,判断这个文件在当前设备上的元数据可读性:

// 计算已返回的字段数量
const provided: number = 
  (this.canvasWidth === '未提供' ? 0 : 1) +
  (this.canvasHeight === '未提供' ? 0 : 1) + 
  (this.delayTime === '未提供' ? 0 : 1) +
  (this.unclampedDelayTime === '未提供' ? 0 : 1) + 
  (this.loopCount === '未提供' ? 0 : 1);

// 根据字段完整度判断是否继续处理
this.fieldSummary = webp === undefined ? 
  'WEBP 元数据对象为空' : 
  `已返回字段 ${provided}/5`;

这样做的好处是:

  • 可以给用户展示"这个文件在这个设备上的元数据完整度"
  • 可以在转码前预警"某些关键字段未提供,可能影响后续处理"
  • 可以记录"哪些设备上的元数据返回不完整"

如果简单补 0,这些状态信息就完全丢失了。

第三层:错误状态和缺失信息的分离

在工程实现中,还需要区分三种情况:

private async loadSelectedSample(): Promise<void> {
  let source: image.ImageSource | undefined = undefined;
  
  try {
    source = await this.createSampleSource(sample.rawFileName);
    const metadata = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA]);
    const webp = metadata.webPMetadata;
    
    // 情况1:WebP 对象本身为空(不是 WebP 格式或读取失败)
    if (webp === undefined) {
      this.fieldSummary = 'WEBP 元数据对象为空';
      return;
    }
    
    // 情况2:WebP 对象存在,但某些字段未提供(格式特性或兼容性原因)
    this.canvasWidth = webp.canvasWidth === undefined ? '未提供' : `${webp.canvasWidth}`;
    this.canvasHeight = webp.canvasHeight === undefined ? '未提供' : `${webp.canvasHeight}`;
    
    // 情况3:字段返回了具体值,包括 0
    this.loopCount = webp.loopCount === undefined ? '未提供' : `${webp.loopCount}`;
    
  } catch (error) {
    // 情况4:读取过程中发生错误
    this.errorMessage = `readImageMetadataByType: ${JSON.stringify(error)}`;
  } finally {
    if (source !== undefined) {
      try {
        await source.release();
      } catch (releaseError) {
        this.errorMessage = `release: ${JSON.stringify(releaseError)}`;
      }
    }
  }
}

保留 undefined 可以清晰地呈现这四种情况:

  • 对象级别的缺失(整个 WebP 元数据为空)
  • 字段级别的缺失(某个字段未提供)
  • 有效的 0 值(字段返回数字 0)
  • 异常状态(读取或释放过程中的错误)

平台能力与应用责任的新边界

平台端下沉的能力

HarmonyOS 6.1.1 的 ImageSource API 在元数据返回时做了一个明确的选择:

  1. 保留原始状态:不在平台侧补齐或转换 undefined
  2. 返回部分元数据:即使某些字段缺失,也返回可用的字段,而不是整体返回失败
  3. 区分空值类型:通过 TypeScript 的 undefined 类型,清晰地表达"这个字段不存在"

这意味着平台已经把"理解元数据的完整性"的能力下沉到了应用层。

应用需要新增的责任

相应地,应用需要承担的新责任包括:

1. 显式处理 undefined

// ❌ 不推荐:假设所有字段都存在
const width = webp.canvasWidth || 0; // 隐含地假设 0 是可接受的默认值
const height = webp.canvasHeight || 0;

// ✅ 推荐:明确区分有值和无值
const width = webp.canvasWidth !== undefined ? webp.canvasWidth : '未提供';
const height = webp.canvasHeight !== undefined ? webp.canvasHeight : '未提供';

2. 根据字段完整度做出业务决策

// 只有同时有 canvasWidth 和 canvasHeight,才能进行尺寸相关的优化
const canPerformSizeOptimization = 
  webp?.canvasWidth !== undefined && 
  webp?.canvasHeight !== undefined;

if (canPerformSizeOptimization) {
  // 执行基于尺寸的转码优化
  this.optimizeByDimensions(webp.canvasWidth!, webp.canvasHeight!);
} else {
  // 降级为基础转码方案
  this.applyBasicTranscoding();
}

3. 在用户界面上透明地呈现元数据的完整性

直播封面检查页面的关键设计:

@Builder
private inspectionResults(): void {
  Column({ space: 10 }) {
    Text('本机读取结果').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1F3730');
    
    // 逐行呈现每个字段,包括那些未提供的
    this.infoRow('Canvas 宽度', this.canvasWidth);
    this.infoRow('Canvas 高度', this.canvasHeight);
    this.infoRow('帧延迟', this.delayTime);
    this.infoRow('未限制帧延迟', this.unclampedDelayTime);
    this.infoRow('循环次数', this.loopCount);
    
    // 整体的字段完整度统计
    this.infoRow('字段状态', this.fieldSummary); // 例如 "已返回字段 3/5"
  }
}

这样做的意义在于:用户可以清楚地看到"这个文件在这个设备上到底返回了多少元数据",而不是看到一堆填充的 0 值产生的假象。


何时保留 undefined,何时可以填充默认值

必须保留 undefined 的场景

1. 转码决策阶段

在确定转码参数之前,应该根据元数据的完整性来选择转码策略。如果提前填充了 0,就无法在转码前做出正确的判断。

2. 元数据验证环节

当应用需要检查"这个 WebP 文件是否完整符合规范"时,必须能够区分"字段为 0"和"字段未提供":

// 检查 WebP 是否是正确的动画格式
private isValidAnimatedWebP(webp: WebPMetadata | undefined): boolean {
  if (webp === undefined) {
    return false; // 无法读取元数据
  }
  
  // 必须同时有尺寸和延迟,才能是有效的动画
  const hasBasicAnimation = 
    webp.canvasWidth !== undefined && 
    webp.canvasHeight !== undefined && 
    webp.delayTime !== undefined;
  
  return hasBasicAnimation;
}

3. 审计和故障排查

在分析"为什么这个转码失败了"时,需要准确记录"哪些字段在读取时就不存在",这对后续的问题诊断至关重要。

// 记录详细的读取结果,用于日志和审计
private async recordMetadataReadResult(
  fileName: string, 
  webp: WebPMetadata | undefined
): Promise<void> {
  const record = {
    fileName: fileName,
    timestamp: new Date().toISOString(),
    metadata: {
      canvasWidth: webp?.canvasWidth,        // 可能是数字或 undefined
      canvasHeight: webp?.canvasHeight,      // 可能是数字或 undefined
      delayTime: webp?.delayTime,            // 可能是数字或 undefined
      loopCount: webp?.loopCount             // 可能是数字或 undefined
    },
    completeness: this.calculateCompleteness(webp)
  };
  // 上传日志
  await this.uploadMetadataRecord(record);
}

可以填充默认值的场景

1. UI 展示层,作为最后一步

当所有的业务决策、验证和记录都已完成,在最终呈现给用户时,可以为了 UI 美观而填充默认值:

@Builder
private displayMetadataField(label: string, value: string | number | undefined): void {
  Row() {
    Text(label).fontSize(11).fontColor('#75867F').layoutWeight(1);
    // 在这里,已经确定了业务逻辑,可以填充默认值用于展示
    Text(
      value === undefined ? '获取中...' : `${value}`,
      TextAlign.End
    ).fontSize(11).fontColor('#29433B').width('58%');
  }
  .width('100%').padding({ top: 10, bottom: 10 });
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2. 有明确业务规则的后处理

当业务规则明确说"如果这个字段不存在,使用标准默认值"时,可以进行转换:

// 只在明确的业务规则下进行转换
private resolveTranscodingQuality(): number {
  const providedQuality = this.metadata.quality; // 可能是 undefined
  
  if (providedQuality !== undefined) {
    return providedQuality;
  }
  
  // 明确的业务规则:未提供质量参数时,使用中等质量
  return 85; // ISO 标准质量等级
}

工程现实中的约束与权衡

设备兼容性的不均匀性

不同 HarmonyOS 版本和不同设备的 ImageSource 实现中,元数据返回的完整度存在差异:

// 转码前检查中观察到的设备差异
private async checkDeviceMetadataSupport(): Promise<void> {
  const testFile = 'test-webp-animated.webp';
  const source = await this.createSampleSource(testFile);
  const metadata = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA]);
  
  const completeness = {
    canvasWidth: metadata.webPMetadata?.canvasWidth !== undefined,
    canvasHeight: metadata.webPMetadata?.canvasHeight !== undefined,
    delayTime: metadata.webPMetadata?.delayTime !== undefined,
    unclampedDelayTime: metadata.webPMetadata?.unclampedDelayTime !== undefined,
    loopCount: metadata.webPMetadata?.loopCount !== undefined
  };
  
  // 不同设备上的实际返回情况:
  // 设备 A:全部 5 个字段都返回
  // 设备 B:只返回了 canvasWidth 和 canvasHeight(动画字段为 undefined)
  // 设备 C:整个 WebP 元数据对象为 undefined
  
  this.logDeviceCapability(completeness);
}

应对策略是:

  • 在应用启动时进行一次能力检测
  • 根据设备的实际元数据支持情况,调整转码前检查的严格度
  • 在日志中记录每个设备的元数据返回完整度

性能考虑

频繁地读取元数据会有一定的 I/O 成本。某些应用可能倾向于"读一次,如果某些字段没有就跳过":

// 性能优化的考虑:批量读取元数据
private async batchReadMetadata(fileNames: string[]): Promise<void> {
  // 如果应用需要处理多个文件,应该:
  // 1. 并发读取,而非串行
  // 2. 缓存读取结果,避免重复读取同一文件
  // 3. 根据字段的可用性,决定是否进行二次读取
  
  const results = await Promise.all(
    fileNames.map(name => this.readMetadataOnce(name))
  );
  
  // 第二轮:仅对某些关键字段缺失的文件进行重试
  const needsRetry = results
    .map((result, index) => ({ result, file: fileNames[index] }))
    .filter(item => this.isMetadataIncomplete(item.result));
  
  if (needsRetry.length > 0) {
    const retryResults = await Promise.all(
      needsRetry.map(item => this.readMetadataWithFallback(item.file))
    );
  }
}

WebP 元数据与其他图像格式的对比

为什么 WebP 元数据会出现 undefined

WebP 格式本身的多样性决定了这一点:

  • 静态 WebP:没有 delayTime 字段
  • 有损 WebP:可能不包含 loopCount 信息
  • 支持有限的设备:某些低端设备的编解码器可能无法提取所有字段

与 PNG 或 JPEG 对比:

  • PNG 的元数据字段相对固定,缺少时通常返回 0 或空值
  • WebP 因为格式灵活性,出现 undefined 的情况更多

标准化的必要性

HarmonyOS 保留 undefined 的设计,实际上是在为未来的标准化铺路:

// 假想的标准化方向:明确列举什么情况返回 undefined
interface WebPMetadataSupport {
  canvasWidth?: 'always' | 'sometimes' | 'never';
  canvasHeight?: 'always' | 'sometimes' | 'never';
  delayTime?: 'always' | 'sometimes' | 'never';
  loopCount?: 'always' | 'sometimes' | 'never';
}

// 应用可以根据设备公告的支持情况,提前做好准备
private async validateDeviceCapability(): Promise<WebPMetadataSupport> {
  // 未来可能的 API:queryMetadataCapability()
  return await getContext(this).resourceManager.queryMetadataCapability(
    image.MetadataType.WEBP_METADATA
  );
}

综合案例:直播封面检查的完整流程

从素材选择到元数据读取,再到结果呈现,整个流程中如何正确处理 undefined:

// 第一步:用户选择封面素材
private openCheckDialog(index: number): void {
  this.selectedQueueIndex = index;
  this.dialogVisible = true;
  // 此时还未读取任何数据
}

// 第二步:用户确认后开始本机检查
private startLocalCheck(): void {
  this.pageMode = 'inspect';
  this.loadSelectedSample();
}

// 第三步:读取元数据,保留 undefined
private async loadSelectedSample(): Promise<void> {
  const sample = this.currentSample();
  this.resetMetadata(); // 所有字段初始化为"未提供"
  
  try {
    const source = await this.createSampleSource(sample.rawFileName);
    const metadata = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA]);
    const webp = metadata.webPMetadata;
    
    // 关键:保留 undefined,而非补 0
    this.canvasWidth = this.valueText(webp?.canvasWidth);
    this.canvasHeight = this.valueText(webp?.canvasHeight);
    this.delayTime = this.valueText(webp?.delayTime);
    this.unclampedDelayTime = this.valueText(webp?.unclampedDelayTime);
    this.loopCount = this.valueText(webp?.loopCount);
    
    // 第四步:计算字段完整度
    const provided = [
      webp?.canvasWidth !== undefined,
      webp?.canvasHeight !== undefined,
      webp?.delayTime !== undefined,
      webp?.unclampedDelayTime !== undefined,
      webp?.loopCount !== undefined
    ].filter(Boolean).length;
    
    this.fieldSummary = webp === undefined ? 
      'WEBP 元数据对象为空' : 
      `已返回字段 ${provided}/5`;
    
    // 第五步:根据完整度做业务决策
    this.makeTranscodingDecision(webp, provided);
    
  } catch (error) {
    this.errorMessage = `读取失败: ${JSON.stringify(error)}`;
  }
}

// 第六步:业务决策
private makeTranscodingDecision(
  webp: WebPMetadata | undefined, 
  fieldCount: number
): void {
  if (webp === undefined) {
    // 无法读取任何元数据,应用保守策略
    this.transcodingStrategy = 'conservative';
    return;
  }
  
  if (fieldCount === 5) {
    // 所有字段都返回了,可以应用精细化策略
    this.transcodingStrategy = 'optimized';
    return;
  }
  
  if (webp.canvasWidth !== undefined && webp.canvasHeight !== undefined) {
    // 至少有尺寸信息,可以进行尺寸优化
    this.transcodingStrategy = 'size-optimized';
    return;
  }
  
  // 其他情况:使用降级策略
  this.transcodingStrategy = 'fallback';
}

// 第七步:呈现结果时,可以为了 UI 美观而补充说明
@Builder
private displayResult(): void {
  Column() {
    // 显示原始读取的元数据值(包括"未提供")
    this.infoRow('Canvas 宽度', this.canvasWidth);
    this.infoRow('Canvas 高度', this.canvasHeight);
    
    // 显示完整度和建议的转码策略
    this.infoRow('字段完整度', this.fieldSummary);
    this.infoRow('推荐策略', this.transcodingStrategy);
    
    // 错误信息(如果有的话)
    if (this.errorMessage !== '暂无错误') {
      Text(this.errorMessage).fontSize(10).fontColor('#B42318');
    }
  }
}

结论:元数据的真实面目

在 HarmonyOS 6.1.1 中看到 undefined 时,不是应用的数据不完整,而是平台诚实地告诉你"在这个设备上,这个文件的这个字段我读不到"

这个设计的深层含义是:

  • 准确性优先:与其给出不确定的默认值,不如保留真实的缺失信息
  • 应用自主性提升:应用可以根据实际的元数据完整度,做出最合适的决策
  • 问题可追溯性加强:当出现错误时,可以清楚地看到"是因为这个字段未提供"
  • 生态的标准化基础:为未来的元数据能力检测和标准化铺路

看到 undefined 不是应该急着补 0,而是应该问"为什么这个字段在这个设备上返回不了",然后根据这个问题来调整应用的行为。

这正是"理解元数据的未提供状态"的真实含义——不是补全缺失,而是理解不完整


FAQ:元数据的 undefined 状态

Q1:如何判断一个字段是"真的不存在"还是"读取失败"?

从工程角度,区分的标准是:

// 情况1:字段为 undefined,但元数据对象存在
if (webp !== undefined && webp.canvasWidth === undefined) {
  // 这表示 WebP 格式本身不包含这个字段
  // 例如静态 WebP 没有 delayTime
  console.log('字段在格式中不存在');
}

// 情况2:整个元数据对象为 undefined
if (webp === undefined) {
  // 可能的原因:1) 不是 WebP 格式  2) 设备不支持  3) 读取过程出错
  console.log('无法读取元数据对象');
}

在直播封面检查中,这两种情况对应的处理是不同的:前者继续处理,后者需要警告用户。


Q2:能否直接对所有 undefined 字段都补 0?

从技术上可以,但业务上有风险:

// ❌ 风险:失去了字段完整度的信息
const width = webp?.canvasWidth ?? 0;
const height = webp?.canvasHeight ?? 0;
// 现在无法区分"真的是 0"还是"字段不存在"

// ✅ 更好的做法:在必要时才转换
if (webp?.canvasWidth === undefined) {
  // 元数据不完整,应用降级策略
  return this.getFallbackDimensions();
} else {
  // 元数据完整,使用实际的尺寸(包括 0)
  return webp.canvasWidth;
}

特别是对于 loopCount:补 0 意味着"循环 0 次",但 undefined 意味着"循环次数信息不存在"。前者是有效的动画配置,后者是元数据不完整的信号。


Q3:为什么不在平台层统一补齐所有字段?

这涉及到设计哲学的差异。平台可以选择:

// 选项A:平台补齐所有字段(某些平台的做法)
const webp = {
  canvasWidth: 1920,
  canvasHeight: 1080,
  delayTime: 100,
  unclampedDelayTime: 100,
  loopCount: 0  // 补齐的默认值
};
// 应用侧看不到元数据不完整的事实

// 选项B:HarmonyOS 6.1.1 的做法 - 保留原始状态
const webp = {
  canvasWidth: 1920,
  canvasHeight: 1080,
  delayTime: undefined,        // 这个文件本身没有
  unclampedDelayTime: undefined,
  loopCount: undefined
};
// 应用侧可以做更精准的决策

HarmonyOS 的选择体现了"诚实地报告真实状态"的原则,这对复杂的媒体处理场景更有帮助。


Q4:在多设备适配中,如何应对不同设备的元数据返回差异?

建议的做法是在应用初始化时进行能力检测:

// 应用启动时检测
private async initializeDeviceCapabilities(): Promise<void> {
  const testFile = 'device-capability-test.webp';
  const source = await this.createSampleSource(testFile);
  const metadata = await source.readImageMetadataByType([image.MetadataType.WEBP_METADATA]);
  
  this.deviceCapability = {
    supportsCanvasWidth: metadata.webPMetadata?.canvasWidth !== undefined,
    supportsDelayTime: metadata.webPMetadata?.delayTime !== undefined,
    supportsLoopCount: metadata.webPMetadata?.loopCount !== undefined
  };
  
  // 基于实际能力调整转码策略
  if (this.deviceCapability.supportsDelayTime) {
    this.enableAnimationOptimization();
  } else {
    this.disableAnimationOptimization();
  }
}

这样比起假设"所有设备都支持所有字段",要更符合现实。


Q5:如何在UI中优雅地呈现"未提供"的字段?

直播封面检查给出了一个参考做法:

@Builder
private infoRow(label: string, value: string): void {
  Row() {
    Text(label).fontSize(11).fontColor('#75867F').layoutWeight(1);
    Text(value)  // "未提供" 或具体数值
      .fontSize(11)
      .fontWeight(FontWeight.Medium)
      .fontColor(value === '未提供' ? '#999999' : '#29433B')  // 不同的颜色区分
      .textAlign(TextAlign.End)
      .width('58%')
      .maxLines(2);
  }
  .width('100%')
  .padding({ top: 10, bottom: 10 })
  .border({ width: { bottom: 1 }, color: '#ECF1EE' });
}

// 使用时:
this.infoRow('Canvas 宽度', this.canvasWidth);  // "1920" 或 "未提供"
this.infoRow('帧延迟', this.delayTime);          // "100" 或 "未提供"

关键是:

  1. 用不同的颜色区分有值和无值
  2. 清晰地标记为"未提供"而非其他默认值
  3. 在必要的位置补充说明"这表示元数据不完整"

Q6:转码配置中,undefined 字段应该怎样处理?

建议的分级策略是:

private configureTranscoding(webp: WebPMetadata | undefined): TranscodingConfig {
  // 分级:严格 → 标准 → 宽松
  
  if (webp === undefined) {
    // 最宽松:几乎无法做任何优化
    return this.getLooseTranscodingConfig();
  }
  
  const hasBasicDimensions = 
    webp.canvasWidth !== undefined && 
    webp.canvasHeight !== undefined;
  
  if (!hasBasicDimensions) {
    // 宽松:缺少尺寸,无法进行尺寸优化
    return this.getLooseTranscodingConfig();
  }
  
  const hasAnimationInfo = 
    webp.delayTime !== undefined && 
    webp.loopCount !== undefined;
  
  if (hasAnimationInfo) {
    // 严格:所有动画关键字段都有
    return this.getStrictTranscodingConfig(webp);
  }
  
  // 标准:有尺寸但缺少动画信息
  return this.getStandardTranscodingConfig(webp);
}

这样可以确保转码配置与实际的元数据完整度相匹配。


Q7:如何在日志中记录 undefined 的元数据?

为了便于问题诊断,建议保留完整的 undefined 信息:

// ✅ 推荐:保留 undefined 信息
const log = {
  timestamp: new Date().toISOString(),
  file: 'cover.webp',
  metadata: {
    canvasWidth: 1920,
    canvasHeight: 1080,
    delayTime: undefined,        // 明确记录为 undefined
    unclampedDelayTime: undefined,
    loopCount: undefined
  },
  completeness: 2,  // 只有 2 个字段
  strategy: 'size-optimized'    // 应用采取的策略
};

// ❌ 不推荐:补 0 后记录
const log = {
  metadata: {
    canvasWidth: 1920,
    canvasHeight: 1080,
    delayTime: 0,          // 补齐后无法区分
    unclampedDelayTime: 0,
    loopCount: 0
  }
};

当后续要分析"为什么这个转码用了降级策略"时,保留 undefined 的记录可以清楚地说明原因。


必要条件|发布前准备清单

发布前逐项确认:

  • SDK/API与构建工具:HarmonyOS 6.1.1 API 24 已安装,entry 模块构建成功。

在这里插入图片描述

  • Kit:本文页面使用的 Kit 已引入,API 与 API 24 匹配。
  • 模块/页面:Stage 配置、页面路由、设备类型和权限声明完整。
  • 设备权限:Camera、AI字幕等能力已完成运行时授权,拒绝状态已处理。

在这里插入图片描述

  • 系统能力/硬件:目标设备具备本文需要的摄像头、麦克风、地图、视觉或文件能力。

在这里插入图片描述
在这里插入图片描述

MapKit文章在系统能力勾选项后增加一项:AppGallery Connect 中项目已选定、应用包名和签名证书与工程一致、MapKit 服务已开通且应用服务凭据/授权配置已完成;服务密钥只保存在安全配置中。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

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

更多推荐