Android开辟方式探求,Bitmap的加载和Cache

日期:2019-09-07编辑作者:编程应用
  1. BitmapFactory类提供四种方法:decodeFile、decodeResource、decodeStream和decodeByteArray;其中decodeFile和decodeResource间接的调用了decodeStream方法;这四个方法最终在Android底层实现。
  2. 如何高效的加载Bitmap?核心思想:按需加载;很多时候ImageView并没有原始图片那么大,所以没必要加载原始大小的图片。采用BitmapFactory.Options来加载所需尺寸的图片。 通过BitmapFactory.Options来缩放图片,主要是用到了它的inSampleSize参数,即采样率。 inSampleSize应该为2的指数,如果不是系统会向下取整并选择一个最接近2的指数来代替;缩放比例为1/(inSampleSize的二次方)。
  3. Bitmap内存占用:拿一张10241024像素的图片来说,假定采用ARGB8888格式存储,那么它占用的内存为10241024*4,即4MB。
  4. 缩放图片

由于Bitmap的特殊性以及Android对单个应用所施加的内存限制,比如16M,这导致加载Bitmap的时候很容易出现内存溢出。比如以下场景:

java.lang.OutofMemoryError:bitmap size exceeds VM budget
 public static Bitmap decodeBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); //1. 将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片。 options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); //2. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。 options.inSampleSize = calcuateInSampleSize(options, reqWidth, reqHeight); //3. 将BitmapFactory.Options的inJustDecodeBounds参数设置为false,然后重新加载图片。 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } //获取采样率 private static int calcuateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { int halfHeight = height / 2; int halfWidth = width / 2; while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } return inSampleSize; }// 显示图片Bitmap bitmap = DecodeBitmap.decodeBitmapFromResource(getResources(), R.mipmap.haimei2, 400, 400);imageView.setImageBitmap;

Android中常用的缓存策略也是很有意思,缓存策略一个通用的思想,可以用到很多场景中,比如在实际开发中经常需要用到Bitmap做缓存。通过缓存策略,我们不需要每次都从网络上请求图片或者从存储设备中加载图片,这样就极大地提高了图片的加载效率以及产品的用户体验。目前比较常用的缓存策略是LruCache和DiskLruCache,其中LruCache常被用做内存缓存,而DiskLruCache用做存储缓存。Lru是Least Recently Used的缩写,即最近最少使用算法,这种算法的核心思想:当缓存快满时,会淘汰近期最少使用的缓存目标,很显然Lru算法的思想是很容易被接受的。

当inJustDecodeBounds参数为true时,BitmapFactory只会解析图片的原始宽/高信息,并不会真正的加载图片。需要注意这时候BitmapFactory获取的图片宽/高信息和图片的位置与程序运行的设备有关。

Bitmap的高效加载

Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。

如何高效地加载Bitmap呢,其实核心思想也简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。主要是用到它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小,当inSampleSize大于1时,比如为2,那么采样后的图片其宽/宽均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。从最新官方文档中指出,inSampleSize的取值应该是2的指数,比如1、2、4、8、16等等。

通过采样率即可有效地加载图片,那么到底如何获取采样率呢,获取采样率也很简单,循序如下流程:

  • 将BitmapFactory.Options的inJustDecodeBounds参数设为True并加载图片
  • 从BitmapFactory.Options中取出图片的原始宽高信息,他们对应于outWidth和outHeight参数
  • 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
  • 将BitmapFactory.Options的inJustDecodeBounds参数设为False,然后重新加载图片。

