剛出來工作,就負責一個APP的某塊功能的編寫,該功能就是類似微博那樣的界面。微博界面的編寫實際上是非常復雜的,雖然它只是一個ListView,但要想讓這個ListView滑得動,是的,在一些配置低的手機,隨便一個負載內容多的Item,都有可能導致OOM。。。如果只是簡單的為了實現了效果,可以選擇將所有內容都寫在xml文件,如果布局不好的話,就會出現嵌套過多的情況,同樣也會出現OOM的情況。。。就算不會出現OOM的情況,也能滑得動,也會面臨是否能夠滑得快。。。
要想能滑得動,也能滑得快,就要動點腦筋了。
一開始非常簡單的代碼就是這樣:
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, null); holder = new ViewHolder(); …… convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } } /** * ViewHolder */ private static class ViewHolder { ImageView appIcon; TextView appName; TextView appInfo; }
這是谷歌推薦的方式,實際上也解決了很多性能上的問題。
Android原生的ListView原本就做了相應的緩存機制,Recycler。
Recycler的工作原理大致如下:
假設屏幕最多能看到11個item,那么當第1個item滾出屏幕,這個item的View進入RecycleBin中,第12個要出現前,通過 getView從回收站(RecycleBin)中重用這個View,然后設置數據,而不必重新創建一個View。
這樣即使有上萬個Item,inflate的次數最多就是n,也就是一個屏幕能夠容納的個數。
大部分的情況都可以用這樣的代碼解決,但我覺得對於每個Adapter都要寫一個ViewHolder實在是太麻煩了,於是進一步將代碼改寫為這樣:
@Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, null); } ImageView icon = (ImageView)CommonViewHolder.get(convertView, R.id.image_view); }
其中CommonViewHolder的代碼如下:
public class CommonViewHolder { /** * 用於獲取ItemView中的控件 * * @param view ItemView * @param id 要獲取的控件的id * @param <T> 返回的控件的類型 * @return 返回的控件 */ public static <T extends View> T get(View view, int id) { SparseArrayCompat<View> viewHolder = (SparseArrayCompat<View>) view.getTag(); if (viewHolder == null) { viewHolder = new SparseArrayCompat<>(); view.setTag(viewHolder); } View childView = viewHolder.get(id); if (childView == null) { childView = view.findViewById(id); viewHolder.put(id, childView); } return (T) childView; } }
實際上,ViewHolder就是通過setTag方法將相應的控件作為View的屬性保存起來,然后下次使用的時候就可以直接復用。
既然明白了它的原理,就可以對它進行改造,CommonViewHolder是利用SparseArrayCompact存儲控件。SparseArrayCompact是SparseArrayCompact是SparseArray的兼容類,本質上就是類似Map的鍵值對容器,谷歌宣稱它的性能比Map要好,因為內在的算法已經優化了。
這里的工作很簡單,同樣是利用setTag方法保存View中的控件,但是卻把findViewById這樣的工作放在了CommonViewHolder中,這樣就不用每次都要調用findViewById方法了。
為了考慮通用,還使用泛型。
代碼量瞬間就減少了很多,但這時就面臨了一個問題:Item項錯亂了。。。
在快速滑動的時候,圖片加載錯了,仔細調試,就發現是Recycler機制出現了問題。當快速滑動的時候,原本應該開始加載圖片的控件已經滑出屏幕,然后我的圖片加載是異步的,所以圖片就會加載在后面的Item上。
解決這個問題的方法同樣得利用setTag方法:
icon.setTag(imageUrl) .... if(imageUrl.equals(icon.getTag()){ .... }
通過setTag保存ImageView要加載的url信息,然后在下次加載的時候判斷是否相同。
問題解決了,但看到getView方法中為了實現微博這樣承載大量信息的界面,不得不編寫大量的業務代碼,而且這樣復用性很差,因為微博詳情的界面和列表項的界面基本一樣,只是有一些不同而已。如果再寫一個,就有點傻逼了,但如果不寫,getView方法中肯定又要寫更多的判斷。
為了解決復用的問題,就開始組件化。
將微博界面拆成兩個部分:HeaderView和BodyView。HeaderView負責微博作者的基本信息,而BodyView就是微博內容。
這樣,我只要在getView方法中這樣寫:
add(new HeaderView()); ... add(new BodyView()); ...
也就是說,我從一個靜態布局改成動態布局,這樣我在詳情那里也可以復用。
到了這里原本也應該結束了,但我又想要為微博業務編寫測試,但Android中View和業務的代碼是各種糾纏,很難完全脫離View來測試業務。
經過思考和查找資料,我找到了一種方式:ViewModel。
編寫相應的ViewModel作為Controller,就可以將View和業務的代碼解綁:
public class ViewModel{ private String text; ... public void setText(TextView view){ view.setText(text); } }
這樣組件里面的代碼就更少了,它只要聲明好控件然后傳進來就行了,數據的獲取和綁定都在ViewModel這里。
然后我們來寫一個簡單的測試:
ViewModel model = new ViewModel(); Button button = new Button(context); button.setText("你好"); model.changeButtonText(button); assertEquals("我好", button.getText()); public void changeButtonText(Button button){ button.setOnClickListener(new OnClickListener(){ button.setText("我好"); }); }
利用ViewModel,我們可以方便的測試Android種的業務。
這是到目前為止的思考和嘗試,實際上,我認為代碼還會不斷演化下去,現在已經開始出現MVVM的一些思想的應用,到了最后,沒准就會完全演化成MVVM模式。
簡單的代碼,只要不斷思考,慢慢的,所能學到的東西就會變得越來越多,最后甚至超出我們的想象。