Android图片的拉取与缓存

Anroid应用中经常会有从网上拉取图片的需求,拉图片说简单也很好做,说难也是很费力的,虽然网上的方案很多,开源框架也不少,但具体的实现还是得看需求。下面分享一下我在项目中用到的两种拉图片方案。

1. 少量图片

如果图片少量,使用框架就显得冗余,直接下载就更简洁一些。

    public static boolean downloadImage(String url, String savePath) {
        LogUtil.v(TAG, "url=" + url + "; savepath=" + savePath);
        if (TextUtils.isEmpty(url) || TextUtils.isEmpty(savePath)) {
            return false;
        }
        // 初始化下载路径,删除已存在的文件以及生成目录
        String tempPath = savePath + "_temp";
        checkFilePath(savePath);
        checkFilePath(tempPath);
        // 拉取资源,具体实现网上很多
        HttpEntity entity = getHttpEntity(url);
        if (entity == null) {
            return false;
        }
        // 检查剩余空间
        long imgLength = entity.getContentLength();
        long spaceLeft = SdCardSize.getUsableSpace(savePath);
        LogUtil.v(TAG, "space left =" + spaceLeft);
        if (imgLength <= 0 || spaceLeft < imgLength*3) {
            LogUtil.v(TAG, "imgLength*3=" + imgLength*3);
            return false;
        }

        try {
            InputStream stream = entity.getContent();
            LogUtil.i(TAG, "imgLength = " + imgLength);
            // 将stream写入到临时文件,如果写出成功,则重命名为目标文件名
            // 确保图片的完整性
            if (imgLength == writeImageFile(stream, tempPath)
                    && FileUtil.renameFile(tempPath, savePath)) {
                return true;
            } else {
                LogUtil.w(TAG, "failed to write image file");
                FileUtil.deleteFile(tempPath);
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

此方法仅仅是下载图片,返回是否下载成功,这个功能应该需求不多,因为只是下载了图片,并没有返回bitmap对象,便于直接使用。如果需要直接使用图片的话在拿到inputstream时就可以直接用BitmapFactory.decodeStream(is)得到bitmap对象;

2. 大量图片

对于需要拉取大量图片的需求,上面的方法不是很好用,主要是线程的封装,以及优先级,还有缓存的处理。在这里根据需求修改volley来实现。

    //在volley ImageRequest上封装了一层,volley的request需要两个listener,
    //而拉取图片只需要关注是否拉取成功,所以只有一个回调
    public void fetchImage(final String url, final String savePath, int width, int height,
            final IFetchDataListener<Bitmap> listener) {
        ImageRequest request = new ImageRequest(url, new Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                LogUtil.v(TAG, "fetchImage ByteCount=" + response.getByteCount());
                // 返回拉取的bitmap
                listener.onFetchFinished(response);
            }
        }, width, height, ScaleType.FIT_XY, Config.ARGB_8888, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                LogUtil.e(TAG, "fetchImage url=" + url);
                LogUtil.e(TAG, "fetchImage error=" + error);
                // volley拉取失败后,再采用第一种方法拉取一遍
                ThreadManager.getPoolProxy().execute(new Runnable() {
                    @Override
                    public void run() {
                        final Bitmap bitmap = ImageFetcher.getImage(url, savePath);
                        LogUtil.v(TAG, "bitmap from imagefetcher:" + bitmap);
                        // 将拉取的结果返回给UI线程
                        new InnerHandle().post(new Runnable() {
                            @Override
                            public void run() {
                                listener.onFetchFinished(bitmap);
                            }
                        });
                    }
                });
            }
        });
        mQueue.add(request);
    }

之所以使用volley,是因为volley对于request以及消息的管理机制很不错,自己实现的很难完善,所以就直接采用volley。在volley拉取失败后又用土办法重新拉一遍是因为实际情况如此,volley拉取时一直都是返回404错误,但是此时土方法每次都能拉取成功,就把两者结合起来了。

    public static Bitmap getImage(String url, String savePath) {
        LogUtil.v(TAG, "url=" + url + ";savePath=" + savePath);
        if (TextUtils.isEmpty(savePath)) {
            return null;
        }
        // 从本地文件读取图片
        Bitmap bitmap = getImageByPath(savePath);
        if (bitmap != null) {
            return bitmap;
        }
        // 从网络拉取
        if (downloadImage(url, savePath)) {
            return getImageByPath(savePath);
        }
        return null;
    }

