2016/02/14

AdView は動的追加にしないとメモリリークする

私が Google Play で公開している Dive! Log! というアプリに少し大きめの画像を扱う機能を追加したところ、どうもメモリリークしているような挙動を示すようになりました。 Heap Dump を見てみたところ、Google AdMob の AdView まわりが参照を握っている様子。
こういう時の常套手段として、AdView しかないアプリを作って動作を確認してみることにしました。

Android Studio の AdMob テンプレートでもメモリリークする

結論から言うと、AdView しか存在しない Activity でもメモリリークが発生します。
確認した環境は以下後述のように Stackoverflow に同様のバグが報告されていたので動作確認は2台で止めました
  • Android Studio 1.5.1
  • com.google.android.gms:play-services-ads:8.4.0
    (Ad Format: Banner)
  • 動作確認端末
    • Android 4.0.4 (Xperia acro HD )
    • Android 6.0 (Emulator)

Android Studio で
File → New → New Project...
Activity テンプレート選択画面で Google AdMob Ads Activity

Ad Format を Banner にする

このアプリを起動すると、以下のように AdView だけの Activity が立ち上がります。

この状態で画面回転して、Activity の再生を促します。
その際の Heap Dump が以下

見事に MainActivity が2つ存在します。
その後、画面回転を繰り返してみましたが、2回目以降に生成された Activity はきちんと破棄される様子。
つまり、Activity が無限に増殖していくわけではないので、大きな画像を使う等メモリを食う Activity でない場合、あまり問題にならず、気づきにくいようです。

回避方法は AdView を動的追加にする

原因がわかって調べてみると、結構あっさりこの問題についてのトピックを見つけることが出来ました。

いろいろトリッキーな方法が考案されているのですが、私の環境では単に以下のように AdView を動的追加することで問題を回避出来ました。

まず AdView を layout xml から消して、その代わりに FrameLayout を追加

activity_main.xml
@@ -7,9 +7,11 @@
         android:text="@string/hello_world" />
 
     <!-- view for AdMob Banner Ad -->
-    <com.google.android.gms.ads.AdView android:id="@+id/adView" android:layout_width="wrap_content"
-        android:layout_height="wrap_content" android:layout_alignParentBottom="true"
-        android:layout_centerHorizontal="true" ads:adSize="BANNER"
-        ads:adUnitId="@string/banner_ad_unit_id" />
+    <FrameLayout
+        android:id="@+id/adArea"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true">
+    </FrameLayout>

MainActivity で動的に AdView を追加
この時、AdView のコンストラクタには getApplicationContext() を与えるのがポイント

MainActivity.java
@@ -14,14 +16,17 @@
     private static final String TOAST_TEXT = "Test ads are being shown. "
             + "To show live ads, replace the ad unit ID in res/values/strings.xml with your own ad unit ID.";
 
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
 
         // Load an ad into the AdMob banner view.
-        AdView adView = (AdView) findViewById(R.id.adView);
+        AdView adView = new AdView(getApplicationContext()); // ←これが重要
+        adView.setAdSize(AdSize.BANNER);
+        adView.setAdUnitId(getString(R.string.banner_ad_unit_id));
+        ((FrameLayout) findViewById(R.id.adArea)).addView(adView);
+
         AdRequest adRequest = new AdRequest.Builder()
                 .setRequestAgent("android_studio:ad_template").build();
         adView.loadAd(adRequest);

どうせなら Fragment にしてみる

AdViewActivity を紐付けなければ良さそうなので、以下のような Fragment を作って実験したところ、問題なく動作しました。
ただ、AdMob が AdFragment を提供していないことを鑑みると、この方法にも問題があるのかもしれません。ご使用の際は自己責任でお願いします。

public class AdFragment extends Fragment {

    private AdView mAdView;

    public AdFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        mAdView = new AdView(getContext().getApplicationContext());
        mAdView.setAdSize(AdSize.SMART_BANNER);
        mAdView.setAdUnitId(getString(R.string.banner_ad_unit_id));

        return mAdView;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        if (mAdView != null) {
            AdRequest.Builder b = new AdRequest.Builder();
            mAdView.loadAd(b.build());
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        if (mAdView != null) {
            mAdView.resume();
        }
    }

    @Override
    public void onPause() {
        if (mAdView != null) {
            mAdView.pause();
        }

        super.onPause();
    }

    @Override
    public void onDestroy() {
        if (mAdView != null) {
            mAdView.destroy();
            mAdView = null;
        }

        super.onDestroy();
    }
}

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" tools:context="com.kokufu.myapplication.MainActivity">

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <!-- view for AdMob Banner Ad -->
    <fragment
        android:id="@+id/adFragment"
        android:name="com.kokufu.android.AdFragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true" />

</RelativeLayout>

0 件のコメント: