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 的正确用法将是鸿蒙应用商业化变现的核心技能之一。

相关推荐
阳光是sunny10 小时前
Vue 项目怎么做用户行为全链路监控?轻量插件方案详解
前端·面试·架构
EMA16 小时前
Docker虚拟化失败解决方案
架构
李斯维16 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
JouYY19 小时前
聊一下多 Agent 编排架构的应用实践
架构·llm·agent
Sunia19 小时前
《AgentX 专栏》10-生产部署:3台2C4G云服务器把企业级Agent真正跑起来的完整方案
java·架构
TrisighT1 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
ZhengEnCi2 天前
Q01-高并发点赞系统架构设计
架构
笨鸟飞不快2 天前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
这个DBA有点耶3 天前
GROUP BY优化全解:如何写出既不丢数据又飞快的分组查询
数据库·mysql·架构
锋行天下3 天前
我试图优化 Vite 的拆包,结果首屏慢了 10 倍
前端·vue.js·架构