修改volley磁盘缓存方案
volley在将缓存写入磁盘时会将数据附带的信息也写入同一个文件,这样导致我们并不能直接使用volley在磁盘缓存的文件,根据我最开始的方法可以看出,我是需要直接使用图片文件。所以修改了volley的磁盘缓存DiskBasedCache。

    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            CacheHeader e = new CacheHeader(key, entry);
            //将写头信息的部分注释掉,这样写入的文件就是可以直接使用的数据了
            // boolean success = e.writeHeader(fos);
            // if (!success) {
            // fos.close();
            // VolleyLog.d("Failed to write header for %s",
            // file.getAbsolutePath());
            // throw new IOException();
            // }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

同样的,还需要修改get方法,不然volley自己就用不了缓存文件了。

    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }
        VolleyLog.v("getEntry key=" + key);
        File file = getFileForKey(key);
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(
                    new FileInputStream(file)));
                    //将读取头信息的部分去掉,这样volley就可以直接读取缓存数据了
// CacheHeader.readHeader(cis); // eat header
// byte[] data = streamToBytes(cis, (int) (file.length()-cis.bytesRead));
            byte[] data = streamToBytes(cis, (int) file.length());
            return entry.toCacheEntry(data);
        } catch (IOException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } catch (NegativeArraySizeException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ioe) {
                    return null;
                }
            }
        }
    }

以上的修改可以使用了,但是在重启应用之后,发现缓存不管用了,原来在volley启动的时候会初始话缓存,也就是initialize();
volley的做法是通过读取缓存目录下所有的文件的头信息,来初始化缓存的,但是我刚才已经把缓存文件的头信息都去掉了,所以我们得采用另外的方式来记录头信息,便于volley初始化。

    public synchronized void initialize() {
        if (!mRootDirectory.exists()) {
            if (!mRootDirectory.mkdirs()) {
                VolleyLog.e("Unable to create cache dir %s",
                        mRootDirectory.getAbsolutePath());
            }
            return;
        }
        //将mEntries对象用Gson写入到mEntryMapJsonPath路径下json文件,
        //在初始化时读取文件并用gson读取为Map<String, CacheHeader>对象
        StringBuilder str = FileUtil.readFile(mEntryMapJsonPath);
        if (TextUtils.isEmpty(str)) {
            return;
        }
        try {
            Map<String, CacheHeader> entries = new Gson().fromJson(str.toString(),
                    new TypeToken<Map<String, CacheHeader>>(){}.getType());
            mEntries.putAll(entries);
            VolleyLog.v("entries=" + entries);
        } catch (Exception e) {
        }
// File[] files = mRootDirectory.listFiles();
// if (files == null) {
// return;
// }
// for (File file : files) {
// BufferedInputStream fis = null;
// try {
// fis = new BufferedInputStream(new FileInputStream(file));
// CacheHeader entry = CacheHeader.readHeader(fis);
// entry.size = file.length();
// putEntry(entry.key, entry);
// } catch (IOException e) {
// if (file != null) {
// file.delete();
// }
// } finally {
// try {
// if (fis != null) {
// fis.close();
// }
// } catch (IOException ignored) {
// }
// }
// }
    }

这样实现的话就需要在增加和删除缓存时更新json文件

    private void putEntry(String key, CacheHeader entry) {
        if (!mEntries.containsKey(key)) {
            mTotalSize += entry.size;
        } else {
            CacheHeader oldEntry = mEntries.get(key);
            mTotalSize += (entry.size - oldEntry.size);
        }
        mEntries.put(key, entry);
        saveCachedJsonToFile();
    }

    private void removeEntry(String key) {
        CacheHeader entry = mEntries.get(key);
        if (entry != null) {
            mTotalSize -= entry.size;
            mEntries.remove(key);
        }
        saveCachedJsonToFile();
    }

    private void saveCachedJsonToFile() {
        synchronized (mEntries) {
            String json = new Gson().toJson(mEntries);
            FileUtil.writeFileWithBackUp(mEntryMapJsonPath, json, false);
        }
    }

