Android开发艺术探索(十二)

来源:互联网 发布:魔兽世界3.22数据库 编辑:程序博客网 时间:2024/06/05 10:36

Bitmap的加载和cache

Bitmap的高效加载

1、如何加载Bitmap?

Bitmap在Android中是指一张图片。Android中BitmapFactory提供了四类方法:
1、decodeFile:从文件中加载
2、decodeResource:从资源中加载
3、decodeStream:输入流中加载
4、decodeByteArray:字节数组

2、如何高效加载Bitmap?

核心思想是采用BitmapFactory.Options来加载所需尺寸的图片。
.
BitmapFactory.Options缩放图片,需要一个采样率参数,主要用到了InSampleSize参数。
当InSampleSize为1时,采样后的图片为原始大小;当InSampleSize大于1时,例如2,采样后的图片的宽、高均为原来的1/2,像素则为原来的1/4,占有的内存大小也为原来的1/4;
注意:只有InSampleSize大于1时,图片才会有缩放效果。

3、获取图片的采样率?

1、将BitmapFactory.Options的inJustDecodeBounds设置为true,并加载图片。
2、从BitmapFactory.Options中读取图片的原始宽高信息,他们对应outWidth和outHeight参数。
3、根据采样率的规则并结合目标View的所需大小计算出采样率InSampleSize
44、将BitmapFactory.Options的inJustDecodeBounds设置为false,重新加载图片

编写工具类代码如下:

public class BitmapUtils {    private static final  String TAG="BitmapUtils";    /**     * 用于返回压缩尺寸后的图片     *     * @param path    图片文件路径     * @param reqWidth          用于显示图片的目标ImageView宽度     * @param reqHeight         用于显示图片的目标ImageView高度     * @return     */    public static Bitmap decodeSampledBitmap(String path,                                             int reqWidth, int reqHeight){        final BitmapFactory.Options options=new BitmapFactory.Options();        //设置inJustDecodeBounds为true,表示只加载图片的边框信息        options.inJustDecodeBounds=true;        //加载图片        BitmapFactory.decodeFile(path,options);        Log.e(TAG,"start    "+options.inSampleSize);        //计算图片的采样率        options.inSampleSize = calculateInSampleSize(options, reqWidth,                reqHeight);        Log.e(TAG,"end    "+options.inSampleSize);        //设置inJustDecodeBounds为false,重新加载图片        return BitmapFactory.decodeFile(path,options);    }    /**     * 计算图片的采样率     *     * @param options     * @param reqWidth     * @param reqHeight     * @return     */    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {        final int height=options.outHeight;        final int width=options.outWidth;        //原始图片采样率,设置为1        int inSampleSize=1;        if (height!=0&&width!=0){            //如果options输出的高度和宽度大于显示图片的ImageView的高度和宽度            if (height>reqHeight||width>reqHeight){                final int heightRatio=Math.round((float) height/(float) reqHeight);                final int widthRatio=Math.round((float)width/(float)reqWidth);                //计算出options输出的宽高和显示图片的imageView宽高比                inSampleSize=heightRatio<widthRatio?heightRatio:widthRatio;            }        }        return inSampleSize;    }}

这个时候,我们如果要在尺寸为100*100像素的ImageView显示一张图片,如下使用:

    //path为图片的路径 iv.setImageBitmap(BitmapUtils.decodeSampledBitmap(path,100,100));

Android中的缓存策略
缓存策略:

内存存储、设备存储、网络获取。
当请求一张图片时,首先从内存中获取;如果没有则从设备中获取;如果设备中也没有在从网络中下载。

一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。
目前常用的一种缓存算法是LRU,LRU是近期最少使用算法。核心思想史,当缓存满时,会优先淘汰那些最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存。

LruCache
LruCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式存储外接的缓存对象,并且提供了get和put方法来完成缓存的获取和添加操作。

先说一下引用类型:

强引用:直接的对象引用;
软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

LruCache是线程安全的:

public class LruCache<K, V> {    private final LinkedHashMap<K, V> map;    //初始化    public LruCache(int maxSize) {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        this.maxSize = maxSize;        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);    }    ....}

上面代码中LruCache初始化时,我们需要给一个最大容量。一般设置为当前进程可用内存的1/8,单位是kb。

  /**获取当前进程可用最大内存*/        int maxMemory= (int) (Runtime.getRuntime().maxMemory()/1024);        LruCache<String,Bitmap> lruCache=new LruCache<String, Bitmap>(maxMemory/8){            /**重写sizeOf方法计算缓存的大小*/            @Override            protected int sizeOf(String key, Bitmap value) {                return value.getRowBytes()*value.getHeight()/1024;            }            /**移除缓存对象需要调用次方法*/            @Override            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {                super.entryRemoved(evicted, key, oldValue, newValue);            }        };        /**获取缓存对象*/        lruCache.get(key);        /**存入缓存对象*/        lruCache.put(key,bitmap);        /**移除缓存对象*/        lruCache.remove(key);

DiskLruCache
DiskLruCache用于实现存储设备缓存。DiskLruCache并不是AndroidSDK的一部分。
下载地址

1、DiskLruCache的创建

  //directory:表示磁盘缓存在文件系统中的存储路径。  //appVersion:应用的版本号  //valueCount:表示单个节点对应数据的个数,一般设置为1  //maxSize:表示缓存的总大小,比如50MB   public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)            throws IOException {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        if (valueCount <= 0) {            throw new IllegalArgumentException("valueCount <= 0");        }        // prefer to pick up where we left off        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);        if (cache.journalFile.exists()) {            try {                cache.readJournal();                cache.processJournal();                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),                        IO_BUFFER_SIZE);                return cache;            } catch (IOException journalIsCorrupt) {//                System.logW("DiskLruCache " + directory + " is corrupt: "//                        + journalIsCorrupt.getMessage() + ", removing");                cache.delete();            }        }        // create a new empty cache        directory.mkdirs();        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);        cache.rebuildJournal();        return cache;    }

2、DiskLruCache的缓存添加
DiskLruCache添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。首先获取到图片url所对应的key。

