147
148

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-11-20

Android Developers Blogでも紹介されているように、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するサンプルアプリ

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

file

概要は以下のとおり

  • 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

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

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

MemoryProfilerを使ってメモリを確認

  • 同じくMemoryMonitorの左側の下から2番目(Heap Java Dump)を押してしばらく待つと、下記のようなプロファイルが生成される
    スクリーンショット 2015-11-20 11.04.10.png

  • この中で、Heap CountとかRetained Heapでソートをかけて、何がメモリを圧迫しているかを確認する(検索で怪しいクラスを探してもOK)
    スクリーンショット 2015-11-20 11.52.37.png
    いましたね、Bitmap。それも大量に。
    ちなみに、下記のようにBitmapのところで右クリックして、View Bitmapを選択すると実際の画像が見れる。
    スクリーンショット 2015-11-20 11.53.08.png

ほら!

file

あとは、RecyclerViewのAdapterインスタンスもいっぱいいた。
スクリーンショット 2015-11-20 11.55.11.png

Leakを修正したアプリで再度MemoryProfilerを使う。

リーク修正方針は下記の通り

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

まずはBitmap

スクリーンショット 2015-11-20 12.04.20.png

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

つづいてAdapterとListener

スクリーンショット 2015-11-20 12.08.23.png

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

スクリーンショット 2015-11-20 12.06.52.png

今回使用したコード

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

  • Fragment
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
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;
            }
        }
    }
}
147
148
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
147
148

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?