目录
1、背景
2、图片压缩
2.1、采样率压缩
2.2、质量压缩
2.3、尺寸压缩
3、图片缓存
3.1、LruCache 内存缓存
3.2、DiskLruCache磁盘缓存
3.2.1、DisLruCache创建
3.2.2、DisLruCache添加缓存
3.2.3、DisLruCache获取缓存
4、ImageLoader
5、RecyclerView卡顿优化
在Android中,图片是以Bitmap对象存在和显示的。但是当图片从jpg或者png格式转换成我们的Bitmap的时候,它所在的内存大小也会发生改变。
比如:我本地存储了一张800*1200的图片,本地大小为200k,但是当我们转换成Bitmap时,它所占的内存变为了3.66M。这是为什么?
这是因为我们Android中Bitmap对象所占内存:
图片的占用内存 = 图片的长度(像素单位) * 图片的宽度(像素单位) * 单位像素所占字节数
其中单位像素占用字节数的大小是变化的,由BitmapFactory.Options的inPreferredConfig决定,为Bitmap.Config类型,一般情况下默认为:ARGB_8888。不同类型占字节大小如下表所示:
现在手机拍的高清图基本上都是2000* 2000 * 4 = 15M+ 了。如果不做任何处理的图片大量使用到我们APP上,结局你懂得!
如果我们按照原图片加载的话,如果不小心处理,很容易我们的app就会走向OOM的道路。但是相应地,Android提供了图片的方法,可以在我们图片从jpg(png)格式转换为Bitmap对象的时候,减小像素量,从而减少内存的占用。
在Android中加载Bitmap对象由BitmapFactory类来完成,并且其提供了四种方法来加载我们的Bitmap对象:
- decodeFile() 从文件中加载Bitmap对象
- decodeResource() 从资源中加载Bitmap对象
- decodeStream() 从输入流中加载Bitmap对象
- decodeByteArray() 从字节数组中加载Bitmap对象
其中decodeFile() 和decodeResource() 又间接地调用了decodeStream() 方法,这四类方法最终是在Android的底层实现的,对应这BitmapFactory的几个native方法。
原理:是通过解码器对原始图像进行二次采样,然后返回一个尺寸更小的图片以节省内存的使用。
作用:减少图片尺寸 和内存占用大小。
适用场景:降低图片内存占用,防止OOM
它的实现主要使用到了BitmapFactory.Options类的inSampleSize(采样率)参数来缩放图片。
当inSampleSize=2;二次采样后的图片的宽高均为原来的1/2,像素总数和内存占用为原来的1/4.
当inSampleSize=4;二次采样后的图片的宽高均为原来的1/4,像素总数和内存占用为原来的1/16.
当inSampleSize<=1,二次采样后的图片宽高和原图相同,没有任何缩放。
详细的步骤看下图:
输出结果:
我们的图片其实都是由无数个像素点拼凑而成。而我们的质量压缩会通过改变这些像素点来达到减少文件大小的目的。
原理:通过算法抠掉(同化)了图片中的一些某个些点附近相近的像素,降低图片质量,从而减小文件的大小。
像素点 A B
C D
压缩后 A A
A A
作用:内存占用没有变化, 本地存储大小会减小。
注意:它其实只能实现对file的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。 因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素(像素大小不会变)。
适用场景:将图片压缩后保存到本地,或者将图片上传到服务器。
原理:通过减少单位尺寸的像素值,真正意义上的降低像素。1080*1080-----压缩2倍--->540*540
作用:将图片的尺寸、像素值、文件大小,都进行了减缩
适用场景:缓存缩略图的时候(头像处理)
上面的图片压缩,告诉了我们如何减少图片所占内存和磁盘空间的大小。
接下来我们讲解,如何缓存图片,让我们减少网络流量的使用。
实现思想:当第一次从网络上下载图片之后,缓存到本地和内存中,这样每次使用该图片的时候,先从内存中获取,如果内存中没有,再从本地存储设备中获取,这两个地方都获取不到的时候,再从网络上获取下来。
目前在Android中,比较常用的缓存算法就是Lru(Least Recently Used)最近最少使用算法了,它的核心思想就是当缓存满时,会优先淘汰哪些近期最少使用的缓存对象。
而LruCache算法可以很好地满足我们Bitmap的缓存策略。
-----------------------------------------------------------------------------------------------------------------
LruCache是一个泛型类,它内部采用一个LinkHashMap以强引用的方式存储外界的缓存对象,其提供了get() 和set()方法来完成缓存的获取和添加操作。
强引用:直接的对象引用;
软引用:当一个对象只有软引用存在时,系统内存不足时,此对象会被GC回收;
弱引用:当一个对象只有弱引用存在时,此对象随时会被gc回收。
-----------------------------------------------------------------------------------------------------------------
下图:
蓝色框---LruCache对象的初始化,其中maxMemory为缓存的总容量大小(=当前进程可用内存的1/8,处理1024是为了转换成KB单位);sizeOf()的作用是计算缓存对象的大小,这里的大小的单位需要和总容量的大小的单位一致。
绿色框---将图片添加到缓存中
黄色框---从缓存中取出图片
DiskLruCache用于实现存储设备缓存,也就是磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache得到Android官方文档的推荐,但是并没有写入到Android源码库中,需要自己引入。
implementation 'com.jakewharton:disklrucache:2.0.2'
DiskLruCache由它的open()函数构建,它有四个参数,如下:
directory:本地的存储路径,缓存目录可以选择SD卡上缓存目录,具体为/sdcard/Android/data/应用包名/cache,当应用被卸载的时候,该目录也会跟着被删除,但是如果你不想这么做的话,可以选择其他目录;
appVersion:应用的版本号,设定为1就行,如果版本号变动的话, 会清除以前所有的缓存数据;
valueCount:表示单个节点对应的数据的个数,一般设定为1就行;
maxSize:就是缓存的总容量大小了,当超过这个值的时候,他就会从缓存中删除一些最近最少使用的缓存对象。
-----------------------------------------------------------------------------------------------------------------
DiskLru的创建过程如下:
DiskLruCache的添加过程是通过Editor完成的,Editor表示一个缓存对象的编辑对象。
第一步:将图片url转换成缓存的key(这里一般会将url转换成它的MD5值,这是为了防止url中有特殊字符导致无法使用)
第二步: 通过mDiskLruCache.edit(key) 来获取Edit对象,如果这个缓存正在被编辑,就会返回null(DiskLruCache不允许同时编辑同一个缓存对象)
第三步: 通过 editor.newOutputStream(DISK_CACHE_INDEX) 获取到图片文件的输出流OutputStream;DISK_CACHE_INDEX 因为我们创建mDiskLruCache的时候,设置一个节点只有一个数据,所以DISK_CACHE_INDEX=0就行。
第四步:下载图片,将图片 InputStream 写入到上面得到的OutputStream中;
第五步:commit()提交写入操作。
第一步:通过url转换成的key获取到SnapShot对象;
第二步:通过SnapShot获取到输入流FileInputStream;
第三步:通过输入流解析成Bitmap。
注意1:直接加载原始图片,对于我们的内存来说,有很大的压力,所以这里我们建议使用上面的采样率压缩的方式来获取合适大小的图片;
注意2:如果通过BitmapFactoty.option来直接对FileInputStream进行压缩的话,会存在问题。这是因为FileInputStream是一种有序的文件流,如果两次decodeStream调用会影响文件流的位置属性,导致第二次decodeStream时,返回null的对象。为了解决这个问题可以通过文件描述符FileDescriptor来加载一张压缩的图片。
上面讲解了图片的压缩加载和图片的缓存技术,这里我们将这些技术结合起来,实现一个功能强大的ImageLoader类。
它具有的功能:
- 图片的异步加载;
- 图片压缩;
- 内存缓存;
- 磁盘缓存;
- 网络拉取图片。
当它加载一个图片的时候,我们首先会尝试从内存缓存中获取,如果获取不到,再从磁盘缓存中去获取,如果都获取不到的话,再从网络拉取。
这样就能大大减少我们对流量的消耗。
文件目录:
ImageLoader类:
package com.yobo.yo_android.test_bitmap.original;
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.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Environment;
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 com.yobo.yo_android.R;
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(), sThreadFactory);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
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 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(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();
}
}
}
/**
* build a new instance of ImageLoader
* @param context
* @return a new instance of ImageLoader
*/
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);
}
/**
* load bitmap from memory cache or disk cache or network async, then bind imageView and bitmap.
* NOTE THAT: should run in UI Thread
* @param uri http url
* @param imageView bitmap's bind object
*/
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);
}
/**
* load bitmap from memory cache or disk cache or network.
* @param uri http url
* @param reqWidth the width ImageView desired
* @param reqHeight the height ImageView desired
* @return bitmap, maybe null.
*/
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;
}
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
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);
}
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;
}
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();
}
MyUtils.close(out);
MyUtils.close(in);
}
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();
}
MyUtils.close(in);
}
return bitmap;
}
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();
}
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(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
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;
}
}
}
ImageResizer类:
package com.yobo.yo_android.test_bitmap.original;
import java.io.FileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth,
int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth,
int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
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;
}
// Raw height and width of image
int height = options.outHeight;
int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
}
MyUtils:
package com.yobo.yo_android.test_bitmap.original;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowManager;
public class MyUtils {
public static String getProcessName(Context cxt, int pid) {
ActivityManager am = (ActivityManager) cxt
.getSystemService(Context.ACTIVITY_SERVICE);
List runningApps = am.getRunningAppProcesses();
if (runningApps == null) {
return null;
}
for (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();
}
}
MyConstants类:
package com.yobo.yo_android.test_bitmap.original;
import android.os.Environment;
public class MyConstants {
public static final String CHAPTER_2_PATH = Environment.getExternalStorageDirectory().getPath()
+ "/singwhatiwanna/chapter_2/";
public static final String CACHE_FILE_PATH = CHAPTER_2_PATH + "usercache";
public static final int MSG_FROM_CLIENT = 0;
public static final int MSG_FROM_SERVICE = 1;
}
1、不要在onBindBlockHolder()中做耗时操作;
2、控制异步任务的执行频率;(当滑动过程中,停止加载图片,等停止滑动后再加载图片)
3、开启硬件加速。