Android异步加载全解析之引入一级缓存

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Android异步加载全解析之引入缓存为啥要缓存 通过对图像的缩放,我们做到了对大图的异步加载优化,但是现在的App不仅是高清大图,更是高清多图,动不动就是图文混排,以图代文,如果这些图片都加载到内存中,必定会OOM。

Android异步加载全解析之引入缓存


为啥要缓存

通过对图像的缩放,我们做到了对大图的异步加载优化,但是现在的App不仅是高清大图,更是高清多图,动不动就是图文混排,以图代文,如果这些图片都加载到内存中,必定会OOM。因此,在用户浏览完图像后,应当立即将这些废弃的图像回收,但是,这又带来了另一个问题,也就是当用户在浏览完一次图片后,如果还要返回去再进行重新浏览,那么这些回收掉的图像又要重新进行加载,保不准就要那些无聊到蛋疼的人在那一边看你回收GC,一边看你重新加载。这两件事情,肯定是互相矛盾的,也是影响性能的一个很重要的原因。

内存缓存

针对这样一个非常需要找到一个彼此平衡点的问题,Google提供了一套内存缓存技术。内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。LruCache 是在support-v4中才引入的,在引入LruCache 之前,Google建议的是使用软引用或弱引用 (SoftReference or WeakReference)来进行内存缓存。但是从Android 2.3开始,GC算法修改,软引用与弱引用同样会优先被GC回收,所以这种方法也就没有太高的使用价值了,现在网上很多还在继续使用SoftReference 和WeakReference的文章,大多都是过时的文章,建议大家跟上党的步伐,与时俱进。

LruCache使用

内存缓存LruCache所使用的内存缓存大小是由开发者决定的,开发者需要根据图像的使用率、分辨率、访问频率、设备性能等很多因素进行考虑。这个平衡点经常需要很多经验和测试来决定。使用LruCache非常简单:

private LruCache<String, Bitmap> mMemoryCaches;

// 获取应用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
// 分配cache
int cacheSize = maxMemory / 10;
mMemoryCaches = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };

// 从LruCache获取中获取缓存对象
public Bitmap getBitmapFromMemoryCaches(String url) {
    return mMemoryCaches.get(url);
}

// 增加缓存对象到LruCache
public void addBitmapToMemoryCaches(String url,Bitmap bitmap) {
    if (getBitmapFromMemoryCaches(url) == null) {
        mMemoryCaches.put(url, bitmap);
    }
}

首先,我们需要声明LruCache,接着,通过LruCache的构造方法创建缓存对象,并为其分配cacheSize,这个cacheSize通常我们需要通过Runtime来获取,获取当前系统分给App的可用内存,并将这些内存的一部分用做LruCache缓存。LruCache中必须重写sizeOf方法,通过这个方法,LruCache可以获取每个缓存对象的大小,子类必须重写,因为默认的LruCache获取的是缓存的个数。。。尼玛。
最后,我们提供两个方法getBitmapFromMemoryCaches和addBitmapToMemoryCaches分别用来获取和增加内存缓存到LruCache。
等等,我们好像还没写释放内存的方法,对,不用你写了,Lru算法可以保证cacheSize不会OOM,一旦超过这个大小,GC就会回收时间最长的对象,释放空间。

为异步处理加入一级缓存

OK,在了解了关于缓存的基础信息后,我们回到现在这个例子,想想怎么利用缓存来进行异步处理的优化。首先,ListView、GridView这些娇生惯养的玩意儿,碰不得摔不得,更不能在它滚的开心的时候,你还在后面拼命玩加载。所以,第一个重点,滚的时候就让它开心的滚,滚完了再开始加载。

滚完再加载

要实现这一点,我们可以通过给Adapter增加AbsListView.OnScrollListener接口来实现。
当然,还有一点需要注意,第一次初始化的时候,一定要手动来加载图片,不然系统判断你没滚,只能调用onScroll方法,不会调用onScrollStateChanged方法。而且我们也需要在onScroll方法中来不断获取可见的Item。特别要注意的是visibleItemCount,只要大于0的时候,才认为是开始显示图片了。
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (scrollState == SCROLL_STATE_IDLE) {
        mImageLoader.loadImages(mStart, mEnd);
    } else {
        mImageLoader.cancelAllTasks();
    }
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    mStart = firstVisibleItem;
    mEnd = firstVisibleItem + visibleItemCount;
    if (mFirstFlag  && visibleItemCount > 0) {
        mImageLoader.loadImages(mStart, mEnd);
        mFirstFlag = false;
    }
}

加载显示的项目

加载数据的时候,获取第一个能显示的Item和最后一个可见的Item,只加载这一部分。所以我们创建一个方法——loadImages(int start, int end)。这个方法用来加载从start到end之间的Item数据。
加载的时候,先从内存缓存中去取,如果有,那说明最近已经加载过了,那直接加载就好了,如果没有取到,那就开启synctask去下载。

