zl程序教程

您现在的位置是:首页 >  其他

当前栏目

使用 AsyncTask 下载图片,并用LurCache 实现图片缓存(超详细)

下载缓存 实现 详细 图片 AsyncTask 并用 使用
2023-09-27 14:28:04 时间

作者: 夏至

欢迎转载,也请保留这段申明,后附源码:
http://blog.csdn.net/u011418943/article/details/53013130

今天要实现的效果如下所示:
这里写图片描述

分析一下,就是简单的用一个ListView 去加载控件,这个没什么好说,很简单,然后我们获取到幕课网的sdk获取一些数据,加载进来,只不过我们这里的图片采用的是网络下载。
首先是ListView 的Item,这里先上布局 content.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="4dp"
    android:orientation="horizontal" >
    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="74dp"
        android:layout_height="64dp"
        android:src="@drawable/ic_launcher" 
        android:scaleType="fitXY"/>
    <LinearLayout
        android:layout_width="wrap_content"
        android:paddingLeft="5dp"
        android:layout_height="64dp"
        android:orientation="vertical" >
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:ellipsize="end"
            android:text="Tite" 
            android:textSize="18sp"/>
        <TextView
            android:id="@+id/tv_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
             android:paddingLeft="10dp"
            android:maxLines="3"
            android:text="content" />
    </LinearLayout>
</LinearLayout>

然后是对这个Item的一个封装 Content.java:

package com.example.picturelurcache.bean;
public class Content {

    private String tvTitle,tvContent;
    private String ivIcon;
    public String getTvTitle() {
        return tvTitle;
    }
    public void setTvTitle(String tvTitle) {
        this.tvTitle = tvTitle;
    }
    public String getTvContent() {
        return tvContent;
    }
    public void setTvContent(String tvContent) {
        this.tvContent = tvContent;
    }
    public String getIvIcon() {
        return ivIcon;
    }
    public void setIvIcon(String ivIcon) {
        this.ivIcon = ivIcon;
    }
}

我们的住布局 activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.picturelurcache.MainActivity" >
    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
         />
</RelativeLayout>

然后我们让 我们的适配器去继承 BaseAdater:

public class ContentAdapter extends BaseAdapter{
    private List<Content> mData;
    private Context mContext;
    private String mUrl;
    public ContentAdapter(Context context,List<Content> data,String url){
        mContext = context;
        mData = data;

    }

    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return mData.size();
    }
    @Override
    public Object getItem(int arg0) {
        // TODO Auto-generated method stub
        return mData.get(arg0);
    }
    @Override
    public long getItemId(int arg0) {
        // TODO Auto-generated method stub
        return arg0;
    }

    class ViewHolder{
        ImageView ivIcon;
        TextView tvTitle,tvContent;
    }
    @Override
    public View getView(int arg0, View contentView, ViewGroup arg2) {
        // TODO Auto-generated method stub
        ViewHolder viewHolder = null;
        if (contentView == null) {
            contentView = LayoutInflater.from(mContext).inflate(R.layout.content, null);
            viewHolder = new ViewHolder();
            viewHolder.ivIcon = (ImageView) contentView.findViewById(R.id.iv_icon);
            viewHolder.tvTitle = (TextView) contentView.findViewById(R.id.tv_title);
            viewHolder.tvContent = (TextView) contentView.findViewById(R.id.tv_content);
            contentView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) contentView.getTag();
        }

        return contentView;
    }
}

这里的初始化,我们采用的用 List 来传递,因为我们不知道会有多少张,所以用List<> 是比较好的方法。然后是ListView的缓存优化,用到viewholder这个类。

接下来就是我们的主函数,首先数据时从网线下载的,这里我们用到 异步下载 AsyncTask,由于封装得比较好,我们这里就拿出来用了。
url 链接为幕课网的课程链接: http://www.imooc.com/api/teacher?type=4&num=30

所以,异步下载这么写:

class myDowndata extends AsyncTask<String, Integer, String> {
        @Override
        // 在 doInBackground 处理一些耗时的操作
        protected String doInBackground(String... arg0) {
            // TODO Auto-generated method stub
            String string = GetJson.getJsonFromMK(arg0[0]);
            return string;// 传入url,获取json数据;;
        }
        @Override
        protected void onPostExecute(String result) {
            // TODO Auto-generated method stub
            super.onPostExecute(result);

        }
    }

可以看到我们在 doInBackground 处理我们获取的 json 数据,这里从网上获取读取数据,采用 HttpURLConnection 这个类:

public class GetJson {
    public static String getJsonFromMK(String url) {
        StringBuffer sb = null;
        try {
            URL httpUrl = new URL(url);
            HttpURLConnection connection = (HttpURLConnection) httpUrl
                    .openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(5000);

            BufferedReader br = new BufferedReader(new InputStreamReader(
                    connection.getInputStream(), "utf-8"));

            sb = new StringBuffer();
            String string = null;
            while((string = br.readLine())!=null){
                sb.append(string);
            }
            br.close();
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return sb.toString();
    }
}

把返回的 gson 字符串,返回到onPostExecute()这里来处理:
所以,这里我们可以这么写:

protected void onPostExecute(String result) {
            // TODO Auto-generated method stub
            super.onPostExecute(result);
            ContentAdapter adapter = new ContentAdapter(MainActivity.this, disposeGson(result),url);
            mListView.setAdapter(adapter);
        }

其中,disposeGson 为json 数据的处理,采用的是 Gson的框架包,我们需要把图片和文字返回,所以类型也用 List ,具体函数如下:

private List<Content> disposeGson(String json) {
        List<Content> mContents = new ArrayList<Content>();//新建一个 List<Content>
        Gson gson = new Gson();
        java.lang.reflect.Type type = new TypeToken<Root>() {
        }.getType();
        Root root = gson.fromJson(json, type); //解析json
         //
        if (root != null) {
            List<Data> data = root.getData();
            for (int i = 0; i < data.size(); i++) {
                Content content = new Content();  //List.add加入的是new 的新对象,所以要放在这里
                // 把获取到的json 数据导进去
                content.setTvTitle(data.get(i).getName());
                content.setTvContent(data.get(i).getDescription());
                content.setIvIcon(data.get(i).getPicSmall());
                mContents.add(content); //把这个泛型存进来
            }
        }

        return mContents;
    }

为什么这么写,因为返回的json数据格式如下:
这里写图片描述

非常简单,至于数据的封装,就不贴出来了。可以看后面的工程。
ok,那么上面的 ListView 应该这么写:

public View getView(int arg0, View contentView, ViewGroup arg2) {
        // TODO Auto-generated method stub
        ViewHolder viewHolder = null;
        if (contentView == null) {
            contentView = LayoutInflater.from(mContext).inflate(R.layout.content, null);
            viewHolder = new ViewHolder();
            viewHolder.ivIcon = (ImageView) contentView.findViewById(R.id.iv_icon);
            viewHolder.tvTitle = (TextView) contentView.findViewById(R.id.tv_title);
            viewHolder.tvContent = (TextView) contentView.findViewById(R.id.tv_content);
            contentView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) contentView.getTag();
        }



        viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);

        viewHolder.tvTitle.setText(mData.get(arg0).getTvTitle());
        viewHolder.tvContent.setText(mData.get(arg0).getTvContent());

        return contentView;
    }

细心的你可能发现,这里的图片,我们用的是原始图片,而不是从网络下载的。因为图片毕竟不是文字,而且,从json数据中,我们看到图片给的是一个 url 的链接,所以还是需要我们从网络下载的,因此我们新建一个类:
首先,以线程的方式下载:

public class getImageFromHttp {
    private ImageView mImageView;
    private String mUrl;
    private Handler mHandler = new Handler() {
        public void handleMessage(android.os.Message msg) {
                mImageView.setImageBitmap((Bitmap) msg.obj);
        };
    };
    public void showBitmap(ImageView imageView, final String url) {
        mImageView = imageView;
        mUrl = url;
        new Thread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                Message msg = new Message();
                msg.obj = getBitmapFromURL(url);
                mHandler.sendMessage(msg);
                try {
                    Thread.sleep(1000); //延时1s用来延时网络不好的情况
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            }
        }).start();
    }
}

非常简单,然后在我们的ContentAdater 那里添加这一句:

new getImageFromHttp().showBitmap(viewHolder.ivIcon, iconUrl);
viewHolder.tvTitle.setText(mData.get(arg0).getTvTitle());
....

这里写图片描述
发现,已经有我们想要的效果了,但是可以发现有些图片多次下载了很多,这是为什么呢?
因为我们在上面有个viewholder类,用来缓存,避免重复加载的,当我们第一次的是为空的,就新建一个,但ListView 只显示当前页面的子 Item,那么它就会从这个缓存中拿出来用,而拿出来用,它并知道如何区分,所以,这就是造成复用的最大原因。

if (contentView == null)

当它滑动再次滑动,那么被移除当前可视部分的,就会被缓存起来,快速滑动,Item 就会被从缓存中拿出来,但这个时候会重新下载,就会被下载多次,重新来。

