1.Bitmap的高效加载
1.1 Bitmap的加载
BitmapFactory提供四类方法加载图片:
decodeFile、 decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native 方法。
1.2 高效加载
通过BitmapFactory.Options按一定的采样率来加载缩小后的图片,将缩小后的图片在ImageView中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap 加载时的性能。BitmapFactory提供的加载图片的四类方法都支持BitmapFactory.Options参数
通过BitmapFactory.Options 来缩放图片,主要用到了inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小;当inSampleSize大于1时,比如为2,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。
拿一张1024×1024像素的图片来说,假定采用ARGB8888格式存储,那么它占有的内存为1024×1024×4 即4MB,如果inSampleSize为2,那么采样后的图片其内存占用只有512×512×4,即1MB。
采样率同时作用于宽/高,这将导致缩放后的图片大小以采样率的2次方形式递减,即缩放比例为1/ (inSampleSize的2次方),比如inSampleSize为4,那么缩放比例就是1/16。有一种特殊情况,那就是当inSampleSize 小于1时,其作用相当于1,即无缩放效果。
获取采样率:
(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true 并加载图片。该参数为true时,BitmapFactory只会解析图片的原始宽高信息,不会去真正加载图片,同时获取到的信息和图片的位置与程序运行的设备有关
(2)从BitmapFactory.Options 中取出图片的原始宽高信息,它们对应于outWidth 和outHeight参数。
(3)根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize.
(4)将BitmapFactory.Options 的inJustDecodeBounds 参数设为false, 然后重新加载图片。
具体实现:
public Bitmap decodeSampledBitmapFromResource(Resources res,int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final 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 int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
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;
// 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;
}
主活动中使用:
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(),R.id.myimage, 100, 100));
2.Android的缓存策略
程序第一次从网络加载图片后,就将其缓存到存储设备.上,然后在内存中再缓存一份,当应用打算从网络上请求一张图片时, 程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。
2.1 LruCache
属于内存缓存,LruCache是线程安全的,是一个泛型类
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
}
它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象
●强引用:直接的对象引用;
●软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收:
●弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
LruCache实现内存缓存
- 初始化
int maxMemory = (int) (Runtime .getRuntime () .maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap> (cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bi tmap) {
return bitmap.getRowBytes() * bitmap .getHeight() / 1024;
}
};
提供缓存的总容量大小:上面总容量的大小为当前进程的可用内存的1/8,单位为KB
重写sizeOf 方法:sizeOf 方法的作用是计算缓存对象的大小,大小的单位需要和总容量的单位一致。上面的sizeOf方法完成了Bitmap对象的大小计算。除以1024是为了将其单位转换为KB。
LruCache的entryRemoved:一些特殊情况下还需要重写该方法,LruCache 移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved中完成一些资源回收工作
- 缓存的获取和添加:
从LruCache中提供一个缓存对象
mMemoryCache.get(key)
向LruCache中添加一个缓存对象
mMemoryCache.put(key, bitmap)
LruCache还支持删除操作,通过remove方法即可删除一个指定的缓存对象
2.2 DiskLruCache
属于存储设备缓存,即磁盘缓存,将缓存对象写入文件系统
不属于SDK,且不能直接使用该源码,源码获取来源:
2.2.1 DiskLruCache的创建
只能通过open()方法创建
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
第一个参数表示磁盘缓存在文件系统中的存储路径。缓存路径可以选择SD卡上的缓存目录,具体是指/sdcard/Android/data/package_name/cache 目录,其中package_name表示当前应用的包名,当应用被卸载后,此目录会一并被删除。 可以选择SD卡上的其他指定目录,还可以选择data下的当前应用的目录,如果应用卸载后就希望删除缓存文件,那么就选择SD卡上的缓存目录,如果希望保留缓存数据那就应该选择SD卡上的其他特定目录。
第二个参数表示应用的版本号,一般设为1。当版本号发生改变时DiskLruCache会清空之前所有的缓存文件
第三个参数表示单个节点所对应的数据的个数,一般设为 1。
第四个参数表示缓存的总大小,比如50MB,当缓存大小超出这个设定值后,DiskLruCache 会清除一些缓存从而保证总大小不大于这个设定值。
private static final long DISK_ CACHE SIZE = 1024 * 1024 * 50; //50MB
File diskCacheDir = getDiskCacheDir (mContext, "bitmap") ;
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs() ;
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_ CACHE SIZE);
}
2.2.2 DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor 完成的,Editor 表示一个缓存对象的编辑对象。
以图片缓存举例,首先需要获取图片url对应的key,然后根据key通过edit()来获取Editor 对象。之所以要把url转换成key,是因为图片的url中很可能有特殊字符,一般采用url的md5值作为key。如果这个缓存正在被编辑,那么edit()会返回null,即DiskLruCache不允许同时编辑一个缓存对象,如果当前不存在其他Editor对象,那么edit会返回一个新的Editor对象
private String hashKeyFormUrl(String url)
private String bytesToHexString(byte[] bytes)
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key)
然后通过该editor对象获得一个文件输出流
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
最后通过这个文件输出流的commit方法写入到文件系统上,出现异常使用abort方法
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
2.2.3 DiskLruCache的缓存查找
将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,通过文件流来得到它所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片
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;
}
2.3 ImageLoader的实现
- 图片压缩功能实现
- 内存缓存和磁盘缓存实现
- 同步加载和异步加载接口的设计
3.ImageLoader的使用
3.1照片墙效果
GridView的布局文件
<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:verticalSpacing="5dp"
android:listSelector="@android:color/transparent"
android:numColumns="3"
android:stretchMode="columnWidth" >
</GridView>
</LinearLayout>
GridView的item的布局文件
<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.ryg.chapter_12.ui.SquareImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/image_default" />
</LinearLayout>
自定义控件SquareImageView 打造正方形的ImageView
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);
}
}
4.优化列表的卡顿现象
主要思想,不在主线程做耗时操作
- 不要在BaseAdapter的getView中执行耗时操作:例如不在getView中加载图片
- 控制异步任务的执行频率:例如考虑在列表滑动时停止加载图片,具体实现时,可以给ListView或者GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged 方法中判断列表是否处于滑动状态,如果是就停止加载图片
public void onScrollStateChanged (AbsListView view, int scrol1State) (
if (scrollState .= OnScrollListener. SCROLL STATE IDLE) {
mIsGridViewIdle = true;
mImageAdapter .noti fyDataSetChanged() ;
} else {
mIsGridViewIdle = false;
}
- 在getView方法中,只有列表静止时才加载图片
if (mIsGridViewIdle && mCanGetBi tmapF romNetwork) {
imageView.setTag(uri) ;
mImageLoader .bindBi tmap (uri, imageView, mImageWidth, mImagewidth) ;
}
- 硬件加速
设置android:hardwareAccelerated="true"为活动开启硬件加速