HarmonyOS 6 实战:基于 Ads Kit 的插屏广告(视频 + 图片)架构与实现全解析

摘要:本文基于一个完整的 HarmonyOS 6 实战项目,深度剖析 Ads Kit(广告服务)的核心用法,手把手带你实现视频插屏广告与图片插屏广告的请求、展示与全生命周期监听,并结合 MVVM 架构、OAID 权限、公共事件机制进行系统性讲解与优化建议。

1、前言

移动应用变现是商业化的核心命题之一,而广告服务是最主流的变现手段。随着 HarmonyOS 6 的正式发布,华为推出了全新升级的 Ads Kit(广告服务),为开发者提供了标准化、高性能的广告接入能力。

与 Android 广告 SDK 不同,HarmonyOS 的 Ads Kit 是系统级 Kit ,无需引入任何第三方依赖包,直接通过 @kit.AdsKit 命名空间即可调用,具备以下核心优势:

  • 🔒 隐私合规:配合 OAID(开放匿名设备标识符)机制,在保护用户隐私的前提下实现精准广告投放
  • 系统级集成:广告服务由系统托管,性能和稳定性更优
  • 🎨 多格式支持:支持插屏(图片/视频)、Banner、激励视频等多种广告形式
  • 📡 完整生命周期:通过公共事件机制监听广告打开、点击、关闭等全状态

本文以一个完整项目为载体,实现了:

  1. 视频插屏广告:全屏视频播放,支持跳过与点击跳转
  2. 图片插屏广告:全屏图片展示,支持点击跳转

项目运行环境:DevEco Studio 6.x,HarmonyOS 6(targetSdkVersion 6.0.0(20)),兼容 HarmonyOS 5.0.5(API Level 17)。


2、整体架构

2.1 技术架构图

复制代码
┌────────────────────────────────────────────────────────────────┐
│                        View 层                                  │
│                     Index.ets (页面)                            │
│  ┌──────────────────┐  onClick   ┌──────────────────────────┐  │
│  │   广告卡片列表    │ ─────────→ │   AdsViewModel.loadAd()  │  │
│  │  (视频 / 图片)   │            └────────────┬─────────────┘  │
│  └──────────────────┘                         │ onEvent 回调    │
│  ┌──────────────────┐  ←──────────────────────┘                │
│  │   活动日志面板    │                                          │
│  └──────────────────┘                                          │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│                      ViewModel 层                               │
│                   AdsViewModel.ets                              │
│                                                                │
│   AdLoader.loadAd(params, options, listener)                   │
│        ├── onAdLoadFailure  → 上报失败事件                      │
│        └── onAdLoadSuccess  → advertising.showAd(...)          │
│                               InterstitialAdStatusHandler      │
│                               .registerPPSReceiver()           │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│                       事件监听层                                 │
│              InterstitialAdStatusHandler.ets                   │
│                                                                │
│   commonEventManager.subscribe(                                │
│     'com.huawei.hms.pps.action                                 │
│      .PPS_INTERSTITIAL_STATUS_CHANGED',                        │
│     publisher: 'com.huawei.hms.adsservice'                    │
│   )                                                            │
│   → onAdOpen / onAdClick / onAdClose(自动注销)                 │
│   → onVideoPlayBegin / onVideoPlayEnd                          │
└────────────────────────────────────────────────────────────────┘

2.2 项目目录结构

复制代码
chat_adskit/
├── AppScope/
│   ├── app.json5                        # 应用全局配置(包名、版本号)
│   └── resources/base/
│       ├── element/string.json          # 全局字符串资源
│       └── media/ic_launcher.png        # 应用图标
│
└── entry/
    └── src/main/
        ├── module.json5                 # ★ 权限声明、Ability 配置
        └── ets/
            ├── entryability/
            │   └── EntryAbility.ets     # ★ 应用入口,颜色模式设置
            ├── pages/
            │   └── Index.ets           # ★★ 主页面(UI + OAID + 广告触发)
            ├── viewmodel/
            │   └── AdsViewModel.ets    # ★★ 广告请求与展示逻辑(ViewModel)
            └── event/
                └── InterstitialAdStatusHandler.ets  # ★★ 插屏广告状态监听

