热门标签 | 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源码


推荐阅读
  • 本文探讨了SSDP(简单服务发现协议)和WSD(Web服务发现)协议,特别是SSDP如何通过固定多播地址239.255.255.250:1900实现局域网内的服务自发现功能。文中还详细介绍了SSDP协议的关键操作类型及其应用场景。 ... [详细]
  • 并发编程 12—— 任务取消与关闭 之 shutdownNow 的局限性
    Java并发编程实践目录并发编程01——ThreadLocal并发编程02——ConcurrentHashMap并发编程03——阻塞队列和生产者-消费者模式并发编程04——闭锁Co ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 深入解析SpringMVC核心组件:DispatcherServlet的工作原理
    本文详细探讨了SpringMVC的核心组件——DispatcherServlet的运作机制,旨在帮助有一定Java和Spring基础的开发人员理解HTTP请求是如何被映射到Controller并执行的。文章将解答以下问题:1. HTTP请求如何映射到Controller;2. Controller是如何被执行的。 ... [详细]
  • 深入解析 Android IPC 中的 Messenger 机制
    本文详细介绍了 Android 中基于消息传递的进程间通信(IPC)机制——Messenger。通过实例和源码分析,帮助开发者更好地理解和使用这一高效的通信工具。 ... [详细]
  • CentOS 7.6环境下Prometheus与Grafana的集成部署指南
    本文旨在提供一套详细的步骤,指导读者如何在CentOS 7.6操作系统上成功安装和配置Prometheus 2.17.1及Grafana 6.7.2-1,实现高效的数据监控与可视化。 ... [详细]
  • 在 Android 开发中,通过 Intent 启动 Activity 或 Service 时,可以使用 putExtra 方法传递数据。接收方可以通过 getIntent().getExtras() 获取这些数据。本文将介绍如何使用 RoboGuice 框架简化这一过程,特别是 @InjectExtra 注解的使用。 ... [详细]
  • ElasticSearch 集群监控与优化
    本文详细介绍了如何有效地监控 ElasticSearch 集群,涵盖了关键性能指标、集群健康状况、统计信息以及内存和垃圾回收的监控方法。 ... [详细]
  • 为了解决不同服务器间共享图片的需求,我们最初考虑建立一个FTP图片服务器。然而,考虑到项目是一个简单的CMS系统,为了简化流程,团队决定探索七牛云存储的解决方案。本文将详细介绍使用七牛云存储的过程和心得。 ... [详细]
  • 由二叉树到贪心算法
    二叉树很重要树是数据结构中的重中之重,尤其以各类二叉树为学习的难点。单就面试而言,在 ... [详细]
  • 访问一个网页的全过程
    准备:DHCPUDPIP和以太网启动主机,用一根以太网电缆连接到学校的以太网交换机,交换机又与学校的路由器相连.学校的这台路由器与一个ISP链接,此ISP(Intern ... [详细]
  • 本文详细介绍了如何在 Android 中使用值动画(ValueAnimator)来动态调整 ImageView 的高度,并探讨了相关的关键属性和方法,包括图片填充后的高度、原始图片高度、动画变化因子以及布局重置等。 ... [详细]
  • 远程过程调用(RPC)是一种允许客户端通过网络请求服务器执行特定功能的技术。它简化了分布式系统的交互,使开发者可以像调用本地函数一样调用远程服务,并获得返回结果。本文将深入探讨RPC的工作原理、发展历程及其在现代技术中的应用。 ... [详细]
  • 本文探讨了如何通过一系列技术手段提升Spring Boot项目的并发处理能力,解决生产环境中因慢请求导致的系统性能下降问题。 ... [详细]
  • 本文详细介绍了如何正确配置Java环境变量PATH,以确保JDK安装完成后能够正常运行。文章不仅涵盖了基本的环境变量设置步骤,还提供了针对不同操作系统下的具体操作指南。 ... [详细]
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社区 版权所有