リワード広告をAIに任せっきりで適当に実装させたら大変なことになってた話
最近のAI(ChatGPTやClaudeなど)は賢くなり、Androidアプリの実装でも「AdMobでリワード広告を実装するクラスを作って」とお願いすれば、数秒でもっともらしいコードを生成してくれます。
しかし今回、AIが吐き出した広告実装クラスをそのまま本番に組み込んだ結果、AdMobの表示率(マッチレート・表示回数)が奈落の底に落ち、さらには「広告を一切見ずにタダで報酬をもらい放題」という大惨事が発生しました。
本記事では、AIが生成した「一見すると完璧に見えるコード」に潜んでいた恐ろしい罠と、それをどのように修正したか(正しい実装方針)を共有します。
😱 発生した「大変なこと」
リリース後、AdMobのダッシュボードを見ると異変が起きていました。
-
広告の表示率(Show Rate)が異常に低い
- リクエストは飛んでいるのに、全く表示されていない。
-
収益が上がらないのに、ユーザーのステータスだけが解放されていく
- 広告視聴を条件に解放されるはずの機能やアイテムが、なぜか広告を見ることなく付与されている。
コードを見直した結果、AIが書いた実装にはアーキテクチャレベルでの致命的な欠陥が3つも潜んでいました。
❌ AIが生成したヤバい実装(アンチパターン)
AIが書いてくれた RewardedAdHelper.java は、おおよそ以下のような構造をしていました。
public class RewardedAdHelper {
private RewardedAd mRewardedAd;
private boolean mIsLoading = false;
// 広告を読み込む
public void loadAd(Activity activity) {
if (mIsLoading || mRewardedAd != null) return;
mIsLoading = true;
// ... (AdMobのロード処理) ...
}
// 広告を表示する
public void showAd(Activity activity, OnRewardEarnedListener listener) {
if (mRewardedAd == null) {
// ★ヤバいポイント①:未読込時の「親切な」フォールバック
Log.w("Ad", "読込が間に合っていないため、そのまま報酬を付与します");
if (listener != null) listener.onRewardEarned();
loadAd(activity);
return;
}
mRewardedAd.show(activity, rewardItem -> {
if (listener != null) listener.onRewardEarned();
});
}
}
そして、各Activityの onCreate で次のように呼ばれていました。
// ★ヤバいポイント②:画面ごとにインスタンスを生成
RewardedAdHelper helper = new RewardedAdHelper();
helper.loadAd(this);
【罠1】「インスタンスの都度生成」によるリクエストの無駄撃ち
AIは「状態を持たないユーティリティクラス」を作る感覚でコードを書いていました。そのため、アプリ内で画面(Activity)を移動するたびに new RewardedAdHelper() されてリクエストが飛んでいました。
ユーザーが画面Aを開き、広告ロードが終わる前に画面Bに移動すると、画面Aの広告は破棄されるか永遠に表示されません。これが**「リクエストは送るのに表示されない(表示率の著しい低下)」**の最大の原因でした。
【罠2】親切すぎる「フォールバック」仕様
AIはエラーハンドリングを丁寧に行おうとしたのか、「もし広告のロードが終わっていない状態でボタンが押されたら、ユーザーをガッカリさせないために無条件で報酬(onRewardEarned)を付与する」というロジックを書いていました。
結果として、**「画面を開いてすぐにボタンを連打すれば、広告を見ずにタダで機能が解放される」**という裏技を自ら実装してしまっていたのです。
【罠3】「無限増殖する」リトライボム
通信エラー時の自動リトライをAIに追加させたところ、Handler.postDelayed() で遅延再帰させるロジックを書いてくれました。
しかし、ここにも罠が。ユーザーが画面を素早く行き来すると、見えないところで loadAd() が複数回呼ばれ、それぞれが失敗して別々の「遅延リトライ」をスケジュールし始めました。指数関数的にリトライのキューが溜まり続け、裏で勝手に通信を連発するリトライボムが完成していました。
🛠️ 正しい(あるべき)実装への改修
この大惨事を防ぐため、最終的に以下の設計に大幅リファクタリングしました。
1. 広告管理クラスの「シングルトン化」
リワード広告やインターステーシャル広告などのフルスクリーン広告は、アプリ全体で1つのインスタンスで管理(事前キャッシュ)するのが基本です。
public class RewardedAdHelper {
private static RewardedAdHelper sInstance;
public static synchronized RewardedAdHelper getInstance() {
if (sInstance == null) sInstance = new RewardedAdHelper();
return sInstance;
}
// ...
これにより、画面Aで読み込んだ広告を画面Bでも瞬時に表示できるようになりました。ロードには activity.getApplicationContext() を使うことで Activity のメモリリークも防いでいます。
2. 未読込時は「ブロック」する
タダで報酬を配るフォールバックを完全廃止し、未準備の場合は素直に Toast を出して処理を中断するようにしました。当たり前ですが、収益化においては必須の割り切りです。
if (mRewardedAd == null) {
Toast.makeText(activity, "広告の準備中です。数秒待ってからお試しください。", Toast.LENGTH_SHORT).show();
if (!mIsLoading) loadAd(activity); // 自力で復帰を試みる
return;
}
3. リトライボムのキャンセルと、復帰時の自動リロード
Handler でリトライをスケジュールする前に、必ず過去の予約をキャンセル(removeCallbacksAndMessages)するようにしました。
さらに、AdMobの広告は「一定時間表示しないと期限切れになる」という隠れ仕様があるため、独自のキャッシュ期限(30分)を設け、BaseActivity の onResume() で事前チェック・再ロードを行うようにしました。
public void checkAndReloadIfExpired(Context context) {
if (mRewardedAd != null && System.currentTimeMillis() - mLoadTime > 30 * 60 * 1000L) {
mRewardedAd = null; // 30分経過で破棄
}
// 未読込なら画面復帰時にひっそりロードをキック
if (mRewardedAd == null && !mIsLoading) {
loadAd(context);
}
}
💡 まとめ
最近のAIは部分的なメソッド単位のコーディングには非常に強いですが、以下のような**「アプリ全体を俯瞰したライフサイクル設計や、SDK特有のお作法」**については絶望的に配慮が足りない(あるいは間違ったベストプラクティスを提案してくる)ことがあります。
- Activityを跨ぐ状態管理(シングルトンやメモリリークの考慮)
- 非同期処理のキャンセル(二重実行ボム)
- ビジネス要件(フォールバックを安易に許容しない等)
「とりあえず動いたからヨシ!」で本番に出すと、今回のように大切な収益や表示率をドブに捨てることになりかねません。とくに広告や課金周りのコードは、AIに書かせたあと**「本当にこのアーキテクチャで良いのか?」**を人間が必ずコードレビューしましょう。