public void loadImages(int start, int end) {
    for (int i = start; i < end; i++) {
        String url = Images.IMAGE_URLS[i];
        Bitmap bitmap = getBitmapFromMemoryCaches(url);
        if (bitmap == null) {
            ASyncDownloadImage task = new ASyncDownloadImage(url);
            mTasks.add(task);
            task.execute(url);
        } else {
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            imageView.setImageBitmap(bitmap);
        }
    }
}

这里我们在设置图片的时候,直接通过findViewWithTag,通过url来找到相应的Imageview,这里与之前不同是因为我们这里是按照start到end来进行加载,直接从ListView对象中获取对应的Imageview比较简单。

下载与Asynctask

下载依然是使用老方法:
private static Bitmap getBitmapFromUrl(String urlString) {
    Bitmap bitmap;
    InputStream is = null;
    try {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        is = new BufferedInputStream(conn.getInputStream());
        bitmap = BitmapFactory.decodeStream(is);
        conn.disconnect();
        return bitmap;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null)
                is.close();
        } catch (IOException e) {
        }
    }
    return null;
}

Asynctask也与之前基本类似:

class ASyncDownloadImage extends AsyncTask<String, Void, Bitmap> {

    private String url;

    public ASyncDownloadImage(String url) {
        this.url = url;
    }

    @Override
    protected Bitmap doInBackground(String... params) {
        url = params[0];
        Bitmap bitmap = getBitmapFromUrl(url);
        if (bitmap != null) {
            addBitmapToMemoryCaches(url, bitmap);
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        ImageView imageView = (ImageView) mListView.findViewWithTag(url);
        if (imageView != null && bitmap != null) {
            imageView.setImageBitmap(bitmap);
        }
        mTasks.remove(this);
    }
}

唯一不同的是,我们在下载好图像之后,会将图像加载到Lrucache。

组装

OK,万事具备,准备刷代码。在刷之前,我们先来重新整理下思路,首先,在Adapter中,一加载ListView,就开始下载显示范围内的Item的图像,这时候缓存中当然没有,所以都去下载了,下完了就显示在Item中,并缓存起来,如果还没下完,你就迫不及待的滚起来了,那么立即取消所有task,让ListView欢快的滚,滚完之后,继续加载。
OK,该讲的都讲了,下面我们开始刷代码了,一切尽在不言中,只有代码最懂你。

package com.imooc.listviewacyncloader;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.ListView;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

public class ImageLoaderWithCaches {

    private Set<ASyncDownloadImage> mTasks;
    private LruCache<String, Bitmap> mMemoryCaches;
    private ListView mListView;

    public ImageLoaderWithCaches(ListView listview) {
        this.mListView = listview;
        mTasks = new HashSet<>();
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 10;
        mMemoryCaches = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
    }

