系統自帶短信程序源碼部分分析

這裏並不打算對整個短信源碼進行分析,完全是看了某部分代碼後的自我總結。我從GIT上clone了Conversation(即短信程序)的所有源碼,結果編譯不過。不過這對分析它的源碼並不造成太大的阻礙。

這裏主要對短信主界面的數據和UI的交互角度進行分析,因爲我自己寫的短信程序在加入獲取聯繫人頭像功能後,程序啓動時花費的查詢時間太長。雖然我也覺得系統默認的短信程序,甚至HandcentSMS,啓動時間都不是很快。(大概是我的機器性能太差)

[b]一、代碼結構[/b]

Conversation中整體結構主要包括com.android.mms.data和com.android.mms.ui,如名字所示,大概就是數據處理部分和UI部分。數據部分主要是獲取/緩存聯繫人信息、獲取/緩存會話信息等。

ConversationList類是程序的主activity,派生於ListActivity,就是一個大的列表。此外:
ConversationListAdapter是這個ListView的adapter,派生於CursorAdapter;
ConversationListItem是一個自定義的ViewGroup,派生於RelativeLayout,用於表示會話列表的每一個item;
Conversation表示一個會話數據;Contact表示一個聯繫人;ContactList維護一個聯繫人列表;
RecipientIdCache用於開線程讀取一個特殊的表,該表映射會話數據到聯繫人信息,也就是通過Recipient就可以獲取聯繫人信息。

[b]二、UI結構[/b]

這裏的UI主要就是ConversationList/ConversationListAdapter/ConversationListItem三者之間的交互。

在layout中,conversation_list_item.xml作爲這個ListView(ConversationList)的item定義,直接使用了ConversationListItem這個view:

[code]
<com.android.mms.ui.ConversationListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="@drawable/conversation_item_background_unread"
android:paddingRight="10dip" >
[/code]

這個自定義item最重要的工作,就是將會話數據綁定到UI控件上,例如QuickContactBadge。在ListView的使用中,要綁定數據,還有個方法就是自寫adapter,在構造adapter時就傳入所有數據。但是如你所見,這種方法需要先讀取出所有的數據。

而這個系統自帶的短信程序,則沒有一次讀入。這個自定義item還有個功能就是,作爲一條聯繫人信息的更新監聽器。讀取聯繫人信息是非常慢的,因爲會涉及到幾個表的查詢。在構造這個item時,程序在另一個線程中異步讀取聯繫人信息,而item只有一個聯繫人的簡要信息(電話號碼)。當聯繫人讀出來後,再通知它的監聽器,也就是這個item,然後更新UI顯示。

ConversationListAdapter中只實現了bindView和newView這兩個函數,此外,它作爲listView的AbsListView.RecyclerListener,還實現了onMovedToScrapHeap函數。

