(转)Android技术积累:图片异步加载

当在ListView或GridView中要加载很多图片时,很容易出现滑动时的卡顿现象,以及出现OOM导致FC(Force Close)。

会出现卡顿现象主要是因为加载数据慢,要等数据加载完才能显示出来。可以通过将数据分页显示,以及将耗时的图片加载用异步的方式和图片缓存,这样就可以解决卡顿的问题。

大部分开发者在ListView或GridView加载图片时,都会在getView方法里创建新的线程去异步加载图片。然而,当屏幕快速向下滑动时,每个划过的Item都会调用getView一次,即会创建出很多线程,同一时间存在的线程太多,内存不够用了,自然就会OOM了。要避免OOM,就得控制好线程的数量,所以加个线程池就非常有必要了。

另外,当向下快速滑动屏幕时,也没必要加载滑动过的所有图片,只要加载滑动停止后当前屏幕的就足够了。仔细观察像微博、facebook或其他优秀的app,滑动屏幕时未加载过的图片是不会被加载的,当滑动停止后,也只加载当前屏幕内的图片。

那么,接下来就讨论实现的问题了。首先,图片是需要缓存的,前一篇文章已经对图片缓存做了总结(Android技术积累:图片缓存管理),直接拿过来用就行。然后,线程池维护多少个线程比较合适呢?这个很难界定,线程太少CPU不能得到充分利用,线程太多会降低性能,也加大了OOM的风险。线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。若在一个具有N个处理器的系统上只有一个工作队列,其中全部是计算性质的任务,在线程池具有N或N+1个线程时一般会获得最大的CPU利用率。

建立线程池的代码如下:

// 获取当前系统的CPU数目
int cpuNums = Runtime.getRuntime().availableProcessors();
//根据系统资源情况灵活定义线程池大小
ExecutorService executorService = Executors.newFixedThreadPool(cpuNums + 1);

从内存缓存读取图片是非常快的,如果内存缓存中有图片就可以直接获取,而不需要另起线程去异步加载,在内存缓存获取不到时才往线程池里添加新线程去加载图片。既然是异步的,那就要知道获取到的图片是要加载到哪个ImageView,可以将ImageView保存起来。另外,为了保证在整个应用中只有一个线程池,也不会出现多份缓存,图片加载的工具类最好用单例模式。ListView或GridView滑动时不加载图片,滑动停止后才加载图片,因此加一个是否允许加载图片的boolean变量。ListView或GridView初始化时是不滑动的,但也要加载图片,所以boolean值变量初始应该为true。

直接看图片加载的工具类ImageLoader的完整代码:

public class ImageLoader {

    private static ImageLoader instance;

    private ExecutorService executorService;   //线程池
    private ImageMemoryCache memoryCache;     //内存缓存
    private ImageFileCache fileCache;        //文件缓存
    private Map<String, ImageView> taskMap; //存放任务
    private boolean allowLoad = true; //是否允许加载图片

    private ImageLoader(Context context) {
        // 获取当前系统的CPU数目
        int cpuNums = Runtime.getRuntime().availableProcessors();
        //根据系统资源情况灵活定义线程池大小
        this.executorService = Executors.newFixedThreadPool(cpuNums + 1);

        this.memoryCache = new ImageMemoryCache(context);
        this.fileCache = new ImageFileCache();
        this.taskMap = new HashMap<String, ImageView>();
    }

    /**
     * 使用单例,保证整个应用中只有一个线程池和一份内存缓存和文件缓存
     */
    public static ImageLoader getInstance(Context context) {
        if (instance == null)
            instance = new ImageLoader(context);
        return instance;
    }

    /**
     * 恢复为初始可加载图片的状态
     */
    public void restore() {
        this.allowLoad = true;
    }

    /**
     * 锁住时不允许加载图片
     */
    public void lock() {
        this.allowLoad = false;
    }

    /**
     * 解锁时加载图片
     */
    public void unlock() {
        this.allowLoad = true;
        doTask();
    }

    /**
     * 添加任务
     */
    public void addTask(String url, ImageView img) {
        //先从内存缓存中获取,取到直接加载
        Bitmap bitmap = memoryCache.getBitmapFromCache(url);
        if (bitmap != null) {
            img.setImageBitmap(bitmap);
        } else {
            synchronized (taskMap) {
                /**
                 * 因为ListView或GridView的原理是用上面移出屏幕的item去填充下面新显示的item,
                 * 这里的img是item里的内容,所以这里的taskMap保存的始终是当前屏幕内的所有ImageView。
                 */
                img.setTag(url);
                taskMap.put(Integer.toString(img.hashCode()), img);
            }
            if (allowLoad) {
                doTask();
            }
        }
    }

