Android Studio 1.5のMemory Profilerを使ってメモリリークを発見する

[Android Developers Blog](http://android-developers.blogspot.jp/2015/11/android-studio-15.html)でも紹介されているように、Android Studio 1.5のstableバージョンがリリースされた。

特に、今回のリリースでMemory Profiler機能がついたというので早速試してみる。
> In addition to the stability improvements and bug fixes, we’ve added a new feature to the memory profiler. It can now assist you in detecting some of the most commonly known causes of leaked activities.

最近は高性能な端末が出てきて、メモリ容量も多くなったとはいえ、メモリ管理はパフォーマンスに影響するため、いつの時代も大事なキーワード。

以前はheap dumpを取得して、EclipseのMemoryAnalyzerで見るという感じでやっていたが、AndroidStudioで完結できるということでだいぶ楽になりそう。
今一度実際に作業をしながらメモリの解放手順確認とともに、MemoryProfilerを使ってみる。


# Leakするサンプルアプリ
とりあえず、強制的にメモリリークするような犬でリークするサンプルアプリを作って試してみる。

<img width="400" alt="file" src="https://qiita-image-store.s3.amazonaws.com/0/29379/6714ca1e-8ba7-1bce-797f-a727af15ed99.png">

概要は以下のとおり

* FragmentにRecyclerViewを使って、Bitmap画像を読みこませる（50個程度）
* FABを押すと更に同じFragmentを生成する
* 画像をクリックすると、トップに戻り、FragmentのBackstackをすべてクリアする

下記要領で、Leakするようにして確認する。

* Bitmapのrecycleを呼ばない
* FragmentのonDestroyViewで、AdapterやListenerを開放しない

## 早速MemoryProfilerを使ってみる
### 手順

* まずはアプリを起動し、AndroidStudio下部のMemoryMonitorを見ながらOutOfMemoryで落ちる寸前まで犬を表示しまくる。
![スクリーンショット 2015-11-20 11.02.24.png](https://qiita-image-store.s3.amazonaws.com/0/29379/b4851dfd-7d61-d60b-261c-401dab7d3e03.png)


みるみるうちに増えていく(段々と上がっている部分で新たに犬リストのFragmentを生成している)

* 一旦トップまで戻って、強制的にGCをかける(MemoryMonitorの左側のトラックアイコンをクリック)  
案の定、メモリは減らない

### MemoryProfilerを使ってメモリを確認

* 同じくMemoryMonitorの左側の下から2番目(Heap Java Dump)を押してしばらく待つと、下記のようなプロファイルが生成される
![スクリーンショット 2015-11-20 11.04.10.png](https://qiita-image-store.s3.amazonaws.com/0/29379/ce258a4e-fd0b-afdc-c2da-1c0e8d1fcfe8.png)  
  
* この中で、Heap CountとかRetained Heapでソートをかけて、何がメモリを圧迫しているかを確認する(検索で怪しいクラスを探してもOK)  
![スクリーンショット 2015-11-20 11.52.37.png](https://qiita-image-store.s3.amazonaws.com/0/29379/c4e76b26-5936-d931-894b-0e4ee1651c8d.png)
いましたね、Bitmap。それも大量に。
ちなみに、下記のようにBitmapのところで右クリックして、View Bitmapを選択すると実際の画像が見れる。
![スクリーンショット 2015-11-20 11.53.08.png](https://qiita-image-store.s3.amazonaws.com/0/29379/78724e51-4430-e724-af8b-f05888bc2695.png)

ほら！

<img width="800" alt="file" src="https://qiita-image-store.s3.amazonaws.com/0/29379/2412df88-193e-1955-171c-7edccf542d8e.png">

あとは、RecyclerViewのAdapterインスタンスもいっぱいいた。
![スクリーンショット 2015-11-20 11.55.11.png](https://qiita-image-store.s3.amazonaws.com/0/29379/f99edd0b-6aeb-d869-8a25-f17d53bce6f9.png)

### Leakを修正したアプリで再度MemoryProfilerを使う。
リーク修正方針は下記の通り

* ViewHolderで保持しているbitmapをrecycleする
* FragmentのonDestroyViewでadapterのリスナーを解放、recyclerViewからadapterをデタッチ
* 上記と同じ手順で操作し、GCをかけた後にMemoryProfilerを見てみる。


まずはBitmap

![スクリーンショット 2015-11-20 12.04.20.png](https://qiita-image-store.s3.amazonaws.com/0/29379/e1a6b5cc-60b3-4dda-2934-720af54bfb7c.png)

犬がいなくなった。
2つ残ってるのは、もともと入っているBitmapっぽい。

つづいてAdapterとListener

![スクリーンショット 2015-11-20 12.08.23.png](https://qiita-image-store.s3.amazonaws.com/0/29379/87684491-3554-3619-8b28-2b40cdc2bff2.png)

Heap Count 0。キレイ。
実際にGCかけた時はこんな感じだったので、これでもある程度のメモリ解放は把握できる。

![スクリーンショット 2015-11-20 12.06.52.png](https://qiita-image-store.s3.amazonaws.com/0/29379/54b7feb6-45da-5e75-7c5f-177d00ca1523.png)

### 今回使用したコード
今回試してみたのはこんな感じのソースコード

* Fragment

```java
public class ImageListFragment extends Fragment implements ImageAdapter.OnImageAdapterListener {

    private RecyclerView mRecyclerView;
    private ImageAdapter mAdapter;

    public ImageListFragment() {
    }

    public static ImageListFragment newInstance() {
        return new ImageListFragment();
    }

    @Override
    public void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getContext());
        mAdapter.setOnImageAdapterListener(this);
    }

    @Nullable
    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        return inflater.inflate(R.layout.list_layout, container, false);
    }

    @Override
    public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        mRecyclerView.setAdapter(mAdapter);
    }

    @Override
    public void onDestroyView() {
//　ここのコメントアウトを外して、メモリ解放する
//        mAdapter.setOnImageAdapterListener(null);
//        final int childrenCount = mRecyclerView.getChildCount();
//        for (int i = 0; i < childrenCount; i++) {
//            mAdapter.recycle((ImageAdapter.ImageViewHolder) mRecyclerView.findViewHolderForAdapterPosition(i));
//        }
//        mAdapter = null;
//        mRecyclerView.setAdapter(null);
//        mRecyclerView = null;
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Override
    public void onClickView() {
        FragmentManager manager = getFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        List<Fragment> fragments = manager.getFragments();
        for (final Fragment fragment : fragments) {
            transaction.remove(fragment);
        }
        transaction.commit();
    }
}

```

* Adapter

```java
public class ImageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private Context mContext;
    private OnImageAdapterListener mOnImageAdapterListener;

    public interface OnImageAdapterListener {
        void onClickView();
    }

    public ImageAdapter(final Context context) {
        mContext = context;
    }

    public void setOnImageAdapterListener(final OnImageAdapterListener listener) {
        mOnImageAdapterListener = listener;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
        final View view = View.inflate(mContext, R.layout.image_row, null);
        return new ImageViewHolder(mContext, view);
    }

    @Override
    public int getItemCount() {
        return 50;
    }

    @Override
    public int getItemViewType(final int position) {
        return super.getItemViewType(position);
    }

    @Override
    public long getItemId(final int position) {
        return super.getItemId(position);
    }

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {

    }

    public void recycle(final ImageViewHolder holder) {
        // これを呼ぶとViewHolderのBitmapをrecycleする
        if (holder != null) {
            holder.clear();
        }
    }

    public class ImageViewHolder extends RecyclerView.ViewHolder {

        private ImageView mImageView;
        private Bitmap mBitmap;

        public ImageViewHolder(final Context context, final View view) {
            super(view);

            mImageView = (ImageView) view.findViewById(R.id.image_view);
            mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.test);
            mImageView.setImageBitmap(mBitmap);
            mImageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(final View v) {
                    if (mOnImageAdapterListener != null) {
                        mOnImageAdapterListener.onClickView();
                    }
                }
            });
        }

        public void clear() {
            if (mBitmap != null && !mBitmap.isRecycled()) {
                mBitmap.recycle();
                mBitmap = null;
            }
        }
    }
}