Edited at

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

More than 3 years have passed since last update.

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;
}
}
}
}