關於RecyclerListener,這裏[url=http://liyan3ban.blog.163.com/blog/static/130459062009102364015288/]有篇文章[/url]從源碼級角度分析了下,大概意思就是ListView在處理item時,有個緩存機制。

[b]三、數據與UI的映射[/b]

這部分纔是重要的分析部分,也是我需要學習的部分。
ConversationList的onStart中,開啓了一個異步查詢,查詢所有的會話:

[code]
@Override
protected void onStart() {
super.onStart();

.......
startAsyncQuery();
[/code]

startyAsyncQuery調用了Conversation.startQueryForAll函數,該函數說白了還是調用AsyncQueryHandler.startQuery函數:

[code]
public static void startQueryForAll(AsyncQueryHandler handler, int token) {
handler.cancelOperation(token);
handler.startQuery(token, null, sAllThreadsUri,
ALL_THREADS_PROJECTION, null, null, Conversations.DEFAULT_SORT_ORDER);
}
[/code]

關於如何獲取會話列表,其實就是個SQL的連表查詢,可以參見這裏:[url=http://kevinlynx.iteye.com/blog/857633]獲取短信會話列表[/url]

當查詢完後,android回調到自己實現的AsyncQueryHandler.onQueryComplete,該函數主要就是告訴adapter,we have done!:

[code]
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
switch (token) {
case THREAD_LIST_QUERY_TOKEN:
mListAdapter.changeCursor(cursor);
[/code]

一旦adapter獲得了一個cursor後,就會主動去取得listview的各項數據。以上便是獲取會話列表的大致流程。

接下來看看聯繫人獲取的流程:
adapter獲得數據後,會調用bindView來綁定數據到UI的item:

[code]
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (!(view instanceof ConversationListItem)) {
Log.e(TAG, "Unexpected bound view: " + view);
return;
}

ConversationListItem headerView = (ConversationListItem) view;
Conversation conv = Conversation.from(context, cursor);

ConversationListItemData ch = new ConversationListItemData(context, conv);
headerView.bind(context, ch);
}
[/code]

Conversation.from函數會先檢查Conversation緩存中是否有該cursor對應的數據,沒有的話則會從cursor中取:

[code]
public static Conversation from(Context context, Cursor cursor) {
// First look in the cache for the Conversation and return that one. That way, all the
// people that are looking at the cached copy will get updated when fillFromCursor() is
// called with this cursor.
long threadId = cursor.getLong(ID);
if (threadId > 0) {
Conversation conv = Cache.get(threadId);
if (conv != null) {
fillFromCursor(context, conv, cursor, false); // update the existing conv in-place
return conv;
}
}
Conversation conv = new Conversation(context, cursor, false);
try {
Cache.put(conv);
} catch (IllegalStateException e) {
LogTag.error("Tried to add duplicate Conversation to Cache");
}
return conv;
}

[/code]

然後主要是fillFromCursor函數(如果是創建新的Conversation,其構造函數中也是調用了該函數),該函數就是簡單地從cursor中getXXXX獲取各個數據,並且,最重要的,獲取聯繫人信息:

[code]
private static void fillFromCursor(Context context, Conversation conv,
Cursor c, boolean allowQuery) {

...
ContactList recipients = ContactList.getByIds(recipientIds, allowQuery);
[/code]

注意這裏allowQuery參數爲false。

ContactList.getByIds函數根據Conversation中recipientIds獲取出對應的address,然後根據address從聯繫人URI中進一步獲取聯繫人信息。

[code]
public static ContactList getByIds(String spaceSepIds, boolean canBlock) {
ContactList list = new ContactList();
for (RecipientIdCache.Entry entry : RecipientIdCache.getAddresses(spaceSepIds)) {
if (entry != null && !TextUtils.isEmpty(entry.number)) {
Contact contact = Contact.get(entry.number, canBlock);
contact.setRecipientId(entry.id);
list.add(contact);
}
}
return list;
}
[/code]

最終的contact被生成於Contact.get(entry.number, canBlock)中。該函數在canBlock爲false的情況下,會push一個異步執行體(Runnable)到一個線程中。然後將contact返回。最終返回到adapter那一層的函數。

這個異步查詢線程,會真正地去查詢聯繫人信息。在此之前,外界獲取出來的聯繫人不過是一個很簡單的信息:只有電話號碼。

adapter的bindView中,緊接着:
[code]
ConversationListItemData ch = new ConversationListItemData(context, conv);
headerView.bind(context, ch);
}
[/code]

bind函數中很重要的操作,就是建立該會話對應的聯繫人對象的監聽:

[code]
ContactList contacts = ch.getContacts();

if (DEBUG) Log.v(TAG, "bind: contacts.addListeners " + this);
Contact.addListener(this);
[/code]

以上過程,即展示了會話數據是如何映射到ListView的item,及聯繫人信息是如何與會話和listview item建立聯繫(即異步查詢,然後同步)。

[color=red]
值得一提的是:查詢聯繫人信息都是在bindView時發生,當一個listview被顯示出來後,未顯示的item是不會被觸發bind的,也就是說:在listview顯示時,不會觸發查詢整個會話對應的所有聯繫人,只有顯示出來的item纔會涉及到查詢聯繫人。
[/color]


基於以上,我寫了一個測試程序。結果似乎很好,就像系統自帶的短信程序一樣。啓動速度依然不算快。問題的所在,似乎並不在於查詢時間花費的很長。listview倒是早就顯示出來了(看見了程序標題),但是items卻要花些時間才能顯示。

而且,我的測試程序更爲噁心的是,listview在上下滾動時,會顯得有點卡。經過一番折騰,發現是bindView裏花的時間過長,後來加了Conversation的緩存,甚至去掉了日誌,依然有點卡。不知道有否高手幫我解決下。

測試例子見附件。

[color=red]
1.10.2011 update

之前例子程序中ListView上下滾動時,會顯得有點卡。本來都放棄不管了,結果今天打開eclipse時莫名其妙就想起這事:也許應該用ListActivity!結果一改,還真不卡了。注:系統的短信程序用的就是ListActivity。
[/color]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章