经过上面4个步骤,加载出的图片就是最终缩放后的图片,当然也有可能不需要缩放。代码如下:

 public 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); } public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { if (reqWidth == 0 || reqHeight == 0) { return 1; } // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; Log.d(TAG, "origin, w= " + width + " h=" + height); 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; } } Log.d(TAG, "sampleSize:" + inSampleSize); return inSampleSize; }
  1. 如何减少流量消耗?缓存。当程序第一次从网络上加载图片后,将其缓存在存储设备中,下次使用这张图片的时候就不用再从网络从获取了。一般情况会把图片存一份到内存中,一份到存储设备中,如果内存中没找到就去存储设备中找,还没有找到就从网络上下载。
  2. 目前常用的缓存算法是LRU,是近期最少使用算法,当缓存满时,优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LRUCache和DiskLruCache。
  3. LruCache是Android3.1所提供的一个缓存类,通过support-v4兼容包可以兼容到早期的Android版本。LruCache是一个泛型类,是线程安全的,内部采用LinkedHashMap以强引用的方式存储外界缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早的使用的缓存对象。LruCache初始化时需重写sizeOf方法,用于计算缓存对象的大小。

Android中的缓存策略

缓存策略在Android中有着广泛的使用场景,尤其在图片加载这个场景下,缓存策略变得更为重要。有一个场景就是批量下载网络图片,在PC上是可以把所有的图片下载到本地再显示即可,但是放到移动设备上就不一样了。不管是Android还是IOS设备,流量对于用户来说都是一种宝贵的资源。

如何避免过多的流量消耗呢,那就是缓存。当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不用从网络上获取了,这样就为用户节省了流量。很多时候为了提高用户的用户体验,往往还会把图片在内存中再缓存一份,这样当应用打算从网络上请求一张图片时,程序首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。因为从内存中加载图片比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。

目前常用的一种缓存算法是LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这二者的完美结合,就可以很方便地实现一个具有很高实用价值的ImageLoader。

LruCache

LruCache是Android 3.1提供的一个缓存类,通过support-v4兼容包可以兼容到早期的Android版本。它是一个泛型类,它内部采用一个LinkedHashMap,当强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。

  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

LruCache是线程安全的,因为用到了LinkedHashMap。从Android 3.1开始,LruCache就已经是Android源码的一部分。

DiskLruCache

DiskLruCache用于实现存储设备缓存,即磁盘存储,它通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache得到了Android官方文档的推荐,但它不属于Android SDK的一部分。

ImageLoader的实现

一般来说,一个优秀的ImageLoader应该具备如下功能:

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取

图片的同步加载是指能够以同步的方式向调用者提供所加载的图片,这个图片可能是从内存缓存读取的,也可能是从磁盘缓存中读取的,还可能是从网络拉取的。

图片的异步加载是一个很有用的功能,很多时候调用者不想再单独的线程中以同步的方式来获取图片,这个时候ImageLoader内部需要自己在线程中加载图片并将图片设置所需的ImageView。图片压缩的作用更需要了,这是降低OOM概率的有效手段,ImageLoader必须合适地处理图片的压缩问题。

内存缓存和磁盘缓存是ImageLoader的核心,也是ImageLoader的意义所在,通过这两级缓存极大地提高了程序的效率并且有效地降低了对用户所造成的流量消耗,只有当这两级缓存都不可用时才需要从网络中拉取图片。

一个实现ImageLoader的例子:

public class ImageLoader { private static final String TAG = "ImageLoader"; public static final int MESSAGE_POST_RESULT = 1; private static final int CPU_COUNT = Runtime.getRuntime() .availableProcessors(); private static final int CORE_POOL_SIZE = CPU_COUNT + 1; private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; private static final long KEEP_ALIVE = 10L; private static final int TAG_KEY_URI = R.id.imageloader_uri; private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; private static final int IO_BUFFER_SIZE = 8 * 1024; private static final int DISK_CACHE_INDEX = 0; private boolean mIsDiskLruCacheCreated = false; private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger; public Thread newThread(Runnable r) { return new Thread(r, "ImageLoader#" + mCount.getAndIncrement; } }; public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), sThreadFactory); private Handler mMainHandler = new Handler(Looper.getMainLooper { @Override public void handleMessage(Message msg) { LoaderResult result = (LoaderResult) msg.obj; ImageView imageView = result.imageView; imageView.setImageBitmap(result.bitmap); String uri =  imageView.getTag(TAG_KEY_URI); if (uri.equals(result.uri)) { imageView.setImageBitmap(result.bitmap); } else { Log.w(TAG, "set image bitmap,but url has changed, ignored!"); } }; }; private Context mContext; private ImageResizer mImageResizer = new ImageResizer(); private LruCache<String, Bitmap> mMemoryCache; private DiskLruCache mDiskLruCache; private ImageLoader(Context context) { mContext = context.getApplicationContext(); int maxMemory =  (Runtime.getRuntime().maxMemory; int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; File diskCacheDir = getDiskCacheDir(mContext, "bitmap"); if (!diskCacheDir.exists { diskCacheDir.mkdirs(); } if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) { try { mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE); mIsDiskLruCacheCreated = true; } catch (IOException e) { e.printStackTrace(); } } } /** * build a new instance of ImageLoader * @param context * @return a new instance of ImageLoader */ public static ImageLoader build(Context context) { return new ImageLoader; } private void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache == null) { mMemoryCache.put(key, bitmap); } } private Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get; } /** * load bitmap from memory cache or disk cache or network async, then bind imageView and bitmap. * NOTE THAT: should run in UI Thread * @param uri http url * @param imageView bitmap's bind object */ public void bindBitmap(final String uri, final ImageView imageView) { bindBitmap(uri, imageView, 0, 0); } public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) { imageView.setTag(TAG_KEY_URI, uri); Bitmap bitmap = loadBitmapFromMemCache; if (bitmap != null) { imageView.setImageBitmap; return; } Runnable loadBitmapTask = new Runnable() { @Override public void run() { Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight); if (bitmap != null) { LoaderResult result = new LoaderResult(imageView, uri, bitmap); mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget(); } } }; THREAD_POOL_EXECUTOR.execute(loadBitmapTask); } /** * load bitmap from memory cache or disk cache or network. * @param uri http url * @param reqWidth the width ImageView desired * @param reqHeight the height ImageView desired * @return bitmap, maybe null. */ public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) { Bitmap bitmap = loadBitmapFromMemCache; if (bitmap != null) { Log.d(TAG, "loadBitmapFromMemCache,url:" + uri); return bitmap; } try { bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight); if (bitmap != null) { Log.d(TAG, "loadBitmapFromDisk,url:" + uri); return bitmap; } bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight); Log.d(TAG, "loadBitmapFromHttp,url:" + uri); } catch (IOException e) { e.printStackTrace(); } if (bitmap == null && !mIsDiskLruCacheCreated) { Log.w(TAG, "encounter error, DiskLruCache is not created."); bitmap = downloadBitmapFromUrl; } return bitmap; } private Bitmap loadBitmapFromMemCache(String url) { final String key = hashKeyFormUrl; Bitmap bitmap = getBitmapFromMemCache; return bitmap; } private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper { throw new RuntimeException("can not visit network from UI Thread."); } if (mDiskLruCache == null) { return null; } String key = hashKeyFormUrl; DiskLruCache.Editor editor = mDiskLruCache.edit; if (editor != null) { OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); if (downloadUrlToStream(url, outputStream)) { editor.commit(); } else { editor.abort(); } mDiskLruCache.flush(); } return loadBitmapFromDiskCache(url, reqWidth, reqHeight); } private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException { if (Looper.myLooper() == Looper.getMainLooper { Log.w(TAG, "load bitmap from UI Thread, it's not recommended!"); } if (mDiskLruCache == null) { return null; } Bitmap bitmap = null; String key = hashKeyFormUrl; DiskLruCache.Snapshot snapShot = mDiskLruCache.get; if (snapShot != null) { FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX); FileDescriptor fileDescriptor = fileInputStream.getFD(); bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight); if (bitmap != null) { addBitmapToMemoryCache(key, bitmap); } } return bitmap; } public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read { out.write; } return true; } catch (IOException e) { Log.e(TAG, "downloadBitmap failed." + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } MyUtils.close; MyUtils.close; } return false; } private Bitmap downloadBitmapFromUrl(String urlString) { Bitmap bitmap = null; HttpURLConnection urlConnection = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); bitmap = BitmapFactory.decodeStream; } catch (final IOException e) { Log.e(TAG, "Error in downloadBitmap: " + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } MyUtils.close; } return bitmap; } private String hashKeyFormUrl(String url) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance; mDigest.update(url.getBytes; cacheKey = bytesToHexString(mDigest.digest; } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(url.hashCode; } 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 { sb.append; } sb.append; } return sb.toString(); } public File getDiskCacheDir(Context context, String uniqueName) { boolean externalStorageAvailable = Environment .getExternalStorageState().equals(Environment.MEDIA_MOUNTED); final String cachePath; if (externalStorageAvailable) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } @TargetApi(VERSION_CODES.GINGERBREAD) private long getUsableSpace(File path) { if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) { return path.getUsableSpace(); } final StatFs stats = new StatFs(path.getPath; return  stats.getBlockSize() *  stats.getAvailableBlocks(); } private static class LoaderResult { public ImageView imageView; public String uri; public Bitmap bitmap; public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) { this.imageView = imageView; this.uri = uri; this.bitmap = bitmap; } }}
  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在的时候,系统内存不足的时此对象会被GC回收。
  • 弱引用 : 当一个对象只有弱引用存在的时候,此对象随时会被GC回收。

