【AdMob】AdNetwork パフォーマンス最大化戦略

【AdMob】AdNetwork パフォーマンス最大化戦略

iOS エンジニアの板谷(AkkeyLab)です!

現在は ONE iOS の開発をメインに行なっています。

はじめに

本稿では AdMob を導入した環境で、パフォーマンスを改善していくまでの過程をご紹介いたします。特に、表示する広告ユニット数の増加や、動画広告などのリソースを多く必要とする広告ユニットの追加を検討している方をターゲットに執筆しました。

実際に iOS で実装する際のサンプルコードも提示しながら解説しますので、最後まで読んでいただけますと幸いです。

前提条件

  • AdMob 実装に関する基本的な知識がある(公式ドキュメントをご覧ください)
  • 次の要件のもと、AdMob を利用したい
    • ネイティブアドバンスフォーマットを利用
    • 1つの広告ユニットで複数の広告を一度に取得したい
  • サンプルコードの動作環境
    • Firebase/AdMob (7.10.0)
    • Xcode 13.1 (13A1030d)
    • macOS Big Sur 11.3

課題整理

弊社サービス ONE では、ユーザに有益な情報を提供する手段の一つとして AdMob を利用しています。また、ユーザ体験を損なってしまっては本末転倒なので、ネイティブアドバンスフォーマットを利用したカスタムデザインで表示されるように実装しています。

  • 広告ユニット数:7
  • 広告ユニットあたりの広告取得個数:1〜12程度
  • 初回ロード対象の広告ユニット数:4

本稿執筆時点では上記のような規模で運用しています。パフォーマンス改善を行う前は、この規模にアプリが耐えられず、画面がカクつくなどとてもリリースできる状態ではありませんでした。

  1. 同時に複数広告ユニットのロード処理が走っていた
  2. 広告ユニット1つで複数の広告をロードする場合も1つずつロードしていた
  3. フォアグラウンド移行時など、広告が表示されたことがなくても一律でリフレッシュしていた

アプリのパフォーマンスを悪化させていた主な原因は上記3つです。1に関しては、扱う広告ユニット数の増加に比例して発生するようになった問題ですが、2と3に関しては AdMob 導入当初から発生していたものになります。

ですから、これから紹介するパフォーマンス改善は、導入広告ユニット数が少ないタイミングでも実践する価値のあるものだと思っています。

1. Queue 制御

まずは課題1つ目の同時ロード問題を解消させましょう。非常に単純ではありますが、ロード処理をリクエストされたら一度キューに格納し、そこから順にロード処理を直列で実行する方法はどうでしょうか?

結論を先に述べると、オススメしません。

起動初期は他の処理が多く走ることもあり、意外と全広告取得完了まで時間がかかります。そもそも、広告取得タイミングのコントロールは各画面の責務であること、AdMob 内の処理も非同期で最適化されている(はず)であることを考慮すると、ここまで厳密にしなくても良さそうです。

以上を考慮した最終的な実装方法を、以下のフローチャートとともにご紹介します。

image

弊社では、複雑なロジック部分をフローチャートなどでまとめるようにしており、新メンバーや別分野のエンジニアでも継続的なメンテナンスが可能になるように心がけています

最近は Mermaid への移行を進めており、面倒になりがちなグラフの作成とメンテナンスにもテコ入れをしています。

まず、「広告取得できるまで次のロードを行わない」という直列制御は排除しています。取得タイミングの制御は画面側の責務としてリファクタリングを実施しました。

次に、ロードリクエストが来たら優先度の高いものから順に実行するようにしました。このロジックの効果が最も発揮されるのがエラー時です。エラーのリトライは高頻度で何度も実行されてしまう可能性を秘めており、これが優先度の低い広告ユニットで発生することを抑制できます。

extension NativeAdPlacement {
    var loadPriority: LoadPriority {
        switch self {
				// アプリ起動時に表示されるデフォルトタブ
        case .home:
            return .high
				// ホーム画面の次に多くユーザが訪れていることがログから分かってる
        case .wallet:
            return .normal
				// 最も取得する広告量が多い画面で、負荷が大きくなりやすいので遅延
        case .news:
            return .low
        }
    }
}

ONE では上記のように優先度を定義しています。

