效果图:
本文旨在提高异步加载的效率。以listview为例,加载大量item时,必须使用异步加载,否则造成滑动卡顿,甚至程序崩溃。
本文主要在三方面提高listview的加载效率:
1.首次启动预加载(首次启动仅加载可见的item);
2.listview滑动停止后才加载可见项;
3.listview滑动时,不进行项加载。
该Demo使用的数据来源于慕课网提供的json数据:https://www.imooc.com/api/teacher?type=4&num=30,主程序的布局非常简单,就一个listview:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:padding="16dp"
tools:context=".MainActivity">
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
listview中item的布局由一个Imagview和一个嵌套的LinearLayout组成,该嵌套的线性布局包含标题和内容这两个Textview:
item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp">
<ImageView
android:id="@+id/icon_item"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:paddingLeft="4dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Title"
android:maxLines="1"
android:textSize="15sp"/>
<TextView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Content"
android:maxLines="3"
android:textSize="10sp"/>
</LinearLayout>
</LinearLayout>
本文使用AsyncTask进行异步处理,AsyncTask是一个抽象类,我们必须写一个子类继承它,在子类中完成具体的业务下载操作。AsyncTask抽象类指定了三个泛型参数类型,这三个泛型类型参数的含义如下:
Params:开始异步任务执行时传入的参数类型,即doInBackground()方法中的参数类型;
Progress:异步任务执行过程中,返回下载进度值的类型,即在doInBackground中调用publishProgress()时传入的参数类型;
Result:异步任务执行完成后,返回的结果类型,即doInBackground()方法的返回值类型。
AsyncTask的基本生命周期过程为:onPreExecute() --> doInBackground() --> onPostExecute()。
(1)onPreExecute():在执行后台下载操作之前调用,运行在主线程中;
(2)doInBackground():核心方法,执行后台下载操作的方法,必须实现的一个方法,运行在子线程中;
(3)onPostExecute():后台下载操作完成后调用,运行在主线程中;
关于想了解更多关于AsyncTask的同学自行网上查找相关资料学习,这里不再赘述。
本文写一个继承AsyncTask的子类NewsAsyncTask ,三个泛型类的参数分别是String(json数据url字符串,如本例中如上面提到慕课网json数据的url),Void(没有用到下载进度值),List(要加载的数据)
class NewsAsyncTask extends AsyncTask<String,Void,List<NewsBeans>>{
@Override
protected List<NewsBeans> doInBackground(String... strings) {
return null;
}
@Override
protected void onPostExecute(List<NewsBeans> list) {
super.onPostExecute(list);
}
}
启动该AsyncTask:
new NewsAsyncTask().execute(NEWS_URL);//NEWS_URL为慕课网提供的json数据url
定义一个实体类表示每一项的三个组件,该实体类的三个变量分别表示图片url,标题,内容:
package mini.org.cachedemo.bean;
public class NewsBeans {
public String mNewsIconUrl;
public String mNewsTitle;
public String mNewsContent;
}
NewsAsyncTask 中的doInBackground回调方法中需要返回一个List,该list存放着每一项内容的NewsBeans对象。
class NewsAsyncTask extends AsyncTask<String,Void,List<NewsBeans>>{
@Override
protected List<NewsBeans> doInBackground(String... strings) {
return getDataJson(strings[0]);
}
@Override
protected void onPostExecute(List<NewsBeans> list) {
super.onPostExecute(list);
}
}
doInBackground该回调函数中的strings[0]得到NewsAsyncTask启动时传入的url,getDataJson该方法通过传入的url获得到listview的数据。
private List<NewsBeans> getDataJson(String url) {
List<NewsBeans> mNewsList = new ArrayList<>();
try {
String jsonString = readStream(new URL(url).openStream());
JSONObject jsonObject;
NewsBeans newsBeans;
try {
jsonObject = new JSONObject(jsonString);
JSONArray jsonArray = jsonObject.getJSONArray("data");
for (int i=0;i<jsonArray.length();i++){
jsonObject = jsonArray.getJSONObject(i);
newsBeans = new NewsBeans();
newsBeans.mNewsIconUrl = jsonObject.getString("picSmall");
newsBeans.mNewsTitle = jsonObject.getString("name");
newsBeans.mNewsContent = jsonObject.getString("description");
mNewsList.add(newsBeans);
}
} catch (JSONException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
return mNewsList;
}
通过readStream方法得到json字符串,然后再获得各个data。readStream方法将输入流转换成字符串:
private String readStream(InputStream is) {
InputStreamReader isr = null;
String result = "";
try {
String line = "";
isr = new InputStreamReader(is,"utf-8");
BufferedReader br = new BufferedReader(isr);
while ((line = br.readLine())!=null){
result += line;
}
return result;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
NewsAsyncTask的onPostExecute回调方法中给listview设置适配器,将数据显示到listview上。
protected void onPostExecute(List<NewsBeans> list) {
super.onPostExecute(list);
NewsAdapter adapter = new NewsAdapter(MainActivity.this,list);
mListView.setAdapter(adapter);
}
public class NewsAdapter extends BaseAdapter{
private List<NewsBeans> mList;
private LayoutInflater inflater;
private ImageLoader imageLoader;
public NewsAdapter(Context context, List<NewsBeans> list){
mList = list;
inflater = LayoutInflater.from(context);
imageLoader = new ImageLoader();
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null){
viewHolder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_layout,null,false);
viewHolder.ivIcon = (ImageView)convertView.findViewById(R.id.icon_item);
viewHolder.tvTitle = (TextView)convertView.findViewById(R.id.title);
viewHolder.tvContent = (TextView)convertView.findViewById(R.id.content);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder)convertView.getTag();
}
viewHolder.ivIcon.setImageResource(R.mipmap.ic_launcher);
String url = mList.get(position).mNewsIconUrl;
viewHolder.ivIcon.setTag(url);//给每个imageview设置tag,避免加载时item间的图片错位
imageLoader.showImageByAsycnTask(viewHolder.ivIcon,url);
viewHolder.tvTitle.setText(mList.get(position).mNewsTitle);
viewHolder.tvContent.setText(mList.get(position).mNewsContent);
return convertView;
}
class ViewHolder{
public TextView tvTitle,tvContent;
public ImageView ivIcon;
}
}
定义一个工具类ImageLoader来异步加载网络图片
public class ImageLoader {
private LruCache<String,Bitmap> mCache;
public ImageLoader(ListView listView){
//获得最大可用内存
int maxMemory = (int)Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory/4;
mCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
//添加到缓存
public void putBitmapToCache(String url,Bitmap bitmap){
Bitmap bm = getBitmapFromCache(url);
if (bm == null) {
mCache.put(url, bitmap);
}
}
//从缓存中读取数据
public Bitmap getBitmapFromCache(String url){
return mCache.get(url);
}
public Bitmap getBitmapByUrl(String url){
Bitmap bitmap;
InputStream is = null;
try {
URL mUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection)mUrl.openConnection();
is = new BufferedInputStream(connection.getInputStream());
bitmap = BitmapFactory.decodeStream(is);
connection.disconnect();
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
public void showImageByAsycnTask(ImageView imageView,String url){
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap != null){
imageView.setImageBitmap(bitmap);
}else {
new NewsAsycnTask(imageView,url).excute(url);
}
}
class NewsAsycnTask extends AsyncTask<String,Void,Bitmap>{
private String mNewsUrl;
private ImageView mImageView;
public NewsAsycnTask(ImageView imageview,String url){
mNewsUrl = url;
mImageView = imageview;
}
@Override
protected Bitmap doInBackground(String... strings) {
String url = strings[0];
Bitmap bitmap = getBitmapByUrl(url);
if (bitmap != null){
putBitmapToCache(url,bitmap);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (imageView.getTag().equals(mUrl)){
imageView.setImageBitmap(bitmap);
}
}
}
}
至此,一个简单的listview异步加载数据就实现了。但不难发现如果listview在加载大量数据时,滑动时体验效果很差,因此还可以再优化下加载方法,一是在listview滑动停止后才加载可见项,二是滑动时不进行加载,三是首次启动时加载可见项。此时listview需要监听OnScrollListener,在onScrollStateChanged回调方法中得到滑动的状态,根据进行加载还是停止加载,从而达到优化的目的。对Adapter需要进行如下改动:
package mini.org.cachedemo.adapter;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import java.util.List;
import mini.org.cachedemo.R;
import mini.org.cachedemo.bean.NewsBeans;
import mini.org.cachedemo.util.ImageLoader;
public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener{
private List<NewsBeans> mList;
private LayoutInflater inflater;
private ImageLoader imageLoader;
private int start,end;
public static String URL[];
private boolean isFirst;
public NewsAdapter(Context context, List<NewsBeans> list, ListView listView){
mList = list;
inflater = LayoutInflater.from(context);
imageLoader = new ImageLoader(listView);
URL = new String[list.size()];
for (int i=0;i<list.size();i++){
URL[i] = list.get(i).mNewsIconUrl;
}
isFirst = true;
listView.setOnScrollListener(this);//监听滑动状态
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null){
viewHolder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_layout,null,false);
viewHolder.ivIcon = (ImageView)convertView.findViewById(R.id.icon_item);
viewHolder.tvTitle = (TextView)convertView.findViewById(R.id.title);
viewHolder.tvContent = (TextView)convertView.findViewById(R.id.content);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder)convertView.getTag();
}
viewHolder.ivIcon.setImageResource(R.mipmap.ic_launcher);
String url = mList.get(position).mNewsIconUrl;
viewHolder.ivIcon.setTag(url);
imageLoader.showImageByAsycnTask(viewHolder.ivIcon,url);
viewHolder.tvTitle.setText(mList.get(position).mNewsTitle);
viewHolder.tvContent.setText(mList.get(position).mNewsContent);
return convertView;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE){//滑动停止时加载数据
imageLoader.loadImages(start,end);
}else{
imageLoader.cancelAllTask();//滑动时取消加载
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
start = firstVisibleItem;
end = firstVisibleItem + visibleItemCount;
if (isFirst && visibleItemCount > 0){//首次加载且可见项目不为0时加载条目
imageLoader.loadImages(start,end);
isFirst = false;
}
}
class ViewHolder{
public TextView tvTitle,tvContent;
public ImageView ivIcon;
}
}
相应的ImageLoader和主程序要进行部分改动:
package mini.org.cachedemo.util;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.ListView;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import mini.org.cachedemo.R;
import mini.org.cachedemo.adapter.NewsAdapter;
public class ImageLoader {
private LruCache<String,Bitmap> mCache;
private Set<NewsAsycnTask> mTasks;//定义一个Set存放加载图片的task
private ListView mListView;
public ImageLoader(ListView listView){
mTasks = new HashSet<>();
mListView = listView;
int maxMemory = (int)Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory/4;
mCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
public void putBitmapToCache(String url,Bitmap bitmap){
Bitmap bm = getBitmapFromCache(url);
if (bm == null) {
mCache.put(url, bitmap);
}
}
public Bitmap getBitmapFromCache(String url){
return mCache.get(url);
}
public void cancelAllTask(){
if (mTasks!=null) {
for (NewsAsycnTask task : mTasks) {
task.cancel(false);
}
}
}
public Bitmap getBitmapByUrl(String url){
Bitmap bitmap;
InputStream is = null;
try {
URL mUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection)mUrl.openConnection();
is = new BufferedInputStream(connection.getInputStream());
bitmap = BitmapFactory.decodeStream(is);
connection.disconnect();
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
public void showImageByAsycnTask(ImageView imageView,String url){
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap != null){
imageView.setImageBitmap(bitmap);
}else {
imageView.setImageResource(R.mipmap.ic_launcher);
}
}
public void loadImages(int start,int end){
for (int i=start;i<end;i++){
String url = NewsAdapter.URL[i];
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap != null){
ImageView imageView = (ImageView)mListView.findViewWithTag(url);
imageView.setImageBitmap(bitmap);
}else {
NewsAsycnTask task = new NewsAsycnTask(url);
task.execute(url);
mTasks.add(task);
}
}
}
class NewsAsycnTask extends AsyncTask<String,Void,Bitmap>{
private String mNewsUrl;
public NewsAsycnTask(String url){
mNewsUrl = url;
}
@Override
protected Bitmap doInBackground(String... strings) {
String url = strings[0];
Bitmap bitmap = getBitmapByUrl(url);
if (bitmap != null){
putBitmapToCache(url,bitmap);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
ImageView imageView = (ImageView)mListView.findViewWithTag(mNewsUrl);
if (imageView!=null && bitmap!=null){
imageView.setImageBitmap(bitmap);
}
mTasks.remove(this);
}
}
}
package mini.org.cachedemo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import mini.org.cachedemo.adapter.NewsAdapter;
import mini.org.cachedemo.bean.NewsBeans;
public class MainActivity extends AppCompatActivity {
private static final String NEWS_URL= "https://www.imooc.com/api/teacher?type=4&num=30";
private ListView mListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView)findViewById(R.id.lv_main);
new NewsAsyncTask().execute(NEWS_URL);
}
class NewsAsyncTask extends AsyncTask<String,Void,List<NewsBeans>>{
@Override
protected List<NewsBeans> doInBackground(String... strings) {
return getDataJson(strings[0]);
}
@Override
protected void onPostExecute(List<NewsBeans> list) {
super.onPostExecute(list);
NewsAdapter adapter = new NewsAdapter(MainActivity.this,list,mListView);
mListView.setAdapter(adapter);
}
}
private List<NewsBeans> getDataJson(String url) {
List<NewsBeans> mNewsList = new ArrayList<>();
try {
String jsonString = readStream(new URL(url).openStream());
JSONObject jsonObject;
NewsBeans newsBeans;
try {
jsonObject = new JSONObject(jsonString);
JSONArray jsonArray = jsonObject.getJSONArray("data");
for (int i=0;i<jsonArray.length();i++){
jsonObject = jsonArray.getJSONObject(i);
newsBeans = new NewsBeans();
newsBeans.mNewsIconUrl = jsonObject.getString("picSmall");
newsBeans.mNewsTitle = jsonObject.getString("name");
newsBeans.mNewsContent = jsonObject.getString("description");
mNewsList.add(newsBeans);
}
} catch (JSONException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
return mNewsList;
}
private String readStream(InputStream is) {
InputStreamReader isr = null;
String result = "";
try {
String line = "";
isr = new InputStreamReader(is,"utf-8");
BufferedReader br = new BufferedReader(isr);
while ((line = br.readLine())!=null){
result += line;
}
return result;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
关于listview的大量数据的异步加载,使用系统提供的AsyncTask进行异步加载,提高加载的效率。另外, 通过一级缓存即LRC机制来优化,这样item再次滑动可见时就没必要再次通过流来获取bitmap,提高了图片显示的效率。