2.3 架构模式说明

项目严格遵循 MVVM(Model-View-ViewModel) 架构模式:

层级 文件 职责
View Index.ets 页面渲染、用户交互、日志展示
ViewModel AdsViewModel.ets 广告请求、展示调用、事件上报
事件层 InterstitialAdStatusHandler.ets 订阅系统公共事件、监听广告生命周期

状态管理使用 HarmonyOS 6 的新版响应式体系@ComponentV2 + @Local,相比旧版 @Component + @State 具备更细粒度的更新控制和更低的性能开销。


3、效果展示

3.1 视频插屏广告

用户点击「视频广告」卡片后,应用请求插屏视频广告,全屏展示华为广告平台下发的视频素材,支持跳过倒计时和广告点击跳转。

3.2 图片插屏广告

用户点击「图片广告」卡片后,应用请求插屏图片广告,全屏展示静态图片素材,同样支持关闭按钮和点击跳转。

3.3 主页面 UI 特点

  • 顶部导航栏 :渐变蓝色(#0B6E99 → #0EA5E9 → #06B6D4),展示 OAID 获取状态(绿色/红色状态卡片)
  • 广告卡片区:视频广告(蓝色强调色)+ 图片广告(棕色强调色),圆角卡片设计
  • 活动日志面板:按时间倒序记录最近 6 条操作日志,按状态分色标签(成功/失败/提醒/信息)
  • 底部 Tab 栏:首页 ↔ 记录双标签,激活态带动画过渡

4、核心功能详解

4.1 权限申请与 OAID 获取

OAID(Open Anonymous Device Identifier) 是华为提供的设备级匿名标识符,用于广告精准投放,替代了旧有的设备 IMEI 等敏感信息,符合隐私合规要求。

权限配置(module.json5):

json5 复制代码
"requestPermissions": [
  {
    // 敏感权限:用户授权类,需运行时弹窗申请
    "name": "ohos.permission.APP_TRACKING_CONSENT",
    "reason": "$string:app_tracking_permission_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"          // 使用时申请,非启动时强制申请
    }
  },
  {
    // 普通权限:系统自动授予,无需弹窗
    "name": "ohos.permission.INTERNET"
  }
]

OAID 获取流程(Index.ets):

typescript 复制代码
async function requestOAID(context: Context): Promise<string | undefined> {
  let isPermissionGranted: boolean = false;
  try {
    // Step 1: 创建权限管理器,向用户申请追踪同意权限
    const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    const result: PermissionRequestResult =
      await atManager.requestPermissionsFromUser(
        context,
        ['ohos.permission.APP_TRACKING_CONSENT']
      );
    // Step 2: 判断授权结果
    isPermissionGranted =
      result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  } catch (err) {
    hilog.error(0x0000, TAG, `Failed to request permission. Code is ${err.code}, message is ${err.message}`);
  }

  if (isPermissionGranted) {
    try {
      // Step 3: 权限授予后获取 OAID
      const oaid = await identifier.getOAID();
      return oaid;
    } catch (err) {
      hilog.error(0x0000, TAG, `Failed to get OAID. Code is ${err.code}, message is ${err.message}`);
    }
  }
  return undefined;  // 未授权或异常时返回 undefined
}

关键点requestPermissionsFromUser 是系统弹窗申请,必须在 UI 上下文中调用;identifier.getOAID() 是异步接口,需要 await 等待返回。


4.2 广告请求核心实现(AdsViewModel)

AdsViewModel 封装了所有 Ads Kit API 的调用逻辑,通过回调函数 onEvent 向 View 层上报状态,实现了完整的职责分离。

typescript 复制代码
export class AdsViewModel {
  // 广告请求配置(空对象=使用平台默认值)
  adOptions: advertising.AdOptions = {};

  // 广告展示参数(静音播放)
  adDisplayOptions: advertising.AdDisplayOptions = {
    mute: true
  };

