Adapter.notifyDataSetChanged與ListView.Post()重新整理問題

porterking發表於2018-02-01

二哈鎮樓

筆者在實際開發中碰到的問題,在這裡記錄一下

描述一下 碰見的問題:在一個listview頁面中,onResume()回來,請求完資料後,對adapter進行notifyDataSetChanged後,需要對item進行ui操作。這時候首先頁面展現的時候,ListView 會getView()一遍,請求完資料notifyDataSetChanged後,又會getView(),這時候直接進行UI操作的話 在主執行緒中 會先執行UI操作,然後在進行getView ,就會導致我的進行UI操作後的item 又被重新getView了一遍。

圖1

##notifyDataSetChanged 非同步機制 首先這個機制問題,不是直接在主執行緒中去更新UI,來看一下里面的程式碼

/**
 * Notifies the attached observers that the underlying data has been changed
 * and any View reflecting the data set should refresh itself.
 */
public void notifyDataSetChanged() {
    mDataSetObservable.notifyChanged();
}
***
繼續進入 DataSetObservable:
***
 /**
 * Invokes {@link DataSetObserver#onChanged} on each observer.
 * Called when the contents of the data set have changed.  The recipient
 * will obtain the new contents the next time it queries the data set.
 */
public void notifyChanged() {
    synchronized(mObservers) {
        // since onChanged() is implemented by the app, it could do anything, including
        // removing itself from {@link mObservers} - and that could cause problems if
        // an iterator is used on the ArrayList {@link mObservers}.
        // to avoid such problems, just march thru the list in the reverse order.
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onChanged();
        }
    }
}
複製程式碼

會呼叫觀察者的onChanged()方法,它的實現方法在DataSetObservable的子類中AdapterDataSetObserver實現。而AdapterDataSetObserver則是在listview.setadapter 時候將AdapterDataSetObserver建立並且繫結,可以看一下setAdapter()方法

/**
 * Sets the data behind this ListView.
 *
 * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
 * depending on the ListView features currently in use. For instance, adding
 * headers and/or footers will cause the adapter to be wrapped.
 *
 * @param adapter The ListAdapter which is responsible for maintaining the
 *        data backing this list and for producing a view to represent an
 *        item in that data set.
 *
 * @see #getAdapter()
 */
@Override
public void setAdapter(ListAdapter adapter) {
    if (mAdapter != null && mDataSetObserver != null) {
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }

    resetList();
    mRecycler.clear();

    if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
        mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
    } else {
        mAdapter = adapter;
    }

    mOldSelectedPosition = INVALID_POSITION;
    mOldSelectedRowId = INVALID_ROW_ID;

    // AbsListView#setAdapter will update choice mode states.
    super.setAdapter(adapter);

    if (mAdapter != null) {
        mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
        mOldItemCount = mItemCount;
        mItemCount = mAdapter.getCount();
        checkFocus();

        mDataSetObserver = new AdapterDataSetObserver();
        mAdapter.registerDataSetObserver(mDataSetObserver);

        mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

        int position;
        if (mStackFromBottom) {
            position = lookForSelectablePosition(mItemCount - 1, false);
        } else {
            position = lookForSelectablePosition(0, true);
        }
        setSelectedPositionInt(position);
        setNextSelectedPositionInt(position);

        if (mItemCount == 0) {
            // Nothing selected
            checkSelectionChanged();
        }
    } else {
        mAreAllItemsSelectable = true;
        checkFocus();
        // Nothing selected
        checkSelectionChanged();
    }

    requestLayout();
}
複製程式碼

再看下AdapterDataSetObserver 中onChanged的實現

class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect the case where a cursor that was previously invalidated has
            // been repopulated with new data.
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            mDataChanged = true;

            if (AdapterView.this.getAdapter().hasStableIds()) {
                // Remember the current state for the case where our hosting activity is being
                // stopped and later restarted
                mInstanceState = AdapterView.this.onSaveInstanceState();
            }

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;
            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;
            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;
            mNeedSync = false;

            checkFocus();
            requestLayout();
        }
複製程式碼

這時候,才會呼叫view的requestLayout()放在主執行緒中重繪頁面。所以notifyDataSetChanged呼叫的時候是非同步呼叫觀察者裡的方法,然後push到主執行緒裡去重新整理UI。因此 notifyDataSetChanged之後直接對UI進行操作的話,view的UI操作會在繪製佇列,才導致問題的關鍵 ##那如何解決問題呢 都只要view中有個方法Post(),將任務push到主執行緒佇列中,意思也是將任務新增到訊息佇列中,保證在UI執行緒執行。從本質上說,它還是依賴於以Handler、Looper、MessageQueue、Message為基礎的非同步訊息處理機制。大家一定多碰到過,在onCreate()裡面去獲取一個view的高寬度時候,往往得到的數值是0;也都知道原因,view要經過onMeasure、onLayout和onDraw三個過程,在onCreate()時候明顯,view 還沒有到onLayout,也就沒有寬度。這時候只要進行view.Post(),將計算高寬的方法放置post裡面,這樣獲取的就沒問題了。可以看下post的原始碼:

public boolean post(Runnable action) {    
  final AttachInfo attachInfo = mAttachInfo;
  if (attachInfo != null) {       
  return attachInfo.mHandler.post(action);
  }    
// Assume that post will succeed later       
  ViewRootImpl.getRunQueue().post(action); 
  return true;
}

複製程式碼

原理就在view.Post()是將任務新增到這個view繪製結束後的訊息佇列中,保證了這個view繪製的優先性。受到了這個啟發,我認為listview的adapter.ontifyDataSetchanged方法也是這樣的道理,親測將重新整理UI操作放在listview.post()中進行操作,這時候列印出來的日誌,就是我UI的操作在getview完成之後進行了。為了防止是因為非同步的先後行,特的將getview次數增加到10次

圖2

最後完美解決問題

相關文章