  private String hashKeyFormUrl(String url) {        String cacheKey;        try {            /**使用url的MD5值作为key*/            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();    }

然后将图片的url转为key之后,就可以获取到Editor对象了。然后通过此Editor获取到一个文件输出流,需要注意的是:前面的DiskLruCache的open方法中设置了一个节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量设置为0即可。

 String key = hashKeyFormUrl(url);        DiskLruCache.Editor editor = mDiskLruCache.edit(key);        if (editor != null) {            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);         }

有了输出流之后,当网络下载图片时,就可以通过这个文件输出流写入到文件系统上。

   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;    }

然后呢,通过Editor.commit()来提交写入操作,如果图片下载发生异常,还可以通过Editor的abort()方法来回退整个操作。

            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);            if (downloadUrlToStream(url, outputStream)) {                editor.commit();            } else {                editor.abort();            }            mDiskLruCache.flush();

3、DiskLruCache的缓存查找

和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法分得到一个Snapshot对象,接着再通过Snapshot对象可以的到缓存的文件输出流,通过文件输出流就可以获取到bitmap对象。

  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);            }        }

自定义图片加载框架的实现
图片加载框架应该具备以下功能:

图片异步加载
图片的同步加载
图片压缩
内存缓存
磁盘缓存
网络获取

1、内存缓存和磁盘缓存的实现

 private LruCache<String, Bitmap> mMemoryCache;    private DiskLruCache mDiskLruCache;    private ImageLoader(Context context) {        mContext = context.getApplicationContext();        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);        int cacheSize = maxMemory / 8;        /**设置内存缓存大小为当前进程可用最大内存的1/8*/        mMemoryCache = new LruCache<String, Bitmap>(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 {                /**设置硬盘缓存为50MB*/                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,                        DISK_CACHE_SIZE);                mIsDiskLruCacheCreated = true;            } catch (IOException e) {                e.printStackTrace();            }        }    }

然后完成缓存的添加和获取:

    /**     * 添加bitmap到缓存中     * @param key     * @param bitmap     */    private void addBitmapToMemoryCache(String key, Bitmap bitmap) {        if (getBitmapFromMemCache(key) == null) {            mMemoryCache.put(key, bitmap);        }    }    /**     * 根据key值获取缓存对象     * @param key     * @return     */    private Bitmap getBitmapFromMemCache(String key) {        return mMemoryCache.get(key);    }

磁盘缓存的读取西药通过Snapshot来完成,通过Snap可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法便捷的进行压缩,所以通过FileDescriptor来加载压缩后的图片,最后将加载后的Bitmap添加到内存缓存中。

    /**     * 从网络获取图片     * @param url     * @param reqWidth     * @param reqHeight     * @return     * @throws IOException     */    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);    }    /**     * 从磁盘中读取缓存内容     * @param url     * @param reqWidth     * @param reqHeight     * @return     * @throws IOException     */    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;        /**获取到key值*/                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、同步加载和异步加载接口的设计
同步加载接口需要外部在线程中调用,同步加载比较耗费时间。

同步加载实现:

    /**     * 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;    }

需要注意的是:上面这个方法不能再主线程中使用,否则会抛出异常。

 if (Looper.myLooper() == Looper.getMainLooper()) {            throw new RuntimeException("can not visit network from UI Thread.");        }

在这类进行了判断,判断当前的Looper是否为主线程的Looper,如果是主线程,就抛出异常。

异步加载实现:

    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);                    //发送消息,通过Handler中转                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();                }            }        };        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);    }

bindBitmap中使用到了线程池和Handler。

/**线程池内,线程数量*/    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;    //CPU核心数的2倍+1    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;    //线程闲置超时时长为10秒    private static final long KEEP_ALIVE = 10L;      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());        }    };    /**THREAD_POOL_EXECUTOR 实现*/    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,            KEEP_ALIVE, TimeUnit.SECONDS,            new LinkedBlockingQueue<Runnable>(), sThreadFactory);

为什么要采用线程池呢?

1、如果我们加载一张单个的图片,直接使用普通的线程加载就可以。但是如果列表中有大量的图片需要加载,随着列表滑动会产生大量的线程,整体效率会低下。
2、没有采用AsyncTask,AsyncTask在3.0以上无法实现并发效果,可以通过改造AsyncTask或者使用AsyncTask的executeOnexecutor方法的形式来执行异步任务。
这里选择线程池和Handler来提供ImageLoader的并发能力和访问UI的能力。

Handler的实现?

ImageLoader直接采用了主线程的Looper来构造Handler对象,这就使得ImageLoader可以在非主线程中构造了。

    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {        @Override        public void handleMessage(Message msg) {            LoaderResult result = (LoaderResult) msg.obj;            ImageView imageView = result.imageView;            imageView.setImageBitmap(result.bitmap);            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!");            }        };    };

在以上的代码中,为了解决View复用造成的列表错位问题,给ImageView设置图片之前,都会检查它的url有没有发生改变,如果发生改变就不在给它设置图片。

ImageLoader的使用

加载图片时使用方式:

/**首先给控件设置相应的tag标签*/ imageView.setTag(uri); /**从左到右参数依次为图片的url、显示目标控件ImageView、显示的宽度、显示的高度*/ mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
0 0