关闭

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

标签: retrofitoomexecutorsdisklruLruCache
2479人阅读 评论(0) 收藏 举报
分类:

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

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

先贴个图:

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<String, Bitmap> 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<BitmapWorkerTask>();
        // 获取应用程序最大可用内存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        // 设置图片缓存大小为程序最大可用内存的1/8
        mMemoryCache = new LruCache<String, Bitmap>(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源码

3
0
查看评论

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

记得在很早之前,我有写过一篇文章Android高效加载大图、多图解决方案,有效避免程序OOM,这篇文章是翻译自Android Doc的,其中防止多图OOM的核心解决思路就是使用LruCache技术。但LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新...
  • sinyu890807
  • sinyu890807
  • 2014-08-07 09:13
  • 100764

Retrofit(重构——图片下载)

这篇文章主要介绍如何下载一张图片,有人可能说服务器都已经返回http的url图片地址给我了,我只要用glide、imageloader或者一些其他的图片加载工具就能获取到图片了,非也非也,就如博主之前公司,图片请求给的是图片的id,而不是url地址,比如2015_01_13_06af17b6-9b0...
  • qqyanjiang
  • qqyanjiang
  • 2016-04-06 17:17
  • 4303

Retrofit Rxjava 实现图片下载、保存并展示

首先我们看一下Retrofit常规的用法,在不使用Rxjava的情况下,我们默认返回的是Call。 public interface ServiceApi { //下载文件 @GET Call downloadPicFromNet(@Url String fileUrl); } ...
  • six_god_2
  • six_god_2
  • 2016-05-10 15:09
  • 8854

妹子图APP(一)—— Retrofit+Glide+Gson加载网络图片

使用Retrofit+Glide+Gson实现简单的网络图片加载
  • ww897532167
  • ww897532167
  • 2017-02-28 11:24
  • 1202

android Retrofit下载图片

Retrofit通过Url下载图片 1.首先工具类: /** * ClassName:DownloadImageUtils * Description TODO 下载图片 * created by 漠天 * Data 2016/12/20 11:35 */ public class Do...
  • u010326875
  • u010326875
  • 2017-05-27 09:51
  • 1379

Android Retrofit 2.0框架上传图片解决方案(一张与多张的处理)

标题: Android Retrofit 2.0框架上传图片解决方案(一张与多张的处理)  1.单张图片的上传    /** * 上传一张图片 * @param description * @param ...
  • u010046908
  • u010046908
  • 2016-01-29 17:01
  • 13825

Retrofit简单使用+ImageLoader图片加载

  • 2017-03-30 12:47
  • 789KB
  • 下载

如何通过Retrofit提交图片

这篇帖子主要介绍如何通过Retrofit同时提交图片和参数,例子中结合了Rxjava。 例子是将一幅图片和2个参数传到服务器,服务器返回该图像处理后的二值化结果并显示(公司内部服务器的接口)。其接口调用方式: /** * http://image.ideayapai.com/upl...
  • zhupumao
  • zhupumao
  • 2016-09-14 11:26
  • 1533

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

人生的旅途,前途很远,也很暗。然而不要怕,不怕的人的面前才有路。——鲁迅自从上一篇博客发布后,已经有很长时间没有更新博客了,一直忙着支付通的事情,在此给大家道个歉。先贴个图:你不要惊讶,这就是第一次从网络获取图片的速度,感觉比本地读取图片的速度还要快吧。加载100张图片真的只要2秒时间,你不要不相信...
  • u012551350
  • u012551350
  • 2016-06-02 17:35
  • 2479

Glide加载图片into(Target)导致OOM异常

最近在项目中遇到Drawable转换成Bitmap的需求,Glide加载的Drawable对象,而需要的是Bitmap对象。于是找到一个可以使用的方法,但是程序运行之后出现OOM异常,通过AndroidStudio自带的内存检测工具观察得到在加载图片的时候,memory的使用大约增长了几十M。经过对...
  • forevercbb
  • forevercbb
  • 2016-03-31 16:30
  • 5520
    个人资料
    • 访问:254418次
    • 积分:3364
    • 等级:
    • 排名:第11862名
    • 原创:63篇
    • 转载:1篇
    • 译文:0篇
    • 评论:194条
    Android开发交流群
    QQ群:478720016

    最新评论