Android知识点<12>BitMap加载和Cache

1. Bitmap

bitmap是一张png、jpg等多种格式的图片,通过BitmapFactory的decodeFile、decodeResource、decodeStream、decodeByteArray四个方法分别从文件系统,资源,输入流以及字节数组中加载一个bitmap对象。这四类方法最终都在android的底层实现,对应着BitmapFactory的类的几个native方法。

2.高效加载

核心思想是通过BitmapFactory.Options来加载所需尺寸的图片。

例如,当我们通过ImageView来加载图片时,若ImageView的尺寸要小于图片的尺寸,此时将图片按原比例放入imageView中,并不会完全显示。通过BitmapFactory.Options按一定的采样率来缩小图片,将缩小后的图片放入ImageView中显示,这样减少了内存的开销,一定程度上避免了OOM,提高了bitmap加载性能。

通过BitmapFactory.Options来缩放图片,主要用到了inSampleSize参数。当参数的值为1时,采样的大小为原始图片大小;当inSampleSize值大于1时,图片将被缩小,假设inSampleSize = 2,图片的长宽均被缩小为原始比例的1/2,像素并为原来的1/4,因此所在内存同样变为原来的1/4。值得注意的是,只有当inSampleSize的值大于1时,图片才会被缩小,由于长、宽被同时作用,图片总是被缩小为inSampleSize的2次方倍,即inSampleSize = 4,则图片将会被缩小为原比例的1/16。当inSampleSize的值小于1时,无效。实际情况中,假如ImageView的大小为100*100像素,图片大小为200*200,此时只要将inSampleSize值设为2即可。如果图片为200*300呢?此时inSampleSize的值还是设置为2比较合适,如果设置成3,那么图片尺寸将远小于ImageView,导致图片将被拉升,从而变得模糊。

3. 如何获取图片的采样率呢?

1 将BitmapFactory.Options的inJustDecodeBounds参数设置为true; 

2. 从BitmapFactory.Options中取出原始图片的宽高,对应于onWidth、onHeight; 

3. 根据目标view所需大小结合采样率规则,计算出inSampleSize 值; 

4. 将BitmapFactory.Options的inJustDecodeBounds参数设置为false,重新加载图片

代码实现

private Bitmap setBitmapImage(Resources res, int id, int width, int height){
       BitmapFactory.Options options = new BitmapFactory.Options();
       options.inJustDecodeBounds = true;
       BitmapFactory.decodeResource(res,id,options);
 
       //获取inSampleSize值
       options.inSampleSize = getinSampleSize(options,width,height);
 
       options.inJustDecodeBounds = false;
       return BitmapFactory.decodeResource(res,id,options);
   }
 
   private int getinSampleSize(BitmapFactory.Options options, int width, int height) {
       int w = options.outWidth;
       int h = options.outHeight;
       int inSampleSize = 1;
       if (w > width || h > height){
           int halfw = w / 2;
           int halfh = h / 2;
           while ((halfw / inSampleSize) >= width && (halfh / inSampleSize) >= height){
               inSampleSize *= 2;
           }
       }
       return inSampleSize;
   }
经过上面的步骤,加载出来的图片就是缩放后的图片,当然也有可能不需要缩放。值得一提的是,当inJustDecodeBounds参数为true时,BitmapFactory只会去解析原始图片的宽高,并不会真正的去加载图片,并且该操作是轻量级的。在使用的时候,假如ImageView的大小为100*100,则:

mImageView.setImageBitmap(setBitmapImage(getResources(),R.id.image,100,100));


Bitmap的recycler()相关内容 :

recycle过程:

java端:

    mBuffer = null;

native端:

    Caches::getInstance().textureCache.removeDeferred(bitmap);
        fPixelRef->unref();   // fPixelRef是上面分配的SkPixelRef
        fPixelRef = NULL;

这里其实就是java端将mBuffer置为垃圾。native端释放SkPixelRef,并延迟删除其对应的TextureCache(最终的删除应该是在下一帧开始前)。


尽快的调用recycle是个好习惯,会释放与其相关的native分配的内存;但一般情况下其图像数据是在JVM里分配的,调用recycle并不会释放这部分内存。

