From:Android开发艺术探索
-
BitmapFactory加载图片的方法
decodeFile【文件】
decodeResource【资源】
decodeStream【输入流】
decodeByteArray【字节数组】
其中decodeFile和decodeResource间接调用decodeStream。 -
高效加载Bitmap
核心思想:使用BitmapFactory.Options来加载所需尺寸的图片。
因为使用ImageView显示的图片并没有图片原始尺寸那么大,若把这个图片全部加载进来后再给ImageView显然没有必要。所以可以按照一定的【采样率】来加载缩小后的图片,将缩小后的图片在ImageView中显示。
options.inSampleSize【采样率】,其值大于等于1,并且会向下取整为最接近2的指数。
当inSampleSize为2时,400 x 400的图像变为200 x 200。
options.inJustDecodeBounds = true,只会解析图片的高/宽信息,不会真正的加载图片,是个轻量级的操作,节省内存开销。
注意:BitmapFactory获取图片的高/宽和图片的位置【不同drawable目录】、设备的屏幕密度有关。
- 获取采样率
public class GenerateBitmapUtils {
public static Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth,
int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId);
options.inSampleSize = calulateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
private static int calulateInSampleSize(BitmapFactory.Options options, int reqWidth,
int reqHeight) {
final int height = options.outHeight;
final int wight = options.outWidth;
int inSampleSize = 1;
while ((height / inSampleSize) >= reqHeight && (wight / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
return inSampleSize / 2 > 1 ? inSampleSize / 2 : 1;
}
}
- 缓存LruCache【属于Android SDK】
LRU【Least Recently Used,近期最少使用】
LruCache内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象。
代码展示:
public class LruCacheTest {
//缓存的总容量为:当前进程可用内存的1/8
int cacheSize = (int) Runtime.getRuntime().maxMemory() / 1024 / 8;
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
public void resize(int maxSize) {
super.resize(maxSize);
}
@Override
public void trimToSize(int maxSize) {
super.trimToSize(maxSize);
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
//移除旧缓存时,可以在此方法中完成一些资源回收工作
}
@Override
protected Bitmap create(String key) {
return super.create(key);
}
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//计算缓存对象的大小,单位和总容量的单位一致
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
public Bitmap get(String key) {
return lruCache.get(key);
}
public void put(String key, Bitmap bitmap) {
lruCache.put(key, bitmap);
}
public void delete(String key) {
lruCache.remove(key);
}
}
- 缓存DiskLruCache【不属于Android SDK】
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* 存储路径。根据卸载APP后是否要删除该目录,可以指定不同的文件路径
* @param valueCount the number of values per cache entry. Must be positive
* 应用的版本号,一般设为1,当版本号发生改变时会清空之前所有的缓存文件
* @param maxSize the maximum number of bytes this cache should use to store
* 单个节点所对应的数据个数,一般设为1
* @throws IOException if reading or writing the cache directory fails
* 缓存的总大小
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
DiskLruCached的缓存添加操作通过Editor完成,如果这个缓存图片正在被编辑,那么editor()返回null,即DiskLruCache不允许同时编辑一个缓存对象。
图片的url可能会有特殊字符,一般将图片的url的md5值作为key。
代码展示:
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
if (null == diskLruCache) return null;
Bitmap bitmap = null;
String key = UrlMd5Utils.hashKeyFromUrl(url);
try {
DiskLruCache.Value value = diskLruCache.get(key);
if (null != value) {
FileInputStream fileInputStream = new FileInputStream(value.getFile(DISK_CACHE_INDEX));
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap =
imageResizer.decodeSampleBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (null != bitmap) {
addBitmapToMemoryCache(key, bitmap);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
通过DiskLruCache的get得到一个Snapshot对象,通过Snapshot对象可得到缓存的文件输入流。【注意:Snapshot现在改为了Value对象】
注意:FileInputStream采用BitmapFactory.Options方法缩放图像存在问题,因为:FileInputStream是一种有序的文件,而两次decodeStream调用会影响文件流的位置属性,导致第二次decodeSteam为null。可以通过文件流得到它所对应的文件描述符,再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片。
- 列表错位问题
由于View复用会导致列表错位,可以在给imageView设置图片之前检查它所对应的url是否发生变化。
String url = (String) imageView.getTag(TAG_KEY_RUI);
if (null != url && url.equals(result.url)) {
imageView.setImageBitmap(result.bitmap);
}
- 优化列表卡顿
1)仅仅在加载图片时采用异步处理是不够的,当用户频繁滑动时,就会瞬间产生上百个异步任务,这些异步任务给线程池造成拥堵,并且会有大量UI更新操作,造成卡顿。
解决:在列表滑动时停止加载图片。
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
isGridViewIdle = true;
imageAdapter.notifyDataSetChanged();
} else {
isGridViewIdle = false;
}
}
if(isGridViewIdle){
imageView.setTag(url);
bindBitmap(uri, imageView, width, height);
}
2)开启硬件加速
android:hardwareAcclerated="true"
-
ImageLoader
图片加载库应具备的功能:
网络拉取、磁盘缓存、内存缓存、图片压缩、同步加载、异步加载。
流程图:
imageLoader.png
-
完整代码
package com.ljg;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.StatFs;
import android.support.annotation.NonNull;
import android.util.LruCache;
import android.widget.ImageView;
import com.bumptech.glide.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.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ImageLoader {
private 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 MAX_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final int TAG_KEY_RUI = 777;
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 isDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r, "ImageLoader#" + count.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR =
new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), sThreadFactory);
private static class LoaderResult {
public ImageView imageView;
public String url;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String url, Bitmap bitmap) {
this.imageView = imageView;
this.url = url;
this.bitmap = bitmap;
}
}
private Handler mainHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String url = (String) imageView.getTag(TAG_KEY_RUI);
if (null != url && url.equals(result.url)) {
imageView.setImageBitmap(result.bitmap);
}
}
};
private Context context;
private ImageResizer imageResizer = new ImageResizer();
private LruCache<String, Bitmap> memoryCache;
private DiskLruCache diskLruCache;
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
private ImageLoader(Context context) {
this.context = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(context, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
diskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
isDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
private long getUsableSpace(File diskCacheDir) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return diskCacheDir.getUsableSpace();
}
final StatFs statFs = new StatFs(diskCacheDir.getPath());
return (long) statFs.getBlockSize() * statFs.getAvailableBlocksLong();
}
private File getDiskCacheDir(Context context, String name) {
//存储状态(如果介质存在并在其安装点处安装,具有读/写访问权限)
boolean externalStorageAvailable =
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
//扩展SDK的路径
cachePath = context.getExternalCacheDir().getPath();
} else {
//手机硬盘的路径
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.pathSeparator + name);
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (null == getBitmapFromMemoryCache(key)) {
memoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemoryCache(String key) {
return memoryCache.get(key);
}
public void bindBitmap(final String url, final ImageView imageView) {
bindBitmap(url, imageView, 0, 0);
}
public void bindBitmap(final String url, final ImageView imageView, final int reqWidth,
final int reqHeight) {
imageView.setTag(TAG_KEY_RUI, url);
Bitmap bitmap = loadBitmapFromMemoryCache(url);
if (null != bitmap) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bmp = loadBitmap(url, reqWidth, reqHeight);
if (null != bmp) {
LoaderResult result = new LoaderResult(imageView, url, bmp);
mainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
private Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemoryCache(url);
if (null != bitmap) {
return bitmap;
}
bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
if (null != bitmap) {
return bitmap;
}
if (isDiskLruCacheCreated) {
bitmap = loadAndSaveDiskBitmapFromHttp(url, reqWidth, reqHeight);
} else {
bitmap = onlyLoadBitmapFromHttp(url);
}
return bitmap;
}
/**
* 网络请求数据
*/
private Bitmap onlyLoadBitmapFromHttp(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 (IOException e) {
e.printStackTrace();
} finally {
if (null != urlConnection) {
urlConnection.disconnect();
}
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}
private Bitmap loadAndSaveDiskBitmapFromHttp(String url, int reqWidth, int reqHeight) {
if (null == diskLruCache) {
return null;
}
String key = UrlMd5Utils.hashKeyFromUrl(url);
try {
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (null != editor) {
OutputStream outputStream = new FileOutputStream(editor.getFile(DISK_CACHE_INDEX));
//网络请求数据
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
diskLruCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
/**
* 网络请求数据
*/
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(), 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) {
e.printStackTrace();
} finally {
if (null != urlConnection) {
urlConnection.disconnect();
}
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return false;
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
if (null == diskLruCache) return null;
Bitmap bitmap = null;
String key = UrlMd5Utils.hashKeyFromUrl(url);
try {
DiskLruCache.Value value = diskLruCache.get(key);
if (null != value) {
FileInputStream fileInputStream = new FileInputStream(value.getFile(DISK_CACHE_INDEX));
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap =
imageResizer.decodeSampleBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (null != bitmap) {
addBitmapToMemoryCache(key, bitmap);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
private Bitmap loadBitmapFromMemoryCache(String url) {
final String key = UrlMd5Utils.hashKeyFromUrl(url);
return getBitmapFromMemoryCache(key);
}
}
- 工具类【ImageResizer】
package com.ljg;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.FileDescriptor;
public class 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 decodeSampleBitmapFromFileDescriptor(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);
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (0 == reqWidth || 0 == reqHeight) {
return 1;
}
final int height = options.outHeight;
final int wight = options.outWidth;
int inSampleSize = 1;
while ((height / inSampleSize) >= reqHeight && (wight / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
return inSampleSize / 2 > 1 ? inSampleSize / 2 : 1;
}
}
- 工具类【UrlMd5Utils】
package com.ljg;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class UrlMd5Utils {
public static String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(url.getBytes());
cacheKey = bytesToHexString(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
String hex;
for (byte aByte : bytes) {
hex = Integer.toHexString(0xFF & aByte);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}
网友评论