参考:《Android开发艺术探索》
涉及到的知识点
- Bitmap的高效加载
通过BitmapFactory.Options
来缩放图片,主要用到了它的inSampleSize
参数,及采样率。当inSampleSize
为1时,采样后的图片大小为原始大小;当inSampleSize
>1时,假设为2,采样后个图片宽高为原来的1/2,像素为原来的1/4。inSampleSize的取值应为2的指数。
BitmapFactory.Options
的inJustDecodeBounds
,当为true时,BitmapFactory
只会解析原图片的宽高信息,不会真正的加载图片,这个操作是轻量级的。
示例:
public static Bitmap decodeSampledBitmap4Resource(Resources res, int resID,
int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resID);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resID, options);
}
private static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
int result = 1;
int outWidth = options.outWidth;
int outHeight = options.outHeight;
if (outWidth > reqWidth || outHeight > reqHeight) {
int halWidth = outWidth / 2;
int halHeight = outHeight / 2;
while (halWidth / result >= reqWidth && halHeight / result >= reqHeight) {
result *= 2;
}
}
return result;
}
- Android缓存策略
LRU(Least Recently Used),近期最少使用算法。
LruCache:是一个泛型类,内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,提供了get和put方法以及删除remove。LruCache是线程安全的。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mLruCache = new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
DiskLruCache:用于实现存储设备存储,即磁盘存储。
DiskLruCache创建:不能通过构造方法来创建,它提供了open方法用于创建自身。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException
directory:磁盘缓存在文件系统中的存储路径。
appVersion:应用版本号,一般设置为1
valueCount:单个节点对用的数据的个数,一般设置为1
maxSize:缓存总大小
DiskLruCache的缓存添加:缓存添加是通过Editor完成的,Editor表示一个缓存对象的编辑对象.可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么edit()就会返回null.
public void test() {
String key = hashKey4Url("www.baidu.com");
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrl2Stream("", outputStream)) {
editor.commit();//必须提交
}else {
editor.abort();//异常,回退
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean downloadUrl2Stream(String urlstr, OutputStream outputStream) {
HttpURLConnection connec = null;
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
URL url = new URL(urlstr);
connec = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(
connec.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(
outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connec != null) {
connec.disconnect();
}
UIUtils.close(out);
UIUtils.close(in);
}
return false;
}
DiskLruCache的缓存查找:通过DiskLruCache的get获取一个Snapshot对象,再通过Snapshot获取到缓存的文件输入流,然后就能拿到Bitmap对象了.
为了避免OOM,一般建议通过BitmapFactory.Options来进行压缩,但是那种方法对FileInputStream的缩放存在问题,原因:FileInputStream是一种有序的文件流,两次的decodeStream影响了文件流的位置顺序,导致第二次decodeStream得到的是null.
解决方法:可以通过文件流来得到文件的描述符,然后再通过BitmapFactory.decodeFileDescriptor来加载一张缩放后的文件.
一个好的imageloader应该具备的功能是
- 图片同步加载
- 图片异步加载
- 图片压缩
- 内存缓存
- 磁盘缓存
- 网络拉取
内存缓存和磁盘缓存是ImageLoader的核心
package klx.app.sd_app.utils;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.StatFs;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import com.jakewharton.disklrucache.DiskLruCache;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import klx.app.sd_app.R;
/**
* Created by xjw on 2017/9/11.
*/
public class ImageLoader {
public static class LoadResult {
ImageView imageView;
String url;
Bitmap bitmap;
public LoadResult(ImageView imageView, String url, Bitmap bitmap) {
this.imageView = imageView;
this.url = url;
this.bitmap = bitmap;
}
}
private static final int MESSAGE_POST_RESULT = 1;
private static final int TAG_KEY_URI = R.id.imageloader_url;
private static final String TAG = "ImageLoader>>";
private final Context mContext;
private LruCache mLruCache;
private DiskLruCache mDiskLruCache;
private int maxDiskSize = 30 * 1204 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private static final int IO_BUFFER_SIZE = 1 * 1024 * 1024;
private boolean mIsDiskLruCacheCreated = false;
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);
@Override
public Thread newThread(Runnable runnable) {
return new Thread(runnable, TAG + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingDeque(), sThreadFactory
);
private Handler mMainHanlder = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoadResult result = (LoadResult) msg.obj;
ImageView imageView = result.imageView;
String url = result.url;
Bitmap bitmap = result.bitmap;
String tag = (String) imageView.getTag(TAG_KEY_URI);
if (tag.equals(url)) {
imageView.setImageBitmap(bitmap);
}else {
Log.d(TAG, "handleMessage: set image bitmap,but imageview changeed,ignored.");
}
}
};
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
init();
}
private void init() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mLruCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
File cacheDir = NewFileUtils.getBitmapCacheDir();
if (getUsableSpace(cacheDir) > maxDiskSize) {
try {
mDiskLruCache = com.jakewharton.disklrucache.DiskLruCache.open
(cacheDir, 1, 1, maxDiskSize);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void addBitmap2MemoryCache(String key, Bitmap bitmap) {
if (loadBitmap4MemoryCache(key) == null) {
mLruCache.put(key, bitmap);
}
}
private Bitmap loadBitmap4MemoryCache(String key) {
return mLruCache.get(key);
}
public Bitmap loadBitmap4Url(String urlstr) {
HttpURLConnection connec = null;
BufferedInputStream in = null;
Bitmap bitmap = null;
try {
URL url = new URL(urlstr);
connec = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(
connec.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connec != null) {
connec.disconnect();
}
UIUtils.close(in);
}
return bitmap;
}
private Bitmap loadBitmap4Http(String url, int reqWidth, int reqHeight) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKey4Url(url);
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
if (downloadUrl2Stream(url, editor.newOutputStream(DISK_CACHE_INDEX))) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
return loadBitmap4DiskCache(url, reqWidth, reqHeight);
}
private Bitmap loadBitmap4DiskCache(String url, int reqWidth, int reqHeight) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("Load bitmap on UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
try {
String key = hashKey4Url(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = inputStream.getFD();
bitmap = decodeSampledBitmap4FileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmap2MemoryCache(key, bitmap);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
private boolean downloadUrl2Stream(String urlstr, OutputStream outputStream) {
HttpURLConnection connec = null;
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
URL url = new URL(urlstr);
connec = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(
connec.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(
outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connec != null) {
connec.disconnect();
}
UIUtils.close(out);
UIUtils.close(in);
}
return false;
}
public static String hashKey4Url(String url) {
String cacheKey = null;
try {
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytes2HexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private static String bytes2HexString(byte[] digest) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xFF & digest[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
public static Bitmap decodeSampledBitmap4FileDescriptor(FileDescriptor fileDescriptor
, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
public static Bitmap decodeSampledBitmap4Resource(Resources res, int resID,
int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resID);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resID, options);
}
private static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
int result = 1;
int outWidth = options.outWidth;
int outHeight = options.outHeight;
if (outWidth > reqWidth || outHeight > reqHeight) {
int halWidth = outWidth / 2;
int halHeight = outHeight / 2;
while (halWidth / result >= reqWidth && halHeight / result >= reqHeight) {
result *= 2;
}
}
return result;
}
public long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
StatFs statFs = new StatFs(path.getPath());
return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
}
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmap4MemoryCache(hashKey4Url(url));
if (bitmap != null) {
Log.d(TAG, "loadBitmap: from MemCache.");
return bitmap;
}
bitmap = loadBitmap4DiskCache(url, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmap: from DiskCache.");
return bitmap;
}
bitmap = loadBitmap4Http(url, reqWidth, reqHeight);
Log.d(TAG, "loadBitmap: from Http.url-->" + url);
if (bitmap == null && !mIsDiskLruCacheCreated) {
bitmap = loadBitmap4Url(url);
}
return bitmap;
}
public void bindBitmap(final String url, final ImageView iv, final int reqWidth, final int reqHeight) {
iv.setTag(TAG_KEY_URI, url);
final Bitmap bitmap = loadBitmap4MemoryCache(hashKey4Url(url));
if (bitmap != null) {
iv.setImageBitmap(bitmap);
return;
}
Runnable runnable = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
if (bitmap != null) {
LoadResult result = new LoadResult(iv, url, bitmap);
mMainHanlder.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(runnable);
}
}
ImageLoader使用
优化列表卡顿
设置setOnScrollListener,在OnScrollListener的onScrollStateChanged方法中判断是否处于滑动状态,如果是停止加载
public void onScrollStateChanged(AbsListView view,int scrollState){
if(scrollState == OnScrollListener.SCROLL_STATE_IDLE){
mIsGridViewIdle = true;
mAdapter.notifyDataChanged();
}else{
mIsGridViewIdle = false;
}
}
或者设置硬件加速:android:hardwareAcceleratcd = "true",即可为Activity开启硬件加速.