您的位置:首页 > 移动开发 > Android开发

Android 图片如何高效加载与缓存

2016-02-17 17:31 411 查看

图片如何高效加载与缓存

这是我写的第一篇博客,我也只是个大三的学生,代码和文章仍有很多的不足之处,还请各位dalao在发现不足之处之后再评论区回复……谢谢


图片加载

一级缓存: 自定义的LRUcache

二级缓存: 本地文件 File

网络请求框架: OKHttp

在得到图片加载请求之后,首先检查一级、二级缓存中是否有与请求 tag 相符合的缓存对象,有缓存对象则使用 FetcherByCache 线程来处理,若是没有则使用 Fetcher 线程来获取。

若请求加载的不是网络图片而是 本地图片 ,则使用 FetcherByLocal 线程来进行处理。

其中 FetcherByCacheFetcherByLocal 线程是共用同一个线程池(最大线程数 3),而 Fetcher 线程是单独的一个线程池(最大线程数 1)。

其中我们的每一个图片加载请求都有一个唯一的 tag 这个 tag 由图片的请求网址或者是本地路径生成,是对应请求的 ImageView 唯一标识,也是线程池中任务的唯一标识。如果线程池中有与请求的 tag 相同的任务没处理完,则会拒绝执行。

●(防止用户在反复滑动ListView之类的控件时产生许多重复的请求)

Fetcher 线程:

private class Fetcher implements Runnable,ThreadinterFace {

private String tag;
private String imageUrl;
private ImageView imageView;
private OnImageLoad onImageLoad;
private Bitmap cacheImage;

public Fetcher(String tag,String imageUrl,ImageView imageView,OnImageLoad onImageLoad) {
this.tag = tag;
this.imageUrl = imageUrl;
this.imageView = imageView;
this.onImageLoad = onImageLoad;
}

public Fetcher(ImageRequired required) {
this.tag = required.getTag();
this.imageUrl = required.getImageURL();
this.imageView = required.getImageView();
this.onImageLoad = required.getOnImageLoad();
}

@Override
public void run() {
if (imageView != null || onImageLoad != null){
OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(5,TimeUnit.SECONDS).build();
Request request = new Request.Builder().url(imageUrl).build();
try {
Call call = okHttpClient.newCall(request);
Response response = call.execute();
cacheImage = BitmapFactory.decodeStream(response.body().byteStream());
if (cacheImage != null) imageCacher.putCache(tag,cacheImage);
} catch (IOException e) {
Log.d("On loading image", "\nurl:" + imageUrl +"\nError:"+ e.toString());
onError(0);
}
runOnUIThread(new Runnable() {
@Override
public void run() {
onDownloadCompleted(cacheImage,imageView,tag,onImageLoad);
}
});
}else {
runOnUIThread(new Runnable() {
@Override
public void run() {
onError(1);
}
});
}
}

@Override
public void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag,OnImageLoad onImageLoadCompleted) {
fetherExecutor.removeName(tag);
if (imageView == null && bitmap != null && onImageLoadCompleted != null){
onImageLoadCompleted.onLoadCompleted(bitmap);
}else if (imageView != null && bitmap != null && tag.equals(imageView.getTag())){
if (durationMillis > 0 ){
Drawable prevDrawable = imageView.getDrawable();
if (prevDrawable == null) {
prevDrawable = new ColorDrawable(TRANSPARENT);
}
Drawable nextDrawable = new BitmapDrawable(imageView.getResources(), bitmap);
TransitionDrawable transitionDrawable = new TransitionDrawable(
new Drawable[] { prevDrawable, nextDrawable });
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(durationMillis);
}else {
imageView.setImageBitmap(cacheImage);
}
}
}

@Override
public void onError(int status) {
fetherExecutor.removeName(tag);
if (onImageLoad != null){
onImageLoad.onLoadFailed();
}
}

@Override
public int hashCode() {
return tag.hashCode();
}

@Override
public boolean equals(Object o) {
return this.hashCode() == o.hashCode() && o instanceof Fetcher;
}

}


● 代码中 fetherExecutor.removeName(tag); 这句话的用途是告知线程池这个图片对应的 tag 的任务已经完成不再拒绝具有相同 tag 的任务请求。 线程池的代码在最后贴出来。

● 代码中 imageCacher.putCache(tag,cacheImage); 是将图片交由图片缓存类处理,这个 ImageCacher 类同样在接下来的 自定义LRUcache 中写出来