    public void showImage(String url, ImageView imageView) {
        Bitmap bitmap = getBitmapFromMemoryCaches(url);
        if (bitmap == null) {
            imageView.setImageResource(R.drawable.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    public Bitmap getBitmapFromMemoryCaches(String url) {
        return mMemoryCaches.get(url);
    }

    public void addBitmapToMemoryCaches(String url,Bitmap bitmap) {
        if (getBitmapFromMemoryCaches(url) == null) {
            mMemoryCaches.put(url, bitmap);
        }
    }

    public void loadImages(int start, int end) {
        for (int i = start; i < end; i++) {
            String url = Images.IMAGE_URLS[i];
            Bitmap bitmap = getBitmapFromMemoryCaches(url);
            if (bitmap == null) {
                ASyncDownloadImage task = new ASyncDownloadImage(url);
                mTasks.add(task);
                task.execute(url);
            } else {
                ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    private static Bitmap getBitmapFromUrl(String urlString) {
        Bitmap bitmap;
        InputStream is = null;
        try {
            URL url = new URL(urlString);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(conn.getInputStream());
            bitmap = BitmapFactory.decodeStream(is);
            conn.disconnect();
            return bitmap;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null)
                    is.close();
            } catch (IOException e) {
            }
        }
        return null;
    }

    public void cancelAllTasks() {
        if (mTasks != null) {
            for (ASyncDownloadImage task : mTasks) {
                task.cancel(false);
            }
        }
    }

    class ASyncDownloadImage extends AsyncTask<String, Void, Bitmap> {

        private String url;

        public ASyncDownloadImage(String url) {
            this.url = url;
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            url = params[0];
            Bitmap bitmap = getBitmapFromUrl(url);
            if (bitmap != null) {
                addBitmapToMemoryCaches(url, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTasks.remove(this);
        }
    }
}

下面是Adapter的代码:

package com.imooc.listviewacyncloader;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;

import java.util.List;

public class MyAdapterUseCaches extends BaseAdapter implements
        AbsListView.OnScrollListener {

    private LayoutInflater mInflater;
    private List<String> mData;
    private ImageLoaderWithCaches mImageLoader;
    private int mStart = 0, mEnd = 0;
    private boolean mFirstFlag;

    public MyAdapterUseCaches(Context context, List<String> data, ListView listView) {
        this.mData = data;
        mInflater = LayoutInflater.from(context);
        mImageLoader = new ImageLoaderWithCaches(listView);
        mImageLoader.loadImages(mStart, mEnd);
        mFirstFlag = true;
        listView.setOnScrollListener(this);
    }

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public Object getItem(int position) {
        return mData.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        String url = mData.get(position);
        ViewHolder viewHolder = null;
        if (convertView == null) {
            viewHolder = new ViewHolder();
            convertView = mInflater.inflate(R.layout.listview_item, null);
            viewHolder.imageView =
                    (ImageView) convertView.findViewById(R.id.iv_lv_item);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.imageView.setTag(url);
        viewHolder.imageView.setImageResource(R.drawable.ic_launcher);
        mImageLoader.showImage(url, viewHolder.imageView);
        return convertView;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == SCROLL_STATE_IDLE) {
            mImageLoader.loadImages(mStart, mEnd);
        } else {
            mImageLoader.cancelAllTasks();
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisibleItem + visibleItemCount;
        if (mFirstFlag  && visibleItemCount > 0) {
            mImageLoader.loadImages(mStart, mEnd);
            mFirstFlag = false;
        }
    }

    public class ViewHolder {
        public ImageView imageView;
    }
}

是不是非常简单,现在引入缓存了,下载过的图片会暂时保存在内存中,妈妈再也不用担心你OOM啦。
我们下拉试试,下载完的图片再次出现也可以马上加载了,除非滑动太多导致GC。



可以就看见,我们的这次利用缓存进行加载有这样几个特点:
1、初始化的时候加载
2、滑动的时候才加载
3、加载的内容暂存缓存中
4、只加载显示的区域


后面我们将继续优化缓存,未完待续~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

我的Github
我的视频 慕课网


目录
打赏
0
0
0
0
16
分享
相关文章
LLM高效推理:KV缓存与分页注意力机制深度解析
随着大型语言模型(LLM)规模和复杂性的增长,高效推理变得至关重要。KV缓存和分页注意力是优化LLM推理的两项关键技术。KV缓存通过存储键值对减少重复计算,而分页注意力则通过将序列分割成小块来降低内存消耗,从而有效处理长序列。本文深入剖析这些技术的工作原理及其在仅解码器模型中的应用,探讨其优势与挑战,并展示其实现示例。
47 16
LLM高效推理:KV缓存与分页注意力机制深度解析
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
安卓与iOS的跨平台开发:Flutter框架深度解析
在移动应用开发的海洋中,Flutter作为一艘灵活的帆船,正引领着开发者们驶向跨平台开发的新纪元。本文将揭开Flutter神秘的面纱,从其架构到核心特性,再到实际应用案例,我们将一同探索这个由谷歌打造的开源UI工具包如何让安卓与iOS应用开发变得更加高效而统一。你将看到,借助Flutter,打造精美、高性能的应用不再是难题,而是变成了一场创造性的旅程。
Android与iOS开发环境搭建全解析####
本文深入探讨了Android与iOS两大移动操作系统的开发环境搭建流程,旨在为初学者及有一定基础的开发者提供详尽指南。我们将从开发工具的选择、环境配置到第一个简单应用的创建,一步步引导读者步入移动应用开发的殿堂。无论你是Android Studio的新手还是Xcode的探索者,本文都将为你扫清开发道路上的障碍,助你快速上手并享受跨平台移动开发的乐趣。 ####
|
4月前
|
深入解析Android系统架构及其对开发者的意义####
【10月更文挑战第21天】 本文旨在为读者揭开Android操作系统架构的神秘面纱,探讨其如何塑造现代移动应用开发格局。通过剖析Linux内核、硬件抽象层、运行时环境及应用程序框架等关键组件,揭示Android平台的强大功能与灵活性。文章强调了理解Android架构对于开发者优化应用性能、提升用户体验的重要性,并展望了未来技术趋势下Android的发展方向。 ####
99 0
如何防止DNS缓存中毒攻击(一)
DNS缓存中毒也称为DNS欺骗
83 10
如何防止DNS缓存中毒(Ⅱ)
服务器应该配置为尽可能少地依赖与其他DNS服务器的信任关系
79 10
如何防止DNS缓存中毒(Ⅱ)
防止DNS缓存中毒的方法包括:减少DNS服务器与其它服务器的信任关系;限制DNS服务器上的服务;使用最新版DNS;加强用户安全教育,如识别可疑网站,仅访问HTTPS网站等。部署SSL证书并选择符合国际Webtrust标准的CA机构,可进一步提高安全性。
77 1

热门文章

最新文章

推荐镜像

更多