当一次应加载很多bitmap,怎么使用缓存

当一次应加载很多bitmap,怎么使用缓存,第1张

在这些控件中,当一个子控件不显示的时候,系统会重用该控件来循环显示 以便减少对内存的消耗。同时垃圾回收机制还会释放那些已经载入内存中的Bitmap资源(假设您没有强引用这些Bitmap)。一般来说这样都是不错的,但是在用户来回滑动屏幕的时候,为了保证UI的流畅性和载入图片的效率,您需要避免重复的处理这些需要显示的图片。 使用内存缓存和磁盘缓存可以解决这个问题,使用缓存可以让控件快速的加载已经处理过的图片。

本文介绍如何使用缓存来提高UI的载入输入和滑动的流畅性。

使用内存缓存

内存缓存提高了访问图片的速度,但是要占用不少内存。 LruCache

类(在API 4之前可以使用Support Library 中的类 )特别适合缓存Bitmap, 把最近使用到的

Bitmap对象用强引用保存起来(保存到LinkedHashMap中),当缓存数量达到预定的值的时候,把

不经常使用的对象删除。

注意: 过去,实现内存缓存的常用做法是使用

SoftReference 或者

WeakReference bitmap 缓存,

但是不推荐使用这种方式。从Android 2.3 (API Level 9) 开始,垃圾回收开始强制的回收掉 soft/weak 引用 从而导致这些缓存没有任何效率的提升。

另外,在 Android 3.0 (API Level 11)之前,这些缓存的Bitmap数据保存在底层内存(native memory)中,并且达到预定条件后也不会释放这些对象,从而可能导致

程序超过内存限制并崩溃。

在使用 LruCache 的时候,需要考虑如下一些因素来选择一个合适的缓存数量参数:

1.程序中还有多少内存可用

2.同时在屏幕上显示多少图片?要先缓存多少图片用来显示到即将看到的屏幕上?

3.设备的屏幕尺寸和屏幕密度是多少?超高的屏幕密度(xhdpi 例如 Galaxy Nexus)

4.设备显示同样的图片要比低屏幕密度(hdpi 例如 Nexus S)设备需要更多的内存。

5.图片的尺寸和格式决定了每个图片需要占用多少内存

6.图片访问的频率如何?一些图片的访问频率要比其他图片高很多?如果是这样的话,您可能需要把这些经常访问的图片放到内存中。

7.在质量和数量上如何平衡?有些情况下保存大量的低质量的图片是非常有用的,当需要的情况下使用后台线程来加入一个高质量版本的图片。

这里没有万能配方可以适合所有的程序,您需要分析您的使用情况并在指定自己的缓存策略。使用太小的缓存并不能起到应有的效果,而使用太大的缓存会消耗更多

的内存从而有可能导致 java.lang.OutOfMemory 异常或者留下很少的内存供您的程序其他功能使用。

Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等 *** 作,并可以指定格式保存图像文件。

重要函数

public void recycle() // 回收位图占用的内存空间,把位图标记为Dead

public final boolean isRecycled() //判断位图内存是否已释放

public final int getWidth()//获取位图的宽度

public final int getHeight()//获取位图的高度

public final boolean isMutable()//图片是否可修改

public int getScaledWidth(Canvas canvas)//获取指定密度转换后的图像的宽度

public int getScaledHeight(Canvas canvas)//获取指定密度转换后的图像的高度

public boolean compress(CompressFormat format, int quality, OutputStream stream)//按指定的图片格式以及画质,将图片转换为输出流。

format:Bitmap.CompressFormat.PNG或Bitmap.CompressFormat.JPEG

quality:画质,0-100.0表示最低画质压缩,100以最高画质压缩。对于PNG等无损格式的图片,会忽略此项设置。

public static Bitmap createBitmap(Bitmap src) //以src为原图生成不可变得新图像

public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)//以src为原图,创建新的图像,指定新图像的高宽以及是否可变。

public static Bitmap createBitmap(int width, int height, Config config)——创建指定格式、大小的位图

public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)以source为原图,创建新的图片,指定起始坐标以及新图像的高宽。

BitmapFactory工厂类:

Option 参数类:

public boolean inJustDecodeBounds//如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息。

public int inSampleSize//图片缩放的倍数

public int outWidth//获取图片的宽度值

public int outHeight//获取图片的高度值

public int inDensity//用于位图的像素压缩比

public int inTargetDensity//用于目标位图的像素压缩比(要生成的位图)

public byte[] inTempStorage //创建临时文件,将图片存储

public boolean inScaled//设置为true时进行图片压缩,从inDensity到inTargetDensity

public boolean inDither //如果为true,解码器尝试抖动解码

public Bitmap.Config inPreferredConfig //设置解码器

public String outMimeType //设置解码图像