FetcherByCache 线程:

private class FetcherByCache implements Runnable,ThreadinterFace{

private String tag;
private String imageUrl;
private ImageView imageView;
private OnImageLoad onImageLoad;
private Bitmap cacheImage;

public FetcherByCache(String tag,String imageUrl,ImageView imageView,OnImageLoad onImageLoad) {
this.tag = tag;
this.imageUrl = imageUrl;
this.imageView = imageView;
this.onImageLoad = onImageLoad;
}

public FetcherByCache(ImageRequired required) {
this.tag = required.getTag();
this.imageUrl = required.getImageURL();
this.imageView = required.getImageView();
this.onImageLoad = required.getOnImageLoad();
}

@Override
public void run() {
cacheImage = imageCacher.getCache(tag);
if (cacheImage != null){
runOnUIThread(new Runnable() {
@Override
public void run() {
onDownloadCompleted(cacheImage,imageView,tag,onImageLoad);
}
});
}else if (imageUrl != null){
runOnUIThread(new Runnable() {
@Override
public void run() {
onError(1);
}
});
}
}

@Override
public void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag, OnImageLoad onImageLoadCompleted) {
cacheExecutor.removeName(tag);
if (imageView == null && bitmap != null && onImageLoadCompleted != null){
onImageLoadCompleted.onLoadCompleted(bitmap);
}else if (imageView != null && bitmap != null && tag.equals(imageView.getTag())){
if (durationMillis > 0 ){
Drawable prevDrawable = imageView.getDrawable();
if (prevDrawable == null) {
prevDrawable = new ColorDrawable(TRANSPARENT);
}
Drawable nextDrawable = new BitmapDrawable(imageView.getResources(), bitmap);
TransitionDrawable transitionDrawable = new TransitionDrawable(
new Drawable[] { prevDrawable, nextDrawable });
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(durationMillis);
}else {
imageView.setImageBitmap(bitmap);
}
}
}

@Override
public void onError(int status) {
cacheExecutor.removeName(tag);
loadImage(tag, imageUrl, imageView, onImageLoad);
}

@Override
public int hashCode() {
return tag.hashCode();
}

@Override
public boolean equals(Object o) {
return this.hashCode() == o.hashCode() && o instanceof FetcherByCache;
}

}


●这个线程中要注意调用的 public void onError(int status)

当缓存不存在的时候,我们就重新执行图片拉取请求,但这次的就是网络获取

FetcherByLocal 线程:

这个线程由于是读取本地图片,所以允许带上 BitmapFactory.Options 进行加载,所以我们也会缓存带选项加载的图片,同时还要与不带选项的图片缓存进行区分,我们在 tag 这里进行区分就行。

private class FetcherByLocal implements Runnable,ThreadinterFace{

private OnImageLoad onImageLoad;
private ImageView imageView;
private String filePath;
private String tag;
private BitmapFactory.Options options;
private Bitmap bitmap;

public FetcherByLocal(ImageView imageView,OnImageLoad onImageLoad, String filePath,BitmapFactory.Options options) {
this.onImageLoad = onImageLoad;
this.imageView = imageView;
this.filePath = filePath;
this.options = options;
this.tag = buildTag(filePath);
}

@Override
public void run() {
File file = new File(filePath);
if (options == null){
bitmap = imageCacher.getByLruCache(filePath);
}else {
bitmap = imageCacher.getByLruCache(filePath+"withop");
}
if (bitmap == null && file.exists() && file.canRead()){
if (options != null){
bitmap = BitmapFactory.decodeFile(filePath,options);
imageCacher.putInLruCaches(filePath+"withop", bitmap);
}
else{
bitmap = BitmapFactory.decodeFile(filePath,options);
imageCacher.putInLruCaches(filePath, bitmap);
}
runOnUIThread(new Runnable() {
@Override
public void run() {
onDownloadCompleted(bitmap, imageView, filePath, onImageLoad);
}
});
}else if (bitmap != null){
runOnUIThread(new Runnable() {
@Override
public void run() {
onDownloadCompleted(bitmap,imageView,filePath,onImageLoad);
}
});
}else{
fetherExecutor.removeName(filePath);
Log.e("On loading image","Image file is not exist or is not readable");
}
}

@Override
public void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag, OnImageLoad onImageLoadCompleted) {
if (options != null) {
fetherExecutor.removeName(tag+"withop");
}else {
fetherExecutor.removeName(tag);
}
if (bitmap == null){
Log.e("On loading image", "Load image failed. Path: " + tag);
return;
}
if (onImageLoadCompleted != null){
onImageLoadCompleted.onLoadCompleted(bitmap);
}
if (imageView != null && imageView.getTag().equals(this.tag)){
if (durationMillis > 0 ){
Drawable prevDrawable = imageView.getDrawable();
if (prevDrawable == null) {
prevDrawable = new ColorDrawable(TRANSPARENT);
}
Drawable nextDrawable = new BitmapDrawable(imageView.getResources(), bitmap);
TransitionDrawable transitionDrawable = new TransitionDrawable(
new Drawable[] { prevDrawable, nextDrawable });
imageView.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(durationMillis);
}else {
imageView.setImageBitmap(bitmap);
}
}
}

