Android开发艺术探索(十二)

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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值