android开发艺术(十)之Bitmap的加载和Cache

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, 100100));

2.Android的缓存策略

程序第一次从网络加载图片后,就将其缓存到存储设备.上,然后在内存中再缓存一份,当应用打算从网络上请求一张图片时, 程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。

2.1 LruCache

属于内存缓存,LruCache是线程安全的,是一个泛型类

public class LruCache<K, V> {
	private final LinkedHashMap<K, V> map;
	}

它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象
●强引用:直接的对象引用;
●软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收:
●弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

LruCache实现内存缓存

  1. 初始化
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中完成一些资源回收工作

  1. 缓存的获取和添加:
从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的实现

  1. 图片压缩功能实现
  2. 内存缓存和磁盘缓存实现
  3. 同步加载和异步加载接口的设计

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.优化列表的卡顿现象

主要思想,不在主线程做耗时操作

  1. 不要在BaseAdapter的getView中执行耗时操作:例如不在getView中加载图片
  2. 控制异步任务的执行频率:例如考虑在列表滑动时停止加载图片,具体实现时,可以给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;
	}
  1. 在getView方法中,只有列表静止时才加载图片
if (mIsGridViewIdle && mCanGetBi tmapF romNetwork) {
		imageView.setTag(uri) ;
		mImageLoader .bindBi tmap (uri, imageView, mImageWidth, mImagewidth) ;
	}
  1. 硬件加速
    设置android:hardwareAccelerated="true"为活动开启硬件加速
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值