首页 > 代码库 > Android异步批量下载图片并缓存

Android异步批量下载图片并缓存

前言

本文引自:http://www.xycoding.com/articles/2014/07/29/android-async-images-download/,作者不详

ImagesDownLoad源码下载:DEMO

接触android开发不久,近段时间需实现一个批量下载图片并显示的小功能。在网上搜索了一圈,发现国内外网上异步加载的例子太多太杂,要么是加载大图decode时报OOM异常,要么内存急剧上升不稳定。所以在前辈们的基础上,做了一些优化,特共享出来,欢迎大家指正。这里主要参见了以下两篇文章,非常感谢:

Android照片墙应用实现,再多的图片也不怕崩溃

Android 异步加载图片,使用LruCache和SD卡或手机缓存,效果非常的流畅

在巨人的肩膀上,主要优化了以下几个地方:

  1. 下载大图decode时,可根据View大小自动缩放图片,不在出现OOMSkImageDecoder::Factory returned null错误
  2. 图片下载失败时,可自定义失败重试次数
  3. 记录正在下载的任务,防止屏幕滚动时多次下载
  4. 缓存目录容量大于给定限制时,清空文件缓存
  5. 其他一些小问题

工具类Utils

Utils类主要封装了一些文件操作的基本方法,包括创建、删除、获取文件大小等。