优化列表的卡顿现象

在一般ListView或者GridView中,使用照片墙的时候,容易出现滑动卡顿,如何优化呢,有三点建议:

  • 不要在getView中执行耗时操作。比如加载图片,肯定会导致卡顿,因为加载图片是一个耗时的操作,这种操作必须通过异步的方式来处理。
  • 控制异步任务的执行频率。比如在异步加载图片时,用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。那该如何解决呢,可以考虑在列表滑动的时候,停止加载图片,尽管这个过程是异步的,等列表停下来以后在加载图片仍然可以获得良好的用户体验。
  • 开启硬件加速可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated = "true"即可为Activity开启硬件加速。
  1. DiskLruCache用于实现磁盘缓存,DiskLruCache得到了Android官方文档推荐,但它不属于Android SDK的一部分源码在这里
  2. 自己实现一个ImageLoader,包含
  3. 图片压缩功能
  4. 内存缓存和磁盘缓存
  5. 同步加载和异步加载的接口设计

详细请看主席的随书源码

  1. 实现照片墙效果,如果图片都需要是正方形;这样做很快,自定义一个ImageView,重写onMeasure方法。
 @Override protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){ super.onMeasure(widthMeasureSpec,widthMeasureSpec); }
  1. 优化列表卡顿现象
  2. 不要在getView中执行耗时操作,不要在getView中直接加载图片。
  3. 控制异步任务的执行频率:如果用户刻意频繁上下滑动,getView方法会不停调用,从而产生大量的异步任务。可以考虑在列表滑动停止加载图片;给ListView或者GridView设置setOnScrollListener并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是的话就停止加载图片。
  4. 大部分情况下,可以使用硬件加速解决莫名卡顿问题,通过设置android:hardwareAccelerated="true"即可为Activity开启硬件加速。

本文由今晚最快开奖现场直播发布于编程应用,转载请注明出处:Android开辟方式探求,Bitmap的加载和Cache

关键词:

Android难题之——自定义View(上)

1、每个Activity包含一个Window对象(Window是abstract的,一般是由PhoneWindow实现)。2、PhoneWindow将DecorView设置为整个应用的...

详细>>

热修复热点框架简析,Android插件框架机制研商

笔者 百度任玉刚 插件化框架 途牛已上线使用帮衬代码能源文件以插件情势加入到host。 插件化的主干形式是将二个...

详细>>

阅读笔记,线程安全性

首先使代码正确运行,然后再提高代码速度。【正确编写并发程序的方法】 线程安全:当多个线程访问某个类(对象...

详细>>

Android开发艺术探索,Bitmap获取缩略图

前言 追思了下此前写的调用相机和相册的机能,准备把它们构成下,想起已经用Motorola在赢得大图时OOM的主题素材,...

详细>>