@Override
public void onError(int status) {
if (onImageLoad != null){
onImageLoad.onLoadFailed();
}
}

@Override
public int hashCode() {
return tag.hashCode();
}

@Override
public boolean equals(Object o) {
return this.hashCode() == o.hashCode() && o instanceof FetcherByLoacl;
}

}


图片加载请求

(pause部分的代码目前还在完善中,现在大家略过即可)

图片加载前将 ImageView 设置为空并设置一个背景色,这样可以避免 ListView 重复利用Item View 的时候造成未加载的 ImageView 会显示图片错乱的现象

public void loadImage(String tag,String imageURL,ImageView imageView,OnImageLoad onImageLoad){
if (tag == null || TextUtils.isEmpty(tag)){
Log.e("On loading image","TAG is empty or null");
return;
}
tag = buildTag(tag);
if (pause){
ImageRequired imageRequired = new ImageRequired(imageView, tag, imageURL, onImageLoad);
if (imageView != null){
imageView.setBackgroundColor(Color.LTGRAY);
imageView.setImageDrawable(null);
}
requiredList.remove(imageRequired);
requiredList.add(imageRequired);
}else {
if (imageCacher == null) imageCacher = new OCImageCacher();
if (imageView != null){
if (imageView.getDrawable() != null && (imageView.getTag() != null && imageView.getTag().equals(tag))){
Log.d("Skipped","TAG:"+tag);
return;
}else {
imageView.setImageBitmap(null);
imageView.setBackgroundColor(Color.LTGRAY);
imageView.setTag(tag);
}
}
if (imageURL == null || imageCacher.isCacheExist(tag)){
Log.d("OnThread","CacheThread");
cacheExecutor.execute(new FetcherByCache(tag,imageURL,imageView,onImageLoad), tag);
}else {
Log.d("OnThread","FetcherThread");
fetherExecutor.execute(new Fetcher(tag,imageURL,imageView,onImageLoad),tag);
}
}
}


● 在代码中的

if (imageView.getDrawable() != null && (imageView.getTag() != null && imageView.getTag().equals(tag)))

要说一下,就是要检查当前请求的 ImageView 是否已经存在图像,若是已经存在图像的我们就选择跳过。

( 比如用户当前显示着的这部分ListView已经加载完成了图像,这时候用户锁屏再解锁,那这部分的图片就不会重新请求加载 )

自定义线程池

我们贴上代码再说

public class OCThreadExecutor extends ThreadPoolExecutor {

private List<String> threadNames;

public OCThreadExecutor(int maxRunningThread, String poolName) {
super(maxRunningThread, maxRunningThread, 0l, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new OCThreadFactory(poolName));
this.threadNames = new ArrayList<>();
}

public void execute(Runnable runnable, String taskTag) {
if (!threadNames.contains(taskTag)) {
this.threadNames.add(0, taskTag);
execute(runnable);
} else {
Log.d("OCExecutor","Same thread tag,thread skipped!"+"  TAG:"+taskTag);
}
}

public void removeName(String tag) {
threadNames.remove(tag);
}

public boolean remove(Runnable task,String tag){
threadNames.remove(tag);
return remove(task);
}

static class OCThreadFactory implements ThreadFactory {

private final String name;

public OCThreadFactory(String name) {
this.name = name;
}

@Override
public Thread newThread(Runnable r) {
return new OCThread(r, name);
}

}

static class OCThread extends Thread {

public OCThread(Runnable runnable, String name) {
super(runnable, name);
setName(name);
}

@Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
super.run();
}
}

}


