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

本文介绍如何在Android客户端的ListView中展示包含歌曲图片、名称及演唱者的音乐列表,并详细解析了通过百度音乐API获取数据的过程。文章还讨论了图片加载的优化策略,如多线程处理、图片压缩与缓存技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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: 工作线程加载完图片之后,把图片保存起来.而后把其保存到缓存当中,保存到文件缓存当中.
最后再显示图片:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值