MoPubのLifecycle Callbacksはメディエーションを使うなら実装したほうがよい

MoPubのRewarded Video実装をする時など、ドキュメントを読んでいると下記のような記述に遭遇する:

Step 5. Optionally Implement Lifecycle Callbacks

Network SDKs need to be notified of changes in the activity lifecycle so they can keep track of the current activity and load and show rewarded videos. For example, you can add lifecycle callbacks to all of your activities (including your Launcher Activity) that will load or show rewarded videos.

For Mediation SDKs that track Activity lifecycle events, you must notify MoPub when those events happen, so that MoPub can pass them onto mediated network SDKs, as shown:

developers.mopub.com

MoPubの提供する Lifecycle Callbacks と称するAPI郡に関する記述なのだが、 "Optionally" と書いてあるし、実装しなくても動作する場合があるし、何より複数に渡る画面へこれを埋め込むのは現実的ではない。そんなわけで無視して進めてしまいそうになる。
しかし "Network SDKs need to be notified of changes in the activity lifecycle" とあるように、実はメディエーション機能を使う場合には実態として必須と言って過言ではない。

Custom SDK Network側ではどのように見えてくるか

今回はリワード広告を例に取る。
前提として、MoPubでリワードを表示するには下記のような手順を取る:

// MyActivity.kt

val conf = SdkConfiguration.Builder(adUnitId)
    .withMediatedNetworkConfiguration(
        MyAdapterConfiguration.javaClass.name,
         mapOf()
    )
    .withLogLevel(MoPubLog.LogLevel.DEBUG)
    .build()

MoPub.initializeSdk(this, conf, listener)

MoPubRewardedVideos.loadRewardedVideo(adUnitId, MyMediationSettings())

MoPubRewardedVideos.showRewardedVideo(adUnitId)

この要求を受け、メディエーション先アドネットワークのSDKは下記のようなコールバックを受ける:

// MyRewardedVideoAdapter.kt

override fun checkAndInitializeSdk(launcherActivity: Activity, adData: AdData): Boolean {
    // do something
    return true
}

override fun load(context: Context, adData: AdData) {
    // do something
    mLoadListener.onAdLoaded()
}

override fun show() {
    // do something
    mInteractionListener.onAdShown()
    mInteractionListener.onAdImpression()

    MoPubReward.success(MoPubReward.NO_REWARD_LABEL, MoPubReward.DEFAULT_REWARD_AMOUNT).let {
        mInteractionListener.onAdComplete(it)
    }

    mInteractionListener.onAdDismissed()
}

これらの詳細な実装は下記を参考にしてほしい。

github.com

loadRewardedVideo の時にはContextを渡していないが、Custom Event側はContextを受け取っている。これは何か?
MoPubの実装を辿ると、これが MoPub::initializeSdk の際に渡したものであることがわかる:

// MoPubRewardedVideoManager.java

public static synchronized void init(@NonNull Activity mainActivity, MediationSettings... mediationSettings) {
    if (sInstance == null) {
        sInstance = new MoPubRewardedVideoManager(mainActivity, mediationSettings);
    } else {
        MoPubLog.log(CUSTOM, "Tried to call initializeRewardedVideo more than once. Only the first " +
                "initialization call has any effect.");
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L170-L177
// MoPubRewardedVideoManager.java

final AdAdapter adAdapter = (AdAdapter) adAdapterConstructor.newInstance(
        sInstance.mMainActivity.get(),
        baseAdClassName,
        adDataBuilder.build()
);

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L600-L604

そしてこの値は最初のinit時に与えられたものを保持し続けていることも確認できる。(これを書き換えるのがLifecycle Callbacksである。後述)

// MoPubRewardedVideoManager.java

public static synchronized void init(@NonNull Activity mainActivity, MediationSettings... mediationSettings) {
    if (sInstance == null) {
        sInstance = new MoPubRewardedVideoManager(mainActivity, mediationSettings);
    } else {
        MoPubLog.log(CUSTOM, "Tried to call initializeRewardedVideo more than once. Only the first " +
                "initialization call has any effect.");
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L170-L177

すなわち最初に与えられたActivityを保持しつづけ、以後Contextが必要になった際はこれを使い回す。これらのコードを字面だけで読み取るなら、MoPubは下記のようなアプリのみを想定した仕様であることになる:*1

  • シングルアクティビティ・マルチフラグメント等、Activityインスタンスがアプリケーション全体を通して共通であるアプリ
  • Activity利用数はともかく、広告の初期化〜読込〜表示をするポイントが単一であるアプリ

問題が起きうるパターン

しかし上記には問題があり、たとえば下記のようなパターンにおいては当然ながら問題を引き起こしうる:

  • Splash表示をするActivityがあり、ここでinitializeを行う。この画面は数秒後にfinishされる。
  • たとえばショップ画面、たとえばゲームのリザルト画面など、異なる複数種類のActivityから動画リワードが繋ぎこまれる
  • メディエーション先SDKが開放されていない(= 通常の)Contextを用いて何かしらの処理を行うため、解放済みのContextでは広告表示に失敗する仕様になっている。

Android F/Wでは開放されていないContextのインスタンスがないと利用できないAPIが多数存在し、また動画リワードのような商材においては表示元となるActivityとの関係性が正しく定義されていることが期待されるため、MoPubのAPI仕様では対応できないことになる。

Lifecycle Callbacksは何をしているか

上記のようなパターンへの対処手段としてMoPubが用意しているのが Lifecycle Callbacks と呼ばれるAPI郡で、これはinit時に握ったActivityインスタンスを入れ替える処理が実行されるものだ。

// MoPub.java

public static void onCreate(@NonNull final Activity activity) {
    MoPubLifecycleManager.getInstance(activity).onCreate(activity);
    updateActivity(activity);
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java#L306-L309
// MoPub.java

try {
    Class moPubRewardedVideoManagerClass = Class.forName(
            MOPUB_REWARDED_VIDEO_MANAGER);
    sUpdateActivityMethod = Reflection.getDeclaredMethodWithTraversal(
            moPubRewardedVideoManagerClass, "updateActivity", Activity.class);
} catch (ClassNotFoundException e) {

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java#L412-L417
// MoPubRewardedVideoManager.java

@ReflectionTarget
public static void updateActivity(@Nullable Activity activity) {
    if (sInstance != null) {
        sInstance.mMainActivity = new WeakReference<>(activity);
    } else {
        logErrorNotInitialized();
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L179-L186

これにより load の際に渡されるContextが入れ替わるため、呼び出し元画面のContextを要求する多くのSDKは初めて期待どおりの挙動を実現可能になる。 load にはContextが渡されるのに対し show ではContextが渡されない問題*2については、Custom Event側で getLifecycleListener を実装することでなんとか回避することも可能だろう。

まとめ

  • 本来MoPubは複数Activityを跨いだ利用を考慮したAPI設計ではない
  • MoPubの提供するLifecycle Callbacksは、メディエーションを利用する限り事実上の必須実装に等しい
  • Custom Eventを実装する場合、期待するContextが常に渡らないことを考慮に入れて設計する必要がある

*1:正直に言って、Android F/W事情に沿っていない至極ナンセンスな仕様である

*2:本当に中途半端である