http://developer.android.com/training/displaying-bitmaps/index.html
.手机内存资源有限
.Bitmap占用的内存大
.App有时需要同时加载多张bitmap到内存
一张 2592x1936 的照片,在默认 ARGB_8888 的情况下,占用的内存: 19MB (2592*1936*4 bytes)
1.图片内存占用的计算
android 3.1之前 (level 12):
int bytes = bmp.getRowBytes() * bmp.getHeight()
android 3.1 开始增加了方法,实现和上述是一样的:(android4.4开始,这个方法返回的数值可能不准)
bmp.getByteCount(); /**
* Returns the number of bytes used to store this bitmap's pixels.
*/
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
从android 4.4开始要用新增的方法:
bmp.getAllocationByteCount();
This can be larger than the result of getByteCount() if a bitmap is reused to decode other bitmaps of smaller size, or by manual reconfiguration. See reconfigure(int, int, Config),setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap. If a bitmap is not modified in this way, this value will be the same as that returned bygetByteCount().
2. Loading Large Bitmaps Efficiently
如果图片的实际大小比界面上显示的要大,会消耗更多的内存和需要额外的缩放计算。
BitmapFactory 提供了一系列从各种资源创建Bitmap的方法,如果直接创建很可能会因为图片过大而OOM。 BitmapFactory.Options 中 inJustDecodeBounds = true 时,只解析出图片的大小等信息,不会创建bitmap:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
加载图片时,设置inSampleSize可以减小加载到内存图片的尺寸,比如inSampleSize=2,则图片的长和高都变为原来的1/2.
//计算inSampleSize:
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2;
final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
} return inSampleSize;
} //根据需要的大小加载图片
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) { // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options); // Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
3.Processing Bitmaps Off the UI Thread
图片的加载通常耗时较长,不应放在UI线程中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
//WeakReference 防止因为AsyncTask保留有ImageView 的引用而导致其不能正常释放,
//所以不能保证执行onPostExecute时,imageView还是有效的,因此要检查null.
private final WeakReference<ImageView> imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
} // Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
} // Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
} public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
如何在复用View的同时,正确的利用多线程加载图片的一个解决方案。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
} public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
} public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
} public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// If bitmapData is not yet set or it differs from the new data
if (bitmapData == 0 || bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
} private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
} class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
... @Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
} if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
4.Caching Bitmaps 图片缓存
为了保证效率和界面的流畅性,需要缓存图片。一般分为mem缓存和disk缓存。
MEM:
Mem缓存时常用到 LruCache类(Added in API level 12,support-lib4中也有)。
其内部实现,是将对象用强引用保存在一个LinkedHashMap中,当达到最大的缓存数量或者缓存内存大小时,会将最近最少使用的对象移除(每次添加一个新对象或者get使用一个对象时,都将其放到链表的头部,尾部的对象就是最少使用,要移除的)
以前常见缓存方式是用softReference和weakReference,但是现在不推荐使用了。 从 2.3开始,GC对软/弱引用的回收更加具有aggressive,也就是说它们很快就会被回收,起不到缓存的作用,也使其效率很低。 其次 3.0之前,bitmap 的内存数据是存储在 native 内存中的。
没有一个固定的缓存大小能适合所有的app,缓存太小影响效率,太大可能导致OOM。
设置缓存的大小需要考虑以下因素:
.最大可用内存多少。
.一次有多少图片在屏幕上,有多少即将要显示。
.屏幕的尺寸和密度。
.bitmap的参数影响其占用内存的多少 。
.图片加载的频率,根据频率高低可以考虑用多个lrucache来缓存不同类别的图片。
.平衡图片的质量和数量,有时需要大量低质量的图片,有时又需要高质量的图片。
Disk:
Mem缓存可能很快被占满。当应用切换到后台,可能被kill掉,导致mem缓存丢失,要重新加载处理图片,所以需要disk缓存。 从disk读数据比mem读慢,而且时间不确定,所以要放在工作线程中。
DiskLruCache
5.Managing Bitmap Memory
.android 2.2(API level 8)和以下的版本中,GC工作时,会阻塞app线程,导致性能降低。 从2.3开始,GC是并行工作的,bitmap没有引用指向时会很快被回收掉。
.Android 2.3.3 (API level 10)和以下的版本中,bitmap的像素数据是存储在 native内存中的,而bitmap对象是分配在虚拟机的堆上,二者是分开的。native内存中像素数据是以一种不可预测的方式释放,很可能导致OOM。
.从Android 3.0 (API level 11)开始,bitmap的像素数据和bitmap对象本身都是存储在dalvik虚拟机的堆上的。所以最好在3.0以上的机器上调试图片内存占用的问题。
所以3.0以下时,推荐使用 recycle()来显示的释放bitmap的内存。 但要注意的是,调用该方法时要确认该bitmap对象后续不会再使用到,否则会抛异常: "Canvas: trying to use a recycled bitmap". bitmapfun示例中使用了一种基于引用计数的解决方案。
Options.inBitmap
从android 3.0 开始,引入了 BitmapFactory.Options.inBitmap 字段,可以传入一个不再使用的bitmap对象,来加载一个新的bitmap,新的对象会复用旧的bitmap的内存,这样可以减少内存的分配和释放,提高了效率。
但是使用inBitmap字段有一定的限制条件:
.首先,要求旧的bitmap必须是mutable的才能复用(options.inMutable = true)。
.Android 4.4 (API level 19)之前,要求新/旧对象的尺寸必须是相等的(the dimensions must match exactly and the inSampleSize must be 1)。
.从4.4开始,新对象占用总字节数<=旧bitmap对象占用的总字节数就可以复用(the byte size of the new bitmap is smaller than the reusable bitmap candidate allocation byte count.)。
在bitmapFun 示例中,从LruCache内存缓存中移除的bitmap,放入了一个 Set<SoftReference<Bitmap>> mReusableBitmaps 中,后续decode新bitmap对象的时候,到这里面去找看是否有能符合条件的,可以复用的bitmap,有的话就复用其内存。
static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// From Android 4.4 (KitKat) onward we can re-use if the byte size of
// the new bitmap is smaller than the reusable bitmap candidate
// allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
} // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
} static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}
6. Displaying Bitmaps in Your UI
略