这个自定义线程池的使用放弃了原本的执行方法,而是用

public void execute(Runnable runnable, String taskTag)

来执行任务,在调用这方法的时候,会将 tag 记录在类里的一个 List 中,防止执行重复的任务。

在线程执行完毕之后再调用

public void removeName(String tag)

将列表中的 tag 移除,以允许执行相同的任务。

图片缓存 自定义LRUcache

首先我们先看自定义的 LRUcache 类

private class CustomLRUCache extends LruCache<String,Bitmap>{

public CustomLRUCache(int maxSize) {
super(maxSize);
}

List<String> keys = new ArrayList<>();

@Override
protected int sizeOf(String  key, Bitmap value) {
return value.getHeight()*value.getRowBytes();
}

public void putANDcount(String key, Bitmap value){
put(key, value);
keys.add(key);
}

public boolean isExist(String key){
return keys.contains(key) ;
}

public Bitmap getCache(String key){
Bitmap cache = get(key);
if (cache == null){
this.keys.remove(key);
return null;
}else {
return cache;
}
}

public void releaseCaches(){
evictAll();
this.keys.clear();
}

}


protected int sizeOf(String key, Bitmap value)

度量每个 Bitmap 对象的大小。这就不用多说了

public boolean isExist(String key)

检查是否 曾经存在有 我们的对象。为啥这么说呢,我们接着看。

public void putANDcount(String key, Bitmap value)

调用LRUcache的方法 put 之后,将这个 Bitmap 的tag存入 List 中,以供我们之后查找一级缓存。

public Bitmap getCache(String key)

获取缓存对象,如果从缓存中获取到的对象是 NULL ,则我们将 List 中的 tag 去除。因为我们没有办法检查LRU缓存,所以我们只能交 由上一级的方法来处理 ( 请求缓存方法,下面就是 )

public void releaseCaches()

释放所有的缓存,同时清空存放 tag 的 List

在这个创建这个类的时候我们应该这样创建:

lruCache = new CustomLRUCache((int)Runtime.getRuntime().totalMemory()/8);


括号中的语句的意思是,使用 app 所能使用的最大内存的 1/8 作为缓存。根据每个设备的RAM不同,阀值不同,app的可以使用最大内存各有不同。

请求缓存的方法:

protected Bitmap getCache(String tag){
Bitmap cacheImage = getByLruCache(tag);
if (cacheImage != null){
Log.d("OnCacher",tag+"  Got by    LRUCache");
return cacheImage;
} else if (canCacheAsFile){
cacheImage =  getByFileCache(tag);
if (cacheImage != null) putInLruCaches(tag,cacheImage);
else {
Log.d("OnCacher",tag+"  No cache here [cache file has been deleted]");
return null;
}
return cacheImage;
}else{
Log.d("OnCacher",tag+"  No cache here");
return null;
}
}


●咱们如果一级缓存没有

Bitmap cacheImage = getByLruCache(tag);
if (cacheImage != null){
...
}


就再判断app是否有文件读取的权限,如果有的话,我们就进行二级缓存的获取

cacheImage =  getByFileCache(tag);


如果还是没有,那么我们就返回 NULL ,再给上一级的方法进行处理。

======================================

关于图片的加载获取其实也没多少可以说的,主要的方法就是上面的这些,下面是包装好的代码使用方法,已经相关的接口啥的:

接口OnImageLoad

public interface OnImageLoad {

void onLoadCompleted(Bitmap image);

void onLoadFailed();
}


接口ThreadinterFace

这个没什么用途,只不过是让线程内部比较规范而已

interface ThreadinterFace {

void onDownloadCompleted(Bitmap bitmap, ImageView imageView, String tag, OnImageLoad onImageLoadCompleted);

void onError(int status);

}


调用方法样例

//获取网络图片
OCImageLoader.loader().loadImage(tag, url, viewHolder.imageView,onImageLoad);

//获取本地图片
OCImageLoader.loader().loadLocalImage(path,viewHolder.imageView,onImageLoad,option);


运行状态

如果看不清图片里的字,请直接打开图片的地址即可查看高清版本。。。

设备一: Nexus 6 3GB RAM API23 ,第一张为本地图片,大小为 2.84MB







设备二:虚拟机 1GB RAM API22,第一张为本地图片,大小为 2.96MB



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  缓存 图片 高效 对象