前言:
對于ListView,大家絕對都不會陌生,隻要是做過Android開發的人,哪有不用ListView的呢?
隻要是用過ListView的人,哪有不關心對它性能優化的呢?
關于如何對ListView進行性能優化,不僅是面試中常常會被問到的(我前段時間面試了幾家公司,全部都問到了這個問題了),而且在實際項目中更是非常重要的一環,它甚至在某種程度上決定了使用者是否喜歡接受你的APP。(如果你的清單滑起來很卡,我敢說很多人會直接解除安裝)
網上關于如何對ListView進行性能優化,提出了很多方案。但是我搜過很多資料,卻感覺很多文章都寫得比較模糊,沒有代碼說明,讓我感到很累。要知道能給程式員最直接感官刺激的,當然是代碼啦!!!
一、Listview 性能優化方案
1).複用convertView
在getItemView中,判斷convertView是否為空,如果不為空,可複用。如果couvertview中的view需要添加listerner,代碼一定要在if(convertView==null){}之外。
2).異步加載圖檔
item中如果包含有webimage,那麼最好異步加載
3).快速滑動時不顯示圖檔
當快速滑動清單時(SCROLL_STATE_FLING),item中的圖檔或擷取需要消耗資源的view,可以不顯示出來;而處于其他兩種狀态(SCROLL_STATE_IDLE 和SCROLL_STATE_TOUCH_SCROLL),則将那些view顯示出來
二、實戰講解如何優化ListView
2.1 我們先定義一個ListView
<ListView
android:id="@+id/listview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="#7A7A7A"
android:dividerHeight="10dp"
/>
2.2 然後我們去寫一個網絡請求,擷取網絡的json字元串。
這裡,我們用到xutils架構的httputil,通過它,可以很友善的進行網絡請求。 至于請求的url,我們使用慕課網提供的視訊資料清單接口“http://www.imooc.com/api/teacher?type=4&num=30”。先讓我們看下我寫的一個HTTP請求的工具類:
import android.content.Context;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.RequestParams;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest.HttpMethod;
import com.lidroid.xutils.util.LogUtils;
/**
* 網絡請求工具類
*
* @author lining
*/
public class HttpUtil {
/**
* 請求的根URL位址
*/
public static final String BASE_URL = "http://www.imooc.com/api/teacher?type=4&num=50";
public static void sendRequest(final Context context,
final HttpMethod method, RequestParams params,
final IOAuthCallBack iOAuthCallBack) {
HttpUtils http = new HttpUtils();
http.configCurrentHttpCacheExpiry(1000 * 5);
// 設定逾時時間
http.configTimeout(5 * 1000);
http.configSoTimeout(5 * 1000);
if (method == HttpMethod.GET) {
http.configCurrentHttpCacheExpiry(5000); // 設定緩存5秒,5秒内直接傳回上次成功請求的結果。
}
http.send(method, BASE_URL, params,
new RequestCallBack<String>() {
@Override
public void onStart() {
LogUtils.d(method.name() + " request is onStart.......");
}
@Override
public void onSuccess(ResponseInfo<String> responseInfo) {
LogUtils.d("statusCode:" + responseInfo.statusCode + " ----->" + responseInfo.result);
iOAuthCallBack.getIOAuthCallBack(responseInfo.result);// 利用接口回調資料傳輸
}
@Override
public void onFailure(HttpException error, String msg) {
LogUtils.d("statusCode:" + error.getExceptionCode() + " -----> " + msg);
iOAuthCallBack.getIOAuthCallBack("FF");// 利用接口回調資料傳輸
}
});
}
}
工具類其實并沒有啥特别之處,無非就是利用Xutils架構的HttpUtil發送網絡請求,擷取資料。 方法參數裡,我們加入了一個IOAuthCallBack回調接口,該接口主要使用者在Activity和工具類之間回調請求結果資料。
/**
* 資料請求回調接口
*/
public interface IOAuthCallBack
{
// 成功
public void getIOAuthCallBack(String result);
}
下面,我們Activity發送一個網絡請求,擷取json資料,并回調處理:
private void qryDataFromServer() {
HttpUtil.sendRequest(this, HttpRequest.HttpMethod.GET, null, this);
}
@Override
public void getIOAuthCallBack(String result) {
RspData rspData = GsonUtil.getGson().fromJson(result, RspData.class);
// 更新UI清單
KechengAdapter mAdapter = new KechengAdapter(this, rspData.data);
listview.setAdapter(mAdapter);
}
這裡關于json資料的解析使用的GSON,無啥特别說明之處,把實體類的代碼貼出來看下:
public class RspData {
public String status;
public List<KeCheng> data;
public String msg;
}
public class KeCheng {
public String id;
public String name;
public String picSmall;
public String picBig;
public String description;
public String learner;
}
2.3 有了集合資料之後,去定義BaseAdapter
在此之前,我們先看下list item的布局檔案:list_item_kecheng.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/picBig"
android:layout_width="fill_parent"
android:layout_height="180dp"
android:scaleType="fitXY"
android:src="@mipmap/ic_launcher"
/>
<TextView
android:id="@+id/name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="CSS動畫實用技巧"
android:singleLine="true"
android:padding="10dp"
android:textSize="15sp"
/>
<TextView
android:id="@+id/description"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="教你使用CSS實作驚豔的動畫效果!"
android:textSize="12sp"
android:lines="2"
android:padding="10dp"
/>
</LinearLayout>
接下來,讓我們好好看看Adapter是如何定義的:
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
public class KechengAdapter extends BaseAdapter {
private Context mContext;
private LayoutInflater mInflater;
private List<KeCheng> mDatas;
public KechengAdapter(Context context, List<KeCheng> datas) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
mDatas = datas;
}
@Override
public int getCount() {
return (mDatas != null ? mDatas.size() : 0);
}
@Override
public Object getItem(int position) {
return (mDatas != null ? mDatas.get(position) : null);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item_kecheng, null);
holder = new ViewHolder();
holder.picBig = (ImageView) convertView.findViewById(R.id.picBig);
holder.name = (TextView) convertView.findViewById(R.id.name);
holder.description = (TextView) convertView.findViewById(R.id.description);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
final KeCheng keCheng = mDatas.get(position);
if (keCheng != null) {
ImageLoaderUtil.getInstance().displayListItemImage(keCheng.picBig, holder.picBig);
holder.name.setText(keCheng.name);
holder.description.setText(keCheng.description);
}
return convertView;
}
static class ViewHolder {
ImageView picBig;
TextView name;
TextView description;
}
}
ListView性能優化的重點就是如何去處理BaseAdapter,且看上面的代碼,我們在getView中,判斷convertView是否為空,如果不為空,可複用。如何複用的呢?
我們通過convertview的setTag方法和getTag方法來将我們要顯示的資料來綁定在convertview上。如果convertview 是第一次展示我們就建立新的Holder對象與之綁定,并在最後通過return convertview 傳回,去顯示;如果convertview 是回收來的那麼我們就不必建立新的holder對象,隻需要把原來的綁定的holder取出加上新的資料就行了。
如果couvertview中的view需要添加listerner,代碼一定要在if(convertView==null){}之外。
看代碼夠仔細的人能夠發現有這麼一行代碼,ImageLoaderUtil.getInstance().displayListItemImage(keCheng.picBig, holder.picBig); 這是使用的圖檔異步加載架構Universal-Image-Loader來完成對網絡圖檔的異步加載、緩存,(強烈推薦使用)使用這個開源架構後,我們就無需再為如何加載緩存網絡圖檔煩惱啦!
快随我一起看看如何配置這個架構吧:
import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.cache.memory.impl.UsingFreqLimitedMemoryCache;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
import java.io.File;
/**
* 配置全局的 Android-Universal-Image-Loader
*/
public class ImageLoaderUtil {
private static ImageLoaderUtil instance = null;
private ImageLoader mImageLoader;
// 清單中預設的圖檔
private DisplayImageOptions mListItemOptions;
// 頭像圖檔
private DisplayImageOptions mUserHeadOptions;
private ImageLoaderUtil(Context context) {
mImageLoader = ImageLoader.getInstance();
mListItemOptions = new DisplayImageOptions.Builder()
// 設定圖檔Uri為空或是錯誤的時候顯示的圖檔
.showImageForEmptyUri(R.mipmap.load_default_img)
.showStubImage(R.mipmap.load_default_img)
// 設定圖檔加載/解碼過程中錯誤時候顯示的圖檔
.showImageOnFail(R.mipmap.load_default_img)
// 加載圖檔時會在記憶體、磁盤中加載緩存
.cacheInMemory()
.cacheOnDisc()
.bitmapConfig(Bitmap.Config.RGB_565)
.delayBeforeLoading(300)
.build();
}
public static ImageLoaderUtil getInstance() {
return instance;
}
public synchronized static ImageLoaderUtil init(Context context) {
if (instance == null) {
instance = new ImageLoaderUtil(context);
}
File cacheDir = context.getExternalFilesDir("news/pictures");
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).threadPriority(
Thread.NORM_PRIORITY - 2).denyCacheImageMultipleSizesInMemory()
// .imageDownloader(imageDownloader).imageDecoder(imageDecoder)
.discCacheFileNameGenerator(new Md5FileNameGenerator()).tasksProcessingOrder(QueueProcessingType.LIFO).memoryCacheExtraOptions(
360, 360).memoryCache(new UsingFreqLimitedMemoryCache(4 * 1024 * 1024)).discCache(
new UnlimitedDiscCache(cacheDir)).build();
// Initialize ImageLoader with configuration.
ImageLoader.getInstance().init(config);
return instance;
}
/**
* 清單圖檔
*
* @param uri
* @param imageView
*/
public void displayListItemImage(String uri, ImageView imageView) {
String strUri = (isEmpty(uri) ? "" : uri);
mImageLoader.displayImage(strUri, imageView, mListItemOptions);
}
public ImageLoader getImageLoader() {
return mImageLoader;
}
private boolean isEmpty(String str) {
if (str != null && str.trim().length() > 0 && !str.equalsIgnoreCase("null")) {
return false;
}
return true;
}
}
這是我寫好的一個Universal-Image-Loader的工具類,以後可以直接使用它進行圖檔的下載下傳緩存處理了。 當然在使用前,還需要進行初始化它,我們推薦在Application中對其進行初始化操作:
public class MyApp extends Application {
public static Context context;
@Override
public void onCreate() {
super.onCreate();
context = this;
ImageLoaderUtil.init(context);
}
}
2.4 處理快速滑動時暫停加載圖檔
我們知道,當快速滑動清單時(SCROLL_STATE_FLING),item中的圖檔擷取需要消耗資源的View,可以不顯示出來(因為滑動的過快,我們也不需要看圖檔啊);而處于其他兩種狀态(SCROLL_STATE_IDLE 和SCROLL_STATE_TOUCH_SCROLL),則将那些view顯示出來。
那如何實作呢? 這裡我還是推薦使用Universal-Image-Loader已經為大家封裝好了的方法,(當然,别的架構,如Xutils也封裝了相關的方法)。Universal-Image-Loader架構的com.nostra13.universalimageloader.core.assist.PauseOnScrollListener監聽器已經封裝了對滾動時圖檔處理的監聽,我們隻需要在為ListView元件設定滾動監聽的時候,把PauseOnScrollListener的執行個體傳入即可。這裡,有必要讓大家先看下PauseOnScrollListener的源碼:
public class PauseOnScrollListener implements OnScrollListener {
private ImageLoader imageLoader;
private final boolean pauseOnScroll;
private final boolean pauseOnFling;
private final OnScrollListener externalListener;
public PauseOnScrollListener(ImageLoader imageLoader, boolean pauseOnScroll, boolean pauseOnFling) {
this(imageLoader, pauseOnScroll, pauseOnFling, (OnScrollListener)null);
}
public PauseOnScrollListener(ImageLoader imageLoader, boolean pauseOnScroll, boolean pauseOnFling, OnScrollListener customListener) {
this.imageLoader = imageLoader;
this.pauseOnScroll = pauseOnScroll;
this.pauseOnFling = pauseOnFling;
this.externalListener = customListener;
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch(scrollState) {
case 0:
this.imageLoader.resume();
break;
case 1:
if(this.pauseOnScroll) {
this.imageLoader.pause();
}
break;
case 2:
if(this.pauseOnFling) {
this.imageLoader.pause();
}
}
if(this.externalListener != null) {
this.externalListener.onScrollStateChanged(view, scrollState);
}
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if(this.externalListener != null) {
this.externalListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}
大家可以看到, PauseOnScrollListener實作了OnScrollListener接口,這也就是剛剛為啥說可以把PauseOnScrollListener的執行個體設定到ListView監聽器的原因。PauseOnScrollListener有兩個重要的構造方法,其中參數pauseOnScroll控制我們緩慢滑動ListView,GridView是否停止加載圖檔,pauseOnFling 控制猛的滑動ListView,GridView是否停止加載圖檔。而另一個參數OnScrollListener customListener則可以用于留給開發者繼續回到處理相應的滑動監聽事件,比如清單是否滑動到了最後等等。
知道了如何利用PauseOnScrollListener,那我們在Activity之中隻需要設定一句簡單的監聽代碼即可:
listview.setOnScrollListener(new PauseOnScrollListener(ImageLoaderUtil.getInstance().getImageLoader(), false, true));
如何你的項目需要下來重新整理或者是滑動加載等功能,你又必須提供滑動事件的回調參數:
listview.setOnScrollListener(new PauseOnScrollListener(ImageLoaderUtil.getInstance().getImageLoader(), false, true, onScrollListener));
private AbsListView.OnScrollListener onScrollListener = new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
// 觸摸後滾動
break;
case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
// 滾動狀态
break;
case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
// 空閑狀态
if (view.getLastVisiblePosition() == view.getCount() - 1) {
System.out.println("************滾動到了最後一個***************");
}
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
};
好啦,這樣做出的ListView已經很完美了,讓我們欣賞下它的效果吧:
結束語:
本文主要通過三個方面:1、複用convertView;2、異步加載圖檔; 3、ListView快速滑動時不顯示圖檔介紹了如何對ListView進行性能優化,這是最常見也是最重要的三個方面,建議大家務必将其使用在自己項目的開發中,以提高清單的易用性!
當然,文章還提到了兩個第三方架構的使用:Xutils和Universal-Image-Loader,這是兩個非常使用的架構,建議大家也能學習下。
如果大家還有别的優化方案,建議提出來,共同學習,共同進步。
源碼下載下傳位址:http://download.csdn.net/detail/zuiwuyuan/9055795
Gitub下載下傳位址:https://github.com/zuiwuyuan/ListViewOptimized