22209


ViewPagerでギャラリーアプリのようなものを作っています。  
写真はVolleyを用いてネットから取得しているのですが、  
例えば横縦横縦・・・と並んでいる写真を7枚ほど閲覧するとメモリ不足になり写真が表示されなくなります。  
現状以下のコードを`Fragment#onDestroyView`のタイミングで呼び出し画像のメモリ解放を行っているのですが、他に対策などありましたら教えて欲しいです。

[code]

    /**
     * Viewを再帰的に探索して保持しているデータを削除
     *
     * @param view FragmentのrootViewなど
     */
    protected static void cleanupView(View view) {
        if (view instanceof PhotoView) {
            PhotoView photoView = (PhotoView) view;
            photoView.setImageBitmap(null);
            //もしタグが残っていたらキャンセル処理をさせておく
            ImageLoader.ImageContainer imageContainer = (ImageLoader.ImageContainer) photoView.getTag();
            if (imageContainer != null) {
                imageContainer.cancelRequest();
                photoView.setTag(null);
            }
        } else if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                cleanupView(viewGroup.getChildAt(i));
            }
        }
    }
[/code]

PhotoViewer(extends ViewPager)を配置したPhotoViewerFragmentのレイアウト

[code]

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 tools:context="com.example.sakaguchi.fragment.PhotoViewerFragment">
    
        <com.example.sakaguchi.view.PhotoViewer
            android:id="@+id/photo_viewer"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </FrameLayout>
[/code]

PhotoViewerで表示させているPhotoFragmentのレイアウト

[code]

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 tools:context="com.example.sakaguchi.fragment.PhotoFragment">
    
        <com.example.sakaguchi.view.ScalingPhotoView
            android:id="@+id/photo"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/black"/>
    
        <com.example.sakaguchi.view.PhotoViewerToolBar
            android:id="@+id/tool_bar"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_gravity="bottom"
            android:background="@color/half_transparent_black">
    
            <com.example.sakaguchi.view.AddCartButton
                android:id="@+id/add_cart"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:background="@drawable/add_cart_button_for_photo"
                android:gravity="center"
                android:paddingLeft="30dp"
                android:paddingRight="30dp"
                android:text="@string/add_cart"
                android:textColor="@color/orange"
                android:textSize="15sp"/>
    
            <TextView
                android:id="@+id/photo_name"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_gravity="right"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="10dp"
                android:gravity="center"
                android:textColor="@android:color/white"/>
        </com.example.sakaguchi.view.PhotoViewerToolBar>
    
    </FrameLayout>
[/code]