所以,这里我们的处理方法就是,加一个 TAG。给一个身份,都一样的采取下载:

String iconUrl = mData.get(arg0).getIvIcon();
viewHolder.ivIcon.setTag(iconUrl);
new getImageFromHttp().showBitmap(viewHolder.ivIcon, iconUrl);
...

完整的getView 如下:

public View getView(int arg0, View contentView, ViewGroup arg2) {
        // TODO Auto-generated method stub
        ViewHolder viewHolder = null;
        if (contentView == null) {
            contentView = LayoutInflater.from(mContext).inflate(R.layout.content, null);
            viewHolder = new ViewHolder();
            viewHolder.ivIcon = (ImageView) contentView.findViewById(R.id.iv_icon);
            viewHolder.tvTitle = (TextView) contentView.findViewById(R.id.tv_title);
            viewHolder.tvContent = (TextView) contentView.findViewById(R.id.tv_content);
            contentView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) contentView.getTag();
        }

        String iconUrl = mData.get(arg0).getIvIcon();
        viewHolder.ivIcon.setTag(iconUrl);
        viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);
        //new getImageFromHttp().showBitmap(viewHolder.ivIcon, iconUrl);
        new getImageFromHttp().showBitmapByAsyncTask(viewHolder.ivIcon, iconUrl);
        viewHolder.tvTitle.setText(mData.get(arg0).getTvTitle());
        viewHolder.tvContent.setText(mData.get(arg0).getTvContent());

        return contentView;
    }

在图片更新下载那里,我们要这样判断:

private Handler mHandler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            if (mImageView.getTag().equals(mUrl)) { // 设置tag 的原因在于 listview
                                                    // 的缓存机制,刷新时没有正确刷新,所以这里使用tag来表示
                mImageView.setImageBitmap((Bitmap) msg.obj);
            }

        };
    };

当然,你也用异步下载的方式:

public void showBitmapByAsyncTask(ImageView imageview,String url){
        new newAsyncTask(imageview,url).execute(url);
    }

    private class newAsyncTask extends AsyncTask<String, Void, Bitmap>{
        public newAsyncTask(ImageView imageView,String url){
            mImageView = imageView;
            mUrl = url;
        }
        @Override
        protected Bitmap doInBackground(String... arg0) {
            // TODO Auto-generated method stub

            return getBitmapFromURL(arg0[0]);
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            // TODO Auto-generated method stub
            super.onPostExecute(result);
            if (mImageView.getTag().equals(mUrl)) {
                mImageView.setImageBitmap(result);
            }

        }

    }

二 使用 lruCache 缓存

当网络比较差,或者说每次都要重新下载那些图片,不仅耗流量,而且用户体验特别差。所以,这个时候,我们就好的办法就是使用缓存的方式。
lruCache 使用非常简单,就是一个 key-value 表。所以首先,我们先创建一个 Cache

private LruCache< String, Bitmap> mCache;//这里的可以对应 url,value对应Bitmap
    public getImageFromHttp(){
        //最大可用内存
        int maxSize = (int) Runtime.getRuntime().maxMemory();
        int memSize = maxSize/8;
        mCache = new LruCache<String,Bitmap>(maxSize){

            protected int sizeOf(String key, Bitmap value) { //默认返回时value 的个数
                //这里我们返回bitmap的大小
                return value.getByteCount();
            }
        };

    }

创建两个方法,用来检查缓存是否已经有value了。

/**
     * 从缓存中获取图片
     * @param key
     * @return
     */
    private Bitmap getBitmapFromCache(String key){
        return mCache.get(key);
    }

    /**
     * 把图片放到缓存
     * @param key
     * @param bitmap
     */
    private void setBitmapToCache(String key,Bitmap bitmap){
        if (getBitmapFromCache(key) == null) { //缓存中没有才创建
            mCache.put(key, bitmap);
        }
    }

我们用asyncTask 来演示一下:

public void showBitmapByAsyncTask(ImageView imageview,String url){
        //如果这个缓存中并没有这个图片,则直接下载,有则用缓存的
        Bitmap bitmap = getBitmapFromCache(url);
        if (bitmap == null) {
            new newAsyncTask(imageview,url).execute(url);
        }else{
            imageview.setImageBitmap(bitmap);
        }

    }
protected Bitmap doInBackground(String... arg0) {
            // TODO Auto-generated method stub
            Bitmap bitmap = getBitmapFromURL(arg0[0]); //从网络下载的图片
            if (bitmap != null) {
                setBitmapToCache(arg0[0], bitmap);
            }
            return bitmap;
        }