    /**
     * 加载存放任务中的所有图片
     */
    private void doTask() {
        synchronized (taskMap) {
            Collection<ImageView> con = taskMap.values();
            for (ImageView i : con) {
                if (i != null) {
                    if (i.getTag() != null) {
                        loadImage((String) i.getTag(), i);
                    }
                }
            }
            taskMap.clear();
        }
    }

    private void loadImage(String url, ImageView img) {
        this.executorService.submit(new TaskWithResult(new TaskHandler(url, img), url));
    }

    /*** 获得一个图片,从三个地方获取,首先是内存缓存,然后是文件缓存,最后从网络获取 ***/
    private Bitmap getBitmap(String url) {
        // 从内存缓存中获取图片
        Bitmap result = memoryCache.getBitmapFromCache(url);
        if (result == null) {
            // 文件缓存中获取
            result = fileCache.getImage(url);
            if (result == null) {
                // 从网络获取
                result = ImageGetFromHttp.downloadBitmap(url);
                if (result != null) {
                    fileCache.saveBitmap(result, url);
                    memoryCache.addBitmapToCache(url, result);
                }
            } else {
                // 添加到内存缓存
                memoryCache.addBitmapToCache(url, result);
            }
        }
        return result;
    }

    /*** 子线程任务 ***/
    private class TaskWithResult implements Callable<String> {
        private String url;
        private Handler handler;

        public TaskWithResult(Handler handler, String url) {
            this.url = url;
            this.handler = handler;
        }

        @Override
        public String call() throws Exception {
            Message msg = new Message();
            msg.obj = getBitmap(url);
            if (msg.obj != null) {
                handler.sendMessage(msg);
            }
            return url;
        }
    }

    /*** 完成消息 ***/
    private class TaskHandler extends Handler {
        String url;
        ImageView img;

        public TaskHandler(String url, ImageView img) {
            this.url = url;
            this.img = img;
        }

        @Override
        public void handleMessage(Message msg) {
            /*** 查看ImageView需要显示的图片是否被改变  ***/
            if (img.getTag().equals(url)) {
                if (msg.obj != null) {
                    Bitmap bitmap = (Bitmap) msg.obj;
                    img.setImageBitmap(bitmap);
                }
            }
        }
    }

}

有一点需要注意,要保证taskMap保存的始终只是当前屏幕内的所有ImageView,在ImageAdapter的getView方法里必须使用ViewHolder模式,这样才能保证item被重用时相应的ImageView也被重用。getView代码类似如下:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.list_item, null);
        holder = new ViewHolder();
        holder.text = (TextView) convertView.findViewById(R.id.text);
        holder.image = (ImageView) convertView.findViewById(R.id.img);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    ListItem item = mItems.get(position); //ListView的Item
    holder.text.setText(item.getText());
    holder.image.setImageResource(R.drawable.default_img); //设置默认图片
    mImageLoader.addTask(item.getImgUrl(), holder.image); //添加任务
    return convertView;
}

static class ViewHolder {
    TextView text;
    ImageView image;
}

ListView或GridView滑动时就需要锁住不允许加载图片,滑动停止后解锁加载图片。因此,给ListView或GridView添加一个OnScrollListener,代码如下:

mImageLoader = ImageLoader.getInstance(context);
mListView.setOnScrollListener(onScrollListener);

OnScrollListener onScrollListener = new OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            switch (scrollState) {
                case OnScrollListener.SCROLL_STATE_FLING:
                    mImageLoader.lock();
                    break;
                case OnScrollListener.SCROLL_STATE_IDLE:
                    mImageLoader.unlock();
                    break;
                case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                    mImageLoader.lock();
                    break;
                default:
                    break;
            }
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
        }
    };

至此,所有关键代码就全都列出来了。

异步加载图片,关键就在于三点:1、缓存;2、线程池;3、只加载当前屏幕

时间: 2024-11-01 00:58:17

(转)Android技术积累:图片异步加载的相关文章

Android编程之图片(异步)加载类

应某人之请,写一篇关于图片加载类.其实,网上有很多这样的类,而且比较推崇的是来自google中开源中的一篇.他写的比较好了,而且注意了内存优化,下面贴出它的图片下载类: [java] view plaincopy /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not 

Android图片异步加载之Android-Universal-Image-Loader

