摘要:本文基于一个完整的 HarmonyOS 6 实战项目,深度剖析 Ads Kit(广告服务)的核心用法,手把手带你实现视频插屏广告与图片插屏广告的请求、展示与全生命周期监听,并结合 MVVM 架构、OAID 权限、公共事件机制进行系统性讲解与优化建议。
1、前言
移动应用变现是商业化的核心命题之一,而广告服务是最主流的变现手段。随着 HarmonyOS 6 的正式发布,华为推出了全新升级的 Ads Kit(广告服务),为开发者提供了标准化、高性能的广告接入能力。
与 Android 广告 SDK 不同,HarmonyOS 的 Ads Kit 是系统级 Kit ,无需引入任何第三方依赖包,直接通过 @kit.AdsKit 命名空间即可调用,具备以下核心优势:
- 🔒 隐私合规:配合 OAID(开放匿名设备标识符)机制,在保护用户隐私的前提下实现精准广告投放
- ⚡ 系统级集成:广告服务由系统托管,性能和稳定性更优
- 🎨 多格式支持:支持插屏(图片/视频)、Banner、激励视频等多种广告形式
- 📡 完整生命周期:通过公共事件机制监听广告打开、点击、关闭等全状态
本文以一个完整项目为载体,实现了:
- 视频插屏广告:全屏视频播放,支持跳过与点击跳转
- 图片插屏广告:全屏图片展示,支持点击跳转
项目运行环境: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
}
注意 :
testb4znbuh3n2和teste9ih9j0rc3是华为官方提供的测试广告位 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(广告服务) 的接入全流程:
-
权限与 OAID :正确申请
APP_TRACKING_CONSENT权限,在用户授权后获取 OAID,实现精准广告投放与隐私合规的平衡。 -
广告请求与展示 :通过
AdLoader.loadAd()+advertising.showAd()两步完成插屏广告的请求与展示,掌握视频(testb4znbuh3n2)和图片(teste9ih9j0rc3)两种广告类型的配置差异。 -
生命周期监听 :借助 HarmonyOS 公共事件(
commonEventManager)订阅广告服务系统进程下发的状态通知,完整覆盖onAdOpen、onAdClick、onAdClose、onVideoPlayBegin、onVideoPlayEnd五个生命周期节点。 -
架构设计 :采用 MVVM 模式将 UI 渲染、业务逻辑、事件监听清晰分层,配合
@ComponentV2+@Local新版响应式体系,构建出清晰、可维护的广告接入架构。 -
工程化优化:从并发防抖、内存管理、OAID 缓存、配置化管理等多个维度给出了实际可落地的优化建议。
Ads Kit 作为 HarmonyOS 系统级 Kit,无需任何第三方 SDK,凭借与系统深度集成的优势,提供了优秀的广告加载性能和稳定性。随着 HarmonyOS 生态持续壮大,掌握 Ads Kit 的正确用法将是鸿蒙应用商业化变现的核心技能之一。