上面的思路是:

  • 下载之前判断是否缓存中已经有这张图片了,若有,则直接获取,若无,则下载
  • 当下载的时候,也把我们的图片放到我们的缓存中。

这里的话,我们还要改一下 ContentAdater.java:
这里写图片描述

三、优化

接下来我们继续优化,当我们的listview 比较复杂,或者说内容比较多,需要下载的东西也比较多,这个时候,我们就需要这样处理了,当滑动的时候,不让控件去加载东西,保证流畅度,当滑动完成的时候,才让它去加载任务。所以,我们的ContentAdapter 需要加进来一个listview 来判断它的状态:
这里写图片描述
然后,我们来看它的两个方法:

//可见部分
    public void onScroll(AbsListView arg0, int first, int count, int arg3) {
        // TODO Auto-generated method stub
        mStart = first;
        mEnd = first+count;  
    }
    // 滑动的时候
    public void onScrollStateChanged(AbsListView arg0, int status) {
        //当listview 静止的时候才会去下载
        if (status == SCROLL_STATE_FLING) {
            //加载图片

        }else{  //滑动的时候取消所有的任务。

        }
    }

至于 first 和 count 呢?是 listview 的可见部分,所以,这里也是我们要的。那图片显示怎么搞呢?我们需要在图片下载那里写个方法:

/**
     * 用来下载图片
     * @param start
     * @param end
     */

    public void showImageLoad(int start,int end){

        for (int i = start; i < end; i++) {
            String url = ContentAdapter.mUrlImage[i];
            //如果这个缓存中并没有这个图片,则直接下载,有则用缓存的
            Bitmap bitmap = getBitmapFromCache(url);
            if (bitmap == null) {
                //从showBitmapByAsyncTask中拿回下载的主动权
                newAsyncTask task = new newAsyncTask(url);
                task.execute(url);
                mTasks.add(task); //把task 加进来

            } else {
                adapter = (ContentAdapter) mListView.getAdapter();
                //如果缓冲有,则直接赋值给listview,直接显示
                ImageView imageview  = (ImageView) mListView.findViewWithTag(url);

                if (imageview != null) {
                    imageview.setImageBitmap(bitmap);
                    adapter.notifyDataSetChanged();
                }
            }

        }

    }

思路是什么?就是,我只加载可见的部分,即我们上面的start和end,然后把下载的部分从showBitmapByAsyncTask 中转移到showImageLoad,然后如果缓存中没有这个图片,就重新下载,然后显示,如果有,则直接显示。
图片的显示部分,可以直接通过 listview.findViewWithTag();这个函数,因为上面我们已经对每一个ImageView 设置了Tag,所以这里就可以直接用了,也少了判断。
然后,我们需要用到 adapter.notifyDataSetChanged()来刷新我们的数据。

我们的AsyncTask 也稍微要变一下:

private class newAsyncTask extends AsyncTask<String, Void, Bitmap>{
        public newAsyncTask(String url){
        //  mImageView = imageView;
            adapter = (ContentAdapter) mListView.getAdapter();
            mUrl = url;
        }
        @Override
        protected Bitmap doInBackground(String... arg0) {
            // TODO Auto-generated method stub
            Bitmap bitmap = getBitmapFromURL(arg0[0]);
            if (bitmap != null) {
                setBitmapToCache(arg0[0], bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            // TODO Auto-generated method stub
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
            if (imageView !=null && bitmap!=null) {

                imageView.setImageBitmap(bitmap);
                adapter.notifyDataSetChanged();
            }
        }   
    }

可以看到就 onPostExecute 与上面的不同,其他的都不变。

函数已经写好了,所以ContentAdapter 应该这样写:

//可见部分
    public void onScroll(AbsListView arg0, int first, int count, int arg3) {
        // TODO Auto-generated method stub
        mStart = first;
        mEnd = first+count;  
        if (mfirstStart && arg3>0) {  //第一次不滑动的时候,也加载

            mfirstStart = false;
            mGetImageFromHttp.showImageLoad(mStart, mEnd);

        }
    }
    // 滑动的时候
    public void onScrollStateChanged(AbsListView arg0, int status) {
        //当listview 静止的时候才会去下载
        if (status == SCROLL_STATE_IDLE) {
             mGetImageFromHttp.showImageLoad(mStart, mEnd);

        }else{  //滑动的时候取消所有的任务。
            mGetImageFromHttp.cancelTask();
        }
    }

注意!如果你是用模拟器,记得用鼠标拖动,如果用中间滑轮,是不会刷新的。

附上源码:http://download.csdn.net/detail/u011418943/9672487

不用积分,也希望能帮到大家,谢谢。