将近一个月没有更新博客了,由于这段时间以来准备毕业论文等各种事务缠身,一直没有时间和精力沉下来继续学习和整理一些东西.最近刚刚恢复到正轨,正好这两天看了下Android上关于图片异步加载的开源项目,就顺便整理记录下来,作为这一个多月来博客的重新开火做饭吧.从今天起我会陆续恢复博客的更新,也希望大家继续支持. 今天要介绍的是Github上一个使用非常广泛的图片异步加载库Android-Universal-Image-Loader,该项目的功能十分强大,可以说是我见过的目前功能最全.性能最优的图片异

Android图片异步加载之Android-Universal-Image-Loader类库的使用

Android开发中我们会经常遇到图片过多或操作不当造成Out of Memory异常,有时虽然是解决了这个问题但却会影响程序的运行效率,例如:当用户在快速滑动滚动条的过程中,我们程序在仍在艰难的加载服务器端的图片,这样给用户造成了极不好的体验.其实网络上关于图片的异步加载和缓存的讲解很多,但是其实,写一个这方面的程序还是比较麻烦的,要考虑多线程,缓存,内存溢出等很多方面,针对这一广大开发者都会遇到的问题,一些牛人们已经帮我们解决了这一问题,今天我为大家介绍一款很流行的开源类库,可以很很好的解决

Android新浪微博客户端(七)——ListView中的图片异步加载、缓存

原文出自:方杰|http://fangjie.sinaapp.com/?p=193转载请注明出处 最终效果演示:http://fangjie.sinaapp.com/?page_id=54该项目代码已经放到github:https://github.com/JayFang1993/SinaWeibo 一.ListView的图片异步加载 我们都知道对每一个Weibo Item都有用户头像,而且每一条微博还可能带有图片.如果在加载列表的同时加载图片,这样有几个缺点,第一很费事,界面卡住,用户体验很不

Android图片异步加载之Android-Universal-Image-Loader(转)

今天要介绍的是Github上一个使用非常广泛的图片异步加载库Android-Universal-Image-Loader,该项目的功能十分强大,可以说是我见过的目前功能最全.性能最优的图片异步加载解决方案.做Android的同学都知道,Android加载大量图片时,由于系统分配给图片加载的内存大小有限,所以,如果加载图片量非常大的话容易报OOM异常,关于这个异常已经有不少解决方案了,我就不赘述.下面就简要介绍下这个开源项目的主要功能和使用: 一.功能概要 多线程图片加载: 灵活更改ImageLo

Android消息处理机制:源码剖析Handler、Looper,并实现图片异步加载

引言 我们在做 Android 开发时,常常需要实现异步加载图片/网页/其他.事实上,要实现异步加载,就需要实现线程间通信,而在 Android 中结合使用 Handler.Looper.Message 能够让不同的线程通信,完成异步任务.虽然 Android 官方为我们提供了 AsyncTask 类来完成异步任务,但这个类存在许多问题,并不好用,而且,AsyncTask 也是通过 Handler 和 Thread 来实现异步加载的,所以学习这方面的知识是有必要的 本文讲解思路大致如下:绘制 A

Android图片异步加载

原:http://www.cnblogs.com/angeldevil/archive/2012/09/16/2687174.html 相关:https://github.com/nostra13/Android-Universal-Image-Loader 开发Android程序,一般情况下都会有两个操作,图片的异步加载与缓存,而图片的异步加载大都是从网络读取图片(还有生成本地图片缩略图等操作),为了减少网络操作,加快图片加载速度就需要对图片进行缓存,所以网上的好多图片异步加载方法都是与图片的

自己动手写android图片异步加载库

尊重他人劳动成果,转载请说明出处:http://blog.csdn.net/bingospunky/article/details/44344085 接触android有半年了,关于图片异步加载,一直只用别人的框架,虽然特别方便,但是始终未见识到图片异步加载的庐山真面目.最近比较悠闲,研究一些高大上的东西.在这篇文章总结一下我对图片异步加载的一些学习心得. 图片加载最重要的无非就是内存和线程.大家都知道关于内存溢出一般的解决方案就是LruCache,在我的这个demo里我只要使用SoftRefe

自己动手写android图片异步加载库(二)

在<自己动手写android图片异步加载库>系列的第一篇文章中,主要是学习了使用ReferenceQueue来实现一个内存缓存.在这篇文章中主要是介绍在下载很多图片是怎么控制线程和队列.在这版代码里,加入信号量和队列,可以控制下载任务的顺序.可以控制暂停和结束. 代码A:ImageLoader.java /** * 图片加载工具类 * * @author qingtian * @blog http://blog.csdn.net/bingoSpunky */ @SuppressLint(&qu