前言
忽然,發現,網上的公開資料都是教你怎么繼承一個baseadapter,然后重寫那幾個方法,再調用相關view的 setAdpater()方法, 接着,你的item 就顯示在手機屏幕上了。很少有人關注android adpater模式機制的實現原理,比較深入的也不過是說說adapter getview()中的回收情況。今天把相關的源碼看了一遍,把自己的理解記錄下來。
AdpaterView 概覽
api手冊的說明:An AdapterView is a view whose children are determined by an Adapter.
實際上android里面ListView, GridView, Spinner , Gallery等view都是基於設計模式上的設配器模式實現的,只要熟悉設配器模式的相關知識,就知道如何從源碼里面找到相關的實現線索。
認識AdapterView
要理解listview等的實現,其父類是不得不看。源碼有1200多行。閱讀完AdapterView,能搞明白以下問題
-
響應數據的更改。
(793 - 842)
-
知道點擊view的時候,獲得對應的位置.
(593 - 615)
響應數據的更改
這里假設你已經打開了AdpaterView 的 793 到 842 行。。
在我剛開始用adapterview 的時候,最讓我費勁的就是,為什么我調用adpater 的 notifyDataSetChanged() 就能更新view 的狀態了呢,然后跟調用notifyDataSetInvalidated() 兩者之間又有什么區別呢?以前,找了一下資料,沒找到很詳細的說明,現在從源碼里面找答案的話,就很清晰了。
首先,我們要明白一種設計模式:觀察者設計模式。
我相信你,應該能明白觀察者模式是個什么樣的實現了。。。
AdapterView 之所以能對Adapter 的數據更新進行響應,就是因為其在Adapter上注冊了一個數據觀察者(AdapterDataSetObserver(793 - 842 ))的內部類,所以,我們只要對adpater 狀態的改變發送一個通知,就能讓AdapterView調用相應的方法了。
DataSetObservable 的源碼,記得要把其父類也看了。 https://github.com/android/platformframeworksbase/blob/master/core/java/android/database/DataSetObservable.java
現在我們就能解決我們一開始的疑問notifyDataSetChanged() 與notifyDataSetInvalidated() 具體回到AdapterView 產生什么影響?
我們對比一下onChange()
與 onInvalidated()
方法,就能對比得出,前者會對當前位置的狀態進行同步,而后者會重置所有位置的狀態。從代碼的注釋里面還可以獲取得到更多的信息。
這樣,我們以后調用notifyDataSetChanged()和notifyDataSetInvalidated() 就更加明白會發生什么情況了。
點擊item 怎么能夠獲取到當前的位置
這里假設你已經打開了AdpaterView 的 593 - 615 行。。
對於getPositionForView()
這個方法,你肯定沒用過,要搞明白為什么我們能夠獲取到adapterView 里面item view對應的位置,我們需要看 其直接子類:AbsListView.class
源碼相關:(2130-2197) (2196 - 2279)
這里又用到一種設計模式:委托模式
假設你已經搞懂委托模式的概念,首先我們來看源碼(2130 - 2197)。
從obtainView()
方法名中我們可以知道,這是一個用於生成itemView的方法。把這塊代碼看完,以后,會不會有個疑問呢(先不用管回收那塊)? position 到哪里了?我們可以看到這個方法實際上並沒有對我們的itemview 設置了任何的監聽器,那為什么最后能對我們的itemview的動作進行反應呢?
接下來我們看:源碼(2196 - 2279)
從代碼里面我們可以看出這是一個委托類,對item 的動作進行初始化,以及響應對應的操作,從源碼里面我們可以獲知得到,一個item view 為什么能對click,longclick,select 動作進行響應,然后,通過調用performItemClick()
最終把事件調用到AdapterView(292-303)的performItemClick()
里面的監聽器方法.
如果,你對委托模式不熟的話,要明白這里的話,需要花點時間。
認識 AbsListView 回收機制
長期以來,都有這么一個說法,listview 會自動把不可見的view進行回收,但是長期以來,我都沒看到有人對其回收機制進行分析說明
回收執行者:RecycleBin
我們回到之前看過的AbsListView.class
obtainView()(2130-2197)
你會看到一個
mRecycler
的變量。
接下來,通過搜索我們可以得知這個變量是在(308)進行初始化,這是一個內部類的
RecycleBin的實例(6139 - 6507)
看到這類,我們大致可以知道,這個類是這個absListView 回收機制的實現者。
請 跳轉到(6139)
現在,我們來看一下這個類的注釋,大體的意思這個類是用來幫助復用view的,用2個不同級別的方式進行存儲(The RecycleBin has two levels of storage)(個人感覺描述得挺變扭的,還是看原文好了。。)
- ActiveViews : 一開始顯示在屏幕的view
- ScrapViews: 潛在的一些可以讓adpater 使用的old views。
然后,注釋里面已經說了,ActiveViews 怎么變成 ScrapViews。就注釋提供的信息這里我們有兩個疑問。
- 什么時候產生 ActiveViews。
- 什么時候產生 ScrapViews。
這要把這兩點搞清楚了,整個回收體系也就清楚了。
AbsListView的回收機制具體實現
從RecycleBin類的注釋里面我們獲知,回收機制的第一步就是屏幕的view 放在ActiveViews,然后通過對ActiveViews進行降級變成ScrapViews,然后通過scrapViews 進行view 的復用
通過,一番的檢索,我們在Listview.class(1562行里面找到fillActiveViews()
的調用)。
我們觀察一下Listview.class(1460 - 1713) 看一下layoutChildren()
這個方法是干嘛用的。
當我們看到(1550)行的時候,就會發現了這個回收類的賦值。接下來我們看下 listview是如何利用回收機制:
- 當數據發生改變的時候,把當前的view放到scrapviews里面,否則標記為activeViews(1557 - 1562)
recycleBin.removeSkippedScrap();
移除所有old viewsrecycleBin.scrapActiveViews();
刷新緩存,將當前的ActiveVies 移動到 ScrapViews。
這里干了些事情呢?我們回到(1557 - 1562) 我們可以看到一個變量dataChanged,從單詞的意思我們就可以,這里的優化規則就是基於數據是否有變化,我們通過搜索成員變量mDataChanged
在 (1693) 的時候變成了false 接着我們在makeAndAddView
(1751 - 1775)發現了這個變量的使用。
閱讀(1756 - 1766) 我們可以看到回收機制的第一次使用,如果數據沒有發生改變,通過判斷ActiveViews(這些些view來自(1557 - 1562)) 列表里面有沒有當前 活動view,有的話直接復用已經存在的view。這樣的好處就是直接復用當前已經存在的view,不需要通過adapter.getview()里面獲取子view。
好了,接下來我們來看下makeAndAddView
(1751 - 1775) 是如何通過adapter.getview()
中 獲取到view。我們回到AbsListView.class(2130 - 2194)
在 (2134) 中我們看到一個很神秘的方法scrapView = mRecycler.getTransientStateView(position);
從單詞的意思里面我們可以得知這是獲取一個瞬間狀態的view,這里就有個疑問什么是瞬間狀態的view?通過對源碼的層層分析終於在View 類的 hasTransientState()方法里面找到描述。從描述中我們得知這個方法是用來標記這個view的瞬時狀態,用來告訴app無需關心其保存和恢復。從注釋中,官方告訴我這種具有瞬時狀態的view,用於在view動畫播放等情況中。
那么,我們就可以明白這句話優化的是absListView 的列表動畫.
接着閱讀到一下代碼的時候,我就困惑了
scrapView = mRecycler.getScrapView(position);
從這行代碼里面我們可知,復用的review是跟位置有關的,我們回去在看看(ListView 1557-1563)
if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); }
我們可以發現,實際上這里放進回收類里面的只有當前的顯示的view,並沒有產生當前屏幕沒有的view,但是,實際使用中,當我們進行滾屏的時候,顯示下個view的時候,就已經能發現getView 第二個參數已經不為null了,那實際實現在哪里了,我們通過搜索用到RecycleBin 的方法,找到
layoutChildren()
scrollListItemsBy()
onMeasure()
measureHeightOfChildren()
通過查看
scrollListItemsBy()
我們就能夠明白,當我們進行滾屏的時候,在listview 移除item view 的時候,把移除的item view放進了
recycleBin.addScrapView(last, mFirstPosition+lastIndex);
於是生成下一個view的時候就能夠復用之前的view了,搞清楚這個機制以后我們回到
AbsListView.class(2139 - 2168)
接下來代碼, 解答了我們一個經典的adapter 優化方法的由來
View child; if (scrapView != null) { child = mAdapter.getView(position, scrapView, this);
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
}
} else {
child = mAdapter.getView(position, null, this);
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
實際上所謂的優化,就是通過利用已有產生的View進行復用,減少在Adapter.getView()進行類的實例化操作優化性能。
從某年google io的文檔中我們得知這個回收機制的效率能夠提供listview 300%的效率。
接着我們還明白了
getView(int position, View convertView, ViewGroup parent) 這個三個參數的由來了。
通過,對回收機制的分析,我們可以查看
listview scrollListItemsBy()
的時候應該注意到,實際上不可見的 item 是會被自動移除,那樣為什么當滾動過多的item的時候會發生oom的情況了?
在我們閱讀完整個回收機制的時候,我們會發現回收機制實際上是通過在內存里面緩存view對象,讓listview能夠快速的獲取view使listview的顯示流暢。而導致OOM的問題也出在這里,由於整個回收機制把所有的imageview中的bitmap對象也保存下來,在進行不斷的滑屏操作中,RecycleBin 類越來越大,最終導致OOM 的發生。
當然,根據整個思路,要避免OOM實際上也很簡單,我們只需要在虛擬機中開辟一個內存塊,專門用於保存bitmap對象的 map對象(一般而言用LRU算法實現),所有的imageview的應用都通過整個map 對象進行引用,當這個map對象大於一定程度的時候釋放部分bitmap,這就可以保證RecycleBin在保存這些imageview的時候,而這些imageview里面的bitmap對象時通過一個固定的內存塊里面獲取,只要我們開辟的用於引用的bitmap 的內存塊的大小合理,那樣就永遠也不會發生oom了。
至於其他繼承自AbsListView 的View 其回收機制都一樣。。
感想
花了,幾個小時,把AdapterView 相關源碼看完,大致計算了行數有3w 來行代碼了,當然,不會是一行不漏的看過去。 這里分享一個看源碼的方法。首先,有接口和,抽象類的地方,一定要把所有方法看全,這一塊基本上是屬於要一行不漏的看完。實際上這些接口,和抽象類是我們看源碼重要的索引,那些4,5k行的代碼,實際上,里面的關鍵,都是這些接口,和相應的抽象類的擴展。