文章目录
Bitmap
Bitmap 简介
- 基本信息
- 简介
Bitmap位图包括像素以及长、宽、颜色等描述信息。长宽和像素位数是用来描述图片的,可以通过这些信息计算出图片的像素占用内存的大小。
位图可以理解为一个画架,把图放到上面然后可以对图片做一些列的处理。
位图文件图像显示效果好,但是非压缩格式,需要占用较大的存储空间。 - Config 图片像素类型
图片像素类型 | 解释 | 适用场景 |
---|---|---|
ARGB_8888 | 四个通道都是8位,每个像素占用4个字节,图片质量是最高的,但是占用的内存也是最大的 | 既要设置透明度,对图片质量要求又高,就用ARGB_8888 |
ARGB_4444 | 四个通道都是4位,每个像素占用2个字节,图片的失真比较严重 | ARGB_4444失真严重,基本不用 |
RGB_565 | 没有A通道,每个像素占用2个字节,图片失真小,但是没有透明度 | 不需要设置透明度,RGB_565是个不错的选择 |
ALPHA_8 | 只有A通道,每个像素占用1个字节大大小,只有透明度,没有颜色值 | ALPHA_8使用场景特殊,比如设置遮盖效果等 |
- CompressFormat 图片压缩格式
图片压缩格式 | 解释 | 文件格式 | 优点 | 缺点 |
---|---|---|---|---|
JPEG | 一种有损压缩(JPEG2000既可以有损也可以无损) | .jpg 或者 .jpeg | 采用了直接色,有丰富的色彩,适合存储照片和生动图像效果 | 有损,不适合用来存储logo、线框类图 |
PNG | 一种无损压缩 | .png | 支持透明、无损,主要用于小图标,透明背景等 | 若色彩复杂,则图片生成后文件很大 |
- 加载
BitmapFactory提供了四类方法:decodeFile、decodeResource、decodeStream、decodeByteArray
- 从文件中读取
try {
FileInputStream in = new FileInputStream("/sdcard/Download/sample.png");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Bitmap bitmap = BitmapFactory.decodeStream(in);
Bitmap bm = BitmapFactory.decodeFile(sd_path); // 间接调用 BitmapFactory.decodeStream
- 从资源中读取
Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.sample); // 间接调用 BitmapFactory.decodeStream
- 从字节序列中读取
// InputStream转换成byte[]
Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);
- 巨图加载:BitmapRegionDecoder,可以按照区域进行加载
- 存储
根据android sdk版本有所不同。
- 2.3以前
图片像素存储在native内存中。缺点是虚拟机无法自动进行垃圾回收,必须手动使用recycle,很容易导致内存泄露。也不方便调试等; - 3.0以后
图片像素存储在Java堆中,垃圾回收能够自动进行,内存占用也能方便的展示在monitor中; - 4.0以后
传输方式发生变化,大数据会通过ashmem(匿名共享内存)来传递(不占用Java内存),小数据通过直接拷贝的方式(在内存中操作),放宽了图片大小的限制; - 6.0以后
加强了ashmen存储图片的方式
- 绘图:Paint & Canvas & Bitmap
Bitmap可以理解为画架或者画布,它是像素的集合,是色彩的表现和承载者;
Canvas可以理解为画家的各种操作,通过操作Paint在Bitmap上进行创作;
Paint可以理解为画笔,可以自定义各种色彩等。
Android利用canvas画各种图形
Canvas.drawBitmap 贴图
/*
* @param
* bitmap 位图
* left 绘制区域距离左边界偏移量
* top 绘制区域距离上边界偏移量
* paint 画笔
* 在View中指定位置绘制bitmap
* 注:传入的参数中的偏移量是指对于View的偏移。
*/
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top,
@Nullable Paint paint)
/*
* @param
* bitmap 位图
* src bitmap需要绘制的面积,若src的面积小于bitmap时会对bitmap进行裁剪,
* 一般来说需要绘制整个bitmap时可以为null
* dst 在画布中指定绘制bitmap的位置,当这个区域的面积与bitmap要显示的面积不匹配时,
* 会进行缩放,不可为null
* paint 画笔
* 在指定位置绘制指定大小的bitmap
*/
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
@Nullable Paint paint)
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
@Nullable Paint paint)
/*
* @param
* bitmap 位图
* matrix 当绘制位图时需要转变时使用的矩阵
* paint 画笔
* 使用指定的矩阵绘制位图
*/
public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix,
@Nullable Paint paint)
Matrix 实现基本变换
// 定义矩阵
Matrix matrix = new Matrix();
// 【缩放图像】
matrix.postScale(0.8f, 0.9f);
// 【向左旋转】
matrix.postRotate(-90);
// 【移动图像】
matrix.postTranslate(100, 100);
// 【裁减图像】
Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)
canvas绘制圆角矩形
// 准备画笔
Paint paint = new Paint();
paint.setAntiAlias(true);
// 准备裁剪的矩阵
Rect rect = new Rect(0, 0, originBitmap.getWidth(), originBitmap.getHeight());
RectF rectF = new RectF(new Rect(0, 0, originBitmap.getWidth(), originBitmap.getHeight()));
Bitmap roundBitmap = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(roundBitmap);
// 圆角矩阵,radius为圆角大小
canvas.drawRoundRect(rectF, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(originBitmap, rect, rect, paint);
- 开源框架
- Picasso包体积小、清晰,但功能有局限不能加载gif、只能缓存全尺寸;
- Glide功能全面,擅长大型图片流,提交较大;
- Fresco内存优化,减少oom,体积更大
Bitmap 导致OOM 原因 & 性能优化
Bitmap 压缩策略
- 更换图片格式
Android目前常用的图片格式有png,jpeg和webp,
- png:无损压缩图片格式,支持Alpha通道,Android切图素材多采用此格式
- jpeg:有损压缩图片格式,不支持背景透明,适用于照片等色彩丰富的大图压缩,不适合logo
- webp:是一种同时提供了有损压缩和无损压缩的图片格式,派生自视频编码格式VP8,从谷歌官网来看,无损webp平均比png小26%,有损的webp平均比jpeg小25%~34%,无损webp支持Alpha通道,有损webp在一定的条件下同样支持,有损webp在Android4.0(API 14)之后支持,无损和透明在Android4.3(API18)之后支持
采用webp能够在保持图片清晰度的情况下,可以有效减小图片所占有的磁盘空间大小
- 质量压缩
质量压缩并不会改变图片在内存中的大小,仅仅会减小图片所占用的磁盘空间的大小,因为质量压缩不会改变图片的分辨率,而图片在内存中的大小是根据widthheight一个像素的所占用的字节数计算的,宽高没变,在内存中占用的大小自然不会变,质量压缩的原理是通过改变图片的位深和透明度来减小图片占用的磁盘空间大小,所以不适合作为缩略图,可以用于想保持图片质量的同时减小图片所占用的磁盘空间大小。另外,由于png是无损压缩,所以设置quality无效
originBitmap.compress(format, quality, bos);
- 采样率压缩
采样率压缩是通过设置BitmapFactory.Options.inSampleSize,来减小图片的分辨率,进而减小图片所占用的磁盘空间和内存大小。
设置的inSampleSize会导致压缩的图片的宽高都为1/inSampleSize,整体大小变为原始图片的inSampleSize平方分之一,当然,这些有些注意点:
1、inSampleSize小于等于1会按照1处理
2、inSampleSize只能设置为2的平方,不是2的平方则最终会减小到最近的2的平方数,如设置7会按4进行压缩,设置15会按8进行压缩。
options.inSampleSize = inSampleSize;
Bitmap resultBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);
- 缩放压缩
通过减少图片的像素来降低图片的磁盘空间大小和内存大小,可以用于缓存缩略图
实现方式如下:
Bitmap bitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
//设置缩放比
int radio = 8;
Bitmap result = Bitmap.createBitmap(bitmap.getWidth() / radio, bitmap.getHeight() / radio, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
RectF rectF = new RectF(0, 0, bitmap.getWidth() / radio, bitmap.getHeight() / radio);
//将原图画在缩放之后的矩形上
canvas.drawBitmap(bitmap, null, rectF, null);
大图加载:从网络加载一个10M的图片,说下注意事项?
Android高效加载大图、多图解决方案,有效避免程序OOM
由于Android加载大图时容易导致OOM,所以应该对大图的加载单独处理,共有3点需要注意:
- 图片压缩
由于图片的分辨率比手机屏幕分辨率高很多,因此应该根据ImageView控件大小对高分辨率的图片进行适当的压缩,防止OOM出现。 - 分块加载
如果图片尺寸过大,但指向获取图片的某一小块区域时,可以对图片分块加载。适用于地图绘制的场景。在Android中BitmapRegionDecoder类的功能就是加载一张图片的指定区域。
// 创建实例
mDecoder = BitmapRegionDecoder.newInstance(mFile.getAbsolutePath(), false);
// 获取原图片宽高
mDecoder.getWidth();
mDecoder.getHeight();
// 加载(10, 10) - (80, 80) 区域内原始精度的Bitmap对象
Rect rect = new Rect(10, 10, 80, 80);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap bitmap = mDecoder.decodeRegion(rect, options);
// 回收释放Native层内存
mDecoder.recycle();
- 图片三级缓存机制——可以让组件快速地重新加载和处理图片,避免网络加载的性能损耗
图片的三级缓存机制是指加载图片时,分别访问内存、文件和网络而获取图片数据的机制。
- 一级:内存缓存LruCache
LruCache是Android提供的一个缓存工具类,采用最近最少使用算法。把最近使用的对象用强引用存储在LinkedHashMap中,并把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
Android先访问内存,如果内存中没有缓存数据,则访问缓存文件。 - 二级:文件缓存
DiskLruCache是缓存工具类,存储位置是外存。
缓存数据的存储路径优先考虑SD卡的缓存目录,在SD卡下新建一个缓存文件用来存储缓存数据。若缓存文件中没有缓存数据,则联网加载图片。 - 三级:联网加载
通过网络请求加载网络图片,并将图片数据保存到内存和缓存文件中。
说一下三级缓存的原理?
/**
* 从缓存(内存缓存,磁盘缓存)中获取Bitmap
*/
@Override
public Bitmap getBitmap(String url) {
if (mLruCache.get(url) != null) {
// 从LruCache缓存中取
Log.i(TAG,"从LruCahce获取");
return mLruCache.get(url);
} else {
String key = MD5Utils.md5(url);
try {
if (mDiskLruCache.get(key) != null) {
// 从DiskLruCahce取
Snapshot snapshot = mDiskLruCache.get(key);
Bitmap bitmap = null;
if (snapshot != null) {
bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0));
}
else{
bitmap = HttpUtils.getImageFromNet(url);
}
// 存入缓存
putBitmap(url, bitmap);
return bitmap;
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 存入缓存(内存缓存,磁盘缓存)
*/
@Override
public void putBitmap(String url, Bitmap bitmap) {
// 存入LruCache缓存
mLruCache.put(url, bitmap);
// 判断是否存在DiskLruCache缓存,若没有存入
String key = MD5Utils.md5(url);
try {
if (mDiskLruCache.get(key) == null) {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (bitmap.compress(CompressFormat.JPEG, 100, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
LruCache & DiskLruCache原理?
- LruCache 内存缓存
LruCache是android提供的一个缓存工具类(android-support-v4包),其算法是LRU(最近最少使用)算法。
它把最近使用的对象用“强引用”存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前就从内存中移除。
适用于缓存图片。 - 源码分析
算法原理:
LruCache把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。(最近使用的数据在尾部,老数据在头部)
put
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
LruCache的put方法
public final V put(K key, V value) {
// 如果该值在缓存中存在便返回,否则在LinkedList中插入新值。并根据缓存大小整理内存,若缓存大小超过预定值,则移除最近最少使用值
V previous;
// 对map进行操作之前,先进行同步操作(HashMap是线程不安全的,应该先进行同步操作)
// synchronized加锁,表示一次只能有一个方法进入该线程
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
// 向map中加入缓存对象,若缓存中已存在,返回已有的值,否则执行插入新的数据,并返回null,并将缓存恢复为之前的值
previous = map.put(key, value)
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
// 根据缓存大小整理内存,看是否需要移除LinkedHashMap中的元素
trimToSize(maxSize);
return previous; }
trimToSize(maxSize)
public void trimToSize(int maxSize) {
while (true) {
// while循环,不断移除LinkedHashMap中双向链表表头表头元素(近期最少使用的数据),直到满足当前缓存大小小于或等于最大可缓存大小
// 如果当前缓存大小已小于等于最大可缓存大小,则直接返回,不需要再移除LinkedHashMap数据
if (size <= maxSize || map.isEmpty()) {
break; }
// 得到双向链表表头header的下一个Entry(近期最少使用的数据=表头数据)
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
// 移除当前取出的Entry并重新计算当前缓存大小
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
get
public final V get(K key) {
// 如果该值在缓存中存在或可被创建便返回,当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,移动到表尾,这个更新过程就是在LinkedHashMap中的get()方法中完成的。
V mapValue;
synchronized (this) {
// 在hashMap中查找有没有这个key对应的节点(这个地方只要是get一次就会把命中的节点往队尾移动)
mapValue = map.get(key);
if (mapValue != null) {
return mapValue;
}
}
}
由此可见LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。
- 当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就删除队头元素,即近期最少访问的元素。
- 当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队尾。
- 使用
初始化缓存类,设定大小并重写sizeOf()方法
/* 内存缓存 */
private int maxMemory = (int)(Runtime.getRuntime().totalMemory()/1024);
int cacheSize = maxMemory / 8;
//1. 初始化这个cache前需要设定这个cache的大小,这里的大小官方推荐是用当前app可用内存的八分之一
mMemoryCache = new LruCache<String, Bitmap>(memoryCache) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 2. 重写此方法来衡量每张图片的大小,默认返回可缓存图片数量。
return bitmap.getRowBytes() * bitmap.getHeght() / 1024;
}
};
重写 添加/删除 缓存
//将bitmap添加到内存中去
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
// 通过key来从内存缓存中获得bitmap对象
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
LruCache加载图片 实例
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
// 首先会在 LruCache 的缓存中进行检查。
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
// 如果找到了相应的键值,则会立刻更新ImageView
imageView.setImageBitmap(bitmap);
} else {
// 否则开启一个后台线程来加载这张图片
imageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
// 在后台加载图片
// BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
- DiskLruCache 硬盘/外存缓存
不同于LruCache,LruCache是将数据缓存到内存中去,而DiskLruCache是外部缓存(默认位置:/sdcard/Android/data//cache),例如可以将网络下载的图片永久的缓存到手机外部存储中去,并可以将缓存数据取出来使用,DiskLruCache不是google官方所写,但是得到了官方推荐,DiskLruCache没有编写到SDK中去,是由square团队开发的一个第三方开源库。 - 打开缓存
首先调用getDiskCacheDir()方法获取到缓存地址的路径,然后判断一下该路径是否存在,如果不存在就创建一下。接着调用DiskLruCache的open()方法来创建实例,并把四个参数传入即可。
DiskLruCache mDiskLruCache = null;
try {
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
// public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
// directory:数据缓存地址
// appVersion:当前应用程序版本号
// 指定同一个key可以对应多少缓存文件,基本为1
// 指定最多可以缓存多少字节数据
mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
getDiskCacheDir
缓存地址通常都会存放在 /sdcard/Android/data//cache 这个路径下面,但如果这个手机没有SD卡,或者SD正好被移除了的情况,应该专门写一个方法来获取缓存地址
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
// 当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径:/sdcard/Android/data/<application package>/cache
cachePath = context.getExternalCacheDir().getPath();
} else {
// 否则就调用getCacheDir()方法来获取缓存路径:/data/data/<application package>/cache,接着又将获取到的路径和一个uniqueName进行拼接,作为最终的缓存路径返回。
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
- 写入缓存
new Thread(new Runnable() {
@Override
public void run() {
try {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
// 获得imageUrl的MD5编码格式字符串,符合文件的命名规则
String key = hashKeyForDisk(imageUrl);
// 获取DiskLruCache.Editor类实例进行写入操作
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
// 调用Editor的newOutputStream()方法来创建一个输出流
OutputStream outputStream = editor.newOutputStream(0);
// 访问urlString中传入的网址,并通过outputStream写入到本地
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit(); // 写入结束后提交,才能写入生效
} else {
editor.abort(); // 否则放弃写入
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
hashKeyForDisk
写入的操作是借助DiskLruCache.Editor这个类完成,通过调用DiskLruCache的edit()方法来获取实例,edit(String key)接口需要传入一个参数key,这个key将会成为缓存文件的文件名,并且必须要和图片的URL是一一对应的。(不适合直接用URL作为key:1.过长2.含特殊字符)最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。
public String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.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();
}
访问urlString中传入的网址,并通过outputStream写入到本地
private 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(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
- 读取缓存
try {
String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl);
// public synchronized Snapshot get(String key) throws IOException
// get()方法要求传入一个key来获取到相应的缓存数据,key 为 将图片URL进行MD5编码后的值
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
// 获取到的DiskLruCache.Snapshot对象,调用它的getInputStream()方法就可以得到缓存文件的输入流了
InputStream is = snapShot.getInputStream(0);
// 使用了BitmapFactory的decodeStream()方法将文件流解析成Bitmap对象,然后把它设置到ImageView当中
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImage.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
- LruCache & DiskLruCache 对比
缓存 | LruCache | DiskLruCache |
---|---|---|
简介 | 内存缓存 | 硬盘/外存缓存 |
核心算法 | LRU | LRU |
存储位置 | 内存 | /sdcard/Android/data//cache SD卡 |
特点 | 读写速度快,存储空间小 | 读写速度稍慢,存储空间大 |
如果让你设计一个图片加载库,你会如何设计?
Android高效异步图片加载框架
整体架构
- 单例实现:单例模式调用图片加载框架
- 缓存策略:三级缓存策略(LruCache内存缓存、DiskCache硬盘缓存、联网加载)
- 任务队列:每发起一个新的加载图片的请求,封装成Task添加到的任务队列TaskQueue中去(FIFO)
- 线程池:后台轮询线程。该线程在第一次初始化实例的时候启动,然后会一直在后台运行;当每发起一次加载图片请求的时候,除了会创建一个新的任务到任务队列TaskQueue中去,同时发一个消息到后台线程,后台线程去使用线程池去TaskQueue去取一个任务执行
(线程池:在任务众多的情况下,系统要为每一个任务创建一个线程,而任务执 行完毕后会销毁每一个线程,所以会造成线程频繁地创建与销毁。) - 图片压缩:将图片实际大小按缩放比进行压缩
具体实现
- 初始化 图片加载类
- 单例模式创建实例,并初始化信息
public class XCImageLoader{
private volatile static XCImageLoader mInstance = null; // 单例模式
LruCache<String,Bitmap> mLruCache; // 内存缓存,存储图片
ExecutorService mThreadPool; // 线程池
LinkedList<Runnable> mTaskQueue; // 任务队列
Semaphore mPoolTThreadSemaphore; // 线程池信号量
// 单例模式实现
public static XCImageLoader getInstance()
{
if (mInstance == null)
{
synchronized (XCImageLoader.class)
{
if (mInstance == null)
{
mInstance = new XCImageLoader(DEAFULT_THREAD_COUNT);
}
}
}
return mInstance;
}
private XCImageLoader(int threadCount){
init(threadCount);
}
/**
* 初始化信息
* @param threadCount 线程池中线程数量
*/
private void init(int threadCount){
// 初始化后台轮询线程
initBackThread();
// 获取当前应用的最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
// 初始化LruCache大小
mLruCache = new LruCache<String,Bitmap>(maxMemory/8){
@Override
protected int sizeOf(String key, Bitmap value) {
// 计算每张图片byte大小
return value.getRowBytes() * value.getHeight();
}
};
// 创建线程池
mThreadPool = Executors.newFixedThreadPool(threadCount);
// 创建任务队列
mTaskQueue = new LinkedList<Runnable>();
// 线程池信号量 = threadCount 用于限制线程池中线程数量
// 信号量的数量和我们加载图片的线程个数一致;
// 每取一个任务去执行,我们会让信号量减一;每完成一个任务,会让信号量+1,再去取任务
mPoolTThreadSemaphore = new Semaphore(threadCount);
}
}
- 后台轮询线程
后台线程中,创建一个Handler用来处理图片加载任务发过来的图片显示消息
/**
* 初始化后台轮询线程
*/
private void initBackThread() {
// 后台轮询线程
mPoolThread = new Thread(){
@Override
public void run() {
Looper.prepare();
mPoolThreadHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
//从线程池中取出一个任务开始执行
mThreadPool.execute(getTaskFromQueue());
try {
// 获取信号量(semaphore --)
mPoolTThreadSemaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//释放信号量(semaphore ++)
mPoolThreadHandlerSemaphore.release();
Looper.loop();
}
};
mPoolThread.start();
}
- 加载图片
采用三级缓存策略处理图片加载
- 从内存LruCache中加载,如果存在则从LruCache中取出显示。否则,新建一个图片加载任务并添加到任务队列,此时会通知后台线程去线程池中取出一个线程来执行。
- 从硬盘DiskCache中加载,如果存在则从本地文件中加载显示。
- 否则从网络直接下载图片并显示。
- 将图片写入LruCache和DiskCache中
/**
* 加载图片并显示到ImageView上
*/
public void displayImage(final String path,final ImageView imageView
,final boolean isFromNet){
imageView.setTag(path);
if(mUIHandler == null){
mUIHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
// 获取得到图片,为imageview回调设置图片
ImageHolder holder = (ImageHolder) msg.obj;
Bitmap bmp = holder.bitmap;
ImageView imageview = holder.imageView;
String path = holder.path;
// 将path与getTag存储路径进行比较,防止错乱
if (imageview.getTag().toString().equals(path))
{
if(bmp != null){
imageview.setImageBitmap(bmp);
}
}
}
};
}
// 根据path在缓存中获取bitmap
Bitmap bm = getBitmapFromLruCache(path);
if (bm != null)
{
refreshBitmap(path, imageView, bm);
}else{//如果没有LruCache,则创建任务并添加到任务队列中
addTaskToQueue(createTask(path, imageView, isFromNet));
}
}
/**
* 添加任务到任务队列中
*/
private synchronized void addTaskToQueue(Runnable runnable)
{
mTaskQueue.add(runnable);
try
{
if (mPoolThreadHandler == null)
mPoolThreadHandlerSemaphore.acquire();
} catch (InterruptedException e)
{
e.printStackTrace();
}
mPoolThreadHandler.sendEmptyMessage(24);
}
/**
* 根据参数,创建一个任务
*/
private Runnable createTask(final String path, final ImageView imageView,
final boolean isFromNet)
{
return new Runnable()
{
@Override
public void run()
{
Bitmap bm = null;
if (isFromNet)
{
File file = getDiskCacheDir(imageView.getContext(),
Utils.makeMd5(path));
if (file.exists())// 如果在缓存文件中发现
{
Log.v(TAG, "disk cache image :" + path);
bm = loadImageFromLocal(file.getAbsolutePath(),
imageView);
} else
{
if (mIsDiskCacheEnable)// 检测是否开启硬盘缓存
{
boolean downloadState = ImageDownloadUtils
.downloadImageByUrl(path, file);
if (downloadState)// 如果下载成功
{
Log.v(TAG,
"download image :" + path
+ " to disk cache: "
+ file.getAbsolutePath());
bm = loadImageFromLocal(file.getAbsolutePath(),
imageView);
}
} else
{// 直接从网络加载
bm = ImageDownloadUtils.downloadImageByUrl(path,
imageView);
}
}
} else
{
bm = loadImageFromLocal(path, imageView);
}
// 3、把图片加入到缓存
setBitmapToLruCache(path, bm);
refreshBitmap(path, imageView, bm);
mPoolTThreadSemaphore.release();
}
};
}
- 显示图片
很多情况下,网络或者本地的图片都比较大,而用于显示ImageView显示大小比较小,这时候就需要我们进行图片的压缩,再显示到ImageView上面去。节省内存。
/**
* 根据url下载图片并压缩
*/
public static Bitmap downloadImageByUrl(String urlStr, ImageView imageview)
{
InputStream is = null;
try
{
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
is = new BufferedInputStream(conn.getInputStream());
is.mark(is.available());
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
// 获取bitmap(获取图片的宽和高)
Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts);
// 获取ImageView显示的宽和高
ImageSize imageViewSize = ImageUtils.getImageViewSize(imageview);
// 按照ImageView控件大小与图片大小的缩放比解析位图
opts.inSampleSize = ImageUtils.calculateInSampleSize(opts,
imageViewSize.width, imageViewSize.height);
opts.inJustDecodeBounds = false;
is.reset();
bitmap = BitmapFactory.decodeStream(is, null, opts);
conn.disconnect();
return bitmap;
} catch (Exception e)
{
e.printStackTrace();
} finally
{
try
{
if (is != null)
is.close();
} catch (IOException e)
{
}
}
return null;
}
通过UIHandler发消息来显示Bitmap到ImageView上去
/**
* 刷新图片到ImageView
*/
private void refreshBitmap(final String path, final ImageView imageView,
Bitmap bm)
{
Message message = Message.obtain();
ImageHolder holder = new ImageHolder();
holder.bitmap = bm;
holder.path = path;
holder.imageView = imageView;
message.obj = holder;
mUIHandler.sendMessage(message);
}
Github 下载经典实例分析
public class ImageLoader {
// 关于异步加载图片的思路是:
// 1.第一次进入时,是没有图片的,这时候我们会启动一个线程池,异步的从网上获得图片数据,为了防止图片过大导致OOM,可以调用BitmapFactory中的Options类对图片进行适当的缩放,最后再显示主线程的ImageView上。
// 2.把加载好的图片以图片的Url做为唯一的key存入内存缓存当中,并严格的控制好这个缓存的大小,防止OOM的发生。
// 3.把图片缓存在SD当中,如果没有SD卡就放在系统的缓存目录cache中,以保证在APP退出后,下次进来能看到缓存中的图片,这样就可以让使你的APP不会给客户呈现一片空白的景象。
// 4.用户第二次进来的时候,加载图片的流程则是倒序的,首先从内容中看是否存在缓存图片,如果没有就从SD卡当中寻找,再没有然后才是从网络中获取图片数据。这样做的既可以提高加载图片的效率,同时也节约了用户的流量。
MemoryCache memoryCache=new MemoryCache();
FileCache fileCache;
private Map<ImageView, String> imageViews= Collections.synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
Handler handler=new Handler();//handler to display images in UI thread
public ImageLoader(Context context){
fileCache=new FileCache(context);
executorService= Executors.newFixedThreadPool(5);
}
final int stub_id= R.drawable.ic_launcher_background;
public void DisplayImage(String url, ImageView imageView)
{
imageViews.put(imageView, url);
Bitmap bitmap=memoryCache.get(url);
if(bitmap!=null)
imageView.setImageBitmap(bitmap);
else
{
queuePhoto(url, imageView);
imageView.setImageResource(stub_id);
}
}
private void queuePhoto(String url, ImageView imageView)
{
PhotoToLoad p=new PhotoToLoad(url, imageView);
executorService.submit(new PhotosLoader(p));
}
private Bitmap getBitmap(String url)
{
File f=fileCache.getFile(url);
//from SD cache
Bitmap b = decodeFile(f);
if(b!=null)
return b;
//from web
try {
Bitmap bitmap=null;
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection)imageUrl.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is=conn.getInputStream();
OutputStream os = new FileOutputStream(f);
Utils.CopyStream(is, os);
os.close();
conn.disconnect();
bitmap = decodeFile(f);
return bitmap;
} catch (Throwable ex){
ex.printStackTrace();
if(ex instanceof OutOfMemoryError)
memoryCache.clear();
return null;
}
}
//decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f){
try {
//decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
FileInputStream stream1=new FileInputStream(f);
BitmapFactory.decodeStream(stream1,null,o);
stream1.close();
//Find the correct scale value. It should be the power of 2.
final int REQUIRED_SIZE=70;
int width_tmp=o.outWidth, height_tmp=o.outHeight;
int scale=1;
while(true){
if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
break;
width_tmp/=2;
height_tmp/=2;
scale*=2;
}
//decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize=scale;
FileInputStream stream2=new FileInputStream(f);
Bitmap bitmap=BitmapFactory.decodeStream(stream2, null, o2);
stream2.close();
return bitmap;
} catch (FileNotFoundException e) {
}
catch (IOException e) {
e.printStackTrace();
}
return null;
}
//Task for the queue
private class PhotoToLoad
{
public String url;
public ImageView imageView;
public PhotoToLoad(String u, ImageView i){
url=u;
imageView=i;
}
}
class PhotosLoader implements Runnable {
PhotoToLoad photoToLoad;
PhotosLoader(PhotoToLoad photoToLoad){
this.photoToLoad=photoToLoad;
}
@Override
public void run() {
try{
if(imageViewReused(photoToLoad))
return;
Bitmap bmp=getBitmap(photoToLoad.url);
memoryCache.put(photoToLoad.url, bmp);
if(imageViewReused(photoToLoad))
return;
BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad);
handler.post(bd);
}catch(Throwable th){
th.printStackTrace();
}
}
}
boolean imageViewReused(PhotoToLoad photoToLoad){
String tag=imageViews.get(photoToLoad.imageView);
if(tag==null || !tag.equals(photoToLoad.url))
return true;
return false;
}
//Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
PhotoToLoad photoToLoad;
public BitmapDisplayer(Bitmap b, PhotoToLoad p){bitmap=b;photoToLoad=p;}
public void run()
{
if(imageViewReused(photoToLoad))
return;
if(bitmap!=null)
photoToLoad.imageView.setImageBitmap(bitmap);
else
photoToLoad.imageView.setImageResource(stub_id);
}
}
public void clearCache() {
memoryCache.clear();
fileCache.clear();
}
}