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; } |
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
""
>(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();
}
//网络获取图片,并写入输出流
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
;
}
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();
}
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