本文知识点大部分来自《Android开发艺术探索》,大佬yyds!
Bitmap的高效加载
高效加载 Bitmap 需要用到 BitmapFactory.Options 类
使用 ImageView 时,图片的原始尺寸与设定的大小通常情况下不会一致,假如图片大小为530X530,而设定的大小为100X100,这时把整个图片加载进来显然就浪费了资源。如果通过一定的采样率加载缩小后的图片,可以降低内存占用,在一定层度上也避免了OOM的出现
通过使用在 BitmapFactory.Options 中的参数 inSampleSize (采样率,图片缩放倍数)可以达到缩放图片的效果:
当 inSampleSize 为1时,宽高长度不变化,图片为原大小
当 inSampleSize 为2时,宽高各缩小 1/2,图片为原来大小的 1/4
以此类推…
注意点:
(1)inSampleSize 的值必须要大于1时才能起作用
(2)采样率同时作用在宽和高上,所以重新采样后,图片的大小以采样率的2次方递减(即 1/(inSampleSize的2次方) )
(3)inSampleSize 的值小于1时作用相当于1,即无效果
(4)如果 inSampleSize 的值不为2的指数,系统会向下取整一个最接近2的指数的数值(书中说此结论并非在所有的Android版本上成立,仅作为建议)
实践
代码:
public static Bitmap decodeSampleFromResource(Resources resources, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
解析:
在上述代码的 decodeSampleFromResource() 方法中,主要分为四个步骤:
(1)将Options实例中的 inJustDecodeBounds 变量设置为true:当该变量为true时仅解析图片原始的宽高信息,并不会真正的加载图片
在第一个步骤中,BitmapFactory获取的宽高信息与图片的位置和程序运行设备的分辨率有关,不是固定的
(2)在 calculateInSampleSize() 方法中取出原始宽高信息
(3)通过设定的值计算出采样率 inSampleSize
(3)将 inJustDecodeBounds 设置为false,并使用采样率重新加载图片
使用:
imageView5.setImageBitmap(decodeSampleFromResource(getResources(), R.drawable.test, 25, 25));
效果呈现:
使用Bitmap的缓存策略
概述
Android设备不同于PC端设备,如果缓存数量或体积较大的图片显然对用户的流(qian)量(bao)不太友好。并且手机的存储容量也有一定限制,这时候就需要有缓存策略来帮用户节省流量等开销
缓存通常分为三级:
(1)内存缓存:优先加载,速度最快
(2)磁盘缓存:次优先加载,速度快
(3)网络缓存:最后加载,速度慢,浪费流量
缓存策略
而缓存策略中至少应该包括缓存的添加、获取、删除三类操作
对于删除操作,如何定义缓存的新旧对应着不同的缓存算法,常用的算法之一是LRU(近期最少使用算法Least Recently Used),核心思想是当缓存满了时优先淘汰近期最少使用的缓存对象。
实现LRU算法的缓存类有 LruCache 类和 DiskLruCache 类,分别实现了内存缓存和磁盘缓存
LruCache内存缓存
参考API:
LruCache | Android Developers
LruCache - Android中文版 - API参考文档 (apiref.com)
LruCache 是一个泛型类,并且其是线程安全的,内部采用了一个 LinkedHashMap 以强引用的方式存储外界的缓存对象
LruCache 内部提供了 get 和 put 方法可以对缓存对象进行操作,当缓存要满的时候,其会先移除较早使用的缓存对象,再将之后的对象存储进去
扩展:
对象的引用被分为四个层次:
(1)强引用:直接的对象引用,垃圾回收器绝不会回收他
(2)软引用:当一个对象只存在软引用时,系统内存不足时此对象会被GC回收
(3)弱引用:当一个对象只有弱引用存在时,此对象会随时被GC回收
(4)虚引用:当一个对象只有虚引用时,随时会被GC回收
关于四者的区别可以参考 理解Java的强引用、软引用、弱引用和虚引用 - 掘金 (juejin.cn)
LruCahce使用举例:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
解析:
通过 Runtime.getRuntime().maxMemory() 方法获取当前程序所能使用的总容量
之后设定缓存大小为最大容量的 1/8
最后定义LruCache的实例,并将缓存容量传入,重写 sizeOf() 方法用于计算缓存对象的大小
有些情况下需要重写 enrtyRemoved() 方法,LruCache在移除旧缓存时会调用该方法,可以在其中完成一些资源回收工作
使用:
获取一个缓存对象: mMemoryCache.get(key)
添加一个缓存对象:mMemoryCache.put(key, bitmap)
删除一个缓存对象:mMemoryCache.remove(key)
DiskLruCache磁盘缓存
参考:
源码参考:DiskLruCache.java - Git at Google
Github:JakeWharton/DiskLruCache
引用:implementation 'com.jakewharton:disklrucache:2.0.2'
创建
通过 DiskLruCache 中包含的 open() 方法创建一个 DiskLruCache 实例对象:
第一个参数directory:表示该磁盘缓存在文件系统中的存储路径,默认地址时 /sdcaard/Android/data/package_name/cache 目录,当应用卸载时该目录下的内容也会一并删除,如果希望应用卸载后保留数据则可以选择SD卡的其他特定目录
第二个参数appVersion:从字面意思可以知道是应用的版本号,当版本号发生变更时会清空之前的所有缓存,一般没必要清除的情况下设置为1即可
第三个参数valueCount:表示单个节点对应数据的个数,设置为1即可
第四个参数maxSize:表示缓存的总大小,当缓存超过这个值时 DiskLruCache 会删除一些缓存保证总大小不超过这个值
创建代码示例:
获取文件位置,判断手机是否有SD卡:
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
创建 DiskLruCache :
DiskLruCache mDiskLruCache = null;
try {
File cacheDir = getDiskCacheDir(this, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
如果需要获取版本号以便更新时删除之前的缓存,可以使用以下方法:
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
添加
使用 DiskLruCache 首先要通过网络请求获取图片 url:
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
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();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
使用两个方法对图片 url 进行 MD5 加密得到需要存储的 key:
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
而 DiskLruCache 中对缓存的添加是通过 Editor 来实现的,Editor代表一个缓存对象的编辑对象。
根据 key 和 edit() 方法就可以获取一个Editor对象,如果这个缓存正在被编辑, edit() 方法会返回null
如果对于当前key不存在一个Editor对象,则会返回一个新的Editor对象,可以通过它获取一个文件输出流
因为在前面 open() 方法的第三个参数中设置一个节点只能有一个数据,所以在获取文件输出流的时候可以设置其中的常量为0
最后得到添加缓存的代码为:
try {
String imageUrl = yourKey;
String key = hashKeyFormUrl(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
查找
通过 DiskLruCache 的 get 方法可以获取一个 Snapshot 对象,需要传入的参数是 key
接着通过 Snapshot 对象的 getInputStream() 方法得到缓存的输入输出流,从而得到Bitmap对象
代码演示:
try {
String imageUrl = yourKey;
String key = hashKeyFormUrl(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
imageView1.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
删除
与上述同理
try {
String imageUrl = yourKey;
String key = hashKeyFormUrl(imageUrl);
mDiskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
完整的使用示例:
代码:
public class ImageLoaderTestActivity extends AppCompatActivity {
private DiskLruCache mDiskLruCache = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_loader_test);
Button button1 = findViewById(R.id.btn3_1);
Button button2 = findViewById(R.id.btn3_2);
ImageView imageView1 = findViewById(R.id.image3_1);
try {
File cacheDir = getDiskCacheDir(this, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
button1.setOnClickListener(v -> {
new Thread(new Runnable() {
@Override
public void run() {
try {
String imageUrl = yourKey;
String key = hashKeyFormUrl(imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
});
button2.setOnClickListener(v -> {
try {
String imageUrl = yourKey;
String key = hashKeyFormUrl(imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
imageView1.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
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();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
}
效果呈现:
点击LoadImage后,等待图片请求缓存完毕再点击FindImage按钮,即可加载图片
在缓存文件目录下也可以看到两个缓存文件:
注:对 journal 的解析请参考文章末尾标注的郭神的文章
使用Glide的缓存策略
概述
上文中提到:在Bitmap中的缓存策略分为三级(内存、磁盘、网络)
而在Glide中,缓存策略同样分为三级,但是略有不同,分别是:
**(1)活动缓存:**在某个Activity中,页面退出则该缓存不保存
作用:分担内存缓存的负担
活动缓存本质上仍是HashMap,相比内存缓存较小,如果活动缓存满了,会自动写到内存缓存中。同时系统也会对内存缓存进行管理,防止出现内存溢出
**(2)内存缓存:**某个App(模块)范围中,应用退出该缓存不保存
作用:加快数据读取
**(3)磁盘缓存:**整个系统范围内,与Bitmap的磁盘缓存相同
作用:进行持久化存储
Glide中仍使用了DiskLruCache框架进行数据保存和读取。
使用
Glide使用缓存的思路与之前的三级缓存相似:
(1)从活动缓存获取
(2)活动缓存没有则到内存缓存中寻找
(3)内存缓存没有,就去磁盘缓存读取
(4)磁盘缓存没有就去网络获取本地文件读取
代码使用:
因为Glide是默认开启内存缓存和磁盘缓存的,所以正常使用即可
Glide.with(context).load(url).into(imageView);
如果要关闭内存缓存则:
Glide.with(context)
.load(url)
.skipMemoryCache(true)
.into(imageView);
对磁盘缓存的操作:
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(imageView);
diskCacheStrategy() 方法中可以传入以下参数:
DiskCacheStrategy.ALL
DiskCacheStrategy.NONE
DiskCacheStrategy.RESOURCE
DiskCacheStrategy.DATA
DiskCacheStrategy.AUTOMATIC
Glide图片的缓存是最多存在两份:
(1)活动或内存缓存(所以这两个加起来相当于之前的内存缓存,但是Glide更快更好用)
(2)磁盘缓存
ImageLoader的实现
在文章上面的缓存策略概述中有写到关于图片的三级缓存,而实现一个优秀的 ImageLoader 所需要的可不仅是这些,而是以下六点:
(1)图片的同步加载:指能够以同步的方式向调用者提供所加载的图片,可以是从三级缓存中任意一个地方读取
(2)图片的异步加载:能够自己在线程中加载图片并将图片设置给所需要的ImageView
(3)图片压缩:压缩图片,降低OOM的概率
(4)内存缓存:三级缓存,是 ImageLoader 的意义所在
(5)磁盘缓存
(6)网络拉取
下面分部分实现:
图片压缩
将功能提取为 ImageResizer 类,其中的知识点已经在上文Bitmap的高效加载中解析过了:
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin , w = " + width + " h = " + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
三级缓存的实现
仍然是采用 LruCache 和 DiskLruCache 来实现内存缓存和磁盘缓存
(1)在 ImageLoader 初始化时的构造方法中实例化 LruCache 和 DiskLruCache
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述代码中设置了内存缓存的最大容量为进程最大可用内存的 1 / 8 == 50MB
同时在创建磁盘缓存之前先判断了是否有足够的空间
(2)添加对于内存缓存的读写方法:
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
(3)在文章前面所述,获取磁盘缓存的 FileInputStream 输入流需要通过 Snapshot 类,而因为 FileInputStream 无法便捷的进行压缩,需要通过 FileDescriptor 来加载图片:
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread,it's not recommended!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
(4)完善一下,得到目前为止添加了缓存功能的 ImageLoader:
public class ImageLoader {
private static final String TAG = "ImageLoader";
private Context mContext;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private boolean mIsDiskLruCacheCreated = false;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
return bitmap;
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread,it's not recommended!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}
同步加载和异步加载
(1)先实现同步加载代码:
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromDisk,url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error,DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
loadBitmap 的代码首先从内存缓存中加载Bitmap,如果对象为空则从磁盘缓存中加载,磁盘缓存为空则从网络中加载
因为网络请求不能在主线程中执行,所以在之前的 loadBitmapFromHttp 中有判断是否为主线程的代码
(2)异步加载接口的设计:
public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
bindBitmap的实现:
(1)方法首先从内存缓存中读取图片,如果找到了则直接返回
(2)如果没找到结果则使用 loadBitmap 方法,当图片加载成功后将图片、图片地址和 ImageView 对象封装成一个 LoaderResult 对象
(3)最后通过 mMainHandler 向主线程发送一个消息,通知主线程设置 ImagView(主线程中更新UI)
线程池实现:
如果直接采用普通的线程加载图片,那么随着列表的滑动会产生大量的线程,显然会影响效率
而 AsyncTask 在安卓低版本和高版本的表现并不相同,在安卓3.0以上的版本中无法实现并发的效果,所以这里不考虑
从下列代码中分析线程池 THREAD_POOL_EXECUTOR 的实现:
(1)核心线程数为当前设备的CPU核心数+1
(2)最大容量是CPU核心数2倍+1
(3)线程闲置超时时长为10秒
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
Handler实现:
(1)ImageLoader 直接采用主线程的 Looper 来构造 Handler 对象,使得 ImageLoader 对象可以在非主线程中构造
(2)为了解决在 ListView 等视图中快速滑动导致图片重复加载闪烁的问题,在给 ImageView 设置图片之前都会检查下它的 url 有没有发生变化,如果发生变化则不设置当前的图片,而是重新加载新的图片
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
完整的 ImageLoader 代码
public class ImageLoader {
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed,ignored!");
}
}
};
private Context mContext;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromDisk,url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error,DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
return bitmap;
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread,it's not recommended!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}
values目录下的ids文件代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="imageloader_uri" type="id"/>
</resources>
实现照片墙
先创建一个SquareImageView类,以便待会使用。SquareImageView 顾名思义可以使加载的图片宽高相同,需要重写ImageView控件的 onMeasure() 方法
SquareImageView类代码:
public class SquareImageView extends ImageView {
public SquareImageView(Context context) {
super(context);
}
public SquareImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SquareImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}
主布局代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="5dp">
<GridView
android:id="@+id/gridView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:horizontalSpacing="5dp"
android:listSelector="@android:color/transparent"
android:numColumns="3"
android:stretchMode="columnWidth"
android:verticalSpacing="5dp"></GridView>
</LinearLayout>
item布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.example.app2.ui.SquareImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/ic_launcher_background" />
</LinearLayout>
MyUtils代码:
public class MyUtils {
public static String getProcessName(Context cxt, int pid) {
ActivityManager am = (ActivityManager) cxt
.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
if (runningApps == null) {
return null;
}
for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
if (procInfo.pid == pid) {
return procInfo.processName;
}
}
return null;
}
public static void close(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static DisplayMetrics getScreenMetrics(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
return dm;
}
public static float dp2px(Context context, float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
context.getResources().getDisplayMetrics());
}
public static boolean isWifi(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetInfo != null
&& activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return true;
}
return false;
}
public static void executeInThread(Runnable runnable) {
new Thread(runnable).start();
}
}
主活动代码:
public class MainActivity extends Activity implements AbsListView.OnScrollListener {
private static final String TAG = "MainActivity";
private List<String> mUrList = new ArrayList<>();
ImageLoader mImageLoader;
private GridView mImageGridView;
private BaseAdapter mImageAdapter;
private boolean mIsGridViewIdle = true;
private int mImageWidth = 0;
private boolean mIsWifi = false;
private boolean mCanGetBitmapFromNetWork = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
mImageLoader = ImageLoader.build(MainActivity.this);
}
private void initData() {
String key = "http://game.ming3.top/test/test";
for (int i = 0; i < 30; i++) {
String temp = key + i + ".jpg";
mUrList.add(temp);
}
int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
int space = (int) MyUtils.dp2px(this, 20f);
mImageWidth = (screenWidth - space) / 3;
mIsWifi = MyUtils.isWifi(this);
if (mIsWifi) {
mCanGetBitmapFromNetWork = true;
}
}
private void initView() {
mImageGridView = (GridView) findViewById(R.id.gridView1);
mImageAdapter = new ImageAdapter(this);
mImageGridView.setAdapter(mImageAdapter);
mImageGridView.setOnScrollListener(this);
if (!mIsWifi) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("初次使用会从网络下载图片,这将要消耗些许流量,确认要下载吗?");
builder.setTitle("注意");
builder.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mCanGetBitmapFromNetWork = true;
mImageAdapter.notifyDataSetChanged();
}
});
builder.setNegativeButton("否", null);
builder.show();
}
}
private class ImageAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private Drawable mDefaultBitmapDrawable;
private ImageAdapter(Context context) {
mInflater = LayoutInflater.from(context);
mDefaultBitmapDrawable = context.getResources().getDrawable(R.drawable.ic_launcher_background);
}
@Override
public int getCount() {
return mUrList.size();
}
@Override
public String getItem(int position) {
return mUrList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.image_list_item, parent, false);
holder = new ViewHolder();
holder.imageView = (ImageView) convertView.findViewById(R.id.image);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ImageView imageView = holder.imageView;
final String tag = (String) imageView.getTag();
final String uri = getItem(position);
if (!uri.equals(tag)) {
imageView.setImageDrawable(mDefaultBitmapDrawable);
}
if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
imageView.setTag(uri);
mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
}
return convertView;
}
}
private static class ViewHolder {
public ImageView imageView;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
mIsGridViewIdle = true;
mImageAdapter.notifyDataSetChanged();
} else {
mIsGridViewIdle = false;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
}
优化列表的卡顿:
(1)不要再 getView() 方法中执行耗时的操作
(2)控制异步任务的执行频率,可以考虑在列表滑动的时候停止加载图片(因为不这样做可能会在一瞬间产生大量的异步任务,导致线程池拥堵)
(3)在 getView() 方法中设置为仅当列表静止时才加载图片
(4)如果经过上面的步骤仍有卡顿情况,可以开启硬件加速(设置 android:hardwareAccelerated="true" )
参考文章:
缓存
Android DiskLruCache完全解析,硬盘缓存的最佳方案_郭霖的专栏-CSDN博客_disklrucache
记:getExternalCacheDir与getCacheDir的区别_CarsonWoo的博客-CSDN博客_getexternalcachedir
Glide
Glide三级缓存理解详细_wenzhi的博客-CSDN博客_glide三级缓存
Android 主流开源框架(七)Glide 的缓存机制 - 掘金 (juejin.cn)
面试官:简历上最好不要写Glide,不是问源码那么简单 - 掘金 (juejin.cn)
if (!uri.equals(tag)) {
imageView.setImageDrawable(mDefaultBitmapDrawable);
}
if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
imageView.setTag(uri);
mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
}
return convertView;
}
}
private static class ViewHolder {
public ImageView imageView;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
mIsGridViewIdle = true;
mImageAdapter.notifyDataSetChanged();
} else {
mIsGridViewIdle = false;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
}
### 优化列表的卡顿:
(1)不要再 `getView()` 方法中执行耗时的操作
(2)控制异步任务的执行频率,可以考虑在列表滑动的时候停止加载图片(因为不这样做可能会在一瞬间产生大量的异步任务,导致线程池拥堵)
(3)在 `getView()` 方法中设置为仅当列表静止时才加载图片
(4)如果经过上面的步骤仍有卡顿情况,可以开启硬件加速(设置 `android:hardwareAccelerated="true"`)
## 参考文章:
### 缓存
[Android DiskLruCache完全解析,硬盘缓存的最佳方案_郭霖的专栏-CSDN博客_disklrucache](https://blog.csdn.net/guolin_blog/article/details/28863651)
[记:getExternalCacheDir与getCacheDir的区别_CarsonWoo的博客-CSDN博客_getexternalcachedir](https://blog.csdn.net/CarsonWoo/article/details/89142756)
### Glide
[Glide三级缓存理解详细_wenzhi的博客-CSDN博客_glide三级缓存](https://blog.csdn.net/wenzhi20102321/article/details/119337059)
[Android 主流开源框架(七)Glide 的缓存机制 - 掘金 (juejin.cn)](https://juejin.cn/post/6844904161327185927)
[面试官:简历上最好不要写Glide,不是问源码那么简单 - 掘金 (juejin.cn)](https://juejin.cn/post/6844903986412126216#heading-0)
|