前言
Bitmap三连
结合Bitmap三级缓存自己做个ImageLoader 解决UI卡顿问题
Android之带你从源码解析Bitmap占用内存正确的计算公式
自己动手写Bitmap高效加载 跟OOM说再见
在Android开发中图片下载和内存的使用是永远绕不开的话题,页面的加载离不开图片的使用,图片的使用必会占用一定的内存,但是手机内存总是有限的,只要你一点使用不当,就会给APP造成非常差的使用体验;所以怎么合适的对图片和内存进行一个合理的搭配就是一个重点了,图片缓存的实现能很好的解决这一个矛盾,现在图片相关的开源框架很多,比如Glide,Fresco,Picasso等,它们都实现了很好的图片缓存策略,但是如果你自己实现怎么做呢,今天就来实践一下
本文所含代码随时更新,可从这里下载最新代码
传送门
演示(因为GIF录制帧数的原因,演示效果没有真实情况下的那么流畅)
三级缓存
现在流行的一般是三级缓存机制,即
- 第一级:内存缓存(从内存中加载图片,速度最快,不浪费流量,但是会消耗手机运行内存)
- 第二级:磁盘缓存,或者说文件缓存(从本地加载图片,速度快,不浪费流量,但是会消耗磁盘容量,不过可以忽略这点,毕竟现在磁盘容量都很大了)
- 第三级:网络缓存(从网络加载图片,速度慢,浪费流量)
使用逻辑是:每次使用图片时,先从内存缓存中取出,如果有即使用,没有就从磁盘缓存中取出,如果有即使用并添加到内存缓存中,没有就从网络下载,下载完成后添加到磁盘缓存和内存缓存中
内存缓存
LRU算法
这里使用Android自带的LruCache,从这个名字可以看出使用的是LRU(Least Recently Used)算法,即最近最少使用算法;
它的核心是存储最近添加最后使用的图片,当你往里继续添加要缓存的元素时,如果预先设置的缓存容量满了,那就剔除掉那些最久添加最少使用的缓存元素
LruCache类支持泛型,内部维护了一个LinkedHashMap,可以接受多种key和value
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
}
这里你会不会有疑虑,为什么使用LinkedHashMap而不是使用HashMap
虽然LinkedHashMap和HashMap都是实现Map接口,同时LinkedHashMap还继承自HashMap,但是它俩有个最大的区别:HashMap中添加的元素是无序存放的,但是LinkedHashMap内部维护了一个双向链表,可以控制元素的迭代顺序,该迭代顺序可以是插入顺序,也可以是访问顺序,相当于是将所有Entry节点链入一个双向链表的HashMap
也正是因为这个特性,所以LinkedHashMap能很好的支持LRU算法
辅助类
/**
* @Description TODO(内存缓存)
* @author cxy
* @Date 2018/11/13 9:48
*/
public class MemoryLruCache {
private String TAG = MemoryLruCache.class.getSimpleName();
private LruCache<String,Bitmap> mMemoryCache;
private MemoryLruCache instance;
private MemoryLruCache(){
//虚拟机能获得的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
//内存缓存所使用的内存
int cache = (int) (maxMemory / 8);
mMemoryCache = new LruCache<String,Bitmap>(cache){
//重写两个方法
//计算一张图片所占内存
@Override
protected int sizeOf(String key, Bitmap value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //API 19
int size = value.getAllocationByteCount();
Log.e(TAG,"sizeOf size="+size);
return size;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12
return value.getByteCount();
}
// 在低版本中使用 Bitmap所占用的内存空间数等于Bitmap的每一行所占用的空间数乘以Bitmap的行数
return value.getRowBytes() * value.getHeight();
}
//回收内存
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
Log.e(TAG,"entryRemoved");
if (oldValue != null && !oldValue.isRecycled()) {
oldValue.recycle();
}
}
};
}
public MemoryLruCache getInstance(){
if (instance == null) {
instance = new MemoryLruCache();
}
return instance;
}
/**
* 从内存中取出图片
* @param key 通常是图片下载地址
* @return
*/
public Bitmap getBitmap(String key){
if (TextUtils.isEmpty(key)) return null;
return mMemoryCache.get(key);
}
/**
* 将bitmap保存到内存
* @param key
* @param bitmap
*/
public void putBitmap(String key,Bitmap bitmap){
mMemoryCache.put(key,bitmap);
}
}
这里有几个注意点:
- 这里使用单例模式,这样全局都使用同一份缓存
- 在构建LruCache实例的时候需要指定缓存大小,通常是JVM虚拟机能获得的最大内存的八分之一
- 重写sizeOf方法,返回一张图片所占内存值,为了适配不同版本,使用不同API
- 重写entryRemoved方法,因为这些图片都是存在内存中,当保存的图片超过缓存容量,就会从LruCache中的LinkedHashMap中去除掉,但是还占用内存的,我们需要将它回收掉,释放内存
这里其实有一个值的计算比较重要,那就是内存缓存空间到底应该多少,毕竟设置大了就可能导致内存浪费,设置小了又会导致图片频繁回收;我这里选用的是一种通用的算法,如果你觉得不好,但是自己又想不到该怎么计算,没关系,去看看一些开源图片框架怎么计算这个值的,比如Glide
// Package private to avoid PMD warning.
MemorySizeCalculator(MemorySizeCalculator.Builder builder) {
this.context = builder.context;
arrayPoolSize =
isLowMemoryDevice(builder.activityManager)
? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
: builder.arrayPoolSizeBytes;
int maxSize =
getMaxSize(
builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);
int widthPixels = builder.screenDimensions.getWidthPixels();
int heightPixels = builder.screenDimensions.getHeightPixels();
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);
int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
int availableSize = maxSize - arrayPoolSize;
if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
memoryCacheSize = targetMemoryCacheSize;
bitmapPoolSize = targetBitmapPoolSize;
} else {
float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(
TAG,
"Calculation complete"
+ ", Calculated memory cache size: "
+ toMb(memoryCacheSize)
+ ", pool size: "
+ toMb(bitmapPoolSize)
+ ", byte array size: "
+ toMb(arrayPoolSize)
+ ", memory class limited? "
+ (targetMemoryCacheSize + targetBitmapPoolSize > maxSize)
+ ", max size: "
+ toMb(maxSize)
+ ", memoryClass: "
+ builder.activityManager.getMemoryClass()
+ ", isLowMemoryDevice: "
+ isLowMemoryDevice(builder.activityManager));
}
}
可以看到Glide是根据每个APP具体内存情况,屏幕分辨率和系统版本计算出一个合理的值
这里还有一个问题,需要图片缓存的通常是有列表这种页面,每个item都含有图片,没有缓存需要大量重复的网络请求;但是缓存过后不再停留在这个页面,用户到别的页面了,或者所在的Activity销毁了,那这个缓存就需要清除,但是LruCache没有提供清空LinkedHashMap和回收其中的bitmap方法,且LinkedHashMap的定义又是private的,那这里只能通过反射去清空缓存了
/**
* 通过反射剔除缓存中的bitmap
* 回收bitmap内存
* @param urls 需要清除的value对应的key 可以为null
*/
public void cleanCache(String[] urls){
try {
Class classType = Class.forName("android.util.LruCache");
Field field = classType.getDeclaredField("map");
field.setAccessible(true);
LinkedHashMap<String,Bitmap> map = (LinkedHashMap<String, Bitmap>) field.get(mMemoryCache);
if (map == null) return;
Iterator<Map.Entry<String,Bitmap>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String,Bitmap> entry = iterator.next();
Bitmap bit = entry.getValue();
if (urls != null && urls.length > 0) {
for (int i=0; i<urls.length; i++) {
if (TextUtils.equals(entry.getKey(),urls[i])) {
if (bit != null && !bit.isRecycled()) {
bit.recycle();
}
iterator.remove();
break;
}
}
} else {
if (bit != null && !bit.isRecycled()) {
bit.recycle();
}
iterator.remove();
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
磁盘缓存
这种缓存通常情况下是将文件保存在SD卡上,不要保存到内置存储中,因为内置存储内存空间太宝贵了,不要浪费;但是保存到外置存储(一般是SD卡)也不要随便放到一个目录,如果你允许第三方应用操作你的数据,那可以随便找个目录存储;除此之外,通常将数据放在外置存储的私有缓存目录,如:
/storage/sdcard0/Android/data/包名/cache
/storage/sdcard0/Android/data/包名/cache/imagecache
辅助类
/**
* @Description TODO(磁盘缓存)
* @author cxy
* @Date 2018/11/14 11:18
*/
public class DiskLruCache{
private String TAG = DiskCache.class.getSimpleName();
private Context mContext;
private File cachePath;
private static DiskCache instance;
private DiskCache(Context context) {
this.mContext = context;
cachePath = FileStorageTools.getInstance(mContext).getExternalStoragePrivateCache();
}
public static DiskCache getInstance(Context context){
if (instance == null) {
instance = new DiskCache(context);
}
return instance;
}
/**
* 设置私有缓存目录
* @param pathName 次级目录名称 比如
* /imagecache
* /httpcache
*/
public void setCachePath(String pathName){
if (TextUtils.isEmpty(pathName)) return ;
cachePath = null;
cachePath = new File(cachePath.getAbsolutePath()+pathName);
cachePath.mkdirs();
}
public void putFileStream(String url, InputStream is){
FileStorageTools.getInstance(mContext).putStreamToExternalStorage(cachePath,encryptUrl(url),is);
}
public void putBitmap(String url, Bitmap bitmap){
FileStorageTools.getInstance(mContext).putBitmapToExternalStorage(cachePath,encryptUrl(url),bitmap);
}
public Bitmap getBitmap(String url,int inSamplesize){
byte[] data = FileStorageTools.getInstance(mContext).getDataFromExternalStorage(cachePath.getAbsolutePath()+File.separator+encryptUrl(url));
if (data == null) return null;
return BitmapTools.byte2Bitmap(data,inSamplesize);
}
/**
* 将url使用md5加密作为文件名
* md5加密是不可逆加密,防止资源盗用
* @param url
* @return
*/
private String encryptUrl(String url){
if (TextUtils.isEmpty(url)) return null;
String result = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] msg = md.digest(url.getBytes());
for (byte b:msg) {
String temp = Integer.toHexString(b & 0xff);
if (temp.length() == 1) {
temp = "0" + temp;
}
result += temp;
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return result;
}
}
这里面的FileStorageTools工具类可以在博主前面讲解Android数据存储文章中找到
这里有的同学可能考虑到这里只是存数据和取数据,那要不要删数据呢;其实存放在外置存储的私有目录的数据在App卸载的时候会被删除掉,而且现在手机SD卡内存容量都是64G,128G甚至更多,可以满足缓存容量需要;不过本着为用户考虑,还是需要提供删除的方法的
/**
* 清除指定目录缓存文件
* @param path 缓存目录
* 如果是null,默认为cachePath.getAbsolutePath()
*/
public void cleanAllCache(final String path){
LocalThreadPools.THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
if (TextUtils.isEmpty(path)) {
FileStorageTools.getInstance(mContext).delFile(cachePath.getAbsolutePath());
} else {
FileStorageTools.getInstance(mContext).delFile(path);
}
}
});
}
这是手动清除缓存,或者说提供一个按钮给用户清除
文件更新逻辑
有人可能会开始提需求了,上面做的只是存文件,而且是一直存下去,那能不能对这些文件进行一些动态管理操作,让缓存空间或者缓存时间控制在一个合理的范围呢?不用担心,这是可以做到的
-
按访问顺序删除文件:我这里以时间进行排序,保存文件和修改文件都会自动更新修改时间,但是我们需要做的是在获取文件的时候也去更新文件的更新时间
file.setLastModified(System.currentTimeMillis());
这样当缓存空间达到一定值的时候,就可以删除那些更早时间的文件
-
按时间删除文件:有时候你缓存文件是没有达到指定缓存大小,因为需要缓存的文件少呀;但是这时候可能出现的情况是:你的文件是上个月的,甚至去年的,那这些可能就是垃圾文件了,有必要删除的
这时候我们就需要派LinkedHashMap上场了
/**
* 存放文件路径和时间信息
* 有两种清除睡眠文件方法:
* 可根据时间删除文件
* 也可根据访问顺序删除文件
*/
private LinkedHashMap<String, Long> map;
map = new LinkedHashMap<>(0, 0.75f, true);
- map的key是文件路径,因为需要唯一确定一个文件
- value是文件更新时间,后续可以通过时间删除文件
- 构造方法第三个参数传入true,这样map中的元素就会以访问顺序保存,方便后续将那些访问最少的文件删除掉
然后需要提前设置缓存空间,缓存时间
//默认缓存空间大小 100M
private long FILE_CACHE_SIZE = 1024 * 1024 * 100;
//文件保留时间 默认保存一个月内的缓存文件
private int FARTHEST_TIME_FROM_NOW = 30 * 1;
接下来就是往map里填充值了,这里一定要注意先将文件数组按修改时间排序好,再往map里存
private void getFileMsg(){
List<File> files = FileStorageTools.getInstance(mContext).listFile(cachePath.getAbsolutePath());
if (files == null) return;
File[] fi = FileStorageTools.getInstance(mContext).sortFile(files,true);
for (int i=0; i<fi.length; i++){
File f = fi[i];
map.put(f.getAbsolutePath(),f.lastModified());
}
}
接下来一定要注意在保存文件和获取文件的时候更新map里面对应的值,然后就进入主题了
/**
* 修正缓存目录文件
* 超出预设缓存大小 就删除那些早期文件
* 早于文件保留时间跨度 删除
*/
public void reviseCacheFile(){
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_YEAR,FARTHEST_TIME_FROM_NOW);
farthestDate = calendar.getTime().getTime();
Iterator<Map.Entry<String, Long>> entrys = map.entrySet().iterator();
while (entrys.hasNext()) {
Map.Entry<String, Long> entry = entrys.next();
String key = entry.getKey();
long value = entry.getValue();
/**
* 先判断缓存空间是否超出预设值,这是最重要的
* 因为map里面的顺序是按时间先后存放的,最先迭代出来的总是更新时间最久远的
*/
if (cachePath.length() > FILE_CACHE_SIZE) {
File file = new File(key);
file.delete();
entrys.remove();
continue;
}
/**
* 再判断文件更新时间是否早于预设时间
* 如果早于预设时间 删除
*/
if (value < farthestDate) {
File file = new File(key);
file.delete();
entrys.remove();
continue;
}
}
}
这里主要分为两步
- 先判断缓存目录的大小有没有超出预设值,这是最重要的,如果超出,哪个文件时间最早就把它删了
- 接着判断文件时间是否早于预设的时间范围,要是早于那就删除
图片异步加载器
有了内存缓存和文件缓存,那我们可以自己做一个简单的图片加载器
一般来说一个好的ImageLoader要做到
- 图片的同步加载
- 图片的异步加载
- 图片压缩处理
- 图片三级缓存机制
那我们就按照这几点来做
/**
* @Description TODO(结合缓存机制异步加载图片)
* @author cxy
* @Date 2018/11/14 11:18
*/
public class EasyImageLoader{
private String TAG = AsyncImageLoader.class.getSimpleName();
private final int LOAD_IMAGE_BITMAP = 1000;
private final int LOAD_IMAGE_ERROR = 2000;
private WeakReference<Context> mContext;
private MemoryLruCache mMemoryCache;
private DiskLruCache mDiskCache;
private int errorLoadId = -1;
private int loadingId = -1;
private static AsyncImageLoader imageLoader;
private AsyncImageLoader(Context context) {
this.mContext = new WeakReference<>(context);
mMemoryCache = new MemoryLruCache();
mDiskCache = new DiskLruCache(context);
}
public static AsyncImageLoader getInstance(Context context){
if (imageLoader == null) {
imageLoader = new AsyncImageLoader(context);
}
return imageLoader;
}
public AsyncImageLoader setErrorLoadView(int resourceID){
errorLoadId = resourceID;
return this;
}
public AsyncImageLoader setLoadingView(int loadingId){
this.loadingId = loadingId;
return this;
}
public AsyncImageLoader setMemoryCache(int cacheSize){
mMemoryCache.setMemoryCache(cacheSize);
return this;
}
public AsyncImageLoader setFarthestTime(int days){
mDiskCache.setFarthestTime(days);
return this;
}
public AsyncImageLoader setCacheSize(long size){
mDiskCache.setCacheSize(size);
return this;
}
public AsyncImageLoader setCachePath(String pathName){
mDiskCache.setCachePath(pathName);
return this;
}
private boolean loadMemoryBitmap(ImageView view, String imgUrl){
if (loadingId != -1) {
view.setBackgroundResource(loadingId);
}
Bitmap bitmap = mMemoryCache.getBitmap(imgUrl);
if (bitmap != null) {
view.setImageBitmap(bitmap);
return true;
}
return false;
}
private boolean loadDiskBitmap(ImageView view, String imgUrl, int targetWidth, int targetHeight){
Bitmap bitmap = mDiskCache.getBitmap(imgUrl, targetWidth,targetHeight);
if (bitmap != null) {
sendMessage(view,bitmap,imgUrl);
mMemoryCache.putBitmap(imgUrl,bitmap);
return true;
}
return false;
}
/**
* 加载图片
* @param view
* @param imageUrl
* @param targetWidth
* @param targetHeight
*/
public void loadImage(final ImageView view, final String imageUrl, final int targetWidth, final int targetHeight) {
//从内存缓存获取
if (loadMemoryBitmap(view,imageUrl)) {
return;
}
LocalThreadPools.THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
//从磁盘读取
if (loadDiskBitmap(view,imageUrl,targetWidth,targetHeight)) {
return ;
}
//从网络下载
try {
URL url = new URL(imageUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
mDiskCache.putFileStream(imageUrl,connection.getInputStream());
loadDiskBitmap(view,imageUrl,targetWidth,targetHeight);
} catch (MalformedURLException e) {
if (errorLoadId != -1) {
Message message = mHandler.obtainMessage();
message.what = LOAD_IMAGE_ERROR;
message.obj = view;
mHandler.sendMessage(message);
}
e.printStackTrace();
} catch (IOException e) {
if (errorLoadId != -1) {
Message message = mHandler.obtainMessage();
message.what = LOAD_IMAGE_ERROR;
message.obj = view;
mHandler.sendMessage(message);
}
e.printStackTrace();
}
}
});
}
private void sendMessage(ImageView view, Bitmap bitmap, String imageUrl){
Message message = mHandler.obtainMessage();
message.what = LOAD_IMAGE_BITMAP;
message.obj = view;
Bundle data = new Bundle();
data.putParcelable("bitmap",bitmap);
data.putString("url",imageUrl);
message.setData(data);
mHandler.sendMessage(message);
}
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
int what = msg.what;
switch (what) {
case LOAD_IMAGE_BITMAP:
ImageView view = (ImageView) msg.obj;
Bundle bundle = msg.getData();
Bitmap bitmap = bundle.getParcelable("bitmap");
String imageUrl = bundle.getString("url");
if (bitmap == null || view.getTag() == null) return;
if (TextUtils.equals((String)view.getTag(),imageUrl)) {
view.setImageBitmap(bitmap);
}
break;
case LOAD_IMAGE_ERROR:
ImageView v = (ImageView) msg.obj;
v.setBackgroundResource(errorLoadId);
break;
}
}
};
public void cleanCache(String[] urls){
mMemoryCache.cleanCache(urls);
}
}
加载图片逻辑就是先从内存缓存中取,如果没有从磁盘缓存中取,都没有就从网络下载,然后再从磁盘加载,最后添加到内存缓存中;这样形成了一个简单的图片加载工具,当然了还有很多Bitmap的加载方法没有写,准备放到下一篇关于Bitmap的详细解析文章中
可以看到我这里使用的异步加载是用线程池实现的(同时结合Handler达到更新UI的效果),为什么呢?因为如果直接new Thread,数据量小还好,如果数据量大的话,那这样大量new内存,手机是吃不消的;还有一个原因是如果使用AsyncTask,就没办法实现并发需求(关于AsyncTask的解析可以参考博主之前的文章带你从源码掌握AsyncTask工作原理(为什么串行执行 为什么内存泄漏)),显然是不行的
全局线程池
/**
* @Description TODO(全局使用的线程池)
* @author cxy
* @Date 2018/11/14 17:22
*/
public class LocalThreadPools {
public static final Executor THREAD_POOL_EXECUTOR;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT-1,4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2;
private static final int KEEP_ALIVE_SECONDS = 16;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "MangoTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<>(16);
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory){
@Override
public void execute(Runnable command) {
//如果激活的线程过多就return,可以在这里提醒用户
if(getActiveCount()+1 >= MAXIMUM_POOL_SIZE){
return;
}
super.execute(command);
}
};
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
}
接下来在Adapter中就很方便使用了
final String imgUrl = list[position];
// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);
// 预设一个图片
holder.img.setImageResource(R.mipmap.ic_launcher);
if (!TextUtils.isEmpty(imgUrl)) {
AsyncImageLoader.getInstance(context).loadImage(holder.img, imgUrl,
DisplayTools.dp2px(context,40),DisplayTools.dp2px(context,40));
}
优化UI卡顿
做到这里其实基本上没有卡顿情况了,不过还是有待优化的地方;一般情况下UI列表卡顿造成的原因包含两个
- 在主线程做了太多耗时操作,比如在adapter中直接在主线程加载图片,就很容易导致卡顿,这个上面异步加载解决了
- 优化内存使用,其实这点上面还没做的更好,因为上面没有控制异步任务的执行频率;假如用户恶意的快速滑动屏幕,在短时间内会造成大量的异步任务在执行,且伴随着大量的UI操作,但是UI操作是在主线程执行,这样势必会在一定情况下造成UI卡顿,解决办法其实很简单:在用户滑动的时候停止异步任务,在用户停止滑动的时候再执行,这样只会产生当前屏幕所包含的定量的异步任务
不管是ListView还是RecyclerView等,都只用设置OnScrollListener监听,然后起个标志位判断下即可
/**
* Author:Mangoer
* Time:2018/11/17 17:40
* Version:
* Desc:TODO()
*/
public class RecycleAdapter extends RecyclerView.Adapter<RecycleAdapter.ViewHolder> {
private Context mContext;
private String[] list;
private boolean isShouldBeLoaded = true;
public RecycleAdapter(Context mContext) {
this.mContext = mContext;
}
public void setList(String[] list) {
this.list = list;
}
public void setmRecyvlerView(RecyclerView mRecyvlerView) {
mRecyvlerView.addOnScrollListener(scrollListener);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View content = LayoutInflater.from(mContext).inflate(R.layout.list_item,parent,false);
ViewHolder viewHolder = new ViewHolder(content);
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final String imgUrl = list[position];
// 给 ImageView 设置一个 tag
holder.view.setTag(imgUrl);
// 预设一个图片
holder.view.setImageResource(R.mipmap.ic_launcher);
if (!TextUtils.isEmpty(imgUrl) && isShouldBeLoaded) {
AsyncImageLoader.getInstance(mContext).loadImage(holder.view, imgUrl, DisplayTools.dp2px(mContext,80),DisplayTools.dp2px(mContext,80));
}
}
@Override
public int getItemCount() {
if (list == null) return 0;
return list.length;
}
class ViewHolder extends RecyclerView.ViewHolder{
ImageView view;
public ViewHolder(View itemView) {
super(itemView);
view = (ImageView) itemView.findViewById(R.id.userimage);
}
}
RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (SCROLL_STATE_IDLE == newState) {
isShouldBeLoaded = true;
notifyDataSetChanged();
} else {
isShouldBeLoaded = false;
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
};
}
这样我们就能省掉大量的非必要的异步操作,节省内存开销
到此关于图片缓存及加载告一段落,然后关于Bitmap的内存占用放在下一篇博客Android之带你从源码解析Bitmap占用内存正确的计算公式