图片列表页的优化策略

项目中经常会有一个用户头像上传的功能,与之匹配的是,有一个预览相册,裁剪相册的页面。在常规的解决方案中,当相册中图片数目庞大的时候,经常会造成页面性能问题(由于UI的关系不使用系统的裁剪功能,而是自己实现另一套,容易造成内存占用大,页面卡顿)
优化前性能指示图.png

如上图所示,该截图取自公司项目中图片预览页面时Monitors的实时图。其中内存有2次明显的上升(实际上内存占用从30M上升到了164.42M),而GPU monitor中则显示掉帧明显(从50s开始,远超16ms,期间夹杂掉帧)。

这样的性能对应用而言显然是不能接受的,因此我们要逐步将其性能优化到合理的范围。

首先,考虑到列表页的图片并非详情页的高清大图,因此在展示的时候大可以对图片进行压缩。可采用如下代码将图片压缩至合理的范围,例如100K,120K。

BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;
newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
// Get bitmap info, but notice that bitmap is null now
Bitmap bitmap = BitmapFactory.decodeFile(path, newOpts);
newOpts.inJustDecodeBounds = false;
int w = newOpts.outWidth;
int h = newOpts.outHeight;
float hh = 200;
float ww = 200;
int be = 1;
if (w > h && w > ww) { 
   be = (int) (newOpts.outWidth / hh);
} else if (w < h && h > hh) {
be = (int) (newOpts.outHeight / ww);
}if (be <= 0) be = 1;
newOpts.inSampleSize = be;
bitmap = BitmapFactory.decodeFile(path, newOpts);

经过压缩后,似乎能起到不错的效果。在此,我们来考虑另一个问题,列表页的图片经常会有页面滚动操作,因此,除了ListView/RecyclerView 本身的复用机制外,一个合理的解决方案是对图片也进行缓存(内存缓存)。在Adapter中我们可以这样修改(以RecyclerView为例):

public class PreviewImageAdapter extends RecyclerView.Adapter<ImageVH> {
        private final int MAX_MEMORY_SIZE = 1024 * 20; //缓存大小
        private LruCache<String,Bitmap> bitmapLruCache;
        /*省略部分代码*/

   @Override
   public void onBindViewHolder(final ImageVH vh, final int i) {
          if (bitmapLruCache.get(obj.getPath())!=null) {
                 // display image
          }else {
                //load image , add to cache and display
         }
  }

添加了LruCache后,接着思考另外的问题,这种做法提升了图片使用率,但还有一个问题没有解决,即不能抑制OOM的发生,在LruCache中的Bitmap都是强引用,存在OOM的风险。在此应该做进一步处理:

public class PreviewImageAdapter extends RecyclerView.Adapter<ImageVH> {
        private final int MAX_MEMORY_SIZE = 1024 * 20; //缓存大小
        private LruCache<String, SoftReference<Bitmap>> bitmapLruCache;
        /*省略部分代码*/

   @Override
   public void onBindViewHolder(final ImageVH vh, final int i) {
          if (lruCache.get(obj.getPath()) != null && lruCache.get(obj.getPath()).get() != null) {
              SoftReference<Bitmap> bitmap = lruCache.get(obj.getPath());
              if (bitmap != null) {
                // display image
                vh.sdv.setImageBitmap(bitmap.get());
              } else {
                  //load image and add to cache 
                 loadAndCacheBitmap(vh, obj.getPath());
                      }
          } else {
                //load image and add to cache
                loadAndCacheBitmap(vh, obj.getPath());
         }
  }

软引用常用于实现高速缓存,在之前的文章中有提及。
做完了这些工作后,可以检验下成果了。如图所示:

优化后性能指示图.png

可以看出,优化后,内存占用处于合理的范围内(最多增加申请的缓存大小的占用),同时GPU图显示绝大部分绘制能保持在16ms内。

至此,我们再思考下,还能继续把优化做下去吗?答案是肯定的,在此抛个砖。

  • 在加载/处理图片的时候,开了新线程进行处理,当有大量的图片操作时,线程的新建,销毁亦存在性能开销,较合理的方式是采用线程池进行管理。
  • 以上的优化是在某个Adapter中进行的,而项目中有类似需求的不止一处

至此,我们会惊奇的发现,绕了一圈,又回来了。是的,压缩,缓存图片(二级缓存/三级缓存),线程池进行调度,处理任务,这不就是ImageLoader具备的功能么?

  • 从GPU图中可以看出,还可以对绘制性能进行进一步优化

  • 与列表联动,滑动时不进行任务操作(加载图片)

  • 其他…