public boolean inPurgeable//当存储Pixel的内存空间在系统内存不足时是否可以被回收

public boolean inInputShareable //inPurgeable为true情况下才生效,是否可以共享一个InputStream

public boolean inPreferQualityOverSpeed //为true则优先保证Bitmap质量其次是解码速度

public boolean inMutable //配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段

public int inScreenDensity //当前屏幕的像素密度

工厂方法:

public static Bitmap decodeFile(String pathName, Options opts) //从文件读取图片

public static Bitmap decodeFile(String pathName)

public static Bitmap decodeStream(InputStream is) //从输入流读取图片

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)

public static Bitmap decodeResource(Resources res, int id) //从资源文件读取图片

public static Bitmap decodeResource(Resources res, int id, Options opts)

public static Bitmap decodeByteArray(byte[] data, int offset, int length) //从数组读取图片

public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)

public static Bitmap decodeFileDescriptor(FileDescriptor fd)//从文件读取文件 与decodeFile不同的是这个直接调用JNI函数进行读取 效率比较高

public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)

Bitmap.Config inPreferredConfig :

枚举变量 (位图位数越高代表其可以存储的颜色信息越多,图像越逼真,占用内存越大)

public static final Bitmap.Config ALPHA_8 //代表8位Alpha位图每个像素占用1byte内存

public static final Bitmap.Config ARGB_4444 //代表16位ARGB位图 每个像素占用2byte内存

public static final Bitmap.Config ARGB_8888 //代表32位ARGB位图 每个像素占用4byte内存

public static final Bitmap.Config RGB_565 //代表8位RGB位图 每个像素占用2byte内存

Android中一张图片(BitMap)占用的内存主要和以下几个因数有关:图片长度,图片宽度,单位像素占用的字节数。一张图片(BitMap)占用的内存=图片长度*图片宽度*单位像素占用的字节数

一、图片加载流程

      首先,我们谈谈加载图片的流程,项目中的该模块处理流程如下:

1.在UI主线程中,从内存缓存中获取图片,找到后返回。找不到进入下一步;

2.在工作线程中,从磁盘缓存中获取图片,找到即返回并更新内存缓存。找不到进入下一步;

3.在工作线程中,从网络中获取图片,找到即返回并同时更新内存缓存和磁盘缓存。找不到显示默认以提示。

二、内存缓存类(PanoMemCache)

      这里使用Android提供的LruCache类,该类保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。当cache已满的时候加入新的item时,在队列尾部的item会被回收。

[java] view plain copy print ?

public class PanoMemoryCache {

  // LinkedHashMap初始容量

  private static final int INITIAL_CAPACITY = 16

  // LinkedHashMap加载因子

  private static final int LOAD_FACTOR = 0.75f

  // LinkedHashMap排序模式

  private static final boolean ACCESS_ORDER = true

  // 软引用缓存

  private static LinkedHashMap<String, SoftReference<Bitmap>>mSoftCache

  // 硬引用缓存

  private static LruCache<String, Bitmap>mLruCache

 

  public PanoMemoryCache() {

  // 获取单个进程可用内存的最大值

  // 方式一:使用ActivityManager服务(计量单位为M)

      /*int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass()*/

  // 方式二:使用Runtime类(计量单位为Byte)

      final int memClass = (int) Runtime.getRuntime().maxMemory()

      // 设置为可用内存的1/4(按Byte计算)

      final int cacheSize = memClass / 4

      mLruCache = new LruCache<String, Bitmap>(cacheSize) {

          @Override

          protected int sizeOf(String key, Bitmap value) {

              if(value != null) {

                  // 计算存储bitmap所占用的字节数

                  return value.getRowBytes() * value.getHeight()

              } else {

                  return 0

              }

          }

         

          @Override

          protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {

              if(oldValue != null) {

                  // 当硬引用缓存容量已满时,会使用LRU算法将最近没有被使用的图片转入软引用缓存

                  mSoftCache.put(key, new SoftReference<Bitmap>(oldValue))

              }

          }

      }

     

  /*

  * 第一个参数:初始容量(默认16)

  * 第二个参数:加载因子(默认0.75)

  * 第三个参数:排序模式(true:按访问次数排序;false:按插入顺序排序)

  */

      mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(INITIAL_CAPACITY, LOAD_FACTOR, ACCESS_ORDER) {

          private static final long serialVersionUID = 7237325113220820312L

          @Override

          protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>>eldest) {

              if(size() >SOFT_CACHE_SIZE) {

                  return true

              }

              return false

          }

      }

  }

 

  /**

   * 从缓存中获取Bitmap

   * @param url

   * @return bitmap

   */

  public Bitmap getBitmapFromMem(String url) {

      Bitmap bitmap = null

      // 先从硬引用缓存中获取

      synchronized (mLruCache) {

          bitmap = mLruCache.get(url)

          if(bitmap != null) {

              // 找到该Bitmap之后,将其移到LinkedHashMap的最前面,保证它在LRU算法中将被最后删除。

              mLruCache.remove(url)

              mLruCache.put(url, bitmap)

              return bitmap

          }

      }

      // 再从软引用缓存中获取

      synchronized (mSoftCache) {

          SoftReference<Bitmap>bitmapReference = mSoftCache.get(url)

          if(bitmapReference != null) {

              bitmap = bitmapReference.get()

              if(bitmap != null) {

                  // 找到该Bitmap之后,将它移到硬引用缓存。并从软引用缓存中删除。

                  mLruCache.put(url, bitmap)

                  mSoftCache.remove(url)

                  return bitmap

              } else {

                  mSoftCache.remove(url)

              }

          }

      }

      return null

  }

 

  /**

   * 添加Bitmap到内存缓存

   * @param url

   * @param bitmap

   */

  public void addBitmapToCache(String url, Bitmap bitmap) {

      if(bitmap != null) {

          synchronized (mLruCache) {

            mLruCache.put(url, bitmap) 

          }

      }

  }

 

  /**

   * 清理软引用缓存

   */

  public void clearCache() {

      mSoftCache.clear()

  mSoftCache = null

  }

}

      补充一点,由于4.0平台以后对SoftReference类引用的对象调整了回收策略,所以该类中的软引用缓存实际上没什么效果,可以去掉。2.3以前平台建议保留。

