效果圖:
本文旨在提高異步加載的效率。以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,提高了圖片顯示的效率。