  private context: common.UIAbilityContext;

  constructor(uiContext: UIContext) {
    this.context = uiContext.getHostContext() as common.UIAbilityContext;
  }

  async loadAd(
    adRequestParams: advertising.AdRequestParams,
    onEvent?: (message: string) => void
  ): Promise<void> {
    onEvent?.('开始请求广告');
    onEvent?.('等待广告平台回调');

    // 构建广告加载回调监听器
    const adLoadListener: advertising.AdLoadListener = {
      // 广告加载失败回调
      onAdLoadFailure: (errorCode: number, errorMsg: string) => {
        hilog.error(0x0000, TAG, `Failed to load ad. Code is ${errorCode}, message is ${errorMsg}`);
        onEvent?.(`广告加载失败:${errorCode} ${errorMsg}`);
      },

      // 广告加载成功回调
      onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
        hilog.info(0x0000, TAG, 'Succeeded in loading ad');

        if (!ads.length) {
          onEvent?.('广告加载成功,但未返回任何素材');
          return;
        }

        onEvent?.(`广告加载成功,返回 ${ads.length} 条素材`);

        // 判断广告类型:12 = 插屏广告
        if (ads[0]?.adType === 12) {
          // 注册插屏广告生命周期监听
          new InterstitialAdStatusHandler((status: string) => {
            onEvent?.(status);
          }).registerPPSReceiver();

          onEvent?.('已注册插屏状态监听');

          try {
            // 调用系统展示广告接口
            advertising.showAd(ads[0], this.adDisplayOptions, this.context);
            onEvent?.('已调用广告展示接口');
          } catch (e) {
            hilog.error(0x0000, 'testTag', `Failed to show ad. Code is ${e.code}, message is ${e.message}`);
            onEvent?.(`广告展示失败:${e.code} ${e.message}`);
          }
          return;
        }

        onEvent?.('返回素材类型不是插屏广告');
      }
    };

    // 创建广告加载器并发起请求
    const adLoader: advertising.AdLoader = new advertising.AdLoader(this.context);
    try {
      adLoader.loadAd(adRequestParams, this.adOptions, adLoadListener);
    } catch (e) {
      hilog.error(0x0000, 'testTag', `Failed to load ad. Code is ${e.code}, message is ${e.message}`);
      onEvent?.(`请求广告异常:${e.code} ${e.message}`);
    }
  }
}

广告请求参数配置(View 层):

typescript 复制代码
// 视频广告参数
{
  adId: 'testb4znbuh3n2',   // 华为官方测试视频广告位 ID
  adType: 12,                // 12 = 插屏广告
  oaid: oaid                 // 用于精准投放的设备标识(可为 undefined)
}

// 图片广告参数
{
  adId: 'teste9ih9j0rc3',   // 华为官方测试图片广告位 ID
  adType: 12,
  oaid: oaid
}

注意testb4znbuh3n2teste9ih9j0rc3 是华为官方提供的测试广告位 ID,正式上线时需替换为在华为广告联盟申请的真实广告位 ID。


4.3 插屏广告生命周期监听(InterstitialAdStatusHandler)

Ads Kit 通过 HarmonyOS 公共事件(Common Event) 机制向应用下发广告状态变更通知,这是一种进程间通信方式,广告服务进程(com.huawei.hms.adsservice)发出事件,应用进程订阅接收。

typescript 复制代码
export class InterstitialAdStatusHandler {
  // 订阅者对象,用于后续取消订阅
  private subscriber: commonEventManager.CommonEventSubscriber | null = null;
  private readonly onStatusChange?: (status: string) => void;

  constructor(onStatusChange?: (status: string) => void) {
    this.onStatusChange = onStatusChange;
  }

  registerPPSReceiver(): void {
    // 防重复注册:先清理已有订阅
    if (this.subscriber) {
      this.unRegisterPPSReceiver();
    }

    // 配置订阅信息:指定事件名和发布者包名
    const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
      events: ['com.huawei.hms.pps.action.PPS_INTERSTITIAL_STATUS_CHANGED'],
      publisherBundleName: 'com.huawei.hms.adsservice'
    };