追記  
クラッシュした時のヒープダンプをMATで調べてみるとbyte[]が一番メモリを消費しているようです。  
画像のメモリ解放処理が甘いのでしょうか？  
[![クラッシュした時のヒープHistgram](https://i.stack.imgur.com/5IE3Y.png)](https://i.stack.imgur.com/5IE3Y.png)

追記2  
@yamacraftさんのおっしゃる通りViewPagerのキャッシュ指定は特にしていないのでデフォルトの1だと思います。  
Fragmentの参照が生きているのでは？という話ですが、自パッケージのオブジェクトについて見てみると、その`Fragment`に配置している`AddCartButton`が5つ・`ScalingPhotoView`のオブジェクトが3つ残っているなどFragmentの参照がいくらか残っているようです。  
ですが、以下のページによるとViewPagerは今と左右の3ページ分保持する仕様のようなので、1ページ1つの`AddCartButton`が5つ残っているのは気になりますが、その`Fragment`の参照は問題ではないような気がします。  
[FragmentPagerAdapterとListFragmentを使ったらはまった –
unsweets.log](http://blog.unsweets.net/2013/03/fragmentpageradapterlistfragment.html)

ちなみにその時の自パッケージのオブジェクトShallow HeapTOP3は以下の通りでした。  
1\. `AddCartButton` － 3,160Byte (オブジェクト数：5)  
2\. `PhotoViewerToolBar` － 1,536Byte (オブジェクト数：3)  
3\. `ScalingPhotoView` － 1,392Byte (オブジェクト数：3)

追記3  
横縦横縦・・・の写真を閲覧していて6枚目で急にローディングに少し時間がかかり、7枚目は他と変わらないものの、8枚目はメモリ不足になります。  
再起動した後などは使用できるメモリがいくらか多かったのか8枚目も見れて9枚目でメモリ不足になったこともありました。

ですが、例えば以下のパターンの場合は多少増減はするものの大した変化は見られず、メモリ不足にならずに50枚全てを閲覧することができます。  
写真1 縦向き 8枚  
写真2 縦向き 8枚  
写真3 縦向き 8枚  
写真4 横向き 8枚  
写真5 横向き 8枚  
写真6 横向き 2枚  
写真番号ごとに同じ見た目ですが、50枚それぞれ読み込みURLは異なっています。  
50枚目を閲覧した後のメモリダンプを調べてみたところ、byte[]のShallow Heapがメモリ不足になった時の2/3ほどでした。  
[![compare](https://i.stack.imgur.com/WiWG9.png)](https://i.stack.imgur.com/WiWG9.png)  
自作の画像を表示させているViewのオブジェクト数などは、50枚閲覧した方はViewPagerの前のListViewで使っている画像を表示させているViewがないこと以外は変わっていません。  
画像によるのか画像の向きが関係しているのか確認してみます。

追記4  
私がiPhone6sで撮影した横向きの写真(ファイルサイズ1.6MBほど)とそれをMacのプレビューで縦向きにしたものを用意して試してみました。  
横向き － <http://imgs.link/BThDaB.jpg>  
縦向き － <http://imgs.link/RMOdBs.jpg>

全て横向きや縦向きの場合は50枚閲覧しても問題なく、縦横交互にすると9枚目でメモリ不足になりました。  
また縦横2枚ずつ交互の場合も9枚目、縦横3枚ずつ交互の場合は11枚目でメモリ不足になりました。  
もしかしたらこれはViewPagerがらみではなくVolleyがらみの問題でしょうか？

追記5  
@yamacraft さん追記ありがとうございます。  
キャンセル処理は最初に書いたように`Fragment#onDestroyView`のタイミングでさせているはずなので問題ないはずです。  
`//もしタグが残っていたらキャンセル処理をさせておく`の部分です。  
読み込みが終わったらタグを消す処理をしているので、`タグが残っている=読み込み処理をまだやっている`と判断してキャンセル処理をさせています。  
そもそも次へ移動する操作は写真が表示されてから行っているので、読み込みタスクは残っていないと思っています。

以前写真一覧でメモリ不足に悩まされたことがありましたが、その時は自前で同様のメモリ解放処理を書いたら解決しました。  
当時の質問  
\-
[写真を一定量読み込むとメモリ不足になってしまうのですが](https://ja.stackoverflow.com/questions/12528/%e5%86%99%e7%9c%9f%e3%82%92%e4%b8%80%e5%ae%9a%e9%87%8f%e8%aa%ad%e3%81%bf%e8%be%bc%e3%82%80%e3%81%a8%e3%83%a1%e3%83%a2%e3%83%aa%e4%b8%8d%e8%b6%b3%e3%81%ab%e3%81%aa%e3%81%a3%e3%81%a6%e3%81%97%e3%81%be%e3%81%86%e3%81%ae%e3%81%a7%e3%81%99%e3%81%8c)  
\-
[byte配列がメモリの大部分を占拠するのを解消したい。](https://ja.stackoverflow.com/questions/12612/byte%E9%85%8D%E5%88%97%E3%81%8C%E3%83%A1%E3%83%A2%E3%83%AA%E3%81%AE%E5%A4%A7%E9%83%A8%E5%88%86%E3%82%92%E5%8D%A0%E6%8B%A0%E3%81%99%E3%82%8B%E3%81%AE%E3%82%92%E8%A7%A3%E6%B6%88%E3%81%97%E3%81%9F%E3%81%84)

VolleyはAPI23で非推奨のものを使っているのは知りませんでした。  
まだ本アプリはまだcompileSdkVersionが22ですが、今回や今後のことも考えて他のライブラリに乗り換えることも前向きに検討してみます。


CC BY-SA 3.0
2016-02-17T05:08:23.887
22194
2016-03-04T10:10:36.983
2017-04-13T12:52:38.920
-1
9710
question
android android-volley viewpager
ViewPagerで表示させているFragmentのメモリ解放がうまくできていない？


特に注釈がないことを考えると、ViewPagerのキャッシュ指定はデフォルトの１になっていると思えばいいでしょうか。  
<http://outcesticide.hatenablog.com/entry/ViewPager_setOffScreenPageLimit>

もし違っていれば、デフォルトの１にすることでメモリを節減することができます。ただ「７枚目で〜」ということですので、おそらく`FragmentPagerAdapter`内でFragmentの参照が生きているせいでオブジェクトが完全に破棄されず、その影響でBitmapの保持が続いてしまいエラーが起きているような気がします。  
ViewPager〜PagerAdapterあたりで、参照が生き残っていないかチェックしてみてください。  
<http://htomiyama.blogspot.jp/2012/08/androidoutofmemoryerror.html>

今回はおそらく違うと思っていますが、破棄するBitmapオブジェクトに`recycle()`を設定することで「解放できるときに解放できるように準備してくれる」ようになります。ただあくまでも体感ですが、基本的にどこからも参照されなくなければ`recycle()`しなくても解放してくれている印象があるので、やはりFragment周りで参照が生き残っていないかを探した方が、原因を見つけられそうな気がします。  
<http://i2key.hateblo.jp/entry/2014/03/07/164501>

＜追記＞  
ちょっとVolleyでの画像取得は自分もやったことがないので検証してみないといけませんが、確かにFragmentを破棄するタイミングでキャンセル処理がされていなければ、どんどん取得した画像がスタックされていっているのかもしれません。  
<http://qiita.com/gari_jp/items/829a54bfa937f4733e29>

ちなみに今回の件とは話が外れますが、VolleyはAPI23から削除されてしまうorg.apache.*関連のクラスを使って実装されているので、現状使い続けるのは危険です。  
picassoやGlideの利用も検討してみてください。

＜再追記＞  
単に別ライブラリの利用を進めるだけというのも良くないと思ったので、おそらくVolleyからの移行として一番採用されているokhttpを使った画像取得＋ViewPagerを使ったサンプルプロジェクトを用意してみました。参考になれば幸いです。

ちなみにこの実装の場合、特にFragment破棄の際にcancel処理などはいれませんでしたが、ページ送りでもアプリが落ちることはありませんでした。（毎回通信も走っているので、キャッシュ保持でなんとかなっているわけではないはず）  
<https://github.com/yamacraft/android.GetImageViewPager>


CC BY-SA 3.0
2016-02-17T10:24:40.593
22209
2016-03-04T10:10:36.983
2016-03-04T10:10:36.983
10880
10880
22194
answer


6.0.0のソースを１０分ほど追いかけてみました。

・サイズが同じBitmapをキャッシュしている(PoolableImageCache.java)  
・デコードのキャンセル処理が見当たらない

以上の情報と再現環境から、このような仮説が立ちます。  
・サイズが違うイメージを何枚か読み込む  
・サイズが違うので別々にキャッシュ、また、読み込み時にゴミが大量に発生  
・さらに読み込む（別スレッド）、が、ここでOoM発生  
・キャッシュをパージして、BitmapをGC対象にする  
・しかしVM管理外のモジュールで読み込み続けている（メモリを確保し続けている）ので、GCが間に合わず、クラッシュ

この仮説が正しいなら、Bitmapキャッシュを切ればいいのですが、API上にはその窓口が  
用意されてないようなので手を出せません。  
設計を見るに、Volleyのイメージ読み込みは小さなイメージを大量に読むことを想定しているようです。  
ソースもいじれませんし、Volleyの利用をやめるしか選択肢がないように思えます。


CC BY-SA 3.0
2016-02-28T06:37:32.460
22622
2016-02-28T06:37:32.460
9191
22194
answer