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