    // 创建订阅者
    commonEventManager.createSubscriber(subscribeInfo,
      (err: BusinessError, commonEventSubscriber: commonEventManager.CommonEventSubscriber) => {
        if (err) {
          hilog.error(0x0000, TAG, `Failed to create subscriber. Code is ${err.code}, message is ${err.message}`);
          return;
        }

        this.subscriber = commonEventSubscriber;

        // 开始订阅,处理事件数据
        commonEventManager.subscribe(this.subscriber,
          (err: BusinessError, commonEventData: commonEventManager.CommonEventData) => {
            if (err) {
              hilog.error(0x0000, TAG, `Failed to subscribe. Code is ${err.code}, message is ${err.message}`);
              return;
            }

            // 读取广告状态字段
            const status: string = commonEventData?.parameters?.['interstitial_ad_status'];

            switch (status) {
              case 'onAdOpen':
                this.onStatusChange?.('广告已打开');
                break;
              case 'onAdClick':
                this.onStatusChange?.('广告被点击');
                break;
              case 'onAdClose':
                this.onStatusChange?.('广告已关闭');
                this.unRegisterPPSReceiver(); // 广告关闭后自动注销,防止内存泄漏
                break;
              case 'onVideoPlayBegin':
                this.onStatusChange?.('视频开始播放');
                break;
              case 'onVideoPlayEnd':
                this.onStatusChange?.('视频播放结束');
                break;
            }
          });
      });
  }

  unRegisterPPSReceiver(): void {
    commonEventManager.unsubscribe(this.subscriber, (err: BusinessError) => {
      if (err) {
        hilog.error(0x0000, TAG, `Failed to unsubscribe. Code is ${err.code}, message is ${err.message}`);
      } else {
        hilog.info(0x0000, TAG, 'Succeeded in unsubscribing');
        this.subscriber = null;
      }
    });
  }
}

广告生命周期事件映射:

事件值 含义 是否自动取消订阅
onAdOpen 广告全屏展示打开
onAdClick 用户点击广告
onAdClose 用户关闭广告
onVideoPlayBegin 视频广告开始播放
onVideoPlayEnd 视频广告播放完毕

4.4 主页面 UI 实现(Index.ets)

4.4.1 新版状态管理
typescript 复制代码
@Entry
@ComponentV2           // 使用新版组件装饰器(HarmonyOS 5+ 推荐)
struct Index {
  @Local private adCards: AdCardOption[] = [];        // 广告卡片列表
  @Local private activityLogs: ActivityLogItem[] = []; // 活动日志
  @Local private oaidSummary: string = '未获取';       // OAID 状态
  @Local private currentTab: number = 0;               // 当前 Tab

  private viewModel: AdsViewModel = new AdsViewModel(this.getUIContext());
}

@ComponentV2 + @Local 是 HarmonyOS 6 推荐的响应式方案,@Local 变量的变更会精准触发依赖它的 UI 节点更新,而不是整组件重渲染。

