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

java.lang.OutofMemoryError:bitmap size exceeds VM budget

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

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;
}

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(1); 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 = (String) 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 = (int) (Runtime.getRuntime().maxMemory() / 1024);
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(context);
} private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
} private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
} /**
* 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(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
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(uri);
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(uri);
} return bitmap;
} private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
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(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
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(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
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()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
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(in);
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(in);
}
return bitmap;
} private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
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() == 1) {
sb.append('0');
}
sb.append(hex);
}
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 (long) stats.getBlockSize() * (long) 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;
}
}
}

优化列表的卡顿现象

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

  • 不要在getView中执行耗时操作。比如加载图片,肯定会导致卡顿,因为加载图片是一个耗时的操作,这种操作必须通过异步的方式来处理。
  • 控制异步任务的执行频率。比如在异步加载图片时,用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。那该如何解决呢,可以考虑在列表滑动的时候,停止加载图片,尽管这个过程是异步的,等列表停下来以后在加载图片仍然可以获得良好的用户体验。
  • 开启硬件加速可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated = "true"即可为Activity开启硬件加速。

阅读扩展

源于对掌握的Android开发基础点进行整理,罗列下已经总结的文章,从中可以看到技术积累的过程。

1,Android系统简介

2,ProGuard代码混淆

3,讲讲Handler+Looper+MessageQueue关系

4,Android图片加载库理解

5,谈谈Android运行时权限理解

6,EventBus初理解

7,Android 常见工具类

8,对于Fragment的一些理解

9,Android 四大组件之 " Activity "

10,Android 四大组件之" Service "

11,Android 四大组件之“ BroadcastReceiver "

12,Android 四大组件之" ContentProvider "

13,讲讲 Android 事件拦截机制

14,Android 动画的理解

15,Android 生命周期和启动模式

16,Android IPC 机制

17,View 的事件体系

18,View 的工作原理

19,理解 Window 和 WindowManager

20,Activity 启动过程分析

21,Service 启动过程分析

22,Android 性能优化

23,Android 消息机制

24,Android Bitmap相关

25,Android 线程和线程池

26,Android 中的 Drawable 和动画

27,RecylerView 中的装饰者模式

28,Android 触摸事件机制

29,Android 事件机制应用

30,Cordova 框架的一些理解

31,有关 Android 插件化思考

32,开发人员必备技能——单元测试

Bitmap的加载和Cache的更多相关文章

  1. Android开发艺术探索》读书笔记 (12) 第12章 Bitmap的加载和Cache

    第12章 Bitmap的加载和Cache 12.1 Bitmap的高速加载 (1)Bitmap是如何加载的?BitmapFactory类提供了四类方法:decodeFile.decodeResourc ...

  2. 《android开发艺术探索》读书笔记(十二)--Bitmap的加载和Cache

    接上篇<android开发艺术探索>读书笔记(十一)--Android的线程和线程池 No1: 目前比较常用的缓存策略是LruCache和DiskLruCache,LruCache常被用作 ...

  3. Android艺术——Bitmap高效加载和缓存代码分析(2)

    Bitmap的加载与缓存代码分析: 图片的压缩 比如有一张1024*768像素的图像要被载入内存,然而最终你要用到的图片大小其实只有128*96,那么我们会浪费很大一部分内存,这显然是没有必要的,下面 ...

  4. Android艺术——Bitmap高效加载和缓存(1)

    通过Bitmap我们可以设计一个ImageLoader,实现应该具有的功能是: 图片的同步加载:图片的异步加载:图片的压缩:内存缓存:磁盘缓存:网络获取: 1.加载 首先提到加载:BitmapFact ...

  5. 高效使用Bitmaps(一) 大Bitmap的加载

    转载:http://my.oschina.net/rengwuxian/blog/182885 高效使用Bitmaps有什么好处? 我们常常提到的“Android程序优化”,通常指的是性能和内存的优化 ...

  6. Bitmap的加载与缓存

    Android系统中图片一般用Bitmap对象表示,它支持png,jpg等常见格式.通常情况下图片的体积都比较大,单个应用允许使用的内存又是有限的,所以我们需要采取一些手段减少内存占用并提高加载速度. ...

  7. Android之Bitmap 高效加载

    一张图片(BitMap)占用的内存=图片长度*图片宽度*单位像素占用的字节数 图片格式(Bitmap.Config) 一张100*100的图片占用内存的大小 ALPHA_8 图片长度*图片宽度 100 ...

  8. 浅谈Android下的Bitmap之大Bitmap加载

    引言 我们常常提到的“Android程序优化”,通常指的是性能和内存的优化,即:更快的响应速度,更低的内存占用.Android程序的性能和内存问题,大部分都和图片紧密相关,而图片的加载在很多情况下很用 ...

  9. Android(java)学习笔记236:多媒体之加载大图片到内存(Bitmap API)

    1.Bitmap (API使用) android里面的bitmap中,一个像素点需要4个byte去表示,这是因为android表示颜色是" argb ":其中 a 表示是透明度,然 ...

随机推荐

  1. Centos7.2 编译安装PHP7

    PHP7,编译安装: 环境:centos7.2    (注意:因为我用的nginx, 此配置参数没有考虑到apache,所以不合适需要用apache的朋友照搬过去运行,但是可以参考.)   直接下载P ...

  2. java系列--HTTP协议

    一.HTTP请求信息 请求行 请求头 空行 消息体 1.防盗链: 枚举类型: 二.中文乱码问题 1.Get提交 String username = request.getParameter(" ...

  3. oracle系列--解锁数据库

    一.安装完成后解锁Scott数据库步骤: 进入SQL Plus 请输入用户名:sys输入口令:sys as sysdba  //这里的口令是不可见的,注意空格SQL> alter user sc ...

  4. nginx 重启

    /etc/init.d/nginx -s reload /etc/init.d/nginx reload

  5. Delphi在Vasta/win 7下通过UAC控制

    在Windows编程时,为了兼容Win7与XP,许多需要底层权限的程序需要通过UAC验证,这是微软出的一个很恶心的东西……还不如Ubuntu那种Root机制呢……但是有困难我们就要克服-现在有一种方式 ...

  6. [bzoj2120][数颜色] (暴力 or 分块)

    Description 墨墨购买了一套N支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问.墨墨会像你发布如下指令: 1. Q L R代表询问你从第L支画笔到第R支画笔中共有几种不同颜 ...

  7. js原生设计模式——12装饰者模式

    1.面向对象模式装饰者 <!DOCTYPE html><html lang="en"><head>    <meta charset=&q ...

  8. HTML 5 服务器发送事件、Input 类型、表单元素、表单属性

    HTML5 服务器发送事件(server-sent event)允许网页获得来自服务器的更新. Server-Sent 事件 - 单向消息传递 Server-Sent 事件指的是网页自动获取来自服务器 ...

  9. TortoiseSVN使用简介

    TortoiseSVN使用简介 2009-04-24 来源: dev.idv.tw 1.安装及下载client 端 2.什么是SVN(Subversion)? 3.为甚么要用SVN? 4.怎么样在Wi ...

  10. Error while registering Oracle JDBC Diagnosabilityh

    Error while registering Oracle JDBC Diagnosability 把ojdbc6.jar换成ojdbc14.jar就可以了: 后来发现又没有问题了:不过据说这个是数 ...