rinunu
6/25/2011 - 2:17 PM

Android Bitmap and OutOfMemoryError

Android Bitmap and OutOfMemoryError

package nu.rinu;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Debug;
import android.util.Log;

/**
 * Bitmap(ネイティブメモリ)のアロケートで OutOfMemoryError が発生する問題の調査
 * 
 * <h4>現象</h4>
 * <ul>
 * <li>メモリが足りないにもかかわらず GC が発生しない場合があるように見える
 * <li>メモリが足りないにもかかわらず SoftReference がクリアされない場合があるように見える
 * <li>Bitmap のサイズが大きいほど発生しやすいように見える
 * <li>GC を明示的に実行すると、許容できる Bitmap のサイズが大きくなる。 GC を複数回呼び出すと、さらに効果がある。
 * <li>端末(や環境?)によって許容できる Bitmap のサイズがことなる(IS06 等は問題が表面化しない)。
 * </ul>
 * 
 * 
 * <h4>解決策</h4>
 * 
 * <h5>妥協</h5>
 * 
 * <ul>
 * <li>1回で確保するネイティブメモリを小さくする
 * <li>GC を明示的に実行する
 * <li>SoftReference のキャッシュ数を少なくする
 * <li>Bitmap.recycle() する。
 * </ul>
 * 
 * <h5>完全</h5>
 * <ul>
 * <li>SoftReference を使わない and (GC を明示的に実行する or Bitmap.recycle() する)
 * </ul>
 * 
 * <h5>メモ</h5> finalize が呼ばれた場合は問題が起きていないため、 Bitmap
 * の開放に問題があるのではなく、開放判断に問題があると思われる TODO recycle
 * 
 */
public class MainActivity extends Activity {
	private static final String TAG = MainActivity.class.getSimpleName();
	private static final int LOOP_COUNT = 200;

	private static interface Recyclable {
		void recycle();
	}

	/**
	 * メモリを確保する
	 */
	private interface Allocator {
		Object allocate(int no);
	}

	/**
	 * 参照を保持する
	 * 
	 * <p>
	 * 参照の保持方法、参照を解除するタイミングはサブクラスが任意に選択する。
	 */
	private interface Cache {
		void add(Object value, int no);
	}

	private interface Debaggable {
		String getStatus();
	}

	// ----------
	// 実装

	private static class HeapObject implements Recyclable {
		private int[] a;

		public HeapObject(int width) {
			a = new int[width * 1000];
		}

		@Override
		public void recycle() {
		}
	}

	private static class BitmapObject implements Recyclable {
		private Bitmap bitmap;
		private int no;

		public BitmapObject(int no, int width) {
			this.no = no;
			bitmap = Bitmap.createBitmap(width, 1000, Bitmap.Config.ARGB_8888);
		}

		@Override
		public void recycle() {
			bitmap.recycle();
		}

		@Override
		protected void finalize() throws Throwable {
			Log.d(TAG, "finalize " + no);
		}

	}

	// ----------
	// Cache 実装

	private static class MySoftReference<T> extends SoftReference<T> {
		private int no;

		public MySoftReference(T value, ReferenceQueue<T> referenceQueue, int no) {
			super(value, referenceQueue);
			this.no = no;
		}
	}

	/**
	 * Reference で参照を一定件数保持する {@link Cache}
	 */
	private static abstract class AbstractCache implements Cache, Debaggable {
		private List<Reference<Object>> list = new ArrayList<Reference<Object>>();
		private Queue<Reference<Object>> queue = new LinkedList<Reference<Object>>();
		private int max;
		private boolean recycle;

		public AbstractCache(int max, boolean recycle) {
			this.max = max;
			this.recycle = recycle;
		}

		public void add(Object value, int no) {
			Reference<Object> ref = getReference(value, no);
			list.add(ref);
			queue.add(ref);
			while (queue.size() > max) {
				Reference<Object> refOld = queue.poll();
				list.remove(refOld);

				Object old = refOld.get();
				if (old != null) {
					if (recycle && old instanceof Recyclable) {
						((Recyclable) old).recycle();
					}
				}
			}
		}

		@Override
		public String getStatus() {
			int softRefCount = 0;
			for (Reference<?> i : list) {
				Object o = i.get();
				if (o != null) {
					++softRefCount;
				}
			}

			return String.format(" refs %d", softRefCount);
		}

		protected abstract Reference<Object> getReference(Object object, int no);
	}

	/**
	 * SoftReference で参照を一定件数保持する {@link Cache}
	 */
	private static class SoftCache extends AbstractCache {
		private ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();

		public SoftCache(int max, boolean recycle) {
			super(max, recycle);
		}

		@Override
		protected Reference<Object> getReference(Object object, int no) {
			return new MySoftReference<Object>(object, referenceQueue, no);
		}
	}

	/**
	 * WeakReference で参照を保持する {@link Cache}
	 */
	private static class WeakCache extends AbstractCache {
		private ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();

		public WeakCache(int max, boolean recycle) {
			super(max, recycle);
		}

		@Override
		protected Reference<Object> getReference(Object object, int no) {
			return new WeakReference<Object>(object, referenceQueue);
		}
	}

	/**
	 * 参照を保持しない {@link Cache}
	 */
	private static class NullCache implements Cache {
		public void add(Object value, int no) {
		};
	}

	// ----------
	// Allocator 実装

	private static class BitmapObjectAllocator implements Allocator {
		private int width;

		public BitmapObjectAllocator(int width) {
			this.width = width;
		}

		@Override
		public Object allocate(int no) {
			Log.d(TAG, "■ allocate");
			return new BitmapObject(no, width);
		}
	};

	private static class HeapAllocator implements Allocator {
		private int width;

		public HeapAllocator(int width) {
			this.width = width;
		}

		@Override
		public Object allocate(int no) {
			Log.d(TAG, "■ allocate");
			return new HeapObject(width);
		}
	};

	// ----------
	// 実行

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		Debug.startAllocCounting();

		final int width = 1100;
		test(new BitmapObjectAllocator(width), new SoftCache(3, false), 0);
	}

	/**
	 * テストを実行する
	 */
	private <T> void test(Allocator allocator, Cache cache, int gcCount) {
		for (int i = 0; i < LOOP_COUNT; ++i) {
			printMemory(i, cache);
			for (int j = 0; j < gcCount; ++j) {
				gc();
			}
			Object memory = allocator.allocate(i);
			cache.add(memory, i);
		}
	}

	private static void printMemory(int no, Cache cache) {
		Log.d(TAG,
				String.format("■ %d native %d-%d / %d", no, Debug.getNativeHeapFreeSize(),
						Debug.getNativeHeapAllocatedSize(), Debug.getNativeHeapSize()));
		Log.d(TAG, String.format(" gc %d %d", Debug.getGlobalGcInvocationCount(), Debug.getThreadGcInvocationCount()));

		if (cache instanceof Debaggable) {
			Log.d(TAG, ((Debaggable) cache).getStatus());
		}
	}

	private static void gc() {
		Log.d(TAG, "■ gc");
		System.gc();
	}

}