4.4.2 顶部渐变导航栏
typescript 复制代码
Row() {
  Column({ space: 6 }) {
    Text('广告服务应用')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
    Text('插屏广告展示与管理平台')
      .fontSize(13)
      .fontColor('rgba(255, 255, 255, 0.85)')
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)

  // OAID 状态卡片(动态背景色)
  Column() {
    Text('OAID').fontSize(11).fontColor('rgba(255, 255, 255, 0.7)')
    Text(this.oaidSummary)
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
  }
  .backgroundColor(
    this.oaidSummary === '已获取'
      ? 'rgba(16, 185, 129, 0.25)'   // 绿色(成功)
      : 'rgba(239, 68, 68, 0.25)'    // 红色(失败)
  )
  .borderRadius(16)
  .backdropBlur(10)  // 毛玻璃效果
}
.linearGradient({
  angle: 135,
  colors: [['#0B6E99', 0], ['#0EA5E9', 0.5], ['#06B6D4', 1]]
})
4.4.3 广告卡片列表渲染
typescript 复制代码
// 使用 Repeat 高效渲染列表
Repeat<AdCardOption>(this.adCards).each((repeatItem: RepeatItem<AdCardOption>) => {
  Row() {
    // 左侧渐变图标
    Row() {
      Text(repeatItem.item.icon)
        .fontSize(24)
        .fontColor('#FFFFFF')
    }
    .width(56).height(56)
    .borderRadius(18)
    .linearGradient({
      angle: 135,
      colors: [[repeatItem.item.accent, 0], [repeatItem.item.accent + 'DD', 1]]
    })
    .shadow({ radius: 12, color: repeatItem.item.accent + '50', offsetX: 0, offsetY: 6 })

    // 中间文字信息
    Column({ space: 6 }) {
      Text(repeatItem.item.title).fontSize(18).fontWeight(FontWeight.Bold)
      Text(repeatItem.item.subtitle).fontSize(13).fontColor('#64748B')
    }
    .layoutWeight(1)
    .margin({ left: 18, right: 14 })

    // 右侧操作按钮
    Button('打开')
      .backgroundColor(repeatItem.item.accent)
      .onClick(() => {
        this.appendLog(`请求${repeatItem.item.title}`);
        // 委托 ViewModel 处理广告请求
        this.viewModel.loadAd(
          repeatItem.item.adRequestParams,
          (message: string) => {
            this.appendLog(message, resolveLogTone(message));
          }
        );
      })
  }
  .borderRadius(22)
  .shadow({ radius: 12, color: 'rgba(15, 23, 42, 0.08)', offsetX: 0, offsetY: 6 })
})
4.4.4 底部 Tab 栏组件
typescript 复制代码
@Component
struct TabButton {
  @Prop label: string = '';
  @Prop iconRes: Resource = $r('app.media.tab_home');
  @Prop active: boolean = false;

  build() {
    Column({ space: 7 }) {
      Image(this.iconRes)
        .width(24).height(24)
        .fillColor(this.active ? '#0B6E99' : '#94A3B8')

      Text(this.label)
        .fontSize(13)
        .fontWeight(this.active ? FontWeight.Bold : FontWeight.Medium)
        .fontColor(this.active ? '#0B6E99' : '#94A3B8')
    }
    .backgroundColor(this.active ? '#E0F2FE' : 'transparent')
    .border({
      width: this.active ? 1.5 : 0,
      color: this.active ? '#0B6E99' : 'transparent'
    })
    .scale({ x: this.active ? 1.02 : 1, y: this.active ? 1.02 : 1 })
    .animation({
      duration: 250,
      curve: Curve.FastOutSlowIn  // 缓出动画,更自然
    })
  }
}
4.4.5 活动日志系统
typescript 复制代码
// 日志分级类型
type LogTone = 'info' | 'success' | 'warning' | 'error';

// 追加日志(最多保留 6 条,最新在前)
private appendLog(message: string, tone: LogTone = 'info'): void {
  const nextItem: ActivityLogItem = {
    id: `${Date.now()}-${this.activityLogs.length}`,
    time: new Date().toLocaleTimeString(),
    message,
    tone
  };
  this.activityLogs = [nextItem, ...this.activityLogs].slice(0, 6);
}

// 根据消息内容自动推断日志等级
function resolveLogTone(message: string): LogTone {
  if (message.includes('失败') || message.includes('异常')) return 'error';
  if (message.includes('成功') || message.includes('已调用'))  return 'success';
  if (message.includes('未获取到')) return 'warning';
  return 'info';
}

4.5 应用入口配置(EntryAbility)

typescript 复制代码
export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', 'Ability onCreate');
    try {
      // 强制应用跟随系统颜色模式(不单独强制深色/浅色)
      this.context.getApplicationContext()
        .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    } catch (e) {
      hilog.error(0x0000, 'testTag', `Failed to set color mode. Code is ${e.code}, message is ${e.message}`);
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 加载页面入口
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', `Failed to load the content. Cause: ${JSON.stringify(err)}`);
        return;
      }
      hilog.info(0x0000, 'testTag', `Succeeded in loading the content.`);
    });
  }
}

