异步批量加载网络图片,并使用二级缓存.

来源:互联网 发布:自动伴奏音源软件 编辑:程序博客网 时间:2024/06/04 18:01

1.实际需求

现在要在一个客户端的一个listview上显示一个列表,信息包括歌曲图像,歌曲的名称,和歌曲的演唱者.数据从百度音乐接口获取.

2.具体问题分析和所用的技术

2.1 由于下载图片属于耗时操作,所以应该在工作线程中完成.由于要加载很多的图片,如果加载一个就启动一个线程.这样就会造成启动过多的线程,从而会使得主线程出现卡顿(掉帧)
的现象.针对这种情况,我们可以采用把所有的加载图片的工作放到一个单工作线程中完成,而这个单工作线程通过轮循任务集合的方式批量加载图片.也即是在adapter的getView当中,一旦要显示图片的时候就在那里向任务集合中添加一个任务.而由工作线程去完成这个任务,之后再将当前完成的任务移除.

2.2 图片优化:
图片压缩:
图片国语庞大的时候,我们就需要对图片进行压缩.因为我们的手机不需要显示那么大的图片.
图片缓存:
分为内存缓存和文件缓存两种.
下面通过一个在listview中显示百度音乐接口中热歌榜的例子来说明以上知识的运用.

下面上代码: 一共4个类.然后再解析代码的执行流程,以及各个类的作用:
Music.java -> 封装Music信息的实体类

package com.fioman.my21_asynctaskright;public class Music{    /**     * 歌曲对应的图片,相对比较大.这是一个网络路径     */    private String pic_big;    /**     * 歌曲对应的图片,相对比较小.这是一个网络路径     */    private String pic_small;    /**     * 歌曲的名字     */    private String title;    /**     * 歌曲的演唱者     */    private String author;    public String getPic_big()    {        return pic_big;    }    public void setPic_big(String pic_big)    {        this.pic_big = pic_big;    }    public String getPic_small()    {        return pic_small;    }    public void setPic_small(String pic_small)    {        this.pic_small = pic_small;    }    public String getTitle()    {        return title;    }    public void setTitle(String title)    {        this.title = title;    }    public String getAuthor()    {        return author;    }    public void setAuthor(String author)    {        this.author = author;    }    @Override    public String toString()    {       return "Music [pic_big=" + pic_big+",pic_small="+pic_small       + ", title=" + title +",author=" + author + "]";    }}

MainActivity.java 主Activity类