public class Utils {    private static final String Util_LOG = makeLogTag(Utils.class);    public static String makeLogTag(Class<?> cls) {        return cls.getName();    }    public static void showToast(Context context, String str) {        Toast.makeText(context, str, Toast.LENGTH_SHORT).show();    }    /**     * 检查是否存在SD卡     *      * @return     */    public static boolean hasSdcard() {        String state = Environment.getExternalStorageState();        if (state.equals(Environment.MEDIA_MOUNTED)) {            return true;        } else {            return false;        }    }    /**     * 创建目录     *      * @param context     * @param dirName     *            文件夹名称     * @return     */    public static File createFileDir(Context context, String dirName) {        String filePath;        // 如SD卡已存在,则存储;反之存在data目录下        if (hasSdcard()) {            // SD卡路径            filePath = Environment.getExternalStorageDirectory()                    + File.separator + dirName;        } else {            filePath = context.getCacheDir().getPath() + File.separator                    + dirName;        }        File destDir = new File(filePath);        if (!destDir.exists()) {            boolean isCreate = destDir.mkdirs();            Log.i(Util_LOG, filePath + " has created. " + isCreate);        }        return destDir;    }    /**     * 删除文件(若为目录,则递归删除子目录和文件)     *      * @param file     * @param delThisPath     *            true代表删除参数指定file,false代表保留参数指定file     */    public static void delFile(File file, boolean delThisPath) {        if (!file.exists()) {            return;        }        if (file.isDirectory()) {            File[] subFiles = file.listFiles();            if (subFiles != null) {                int num = subFiles.length;                // 删除子目录和文件                for (int i = 0; i < num; i++) {                    delFile(subFiles[i], true);                }            }        }        if (delThisPath) {            file.delete();        }    }    /**     * 获取文件大小,单位为byte(若为目录,则包括所有子目录和文件)     *      * @param file     * @return     */    public static long getFileSize(File file) {        long size = 0;        if (file.exists()) {            if (file.isDirectory()) {                File[] subFiles = file.listFiles();                if (subFiles != null) {                    int num = subFiles.length;                    for (int i = 0; i < num; i++) {                        size += getFileSize(subFiles[i]);                    }                }            } else {                size += file.length();            }        }        return size;    }    /**     * 保存Bitmap到指定目录     *      * @param dir     *            目录     * @param fileName     *            文件名     * @param bitmap     * @throws IOException     */    public static void savaBitmap(File dir, String fileName, Bitmap bitmap) {        if (bitmap == null) {            return;        }        File file = new File(dir, fileName);        try {            file.createNewFile();            FileOutputStream fos = new FileOutputStream(file);            bitmap.compress(CompressFormat.JPEG, 100, fos);            fos.flush();            fos.close();        } catch (IOException e) {            e.printStackTrace();        }    }    /**     * 判断某目录下文件是否存在     *      * @param dir     *            目录     * @param fileName     *            文件名     * @return     */    public static boolean isFileExists(File dir, String fileName) {        return new File(dir, fileName).exists();    }}

 

图片下载管理类ImageDownLoader

ImageDownLoader类主要是图片下载管理,包括缓存管理、异步下载管理等。

public class ImageDownLoader {    private static final String ImageDownLoader_Log = Utils            .makeLogTag(ImageDownLoader.class);    /** 保存正在下载或等待下载的URL和相应失败下载次数(初始为0),防止滚动时多次下载 */    private Hashtable<String, Integer> taskCollection;    /** 缓存类 */    private LruCache<String, Bitmap> lruCache;    /** 线程池 */    private ExecutorService threadPool;    /** 缓存文件目录 (如无SD卡,则data目录下) */    private File cacheFileDir;    /** 缓存文件夹 */    private static final String DIR_CACHE = "cache";    /** 缓存文件夹最大容量限制(10M) */    private static final long DIR_CACHE_LIMIT = 10 * 1024 * 1024;    /** 图片下载失败重试次数 */    private static final int IMAGE_DOWNLOAD_FAIL_TIMES = 2;    public ImageDownLoader(Context context) {        // 获取系统分配给每个应用程序的最大内存        int maxMemory = (int) Runtime.getRuntime().maxMemory();        // 给LruCache分配最大内存的1/8        lruCache = new LruCache<String, Bitmap>(maxMemory / 8) {            // 必须重写此方法,来测量Bitmap的大小            @Override            protected int sizeOf(String key, Bitmap value) {                return value.getRowBytes() * value.getHeight() / 1024;            }        };        taskCollection = new Hashtable<String, Integer>();        // 创建线程数        threadPool = Executors.newFixedThreadPool(10);        cacheFileDir = Utils.createFileDir(context, DIR_CACHE);    }    /**     * 添加Bitmap到内存缓存     *      * @param key     * @param bitmap     */    private void addLruCache(String key, Bitmap bitmap) {        if (getBitmapFromMemCache(key) == null && bitmap != null) {            lruCache.put(key, bitmap);        }    }    /**     * 从内存缓存中获取Bitmap     *      * @param key     * @return     */    private Bitmap getBitmapFromMemCache(String key) {        return lruCache.get(key);    }    /**     * 异步下载图片,并按指定宽度和高度压缩图片     *      * @param url     * @param width     * @param height     * @param listener     *            图片下载完成后调用接口     */    public void loadImage(final String url, final int width, final int height,            AsyncImageLoaderListener listener) {        Log.i(ImageDownLoader_Log, "download:" + url);        final ImageHandler handler = new ImageHandler(listener);        Runnable runnable = new Runnable() {            @Override            public void run() {                Bitmap bitmap = downloadImage(url, width, height);                Message msg = handler.obtainMessage();                msg.obj = bitmap;                handler.sendMessage(msg);                // 将Bitmap 加入内存缓存                addLruCache(url, bitmap);                // 加入文件缓存前,需判断缓存目录大小是否超过限制,超过则清空缓存再加入                long cacheFileSize = Utils.getFileSize(cacheFileDir);                if (cacheFileSize > DIR_CACHE_LIMIT) {                    Log.i(ImageDownLoader_Log, cacheFileDir                            + " size has exceed limit." + cacheFileSize);                    Utils.delFile(cacheFileDir, false);                    taskCollection.clear();                }                // 缓存文件名称( 替换url中非字母和非数字的字符,防止系统误认为文件路径)                String urlKey = url.replaceAll("[^\\w]", "");                // 将Bitmap加入文件缓存                Utils.savaBitmap(cacheFileDir, urlKey, bitmap);            }        };        // 记录该url,防止滚动时多次下载,0代表该url下载失败次数        taskCollection.put(url, 0);        threadPool.execute(runnable);    }    /**     * 获取Bitmap, 若内存缓存为空,则去文件缓存中获取     *      * @param url     * @return 若缓存中没找到,则返回null     */    public Bitmap getBitmapCache(String url) {        // 去处url中特殊字符作为文件缓存的名称        String urlKey = url.replaceAll("[^\\w]", "");        if (getBitmapFromMemCache(url) != null) {            return getBitmapFromMemCache(url);        } else if (Utils.isFileExists(cacheFileDir, urlKey)                && Utils.getFileSize(new File(cacheFileDir, urlKey)) > 0) {            // 从文件缓存中获取Bitmap            Bitmap bitmap = BitmapFactory.decodeFile(cacheFileDir.getPath()                    + File.separator + urlKey);            // 将Bitmap 加入内存缓存            addLruCache(url, bitmap);            return bitmap;        }        return null;    }    /**     * 下载图片,并按指定高度和宽度压缩     *      * @param url     * @param width     * @param height     * @return     */    private Bitmap downloadImage(String url, int width, int height) {        Bitmap bitmap = null;        HttpClient httpClient = new DefaultHttpClient();        try {            httpClient.getParams().setParameter(                    CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);            HttpPost httpPost = new HttpPost(url);            HttpResponse httpResponse = httpClient.execute(httpPost);            if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {                HttpEntity entity = httpResponse.getEntity();                //解决缩放大图时出现SkImageDecoder::Factory returned null错误                byte[] byteIn = EntityUtils.toByteArray(entity);                BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options();                bmpFactoryOptions.inJustDecodeBounds = true;                BitmapFactory.decodeByteArray(byteIn, 0, byteIn.length,                        bmpFactoryOptions);                int heightRatio = (int) Math.ceil(bmpFactoryOptions.outHeight                        / height);                int widthRatio = (int) Math.ceil(bmpFactoryOptions.outWidth                        / width);                if (heightRatio > 1 && widthRatio > 1) {                    bmpFactoryOptions.inSampleSize = heightRatio > widthRatio ? heightRatio                            : widthRatio;                }                bmpFactoryOptions.inJustDecodeBounds = false;                bitmap = BitmapFactory.decodeByteArray(byteIn, 0,                        byteIn.length, bmpFactoryOptions);            }        } catch (ClientProtocolException e) {            e.printStackTrace();        } catch (ConnectTimeoutException e) {            e.printStackTrace();        } catch (Exception e) {            e.printStackTrace();        } finally {            if (httpClient != null && httpClient.getConnectionManager() != null) {                httpClient.getConnectionManager().shutdown();            }        }        // 下载失败,再重新下载        // 本例是图片下载失败则再次下载,可根据需要改变,比如记录下载失败的图片URL,在某个时刻再次下载        if (taskCollection.get(url) != null) {            int times = taskCollection.get(url);            if (bitmap == null                    && times < IMAGE_DOWNLOAD_FAIL_TIMES) {                times++;                taskCollection.put(url, times);                bitmap = downloadImage(url, width, height);                Log.i(ImageDownLoader_Log, "Re-download " + url + ":" + times);            }        }        return bitmap;    }    /**     * 取消正在下载的任务     */    public synchronized void cancelTasks() {        if (threadPool != null) {            threadPool.shutdownNow();            threadPool = null;        }    }    /**     * 获取任务列表     *      * @return     */    public Hashtable<String, Integer> getTaskCollection() {        return taskCollection;    }    /** 异步加载图片接口 */    public interface AsyncImageLoaderListener {        void onImageLoader(Bitmap bitmap);    }    /** 异步加载完成后,图片处理 */    static class ImageHandler extends Handler {        private AsyncImageLoaderListener listener;        public ImageHandler(AsyncImageLoaderListener listener) {            this.listener = listener;        }        @Override        public void handleMessage(Message msg) {            super.handleMessage(msg);            listener.onImageLoader((Bitmap) msg.obj);        }    }}

 

GridView适配器类ImageGridViewAdapter

该类为GridView适配器,并实现滚动监听等的功能。

public class ImageGridViewAdapter extends BaseAdapter implements OnScrollListener {    /** 数据源 */    private List<String> data;    /** 图片下载类 */    private ImageDownLoader loader;    /** 判定是否第一次加载 */    private boolean isFirstEnter = true;    /** 第一张可见Item下标 */    private int firstVisibleItem;    /** 每屏Item可见数 */    private int visibleItemCount;    /** GridView实例 */    private GridView gridView;    private Context context;    public ImageGridViewAdapter(Context context, GridView gridView, List<String> data) {        this.context = context;        this.gridView = gridView;        this.data = http://www.mamicode.com/data;>

 

最后的最后,别忘加上权限,并附效果图如下:

<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
upload

Android异步批量下载图片并缓存