我们用createBitmap创建的Bitmap且没有被硬件加速Canvas draw过,则主动调用recycle产生的意义比较小,仅释放了native里的SkPixelRef的内存,这种情况我觉得可以不主动调用recycle。

被硬件加速Canvas draw过的由于有TextureCache应该尽快调用recycle来尽早释放其TextureCache。

像截屏这种不是在JVM里分配内存的情况也应该尽快调用recycle来马上释放其图像数据。

(一个例外,如果是通过Resources.getDrawable得到的Bitmap,不应该调用recycle,因为它可能会被重用)



缓存 :

在应用的开发过程中,经常会涉及到网络请求,以获取图片、视频等资源。但是对于移动设备来说,数据流量的消耗对用户来说是很关心的,

如何为用户减少网络请求,节省流量消耗,显得很有必要。因此,就引入了缓存的概念。

以图片为例,当用户第一次网络获取图片后,通过缓存机制将图片存储到存储设备上,下次需要再次用到该图片时,直接从存储设备获取,

不需要网络获取。多数情况下,为了提高应用的交互性,通常会将部分图片存储到内存中,再次使用时直接从内存获取,

这样速度快于从存储设备、网络中获取。

上面引入了缓存机制,那么具体怎么实现呢?针对缓存机制,有不同的缓存策略,各种策略间并没有统一的标准,

但基本包括添加、获取以及删除操作。添加和获取比较好理解,但是为什么还要删除呢?

我们都知道,对于手机等移动设备来说,硬件是有限的,特别是存储能力,不可能达到无穷大。

因此,在进行缓存时,当缓存容量满时,需要进行旧缓存的删除,以便进行新缓存。

如何进行新旧文件的替换,这里就需要缓存算法的支持。

目前最常用的缓存算法为近期最少使用算法LRU,核心思想为当缓存满时,会优先淘汰那些近期最少使用的缓存对象。

采用Lru缓存算法的缓存机制有两种:LruCache和DiskLruCache,LruCache实现内存缓存,DiskLruCache实现储存设备缓存。

LruCache

LruCache是一个泛型类,内部采用LinkedHashMap以强引用的方式存储外部缓存对象,提供get和put方法来实现缓存对象的获取和添加,当缓存满时,LruCache会移除较早使用的缓存对象,加入新的缓存对象。介绍下几个概念:

1、强引用:直接的对象引用

2、软引用:当一个对象只有软引用存在时,内存资源不足时,此对象会被gc回收;

3、 弱引用:当一个对象只有弱引用存在时,此对象随时会被gc回收;

创建LruCache代码 :

int maxMemory = ( int ) (Runtime.getRuntime().maxMemory() / 1024 );
int memory = maxMemory / 8 ;
mImageCache = new LruCache<string, bitmap= "" >(memory){
     @Override
     protected int sizeOf(String key, Bitmap value) {
         return value.getRowBytes()*value.getHeight() / 1024 ;
     }
};

 常用方法:

mImageCache.put(key,value);
mImageCache.get(key);
mImageCache.remove(key);

DisLruCache

DisLruCache用于实现磁盘缓存,通过将缓存对象写入文件系统而实现缓存的效果。值得注意的是,虽然DisLruCache得到了Android官方文档的推荐,但其并不包括在SDK中。当我们需要使用DisLruCache是,需要手动下载源文件,并引入项目中,下载地址:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java
如果不能访问可以进行下载。下面分别从DiskLruCache的创建、添加以及查找来描述其使用。

1、DiskLruCache的创建

DiskLruCache的创建并不能通过构造方法实现,

public static DiskLruCache open(File directory, int appVersion, int value)

示例代码:

