どのユニットがどこで出ているのか分からなくなる
累計5,000万ダウンロードの壁紙アプリを長く運用していると、広告ユニットは少しずつ増えていきます。最初はインタースティシャルが一つだけだったのに、A/Bのために二つ目を足し、報酬型を入れ、アプリ起動広告(App Open)を試し……と重ねるうちに、ある日「このユニットIDはどの画面で出ているんだったか」を即答できなくなりました。収益に直結する部分なのに把握が曖昧というのは、運用として危うい状態です。
そこで、機能追加の前に一度きちんと棚卸しをすることにしました。やったことは派手ではありませんが、同じように広告まわりが膨らんできた個人開発者の役に立つと思うので、手順を残しておきます。私は2014年から個人開発を続けていますが、こういう地味な整理こそ後から効いてきます。
まずは grep で全ユニットを洗い出す
AdMobの広告ユニットIDはコード上に文字列で埋まっているので、定義箇所はすぐ見つかります。Androidなら次の一行で十分です。
grep -rn "ca-app-pub" app/src/main/java app/src/main/res
これで、定数として定義しているファイル(私の場合は Globals.java と AdManager.java)と、レイアウトXMLに残っている記述が一気に出ます。出力を眺めて、ユニットIDの末尾数桁をキーに、それぞれが「どこで定義され」「どこから呼ばれているか」を対応づけていきます。
この作業で一番の収穫は、もう使っていない旧バナーユニットの残骸がXMLに残っていたのを見つけられたことでした。コード上は誰も参照していないのに、レイアウトには AdView の定義だけが生き残っていた。こういう「幽霊ユニット」は、棚卸しをしないと永遠に気づきません。放置すると、無効なインプレッションやレイアウト崩れの遠因にもなる落とし穴です。
棚卸し表にまとめる
洗い出した結果は、頭の中ではなく表にして残します。私は次の項目で整理しました。
- ユニット名(AdMob管理画面の名前)
- ユニットIDの末尾数桁
- 種別(インタースティシャル / 報酬型 / App Open / バナー)
- 使用画面(どのActivityやFragmentで出るか)
- 頻度制御(どの設定値で出方を決めているか)
頻度制御の項目が重要です。私のアプリでは、インタースティシャルの表示頻度を action_inters_frequency(何回の操作ごとに出すか)と action_inters_max(1セッションの上限)でサーバー側から制御しています。報酬型は reward_period reward_launch_count reward_action_count の組み合わせで、起動回数と操作回数の両方を見て出すかどうかを決めています。表にすると、こうした制御がユニットごとにバラバラだったことが一目で分かりました。サーバー側で値を持っているおかげで、アプリを再リリースせずに頻度を調整でき、eCPMやリテンションの変化を見ながら微調整できるのは、長く運用するうえで大きな利点です。
App Open広告は「起動のたび」に出してはいけない
棚卸しの中で一番手を入れたかったのが、後から足したApp Open広告です。実装を確認すると、コールドスタートのたびに無条件で表示するようになっていました。これは体験を確実に損ねます。アプリを開くたびに全画面広告が挟まれば、起動が目的のユーザーほどストレスを感じます。AdMobのポリシー的にも、ロードが間に合わなかったときに広告を待たせず素通りさせる設計が求められます。
そこで、次の三つの条件を満たしたときだけ表示する方針にしました。
- 初回起動(インストール直後の最初の起動)ではスキップする
- 前回表示から一定時間(私は4時間に設定)が経過している
- 広告のロードが完了している。間に合っていなければ広告を待たずに画面を見せる
クールダウンの判定はシンプルに、最後に表示した時刻を保存しておいて差分で見ます。
class AppOpenManager(private val prefs: SharedPreferences) {
private val cooldownMillis = 4 * 60 * 60 * 1000L // 4時間
fun canShow(): Boolean {
val isFirstLaunch = prefs.getBoolean("is_first_launch", true)
if (isFirstLaunch) {
prefs.edit().putBoolean("is_first_launch", false).apply()
return false // 初回はスキップ
}
val last = prefs.getLong("app_open_last_shown", 0L)
return System.currentTimeMillis() - last >= cooldownMillis
}
fun markShown() {
prefs.edit().putLong("app_open_last_shown", System.currentTimeMillis()).apply()
}
}
表示側では、canShow() が偽ならそもそも広告を読みにいかず、真でもロードが完了していなければ待たずに通します。
fun showIfAvailable(activity: Activity, onDone: () -> Unit) {
if (!manager.canShow() || appOpenAd == null) {
onDone() // 広告を出さずにそのまま画面へ
return
}
appOpenAd?.fullScreenContentCallback = object : FullScreenContentCallback() {
override fun onAdDismissedFullScreenContent() {
manager.markShown()
appOpenAd = null
onDone()
}
}
appOpenAd?.show(activity)
}
onDone() を必ず通るようにしておくのがコツです。広告を出すかどうかに関わらず、最終的にユーザーが目的の画面へ進めることを保証します。これを忘れると、広告のロード失敗で画面遷移が止まる、という本番で最悪のバグになります。私は4時間という値を、起動頻度の高い壁紙アプリで「1日に出ても2〜3回まで」に収まるよう逆算して決めました。ここは自分のアプリの起動間隔を見て調整することをお勧めします。
Before / After で見た出し方の違い
整理前は「コールドスタート → 無条件で全画面広告」でした。整理後は「コールドスタート → 初回判定 → クールダウン判定 → ロード済み判定 → 通過したときだけ表示」になります。広告の表示回数そのものは減りますが、1回あたりが嫌われにくくなるので、長い目で見れば eCPM を保ちながら継続率を守れる、というのが私の実感です。実際、表示回数を絞っても eCPM はほぼ横ばいで、むしろ早期離脱が減ったぶん1ユーザーあたりの生涯広告表示はわずかに増えました。広告は出した回数ではなく、嫌われずに見てもらえた回数で効いてきます。
広告とレビュー依頼を重ねない
もう一つ、棚卸しで気づいたのが、広告表示とアプリ内レビュー依頼(review_action_count で制御)のタイミングがぶつかり得ることでした。全画面広告を閉じた直後にレビューのダイアログが出ると、ユーザーには「広告のあとに催促された」という連続した割り込みに感じられます。レビュー評価にも悪く出やすい。なので、レビュー依頼は広告を表示したアクションとは別の操作で出すよう、カウンタの参照を分けて回避しました。
広告は収益の柱ですが、出しすぎれば継続率を削り、結局は長期の収益を細らせます。私はいつも「この一回の広告で、明日も開いてもらえる確率をどれだけ下げるか」を天秤にかけて頻度を決めています。
棚卸しの手順を整理すると
最後に、私が実際に進めた順番をまとめておきます。
grep "ca-app-pub" でユニットIDの定義箇所をすべて洗い出す
- 末尾数桁をキーに、定義 → 呼び出し画面 → 頻度制御を対応づけて表にする
- 参照されていない幽霊ユニットを特定し、コードとXMLから除去する
- 体験を損ねている出し方(無条件App Open等)を洗い出す
- クールダウン・初回スキップ・ロード未完時の素通りを実装する
- 広告とレビュー依頼が重ならないよう、カウンタの参照を分ける
新しい広告フォーマットを試す前に、まず今あるものを正確に把握する。地味ですが、これをやっておくと「どこをいじれば何が動くか」が分かるので、その後の収益改善が一気にやりやすくなります。広告まわりが膨らんできたと感じたら、機能を足す前に一度 grep "ca-app-pub" から始めてみることをお勧めします。頭の中が驚くほど整理されます。