热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

retrofit2+Executors+DiskLruCache2秒加载100张图片从此告别OOM的困扰

人生的旅途,前途很远,也很暗。然而不要怕,不怕的人的面前才有路。——鲁迅自从上一篇博客发布后,已经有很长时间没有更新博客了,一直忙着支付通的事情,在此给大家道个歉。先贴个图:你不要惊讶

人生的旅途,前途很远,也很暗。然而不要怕,不怕的人的面前才有路。——鲁迅

自从上一篇博客发布后,已经有很长时间没有更新博客了,一直忙着支付通的事情,在此给大家道个歉。

先贴个图:

red

你不要惊讶,这就是第一次从网络获取图片的速度,感觉比本地读取图片的速度还要快吧。加载100张图片真的只要2秒时间,你不要不相信,不信你就来看。

一、概述

在众多的app当中,缓存可以作为衡量一款产品的好坏,既能节省流量,减少电量消耗,最重要的是用户体验好。你想想一款产品每个月消耗你100M以上的流量,你愿意用吗?当然这里除了游戏以外。那么怎么才能做好缓存呢?这里要介绍两个重要的概念,一个是内存缓存LruCache,一个是硬盘缓存DiskLruCache,大家对这两个概念肯定不会陌生,如果你还不了解的话请链接郭神的Android DiskLruCache完全解析,硬盘缓存的最佳方案 真心写的很棒。从标题中就可以看出今天还有一个主角就是线程池这个概念我很久以前都听说过了,但没具体去研究过,我也只会使用它。

相关文章请链接一下地址:

Retrofit2与RxJava用法解析

android中对线程池的理解与使用

Android DiskLruCache完全解析,硬盘缓存的最佳方案

二、Executors初探线程池

Android常用的线程池有以下几种,在Executors里面对应的方法:

  1. newFixedThreadPool 每次执行限定个数个任务的线程池
  2. newCachedThreadPool 所有任务都一次性开始的线程池
  3. newSingleThreadExecutor 每次只执行一个任务的线程池
  4. newScheduledThreadPool 创建一个可在指定时间里执行任务的线程池,亦可重复执行

获取实例:

Executors.newSingleThreadExecutor();// 每次只执行一个线程任务的线程池
Executors.newFixedThreadPool(3);// 限制线程池大小为3的线程池
Executors.newCachedThreadPool(); // 一个没有限制最大线程数的线程池
Executors.newScheduledThreadPool(3);// 一个可以按指定时间可周期性的执行的线程池

我们来看看下面这个例子:

 new Thread(new Runnable() {
@Override
public void run() {

}
}).start();

在功能上等价于:

        mMyHandler.post(new Runnable() {
@Override
public void run() {

}
});

还等价于:

        executors.execute(new Runnable() {
@Override
public void run() {

}
});

我们为啥要使用ExecutorService呢,而不使用ThreadHandler?使用线程池我觉得可以对我们开启的线程进行跟进,可以复用这点很重要,能够减少内存消耗,当然也可以指定个数来执行任务的线程池、创建一个可在指定时间里执行任务的线程池。

线程池使用

三、DiskLruCache简单介绍

如果你想详情了解的话,请链接相关文章。

注意:在你的项目当中依赖了相关retrofit包,DiskLruCache类也包含在其中,免得你 重复导包。

先来看看DiskLruCache的实例方法:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)  

open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据,好了我这里就不再重复讲解了。不懂请查看相关文章链接。

下面我们一起来看看,文章开头那个快速加载出图片的程序是怎么实现的。我通过自己的尝试,能使图片加载那么迅猛,还是蛮激动的。

1、xml布局

    <ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent">

ListView>

就一个ListView,没什么好说的。

2、activity文件

        lv= (ListView) findViewById(R.id.lv);

mAdapter=new TestAdapter(Images.imageThumbUrls,R.layout.photo_layout,this,lv);

lv.setAdapter(mAdapter);

都是比较常规的写法,这里主要说下Adapter的参数:

public TestAdapter(String[] datas, int layoutId, Context context, ViewGroup view)

第一个参数代表 图片地址数组
第二个参数代表 子布局Id
第三个参数代表 上下文 context
第四个参数代表 当前的ListView,请求网络是异步加载,防止图片错位

3、adapter文件

成员变量:

   /**
* 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。
*/

private LruCache mMemoryCache;

/**
* 图片硬盘缓存核心类。
*/

private DiskLruCache mDiskLruCache;

/**
* 线程池下载图片
*/

private ExecutorService executors;

private String[] datas; //数据源
private int layoutId; //布局Id
private Context mContext; //上下文
private ViewGroup mViewGroup; //对应listview
private MyHandler mMyHandler; //hanler

对应的初始化:

    public TestAdapter(String[] datas, int layoutId, Context context, ViewGroup view) {
this.datas = datas;
this.layoutId = layoutId;
mCOntext= context;
mViewGroup = view;

// taskCollection = new HashSet();
// 获取应用程序最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
// 设置图片缓存大小为程序最大可用内存的1/8
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount();
}
};
try {
// 获取图片缓存路径
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
// 创建DiskLruCache实例,初始化缓存数据
mDiskLruCache = DiskLruCache
.open(cacheDir, getAppVersion(context), 1, 20 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}

executors = Executors.newFixedThreadPool(3);
mMyHandler = new MyHandler(this);

}

接下来一起来看看getView方法:

    @Override
public View getView(int position, View convertView, ViewGroup parent) {
String url = (String) getItem(position);
View view;
if (cOnvertView== null) {
view = LayoutInflater.from(mContext).inflate(layoutId, null);
} else {
view = convertView;
}
ImageView imageView = (ImageView) view.findViewById(R.id.photo);
imageView.setTag(url);//防止图片错位
imageView.setImageResource(R.drawable.empty_photo);
loadBitmaps(imageView, url);
return view;
}