5、完整数据流分析

复制代码
用户操作:点击「打开」按钮
    │
    ▼
Index.ets --- onClick
    ├── appendLog('请求视频广告 / 图片广告')
    └── viewModel.loadAd(adRequestParams, onEvent)
            │
            ▼
    AdsViewModel.loadAd()
        ├── onEvent('开始请求广告')
        ├── onEvent('等待广告平台回调')
        ├── 构建 AdLoadListener
        └── adLoader.loadAd(params, options, listener)
                │
                │ [异步等待广告平台响应]
                │
                ├── onAdLoadFailure(errorCode, errorMsg)
                │       └── onEvent('广告加载失败:...')
                │               └── appendLog(..., 'error')
                │
                └── onAdLoadSuccess(ads[])
                        ├── onEvent('广告加载成功,返回 N 条素材')
                        ├── new InterstitialAdStatusHandler(onEvent)
                        │       └── registerPPSReceiver()
                        │               └── 订阅系统公共事件
                        ├── onEvent('已注册插屏状态监听')
                        ├── advertising.showAd(ads[0], displayOptions, context)
                        └── onEvent('已调用广告展示接口')
                                │
                                ▼
                        [广告全屏展示中]
                                │
                        系统广告服务发出公共事件
                                │
                        InterstitialAdStatusHandler 接收
                                ├── 'onAdOpen'         → '广告已打开'
                                ├── 'onVideoPlayBegin' → '视频开始播放'
                                ├── 'onVideoPlayEnd'   → '视频播放结束'
                                ├── 'onAdClick'        → '广告被点击'
                                └── 'onAdClose'        → '广告已关闭'
                                                         └── 自动注销订阅

6、代码分析与优化建议

6.1 现有实现的亮点

职责分离清晰:View、ViewModel、事件层三层解耦,互不侵入,便于单元测试和功能扩展。

防重复订阅机制registerPPSReceiver 调用前检查 this.subscriber 是否为空,有效防止多次点击导致的重复订阅问题。

自动资源释放 :广告关闭时在 onAdClose 回调中自动调用 unRegisterPPSReceiver(),避免订阅者泄漏。

可选链安全调用onEvent?.('...') 使用可选链操作符,回调不存在时不会抛出异常,代码更健壮。

日志系统完善:内置分级日志(info/success/warning/error)配合颜色标签,便于调试和用户体验。

新版响应式体系 :使用 @ComponentV2 + @Local 替代旧版 @Component + @State,响应更精准高效。


6.2 可优化点及改进方案

优化 1:并发防抖(防止重复请求)

问题:用户快速多次点击「打开」按钮时,会并发发起多个广告请求,导致重复展示或资源浪费。

改进:在 ViewModel 中加入请求锁:

typescript 复制代码
export class AdsViewModel {
  private isLoading: boolean = false;  // 请求状态锁

  async loadAd(adRequestParams: advertising.AdRequestParams, onEvent?: (message: string) => void): Promise<void> {
    // 防重复请求
    if (this.isLoading) {
      onEvent?.('广告请求中,请稍后再试');
      return;
    }
    this.isLoading = true;
    onEvent?.('开始请求广告');

    try {
      // ... 原有逻辑 ...
    } finally {
      this.isLoading = false;
    }
  }
}

同时在 View 层禁用按钮:

typescript 复制代码
Button('打开')
  .enabled(!this.isLoading)
  .opacity(this.isLoading ? 0.5 : 1.0)

优化 2:InterstitialAdStatusHandler 的内存管理

问题 :当前每次调用 loadAd 都会 new InterstitialAdStatusHandler(),如果前一次广告尚未关闭,旧的 handler 的订阅仍然存在(虽然会在 onAdClose 时自动注销,但实例层面无法手动控制)。

改进:在 ViewModel 中持久化 handler 实例:

typescript 复制代码
export class AdsViewModel {
  // 持久化 handler 实例,由 ViewModel 统一管理生命周期
  private statusHandler: InterstitialAdStatusHandler | null = null;