到这里就完成了volley的修改。。。。

图片三级缓存

    public Bitmap getBitmapFromCache(String key) {
        checkCacheParams();
        Bitmap bitmap = null;
        // 从lrucache,即硬连接获取图片
        synchronized (mLruCache) {
            bitmap = mLruCache.remove(key);
            if (bitmap != null) {
                // found,move the file to the last of LinkedHashMap
                mLruCache.put(key, bitmap);
                LogUtil.v(TAG, "getBitmapFrom mLruCache");
                return bitmap;
            } else {
                LogUtil.w(TAG, "getBitmapFrom mLruCache failed");
            }
        }

        // 从软链接获取图片
        synchronized (mSoftCache) {
            SoftReference<Bitmap> bitmapReference = mSoftCache.remove(key);
            if (bitmapReference != null) {
                bitmap = bitmapReference.get();
                if (bitmap != null) {
                    // if hit, move bm to lrucache
                    addBitmapToCache(key, bitmap);
                    LogUtil.v(TAG, "getBitmapFrom mSoftCache");
                    return bitmap;
                } else {
                    LogUtil.w(TAG, "getBitmapFrom mSoftCache failed");
                }
            } else {
                LogUtil.w(TAG, "getBitmapFrom mSoftCache failed");
            }
        }
        // 从本地文件获取图片
        synchronized (mFileCache) {
            bitmap = mFileCache.getImageByURL(key);
            if (bitmap != null) {
                addBitmapToCache(key, bitmap);
                LogUtil.v(TAG, "getBitmapFrom mFileCache");
                return bitmap;
            } else {
                LogUtil.w(TAG, "getBitmapFrom mFileCache failed");
            }
        }
        return null;
    }

这就是传说中的三级缓存,呵呵!

初始化是酱紫的:

    public void initCacheParams(String cacheDir, int maxCacheByteSize) {
        mFileCache = new ImageFileCache(cacheDir);
        mLruCache = new LruCache<String, Bitmap>(maxCacheByteSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (value != null) {
                    return value.getRowBytes() * value.getHeight();
                } else {
                    return 0;
                }
            }
        // 缓存占用内存超过设定的最大值时移除lru的缓存
            @Override
            protected void entryRemoved(boolean evicted, String key,
                    Bitmap oldValue, Bitmap newValue) {
                if (oldValue != null && mSoftCache != null) {
                    // 将移除的缓存加入到软连接
                    mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
                }
            }
        };

        mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(
                SOFT_CACHE_SIZE, 0.75f, true) {
            private static final long serialVersionUID = 6040103833179403725L;
        // 当缓存的数量超过设定的最大值,或者内存不够用时,从软链接移除最先加入的缓存
            @Override
            protected boolean removeEldestEntry(
                    Entry<String, SoftReference<Bitmap>> eldest) {
                if (size() > SOFT_CACHE_SIZE) {
                    return true;
                }
                return false;
            }
        };

    }

添加缓存就很简单啦:

    public void addBitmapToCache(String key, Bitmap bitmap) {
        checkCacheParams();
        if (bitmap != null) {
            synchronized (mLruCache) {
                mLruCache.put(key, bitmap);
            }
        }
    }

仅仅是把缓存加入到lru,若缓存增加就会把以前的缓存移除到二级缓存(软链接),但是从软链接移除后并没有第三级缓存。
注意,这里并没有第三级缓存,网上很多实现在这里加入了第三级,就是把bitmap写入文件。思路上并没有问题,问题是将bitmap写入缓存文件时很容易出问题,很多实现是把bitmap按jpg或png格式压缩并写文件。然后在下次读缓存时再把缓存文件加载为bitmap,如此循环多次后,缓存文件会越来越大,并且图片越来越不清晰,原因是bitmap转jpg或png时是有损压缩,这样说就明白了吧,缓存文件不需要每次都去写,只需要第一次写入原文件,以后每次取缓存时直接读原文件就OK了。但是这样也会引入一个新的问题,就是缓存文件如何管理,因为缓存文件不能一直增加,这个我还没有找到简便的方法,以后再补上吧!

你可能感兴趣的:(android,图片,Volley)