loadBitmaps方法:

    public void loadBitmaps(ImageView imageView, String imageUrl) {
try {
Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
if (bitmap == null) {
startExecutor(imageUrl);
} else {
if (imageView != null && bitmap != null) {
imageView.setImageBitmap(bitmap);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

加载Bitmap对象。此方法会在LruCache中检查所有屏幕中可见的ImageViewBitmap对象, 如果发现任何一个ImageViewBitmap对象不在LruCache缓存中,那么就会接着去检测该Bitmap是否在DiskLruCache,如果不在就开启异步线程去下载图片,反之就添加到LruCache中并展示出来。DiskLruCache文件转换成Bitmap是个耗时操作,防止UI线程卡顿,所以在线程池中进行。

startExecutor方法又是怎么实现的呢:

    public void startExecutor(final String imageUrl) {
executors.execute(new Runnable() {
@Override
public void run() {
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
DiskLruCache.Snapshot snapShot = null;
try {
// 生成图片URL对应的key
final String key = hashKeyForDisk(imageUrl);
// 查找key对应的缓存
snapShot = mDiskLruCache.get(key);
if (snapShot == null) {
// 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
// 缓存被写入后,再次查找key对应的缓存
snapShot = mDiskLruCache.get(key);
}
if (snapShot != null) {
fileInputStream = (FileInputStream) snapShot.getInputStream(0);
fileDescriptor = fileInputStream.getFD();
}
// 将缓存数据解析成Bitmap对象
Bitmap bitmap = null;
if (fileDescriptor != null) {
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
if (bitmap != null) {
// 将Bitmap对象添加到内存缓存当中
addBitmapToMemoryCache(imageUrl, bitmap);
}
mMyHandler.post(new Runnable() {
@Override
public void run() {
ImageView imageView = (ImageView) mViewGroup.findViewWithTag(imageUrl);
Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
if (imageView != null && bitmap != null) {
imageView.setImageBitmap(bitmap);
}
}
});
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileDescriptor == null && fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
}
}
}

}
});
}

代表比较长,需要耐着性子看。

获取图片流:

    /**
* 建立HTTP请求,并获取Bitmap对象。
*
* @param urlString 图片的URL地址
* @return 解析后的Bitmap对象
*/

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlCOnnection= null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
InputStream inputStream = null;
try {
in = new BufferedInputStream(new URL(urlString).openStream());
out = new BufferedOutputStream(outputStream);
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();
}
if (inputStream != null) {
inputStream.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}

经过测试new URL(urlString).openStream()获取图片流的方法最快。这里获取流也可以使用retrofit

        try {
ResponseBody respOnseBody= client.getRectService().downBitmaps(urlPath).execute().body();
if (responseBody != null) {
return responseBody.byteStream();//返回图片流
}
} catch (IOException e) {
e.printStackTrace();
}

由于retrofit内部进行了一些封装,获取流的时间较长,这里不推荐使用。

还可以这样获取流:

            final URL url = new URL(urlString);
urlCOnnection= (HttpURLConnection) url.openConnection();
urlConnection.getInputStream();

四、结论

red

第一个时间是开始加载第一张图片的时间

第二个时间是加载完最后一张图片的时间

它们的时间戳就2秒多。

在来看一下 monitors:

red

github源码


推荐阅读
  • 一、概述ceph为k8s提供存储服务主要有两种方式,cephfs和cephrdb;cephfs方式支持k8s的pv的3种访问模式ReadWriteOnce,ReadOnlyMany ... [详细]
  • Python排序函数完美体现了Python语言的简洁性,对于List对象,我们可以直接调用sort()函数(这里称为方法更合适)来进行排序ÿ ... [详细]
  • 关于初学PHP时的知识积累总结【PHP】
    后端开发|php教程PHP,知识积累后端开发-php教程PHP基础A、初识PHPPHP是与HTML混合使用的嵌入式语言。1、PHP标记默认标记短标记,需在php.ini中将shor ... [详细]
  • phpcms v9无法连接数据库怎么办
    CMS教程|PHPCMSphpcmsCMS教程-PHPCMSqq骂人源码,vscode搜索不到中文插件,ubuntu输入法下载,f14tomcat,sqliteknex,网页设计图 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • linux js文件怎么打开文件夹路径,js 获取文件本地路径
    1.代码获取文件本地路径选择导入数据源:functionbrowseFolder(){try{varMessage“\u8bf7\u9009\u62e9\u6587\u4ef6\u ... [详细]
  • 大数据学习环境安装关于防火墙​centos7使用的是firewalld,centos之前使用的是iptablesCentOS7关闭防火墙查看防火墙状态sudosy ... [详细]
  • 本博客是本人的一些小结,如有纰漏,欢迎拍砖。首先若是想做大项目的话,不要觉的框架写着没有用,而不写,先把框架写好,以后有需要的时候才可以填充这个框架,项目才可以做大。 ... [详细]
  • 就我个人在学习Python的过程中,经常会出现学习了新方法后,如果隔上几天不用,就忘了的情况,或者刚学习的更好的方法没有得到 ... [详细]
  • 好久没玩过C语言了,上一次还是在大二的时候。。。废话不多说,这里有一个C语言实现的学生选课系统代码,分享给大家,具体如下&# ... [详细]
  • Mask-RCNN源码阅读笔记
    阅读了https:blog.csdn.netu011974639articledetails78483779?locationNum9&fps1这篇博客这篇博客介 ... [详细]
  • iOS网络开发(7)大牛们的杰作AFNetworking
    本篇文章介绍传说中的 AFN框架的使用AFNetworking是iOS开发中最广泛使用的开源项目之一,是最活跃最有影响力的开源项目之一。    ... [详细]
  • php实现中文文件下载
    php教程|PHP源码php实现中文文件下载php教程-PHP源码php代码爱之谷2015源码,ubuntu16桌面,tomcat9解压缩半,python爬虫带页面,php批量删除 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
author-avatar
大帅哥晶晶_527
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有