  async loadAd(adRequestParams: advertising.AdRequestParams, onEvent?: (message: string) => void): Promise<void> {
    // ...
    onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
      // ...
      if (ads[0]?.adType === 12) {
        // 先清理旧的 handler
        this.statusHandler?.unRegisterPPSReceiver();

        // 创建新的 handler
        this.statusHandler = new InterstitialAdStatusHandler((status: string) => {
          onEvent?.(status);
          // 广告关闭时清理 ViewModel 层的引用
          if (status === '广告已关闭') {
            this.statusHandler = null;
          }
        });
        this.statusHandler.registerPPSReceiver();
        // ...
      }
    }
  }
}

优化 3:OAID 缓存机制

问题aboutToAppear 中每次页面创建都会重新申请权限和获取 OAID,虽然 requestPermissionsFromUser 在已授权时不会再次弹窗,但仍有不必要的 API 调用开销。

改进 :使用 AppStorageV2 跨组件缓存 OAID:

typescript 复制代码
async aboutToAppear() {
  // 先检查全局缓存
  const cachedOaid = AppStorageV2.connect<string>(String, 'cached_oaid')?.get();

  if (cachedOaid) {
    this.oaidSummary = '已获取';
    this.buildAdCards(cachedOaid);
    return;
  }

  const oaid = await requestOAID(this.context);
  if (oaid) {
    // 写入全局缓存,供后续复用
    AppStorageV2.connect<string>(String, 'cached_oaid')?.set(oaid);
  }
  this.oaidSummary = oaid ? '已获取' : '未获取';
  this.buildAdCards(oaid);
}

优化 4:广告位 ID 配置化

问题:广告位 ID(adId)硬编码在页面代码中,测试环境与生产环境切换繁琐,容易误提交测试 ID 到线上。

改进:通过资源文件分环境管理:

json5 复制代码
// resources/base/element/string.json(测试环境)
{ "name": "ad_id_video", "value": "testb4znbuh3n2" }
{ "name": "ad_id_picture", "value": "teste9ih9j0rc3" }

// resources/release/element/string.json(生产环境覆盖)
{ "name": "ad_id_video", "value": "your_real_video_ad_id" }
{ "name": "ad_id_picture", "value": "your_real_picture_ad_id" }

代码中通过资源引用读取:

typescript 复制代码
adRequestParams: {
  adId: getContext().resourceManager.getStringSync($r('app.string.ad_id_video')),
  adType: 12,
  oaid: oaid
}

优化 5:错误码说明增强

问题 :错误日志仅显示原始错误码(如 广告加载失败:401 xxx),用户和调试人员难以快速定位问题。

改进:增加常见错误码说明:

typescript 复制代码
function getAdErrorDesc(errorCode: number): string {
  const errorMap: Record<number, string> = {
    401:   '参数错误(请检查 adId 是否正确)',
    21001: '广告位无填充(稍后重试)',
    21002: '网络异常(检查网络连接)',
    21006: '广告请求频繁(限流,稍后重试)',
  };
  return errorMap[errorCode] ?? `未知错误(${errorCode})`;
}

// 在监听器中使用
onAdLoadFailure: (errorCode: number, errorMsg: string) => {
  onEvent?.(`广告加载失败:${getAdErrorDesc(errorCode)}`);
}

6.3 生产环境 Checklist

在将应用发布到华为应用市场之前,务必完成以下检查:

检查项 说明
替换测试广告位 ID testb4znbuh3n2 → 正式视频广告位;teste9ih9j0rc3 → 正式图片广告位
华为广告联盟资质审核 在华为广告联盟完成应用审核并创建广告位
AGConnect 配置 确保 agconnect-services.json 已正确配置并放入项目
隐私政策声明 应用内须包含对 OAID 收集用途的用户可见说明
代码签名 使用正式签名证书打包,广告服务在测试签名下功能可能受限
混淆配置 obfuscation-rules.txt 中需保留 AdsKit 相关类不被混淆

7、关键 API 速查