File dir = new File( "/sdcard/Android/data/com.example.huangzheng.bitmaptest/cache" );
int maxSize = 1024 * 1024 * 50 ;
if (!dir.exists()){
     dir.mkdir();
}
try {
     mDiskCache = DiskLruCache.open(dir, 1 , 1 ,maxSize);
} catch (IOException e) {
     e.printStackTrace();
}
DiskLruCache的缓存添加
还是一样的,我们以网络获取一张图片,并写入磁盘缓存为例。首先通过下面的代码获取网络图片,并通过OutputStream写入本地。
//网络获取图片,并写入输出流
     private boolean downloadUrlToStream(String urlstring, OutputStream outputstream){
         HttpURLConnection httpconnection = null ;
         BufferedOutputStream out = null ;
         BufferedInputStream in = null ;
         try {
             final URL url = new URL(urlstring);
             httpconnection = (HttpURLConnection) url.openConnection();
             in = new BufferedInputStream(httpconnection.getInputStream(), 8 * 1024 );
             out = new BufferedOutputStream(outputstream, 8 * 1024 );
             int b;
             if ((b = in.read()) != - 1 ){
                 out.write(b);
             }
             return true ;
         } catch (IOException e) {
             e.printStackTrace();
         } finally {
             if (httpconnection != null ){
                 httpconnection.disconnect();
             }
             try {
                 if (out != null ){
                     out.close();
                 }
                 if (in != null ){
                     in.close();
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         return false ;
     }
DiskLruCache的写入操作是通过其Editor 类实现,Editor 不能new,需要调用DiskLruCache的edit方法来创建。
public Editor edit(String key) throws IOException {
         return edit(key, ANY_SEQUENCE_NUMBER);
     }

从edit方法可以看到,需要传入key值,通常情况下图片的url中包括一些特殊字符,比较常规的做法是通过MD5进行编码,保证字符串的唯一性,并且所有字符都在0-F之间,再写入:

//MD5转换图片url key
    private String hashKeyForDisk(String key){
        String cacheKey = null ;
        try {
            MessageDigest md = MessageDigest.getInstance( "MD5" );
            md.update(key.getBytes());
            cacheKey = bytesToHexString(md.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return cacheKey;
    }
 
    private String bytesToHexString( byte [] bytes) {
        StringBuilder sb = new StringBuilder();
        for ( int i = 0 ; i < bytes.length; i++) {
            String hex = Integer.toHexString( 0xFF & bytes[i]);
            if (hex.length() == 1 ) {
                sb.append( '0' );
            }
            sb.append(hex);
        }
        return sb.toString();
    }




上面的准备做完,有了DiskLruCache.Editor实例后,通过它的newOutputStream方法获取OutputStream,
并传入downloadUrlToStream方法实现下载和缓存,最后通过commit()进行提交,
如果downloadUrlToStream返回false,则通过abort()方法进行中断。


new Thread( new Runnable() {
             @Override
             public void run() {
                 String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg" ;
                 String key = hashKeyForDisk(imageUrl);
                 try {
                     DiskLruCache.Editor edit = mDiskCache.edit(key);
                     OutputStream out = edit.newOutputStream( 0 );
                     if (downloadUrlToStream(imageUrl,out)){
                         edit.commit();
                     } else {
                         edit.abort();
                     }
                     mDiskCache.flush();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }).start();

public File getDiskCacheDir(Context context, String uniqueName) {
         String cachePath;
         if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                 || !Environment.isExternalStorageRemovable()) {
             cachePath = context.getExternalCacheDir().getPath();
         } else {
             cachePath = context.getCacheDir().getPath();
         }
         return new File(cachePath + File.separator + uniqueName);
     }

DiskLruCache的缓存读取

缓存成功后,从缓存文件里读取缓存相对就比较简单了。通过DiskLruCache.Snapshot读取缓存,同样的,Snapshot不可以直接new,需要DiskLruCache的get(key)方法获取实例,最后由Snapshot的getInputStream()方法获取缓存文件的输入流,BitmapFactory将输入流转换成bitmap。


try {
     DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
     if (snapshot != null ){
          InputStream is = snapshot.getInputStream( 0 );
          Bitmap img= BitmapFactory.decodeStream(is);
          mImageView.setImageBitmap(img);
      }
  } catch (IOException e) {
      e.printStackTrace();
  }


关于列表卡顿现象的优化 :

1. 在getView中不要执行耗时操作

2. 控制异步执行的频率,比如在列表滚动的时候,停止加载图片。


LRU算法的实现 : 参考 : http://flychao88.iteye.com/blog/1977653

1. 使用一个链表保存缓存数据

2.   LRU-K

3. Two queues(2Q)
4. Multi Queue(MQ)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值