package com.fioman.my21_asynctaskright;import java.util.ArrayList;import java.util.List;import android.os.Bundle;import android.support.v4.app.Fragment;import android.support.v4.app.FragmentActivity;import android.support.v4.app.FragmentManager;import android.support.v4.app.FragmentPagerAdapter;import android.support.v4.view.ViewPager;public class MainActivity extends FragmentActivity{    private ViewPager vpPages;    private Fragment fragment;    private List<Fragment> fragments;    private FragmentPagerAdapter pageAdapter;    @Override    protected void onCreate(Bundle savedInstanceState)    {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        //初始化控件        vpPages = (ViewPager) findViewById(R.id.vp_music);        //准备要显示的Fragmeng数据源        fragments = new ArrayList<Fragment>();        fragment = new HotMusicFragment();        fragments.add(fragment);        //为ViewPager配置Adapter        pageAdapter = new MainPagerAdapter(getSupportFragmentManager());        vpPages.setAdapter(pageAdapter);    }    class MainPagerAdapter extends FragmentPagerAdapter    {        public MainPagerAdapter(FragmentManager fm)        {            super(fm);        }        @Override        public Fragment getItem(int position)        {            return fragments.get(position);        }        @Override        public int getCount()        {            return fragments.size();        }    }}

HotMusicFragment.java 主界面中要用到的Fragment类

package com.fioman.my21_asynctaskright;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;import java.util.ArrayList;import java.util.List;import org.xmlpull.v1.XmlPullParser;import android.os.AsyncTask;import android.os.Bundle;import android.support.v4.app.Fragment;import android.util.Log;import android.util.Xml;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ListView;/** * 热歌榜的fragment,也即是当主Activity显示这个碎片界面的时候会执行它里面的onCreateView()生命周期方法 */public class HotMusicFragment extends Fragment {    private ListView lsvMusic;    private List<Music> musics =new ArrayList<Music>();    private MusicAdapter adapter;    /**     * 该生命周期方法由系统自动调用,当viewPager需要获取Fragment的view对象时     */    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState)    {        View view = inflater.inflate(R.layout.music_list_fragment, null);        //初始化Fragment中的控件        lsvMusic = (ListView) view.findViewById(R.id.lsv_music);        //调用方法,获取热歌榜的数据.前面20首歌曲        getHotMusicList(0,20);        return view;    }    /**     * 获取新歌榜列表, 需要发送http请求.该操作需要在工作线程中执行.这里采用异步任务的方式.     * @param offset  起始位置     * @param size    下载的歌曲的数量     */    private void getHotMusicList(final int offset, final int size)    {        AsyncTask< String, String, List<Music>> task = new AsyncTask<String, String, List<Music>>()        {            @Override            protected List<Music> doInBackground(String... params)            {                try                {                    //发送http请求,准备url                    String path = "http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.billboard.billList&format=xml&type=2&offset="+offset+"&size="+size;                    URL url = new URL(path);                    //根据url打开获取httpUrlconnection                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();                    //"GET"                    conn.setRequestMethod("GET");                    //获取输入流                    InputStream is = conn.getInputStream();                    //根据输入流解析数据源,获取输入流                    XmlPullParser parser = Xml.newPullParser();                    parser.setInput(is, "utf-8");                    //根据时间类型循环读取xml文件,解析出数据                    int event = parser.getEventType();                    Music music = null;                    while(event != XmlPullParser.END_DOCUMENT)                    {                        switch(event)                        {                            case XmlPullParser.START_TAG:                                String tagName = parser.getName();                                if(tagName.equals("song"))                                {                                    music = new Music();                                    musics.add(music);                                    //Log.i("toLook", music.toString());                                }                                else if(tagName.equals("pic_big"))                                {                                    music.setPic_big(parser.nextText());                                }                                else if(tagName.equals("pic_small"))                                {                                    music.setPic_small(parser.nextText());                                }                                else if(tagName.equals("title"))                                {                                    music.setTitle(parser.nextText());                                }                                else if(tagName.equals("author"))                                {                                    music.setAuthor(parser.nextText());                                }                                break;                        }                        //手动触发下一个事件                        event = parser.next();                    }                    return musics;                }                catch (Exception e)                {                    e.printStackTrace();                }                return null;            }            /**             * 当doInBackground()方法执行完毕之后,在主线程执行的方法             */            @Override            protected void onPostExecute(List<Music> result)            {                Log.i("toLook", result.toString());                /**                 * 更新UI,也即是为ListView配置Adapter                 */                adapter = new MusicAdapter(getActivity(), lsvMusic,result);                lsvMusic.setAdapter(adapter);                Log.i("toLook", "onPostExecute()->执行完毕");            }        };        task.execute();    }}

MusicAdapter.java listview配置的adapter

package com.fioman.my21_asynctaskright;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.lang.ref.SoftReference;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.net.URLConnection;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import javax.crypto.spec.IvParameterSpec;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Bitmap.CompressFormat;import android.graphics.BitmapFactory.Options;import android.os.Handler;import android.os.Message;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.webkit.WebView.FindListener;import android.widget.BaseAdapter;import android.widget.ImageView;import android.widget.ListView;import android.widget.TextView;public class MusicAdapter extends BaseAdapter{    /**     * 声明缓存     */    private Map<String, SoftReference<Bitmap>> cache = new HashMap<String, SoftReference<Bitmap>>();    /**     * 声明用于轮循的任务集合     */    private List<ImageLoaderTask> tasks = new  ArrayList<MusicAdapter.ImageLoaderTask>();    /**     * 声明一个工作线程,用于轮循任务集合     */    private Thread workThread;    private boolean isLoop = true;    /**     * 消息类型,工作线程发送的消息.当其处理完图片之后发送这个消息给主线程.让其更新ImageView     */    private static final int HANDLER_IAMGE_LOADED = 10;    /**     * Handler 用于处理工作线程发送的消息的Handler     */    private Handler handler = new Handler()    {        public void handleMessage(Message msg)         {            switch(msg.what)            {                case HANDLER_IAMGE_LOADED:                    //更新ui,设置对应的图片                    ImageLoaderTask task = (ImageLoaderTask) msg.obj;                    ImageView ivAlbum = (ImageView) lsvMusic.findViewWithTag(task.path);                    if(ivAlbum != null) //找到了对应的ImageView控件                    {                        Bitmap bitmap = task.bitmap;                        if(bitmap != null) //图片加载处理成功                        {                            // 更新UI                            ivAlbum.setImageBitmap(bitmap);                            /**                             * 先将其存入缓存当中                             */                            SoftReference<Bitmap> ref = new SoftReference<Bitmap>(bitmap);                            cache.put(task.path, ref);                            /**                             * 在存入文件当中                             */                            String filepath = task.path.substring(task.path.indexOf("/"));                            File file = new File(context.getCacheDir(),"images"+filepath);                            if(file.exists())                            {                                //存入文件当中                                try                                {                                    FileOutputStream fos  = new FileOutputStream(file);                                    //将一个bitmap压缩到一个文件中的方法,先打开文件输出流.让后调用bitmap.compress()方法即可                                    bitmap.compress(CompressFormat.JPEG, 100, fos);                                } catch (FileNotFoundException e)                                {                                    e.printStackTrace();                                }                            }                        }                    }                    break;            }        }    };    private Context context;    private ListView lsvMusic;    private List<Music> musics ;    private LayoutInflater inflater;    public MusicAdapter(Context context, ListView lsvMusic, List<Music> musics)    {        super();        this.context = context;        this.lsvMusic = lsvMusic;        this.musics = musics;        this.inflater = LayoutInflater.from(context);        this.lsvMusic = lsvMusic;        //在构造方法中创建一个线程,用于轮循任务集合.加载图片        workThread = new Thread()        {            @Override            public void run()            {                while(isLoop)                {                    if(!tasks.isEmpty())                    {                        //获取并移除当前第一个任务                        ImageLoaderTask task = tasks.remove(0);                        String path = task.path;                        //根据网络路径访问网络                        try                        {                            //1.准备URL                            URL url = new URL(path);                            //2.HTTPURLConnection                            HttpURLConnection conn = (HttpURLConnection) url.openConnection();                            //3.设置访问方式为"GET"                            conn.setRequestMethod("GET");                            //4.设置网络输入流                            InputStream is = conn.getInputStream();                            //5.根据这个输入流,按照用户需要的大小进行图片压缩,这里压缩的高和宽分别设定为50,50                            //5.1 从网络输入流中读取byte[],把输入流中的数据,写到字节输出流中                            ByteArrayOutputStream bos = new ByteArrayOutputStream();                            byte[] buffer = new byte[1024*10];//字节缓存                            int length = 0;                            while((length = is.read(buffer))!= -1)                            {                                bos.write(buffer, 0, length);                                bos.flush();                            }                            //5.2 从输出流中得到byte[],描述一个完整的bitmap数据                            byte[] bytes = bos.toByteArray();                            bos.close();                            //5.3 解析bitmap的byte[],获取图片的原始宽和高                            Options opts = new Options();                            //5.3 仅仅加载图片的bounds(边界属性),以计算压缩比例                            opts.inJustDecodeBounds = true;                            BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts);                            int widScale = opts.outWidth / 50;                            int heiScale = opts.outHeight / 50;                            //5.4 压缩比例为宽和高的比例的最大值                            int scale = widScale > heiScale ? widScale : heiScale;                            //5.5根据所获取的压缩比例,再次解析byte[] 获取压缩后的图片                            opts.inJustDecodeBounds = false;                            opts.inSampleSize = scale;                            Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts);                            //封装到ImageLoadTask中                            task.bitmap = bitmap;                            //发送消息给主线程: 将bitmap设置到ImageView当中                            Message msg = new Message();                            msg.what = HANDLER_IAMGE_LOADED;                            msg.obj = task;                            handler.sendMessage(msg);                        } catch (Exception e)                        {                            e.printStackTrace();                        }                    }                    else //集合中没有任务的时候                    {                        //调用wait方法,将会让当前线程进入等待                        synchronized (workThread)                        {                            try                            {                                workThread.wait();                            } catch (InterruptedException e)                            {                                e.printStackTrace();                            }                        }                    }                }            }        };        workThread.start();    }    @Override    public int getCount()    {        return musics.size();    }    @Override    public Music getItem(int position)    {        return musics.get(position);    }    @Override    public long getItemId(int position)    {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent)    {        //准备饺子皮        ViewHolder vHolder;        if(convertView == null)        {            convertView = inflater.inflate(R.layout.music_list_item, null);            vHolder = new ViewHolder();            vHolder.ivAlbum =  (ImageView) convertView.findViewById(R.id.iv_album);            vHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title);            vHolder.tvSinger = (TextView) convertView.findViewById(R.id.tv_author);            convertView.setTag(vHolder);        }        vHolder = (ViewHolder) convertView.getTag();        //准备饺子馅        Music music = musics.get(position);        //包饺子,注意这里要用到handler,因为这里要显示图片的时候.图片还没有,图片只是一个地址.要去加载图片.而图片的加载是一个耗时        //任务,不可能在UI线程中去做.只有异步批量加载图片.        vHolder.tvTitle.setText(music.getTitle());        vHolder.tvSinger.setText(music.getAuthor());        //通过图片的网络路径,下载图片.然后设置图片        //1.先获取图片的网络路径        String picSmallPath = music.getPic_small();        String picBigPath  = music.getPic_big();        //2.把路径设置成ImageView的tag,因为在handler中需要使用这个imageView控件.        vHolder.ivAlbum.setTag(picSmallPath);        //3.先去内存缓存中寻找,是否已经加载过        SoftReference<Bitmap> ref = cache.get(picSmallPath);        Bitmap bitmap = null;        if(ref != null) //前面已经缓存过        {            bitmap = ref.get();            if(bitmap != null) //已经缓存过,并且没有被销毁.直接从缓存中取出图片            {                vHolder.ivAlbum.setImageBitmap(bitmap);                return convertView;            }        }        else  //如果内存中没有缓存过,就去看看文件中是否有缓存.        {            String filename = picSmallPath.substring(picSmallPath.lastIndexOf("/"));            File file = new File(context.getCacheDir(), "images"+filename);            if(file.exists()) //如果文件是存在的就去根据文件解析图片            {                bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); //根据文件路径去加载                vHolder.ivAlbum.setImageBitmap(bitmap);                //把从文件中读取的bitmap存入内存缓存                cache.put(picSmallPath, new SoftReference<Bitmap>(bitmap));                return convertView;            }        }        //如果内存缓存中和文件中都没有的时候,就通过任务轮循的方式去网络中加载        //1.创建ImageLoadTask对象,添加到任务集合中        ImageLoaderTask task = new ImageLoaderTask();        task.path = picSmallPath;        tasks.add(task);        //2.任务集合中有任务了,唤醒工作线程起来干活        synchronized (workThread)        {            workThread.notify();        }        return convertView;    }    class ImageLoaderTask    {        String path;     //图片路径,封装到task任务中,传递给工作线程.让工作线程根据这个路径,而获取一个bitmap格式图片.        Bitmap bitmap;   //根据图片路径下载到的图片    }    class ViewHolder    {        ImageView ivAlbum;        TextView tvTitle;        TextView tvSinger;    }}

下面是3个布局文件:
activity.main

<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.fioman.my21_asycntask.MainActivity"    tools:ignore="HardcodedText" >    <TextView        android:id="@+id/tv_title"        android:layout_width="match_parent"        android:layout_height="50dp"        android:textSize="18sp"        android:textColor="#000000"        android:background="#234de1"        android:gravity="center"        android:text="百度音乐热歌榜" />    <android.support.v4.view.ViewPager        android:id="@+id/vp_music"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_below="@+id/tv_title"        /></RelativeLayout>

music_list_fragment.xml fragmeng布局文件.里面就是放了一个ListView

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent" >    <ListView         android:id="@+id/lsv_music"        android:layout_width="match_parent"        android:layout_height="match_parent"        ></ListView></RelativeLayout>

music_list_item.xml 最后呈现listview的模板资源文件

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent" >    <ListView         android:id="@+id/lsv_music"        android:layout_width="match_parent"        android:layout_height="match_parent"        ></ListView></RelativeLayout>

下面分析一下这个程序的执行流程:
1>程序启动 -> 在主Activity中会加载Fragment,然后Fragment在显示的时候会执行Fragment的
生命周期方法,由android容器来调用:
onCreateView()方法会得到执行:

2>在onCreateView()方法中,我们显示获取一个view对象,根据fragment模板,然后初始化
fragment中的listView控件;
然后调用getHotMusicList(0,20);

3>接着我们看看我们在getHotMusicList(0,20)方法中做了什么事情:
这里采用异步任务的方式:
在doInBackground()方法中我们做了什么:
向网络发送请求,然后解析xml,把从网络上获取的音乐数据封装到音乐实体对象中.

当doInBackground()执行完毕之后:
onPostExcute(List result)方法,会根据doInBackGround() 方法中的返回值
即获取的音乐数据列表,去在主线程中更新adapter
adapter = new MusicAdapter(getActivity(),lsvMusic,result);
lsvMusic.setAdapter(adapter);

4>而在MusicAdapter的构造方法中,我们启动一个工作线程去以轮循的方式去加载图片.
因为图片可能会从网络上获取,这样是耗时的.

5> 分析MusicAdapter中的代码,然后看一下他的执行流程:
首先在setAdapter(adapter)方法中的时候,
首先系统会调用getView方法获取view:
在getView中,我们显示歌曲名称和歌曲的演唱者,很容易.
因为音乐实体类中已经封装过了.但是图片的获取就没那么简单,因为实体类中封装的只是一个
网络路径地址,我们要根据这个网络路径地址去获取这张图片:
5.1>首先是判断内存的缓存是否有这张图片
//3.先去内存缓存中寻找,是否已经加载过
SoftReference ref = cache.get(picSmallPath);
Bitmap bitmap = null;
if(ref != null) //前面已经缓存过
{
bitmap = ref.get();
if(bitmap != null) //已经缓存过,并且没有被销毁.直接从缓存中取出图片
{
vHolder.ivAlbum.setImageBitmap(bitmap);
return convertView;
}
}
5.2 然后是从文件中查看是否之前已经保存过:
String filename = picSmallPath.substring(picSmallPath.lastIndexOf(“/”));
File file = new File(context.getCacheDir(), “images”+filename);
if(file.exists()) //如果文件是存在的就去根据文件解析图片
{
bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); //根据文件路径去加载
vHolder.ivAlbum.setImageBitmap(bitmap);
//把从文件中读取的bitmap存入内存缓存
cache.put(picSmallPath, new SoftReference(bitmap));
return convertView;
}
5.3:如果内存中和文件中都没有,这个时候就要去网络中获取.这个时候采用任务集合的方式轮循获取:
这里根据图片的网络路径创建一个task,然后将task添加到list tasks 集合当中.
然后通知正在等待干活的workThread()起来加载图片了.
5.4: 工作线程加载完图片之后,把图片保存起来.而后把其保存到缓存当中,保存到文件缓存当中.
最后再显示图片:

0 0
原创粉丝点击