API 所属 Kit 作用
advertising.AdLoader @kit.AdsKit 广告加载器,发起广告请求
advertising.AdLoader.loadAd() @kit.AdsKit 发起广告请求,异步回调
advertising.showAd() @kit.AdsKit 展示插屏广告(系统级调用)
advertising.AdRequestParams @kit.AdsKit 广告请求参数(adId、adType、oaid)
advertising.AdOptions @kit.AdsKit 广告配置选项
advertising.AdDisplayOptions @kit.AdsKit 展示参数(如 mute 静音)
advertising.AdLoadListener @kit.AdsKit 广告加载回调接口
identifier.getOAID() @kit.AdsKit 获取设备 OAID
abilityAccessCtrl.AtManager @kit.AbilityKit 权限管理器
atManager.requestPermissionsFromUser() @kit.AbilityKit 运行时申请权限(弹窗)
commonEventManager.createSubscriber() @kit.BasicServicesKit 创建公共事件订阅者
commonEventManager.subscribe() @kit.BasicServicesKit 订阅公共事件
commonEventManager.unsubscribe() @kit.BasicServicesKit 取消订阅公共事件
hilog.info/error() @kit.PerformanceAnalysisKit 结构化日志输出

8、总结

本文以一个完整的 HarmonyOS 6 实战项目为载体,系统地讲解了 Ads Kit(广告服务) 的接入全流程:

  1. 权限与 OAID :正确申请 APP_TRACKING_CONSENT 权限,在用户授权后获取 OAID,实现精准广告投放与隐私合规的平衡。

  2. 广告请求与展示 :通过 AdLoader.loadAd() + advertising.showAd() 两步完成插屏广告的请求与展示,掌握视频(testb4znbuh3n2)和图片(teste9ih9j0rc3)两种广告类型的配置差异。

  3. 生命周期监听 :借助 HarmonyOS 公共事件(commonEventManager)订阅广告服务系统进程下发的状态通知,完整覆盖 onAdOpenonAdClickonAdCloseonVideoPlayBeginonVideoPlayEnd 五个生命周期节点。

  4. 架构设计 :采用 MVVM 模式将 UI 渲染、业务逻辑、事件监听清晰分层,配合 @ComponentV2 + @Local 新版响应式体系,构建出清晰、可维护的广告接入架构。

  5. 工程化优化:从并发防抖、内存管理、OAID 缓存、配置化管理等多个维度给出了实际可落地的优化建议。

Ads Kit 作为 HarmonyOS 系统级 Kit,无需任何第三方 SDK,凭借与系统深度集成的优势,提供了优秀的广告加载性能和稳定性。随着 HarmonyOS 生态持续壮大,掌握 Ads Kit 的正确用法将是鸿蒙应用商业化变现的核心技能之一。

相关推荐
大师兄66686 小时前
HarmonyOS 服务卡片开发之JS 卡片开发
javascript·华为·harmonyos·harmonyos6·formkit
逆境不可逃6 小时前
Hello-Agents 第二部分-第六章:框架开发实践
java·人工智能·分布式·学习·架构·rabbitmq
小婷资料库6 小时前
新高考日语历年真题、听力音频mp3及答案解析(1998-2025年)
音视频·高考
jushi89996 小时前
抖音APP抖音助手增强版 内置逗音小手 支持无水印下载/音频提取/去广告等功能
android·智能手机·音视频
程序猿追6 小时前
HarmonyOS 6.0 NEXT:基于 Map Kit 实现一款“手绘路线”骑行导航应用
华为·harmonyos
Ailrid7 小时前
设计模式——创建型设计模式:阅读笔记与个人思考
架构·设计
网管NO.17 小时前
视频核心技术 07:音视频同步与延迟优化 —— 为什么直播会卡顿 / 不同步?怎么解决?
音视频
用户65868180338407 小时前
业务系统集成 OpenClaw 多 Agent 方案:从架构到落地的完整指南
架构
小码哥0687 小时前
一套可复用的打车系统模板,微服务版网约车系统|类似滴滴的打车平台
微服务·云原生·架构·滴滴·打车