fred-ye / summary

my blog
43 stars 9 forks source link

[Android] 图片性能处理的一些总结 #44

Open fred-ye opened 9 years ago

fred-ye commented 9 years ago

以下内容基本上都是来自于Google Android 的官方文档,其中有部份自己从网上搜集的,同时有少部份是根据自己在实际使用过程中总结的一些知识点。关于Google Android 官方文档国内不能访问这个问题,其实国内有部份良心企业为我们做了些镜像,如这个:http://wear.techbrood.com/training/displaying-bitmaps/load-bitmap.html

在做图片处理之前,我们需要知道图片的内存存储: 在Android 2.3.3(API Level 10)以及之前,Bitmap的backing pixel 数据存储在native memory, 与Bitmap本身是分开的,Bitmap本身存储在dalvik heap 中。导致其pixel数据不能判断是否还需要使用,不能及时释放,容易引起OOM错误。 从Android 3.0(API 11)开始,pixel数据与Bitmap一起存储在Dalvik heap中。所以在Android 2.3.3以及之前,建议使用Bitmap.recycle()方法,及时释放资源。

1. 图片大小的计算。

在Android 中图片有四种属性,分别是: ALPHA_8:每个像素占用1byte内存 ARGB_4444:每个像素占用2byte内存 ARGB_8888:每个像素占用4byte内存 (默认) RGB_565:每个像素占用2byte内存(没有alpha属性) 对于一张图片,假设其尺寸为2592x1936 pixels,默认情况下加载进来,Android系统需要为其分配2592_1936_4 bytes的空间,也就要19M。

2.在加载图片之前获取图片的Dimensions和Type.

我们可以采用BitmapFactory中的decodeByteArray(), decodeFile(),decodeResource() 去加载图片,如果直接加载,Android系统会为图片分配内存,容易出现OOM. 通过设置BitmapFactory.Options中的inJustDecodeBounds属性为true, 先获取Bitmap 的outWidth, outHeight, outMimeType(图片的类型,如image/jpeg, image/png等)选项。再根据获取到的选项,看是否在显示图片之前需要进行相应的处理(缩放)。

3.关于图片的缩放

图片的缩放是通过设置options.inSampleSize来实现。当inSampleSize = 2时,意味着图片的宽高都缩小为原来的1/2, 那么整体上图片的体积就变为原来的1/4. Google的官方文档中给出了具体的操作示例。

计算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);
}
调用
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

这个方法是为一个100100像素的ImageView提供合适的bitmap. 并不意味着生成的bitmap的尺寸会是100 \ 100,这一点需要注意。 读者也可以用如下代码来进行试验:

            InputStream is = getResources().openRawResource(R.drawable.image_1);
            BitmapFactory.Options options = new  BitmapFactory.Options();
            Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);
            Log.i("TEST", "pre width:" + btp.getWidth() + "   height:" + btp.getHeight());

            Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), res[position], 100, 100);

            Log.i("TEST", "width:" + bitmap.getWidth() + "   height:" + bitmap.getHeight());

4. 图片的缓存

图片的缓存Google推荐的是采用LruCache这个类,不要再采用SoftReference或WeakReference了,因为自从2.3之后垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。Google给出的例子是这样写的:

LruCache的初始化
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items. 返回缓存图片的大小, 每向 lru cache中添加一个item,这个方法便会执行一次
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}
加戴图片的方法
public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}
实际加载图片的Task
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

5. 采用MAT来定位内存泄露的图片

在上次做我们App性能优化问题的时候,发现在App中泄露的内存很大一部份是因为Memory Leak导致的Activity没有被回收,而在没有被回收的内存中,很大一部份是图片引起的。如果能定位到是哪一张图片的内存泄露,那么就可以有针对性的去检查代码了。在stackoverflow上有一篇文章记录着如利用MAT + GIMP 去定位内存泄露的图片。链接在这里. MAT的使用就不说了,下面把stackoverflow上找到的从MAT中导出图片数据,并用GIMP定位图片的Solution贴出来(此处不想翻译了):

  1. First, you need to download and install GIMP
  2. Next, find your Bitmap object in MAT, right-click on mBuffer field, in the popup menu choose "Copy" -> "Save Value To File" menu item and save value of this array to some file
  3. give extension .data to that file
  4. launch GIMP, choose "File" -> "Open", select your .data file and click Open button
  5. "Load Image from Raw Data" dialog will appear. Here you need to set correct parameters for your bitmap
  6. first, choose "Image type" as "RGB Alpha" (most Android resources have this image type, but you may need to experiment with other image types)
  7. second, set correct Width and Height for your bitmap (correct dimensions can be found in the memory dump)
  8. At that point you should already observe preview of original image. If you didn't, you can try to change some other parameters in "Load Image from Raw Data" dialog.

NOTE: to get a width and height of image you can look at mWidth and mHeight fields in MAT.