まず、前準備として、広告表示領域ごとに広告ユニットを作成しておきます。そして、広告ユニットを enum で定義し、それを拡張して優先度を定義します。上記コメントにあるように、アプリの仕様やビジネス的観点を加味して優先度を決めると良いかもしれません。

2. ロードオプション

次に、課題2つ目、広告ユニット1つで複数の広告を取得する際の高負荷問題を解消させましょう。これから紹介するものは、公式ドキュメントを熟読されている方にとっては簡単に思いつく解決策かもしれません。

let loaderOptions = GADMultipleAdsAdLoaderOptions()
loaderOptions.numberOfAds = min(count, 5)
...
let loader = GADAdLoader(
    adUnitID: ...,
		rootViewController: ...,
		adTypes: ...,
		options: [..., loaderOptions]
)

GADAdLoader 関数の options 引数に指定できる GADMultipleAdsAdLoaderOptions を利用することで取得したい広告の数を指定することができます。取得できた広告は必ず重複しないことが保証されているため、非常に便利です。

本稿執筆段階では、最大で5件まで指定可能で、メディエーション利用時は利用できないなどの制約があります。ここは SDK 側の仕様に左右される箇所ですので、公式ドキュメントも同時にご覧ください。

一見、このオプションを指定すれば課題2つ目が解決するようにも感じますが、最大で5件という制約が非常に厄介です。つまり、5件取得できることが保証されておらず、5件取れなかった時を考慮しなければなりません。

struct Request {
		let loader: GADAdLoader
		var count: Int
    var isLoadDisabled = false
}
...
private var requests: [NativeAdPlacement: Request] = [:] // Queue

そこで、ONE では広告の取得リクエストをキューに積む際、 count という取得したい広告数もセットで保持するようにしています。

すると、広告取得完了時に、まだ取得できていない個数が計算できるようになり、それを元に再度ローダーを作成できるようになります。これにより、取得したい広告個数に達するまで施行してくれるようになります。

3. リフレッシュの最適化

最後に、まだ表示すらされていない広告であっても、トリガーが踏まれると毎回広告を再取得していた問題を解決させましょう。では、逆にリフレッシュをしなければならない状態とはどんなときでしょうか。

  • 広告取得から1時間以上が経過(必須)
  • 1回以上画面に表示された広告(任意)

大きく上記の2つが考えられるでしょう。

まず、公式ドキュメント にも記載があるように、広告を取得してから1時間以上が経過したものを利用することは推奨されていないため、リフレッシュは必須と言えます。ユーザに正しい情報を届けるためにも、これは必ず守るようにしましょう。

次に、1回以上表示された広告です。これはユーザが何かしらアクションを起こした広告かもしれませんし、興味のなかった広告かもしれません。ですから、新しいものに置き換えてあげるべきだと私は判断しました。

struct NativeAd {
		let ad: GADUnifiedNativeAd
		let createdAt = Date()
		var isShown = false

		mutating func show() {
			isShown = true
		}
}
...
private var adsRelay: BehaviorRelay<[NativeAdPlacement: [NativeAd]]> = .init(value: [:])

実装はシンプルで、取得できた Ad オブジェクトと取得時間・表示されことがあるかを示すフラグを持った struct を定義して管理しています。

isShown を true にするロジックはサービスの性質に合わせてチューニングしてあげると良いかもしれません。例えば、下記のような条件が考えられます。

  • スクロールを止めて1秒以上
  • 画面内に広告枠の80%が表示
  • 動画広告が最後まで再生された

効果検証

B
Before
A
After

3ステップに分けて紹介してきたポイントを実装に落とし込み、処理の最適化を反映させたところ、改善前に見られていた UIScrollView を継承するパーツのカクつきが軽減されました。

しかし、これで全てが解決できたというわけではありません。実は、取得した広告オブジェクト(GADUnifiedNativeAd など)は表示されていない状態でもリソースをある程度消費します。 AdMob SDK はオープンソースではないので推測になりますが、動画などのコンテンツがメモリ上に展開されて、いつでも再生・表示できるように待機しているからと考えています。

ですから、技術的・ユーザ体験的観点を加味して、導入する広告枠の数と種類を慎重に決める必要があることを忘れてはいけないと私は考えます。

さいごに

最後まで読んでいただき、ありがとうございます。

広告に関するチューニングテクニックはサービスの特性によっても大きく変わってくるため、自身のサービスと照らし合わせながら、参考にしていただけたらなと思います。