三、磁盘缓存类(PanoDiskCache)

五、使用decodeByteArray()还是decodeStream()?

      讲到这里,有童鞋可能会问我为什么使用BitmapFactory.decodeByteArray(data, 0, data.length, opts)来创建Bitmap,而非使用BitmapFactory.decodeStream(is, null, opts)。你这样做不是要多写一个静态方法readInputStream()吗?

      没错,decodeStream()确实是该使用情景下的首选方法,但是在有些情形下,它会导致图片资源不能即时获取,或者说图片被它偷偷地缓存起来,交 还给我们的时间有点长。但是延迟性是致命的,我们等不起。所以在这里选用decodeByteArray()获取,它直接从字节数组中获取,贴近于底层 IO、脱离平台限制、使用起来风险更小。

六、引入缓存机制后获取图片的方法

[java] view plain copy print ?

/**

   * 加载Bitmap

   * @param url

   * @return

   */

  private Bitmap loadBitmap(String url) {

      // 从内存缓存中获取,推荐在主UI线程中进行

      Bitmap bitmap = memCache.getBitmapFromMem(url)

      if(bitmap == null) {

          // 从文件缓存中获取,推荐在工作线程中进行

          bitmap = diskCache.getBitmapFromDisk(url)

          if(bitmap == null) {

              // 从网络上获取,不用推荐了吧,地球人都知道~_~

              bitmap = PanoUtils.downloadBitmap(this, url)

              if(bitmap != null) {

                  diskCache.addBitmapToCache(bitmap, url)

                  memCache.addBitmapToCache(url, bitmap)

              }

          } else {

              memCache.addBitmapToCache(url, bitmap)

          }

      }

      return bitmap

  }

七、工作线程池化

      有关多线程的切换问题以及在UI线程中执行loadBitmap()方法无效的问题,请参见另一篇博文: 使用严苛模式打破Android4.0以上平台应用中UI主线程的“独断专行”。

有关工作线程的处理方式,这里推荐使用定制线程池的方式,核心代码如下:

[java] view plain copy print ?

// 线程池初始容量

private static final int POOL_SIZE = 4

private ExecutorService executorService

@Override

public void onCreate(Bundle savedInstanceState) {

  super.onCreate(savedInstanceState)

  // 获取当前使用设备的CPU个数

  int cpuNums = Runtime.getRuntime().availableProcessors()

  // 预开启线程池数目

  executorService = Executors.newFixedThreadPool(cpuNums * POOL_SIZE)

  ...

  executorService.submit(new Runnable() {

      // 此处执行一些耗时工作,不要涉及UI工作。如果遇到,直接转交UI主线程

      pano.setImage(loadBitmap(url))

  })

  ...

}

      我们知道,线程构造也是比较耗资源的。一定要对其进行有效的管理和维护。千万不要随意而行,一张图片的工作线程不搭理也许没什么,当使用场景变为 ListView和GridView时,线程池化工作就显得尤为重要了。Android不是提供了AsyncTask吗?为什么不用它?其实 AsyncTask底层也是靠线程池支持的,它默认分配的线程数是128,是远大于我们定制的executorService。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/bake/11285231.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-15
下一篇 2023-05-15

发表评论

登录后才能评论

评论列表(0条)

保存