fanfoudroid 總體運作流程

引用fanfoudroid 原創的話   地址:http://code.google.com/p/fanfoudroid/wiki/SourceDescription#總體運作流程

說在前面

安能飯否的代碼不算複雜,架構也不算特別優秀。我不打算通過畫類圖或者架構圖的形式來說明。而是嘗試用文字來說清楚代碼的總體結構、相互關係及演化歷史。至於細節,可以直接查看代碼,或者提出疑問。當然有需要改進之處也可以提出來討論,定出更好的方案的話就着手進行改進。

另外,因爲項目中的代碼並非我一個人所爲,所以難免有些不是我自己參與的部分我會解釋不清或者解釋錯誤,也煩請參與者或者有自己見解的也提出自己的見解。

基礎項目的選擇

安能飯否並不是一個從頭編寫的項目。這是我第N次強調這一點了。這一點會導致我們的代碼有若干遷就和不合理之處,這是遺留代碼所帶來的。

當初打算開始做安能飯否的時候,我的第一想法就是找現成的twitter客戶端來改。(因爲之前我也沒有做過完整的android項目,從頭開始有點難度。)於是在google code上找了一下,最初是找到了 andtweet (http://code.google.com/p/andtweet/) 這個項目。這個項目其實做的是很完善的,代碼架構也算是不錯。但是對我來說它太複雜了,改了一個多星期也沒改出一個能用的玩意,反而越改越混亂。最終我不得不放棄了。

然後又找了很多其他的項目,但是要麼功能太過簡單要麼代碼不完整,總之都不如意。最後找到了現在的這個基礎項目 twitta (http://code.google.com/p/twitta/) 。當初選擇這個項目的原因有兩個:第一,它提供了基本完整的功能;第二,它很容易修改。我只花了一個晚上的時間就改出了基本能用的版本。再加上後面的完善和增加功能,大概只用了一個星期就弄出了一個自己可用的東西了。

但是這個項目也不是沒有缺點,實際上它有很多缺點。它的代碼結構並不好,最初的時候所有代碼都堆在一起,而且整體架構也不算很好。不過在開始的時候,我並沒有打算把這個程序做成什麼樣,只是想弄個自己能用的東西出來而已,所以根本就沒有重構代碼的打算,十分遷就的把功能給趕出來了。

一個典型的例子就是當時的twitta只有一個TwitterActivity,同時用於TwitterList和MentionList——因爲在Twitter裏,UserID和UserName是一致的,從消息本身就可以獲知是否mention——不過這個在飯否行不通,於是我就copy了一份TwitterActivity,改了個名字叫MentionActivity,把裏面的原來是twitter的內容都改成mention,就這麼搞出了一個mention功能。

並且當時我對android還有很多不瞭解的地方,也沒有時間去系統的學習,就是在胡改瞎改中慢慢摸索慢慢領悟的。

所以第一個版本,代碼是非常糟糕的。

後來三日坊主給了我一份新的界面。我看了之後相當激動,二話不說就把他拉到了項目裏來。然後我開始有了想好好的做一下這個項目的想法。於是開始較爲認真的對待代碼了。(之所以是“較爲”,是因爲畢竟這是一個業餘項目,時間真的有限)

幾次演化之後,才形成了今天的代碼結構。接下來我會介紹一下目前的代碼結構,並順便介紹一下一些必要的演化史、設計思路、注意點及可改進之處。

代碼結構介紹

包結構

  • com.ch_linghu.fanfoudroid: 總包名,存放Application和各個實際的功能Activity
  • com.ch_linghu.fanfoudroid.data: 交互用的數據封裝
  • com.ch_linghu.fanfoudroid.data.db: 數據庫相關
  • com.ch_linghu.fanfoudroid.ui.base: Activity的基類
  • com.ch_linghu.fanfoudroid.ui.module: 非Activity的UI組件(Menu、ListAdapter,等等)
  • com.ch_linghu.fanfoudroid.service: 後臺提醒等服務相關
  • com.ch_linghu.fanfoudroid.task: 併發任務(AsyncTask封裝)相關
  • com.ch_linghu.fanfoudroid.helper: 各種輔助函數、類
  • com.ch_linghu.fanfoudroid.weibo: API封裝
  • com.ch_linghu.fanfoudroid.http: API封裝所需的網絡操作相關的封裝

其他非com.ch_linghu.fanfoudroid開頭的包屬於第三方包。

Activity結構

目前整個系統,幾乎全部的Activity都派生自BaseActivity(有幾個特殊的下面說)。BaseActivity包含了統一的對登錄狀態的判斷及跳轉、統一的OptionMenu。三日坊主引入新的界面之後,增加了一個WithHeaderActivity,這個Activity定義了Header的各種形式以及Header上各個按鈕的操作。(順便說一句,我認爲這個WithHeaderActivity可以考慮再做一層抽象,以應付越來越多的Header種類。)

從WithHeader往下,所有跟消息相關,都抽象成了TwitterListBaseActivityTwitterCursorBaseActivity。而跟UserList相關的(目前主要就是Follower和Following),就被以類似的手法抽象成了UserListBaseActivity和UserCursorBaseActivity/UserArrayBaseActivity(其實按照TweetList的設計,是沒有必要單獨存在ArrayBase的,直接從ListBase派生就好),UserList的相關工作都是dodo la完成的。dodo la可以補充一下UserList部分的資料。

跟上述兩類無關的頁面,除了下面提到的幾個特殊的,其他都是直接派生自WithHeader。比如DM、Write、WriteDM等。

有幾個特殊的頁面,是BaseActivity體系之外的:

  • LoginActivity: 直接派生自Activity,用於處理登錄事項。
  • PreferencesActivity: 派生自PreferenceActivity,用於處理配置選項。(順便說一句,這兩個名字我覺得太過接近了,是不是把我們的類名字改一下)
  • AboutDialog: 這實際上並不是一個Activity,它只是一個helper類,提供了一個show函數,用於顯示一個基本的關於對話框。我曾經在issue裏寫過一條,AboutDialog誰有空可以重新設計一下。

消息列表抽象類設計

【注】目前對消息的稱呼,在項目代碼中有兩種不同的名字:Status和Tweet,可以考慮統一起來。

在整個系統中,消息列表可以說是最常見的。每個消息列表來源各不相同,有搜索的、有隨便看看的,有提到自己的、有用戶的,它們有些直接來自API,有些來自API和本地緩存。但是它們都有一些共同的操作,比如都可以回覆、轉發、收藏,等等。因此,有必要爲所有的消息列表頁面做一個抽象,並且抽象要能滿足各種需要,但是要讓新頁面的編寫變得比較方便。

基於以上考慮,我們對消息列表頁面做了兩層抽象。

我們做了兩個層次的封裝,第一層是TwitterListBaseActivity,這一層是僅僅假設頁面上有一個Tweets List,那麼我們可以確定需要顯示的消息內容(頭像、ScreenName、消息文本、時間、來源等),但是對於數據來源我們不做任何假設。這個基類可以用於派生各種跟Twitter消息列表有關的頁面。包括那些消息是臨時產生的,不經過數據庫的頁面,比如搜索結果、User timeline之類。

我們爲這個基類定義了一系列抽象函數,交給派生類去實現。而基類本身,利用這些抽象函數進行框架性的操作。

/**
 * 用於指定頁面對應的layout
 */
abstract protected int getLayoutId();

/**
 * 用於指定頁面上的ListView,這個抽象主要是考慮到有一些頁面可能會使用 
 * ListView的擴展類。比如SearchResultActivity,用的就是
 * MyListView(可以自動獲取更多結果)
 */
abstract protected ListView getTweetList();

/**
 * 用於指定ListView對應的Adapter
 */
abstract protected TweetAdapter getTweetAdapter();

/**
 * 完成初始化設置
 * FIXME:這個函數應該是從原始代碼中遺留下來的,主要目的是在對TweetList操作
 * 之前完成一些初始化的工作。
 * 但是我在實際使用的時候感覺這個函數的實現其實不太好寫,因爲時間點控制太
 * 嚴格。考慮是不是可以去掉這個函數,僅僅使用 _onCreate ?
 */
abstract protected void setupState();

/**
 * 指定頁面標題。
 * 目前這個函數沒有被用到。可以考慮去掉,或者把它用起來。
 */
abstract protected String getActivityTitle();

/**
 * 指定是否使用基本菜單。
 * 我們在TwitterListBaseActivity中定義了一套針對每一條消息的基本菜單
 * 主要有:xxx的空間、回覆、熱飯、私信、收藏/取消收藏 操作
 * 這樣可以方便各個派生類的彈出菜單基本保持一致。(派生類可以在這幾項
 * 後面增加更多菜單項,無須把useBasicMenu設置成false)
 * 但是考慮到某些頁面可能不需要這些菜單,或者需要使用完全不同的菜單
 * 我們做了這樣一個函數,如果不需要的話可以讓這個函數返回false,這樣就不會
 * 有任何默認菜單,你可以從頭構建自己的菜單項
 */
abstract protected boolean useBasicMenu();

/**
 * 用於返回指定位置的Tweet數據
 * Position是Item選擇或彈出菜單選擇時傳入的,
 * item在整個List中的絕對位置
 */
abstract protected Tweet getContextItemTweet(int position);

/**
 * 沒有實際用到。我也忘記放在這裏幹嗎用的了。
 * 考慮刪掉
 */
abstract protected void updateTweet(Tweet tweet);

考慮到有不少頁面是直接從數據庫獲得數據,它們的操作幾乎是一致的,僅僅是數據庫的查詢條件不同。我們在TwitterListBaseActivity的基礎上又派生了一個新的抽象類:TwitterCursorBaseActivity,專門用於跟數據庫關聯的tweet list頁面。它從TwitterListBaseActivity派生,對上面提到的那些抽象函數做了自己的實現。然後又給出了自己的抽象函數:

/**
 * 標記全部記錄爲“已讀”
 * “已讀”標記是twitta中引入的概念,但是實際上包括twitta自己,
 * 以及後來我們的代碼,都沒有用到這個標記。
 * 因此,這裏的操作純粹是浪費時間,可以考慮暫時忽略。
 */
abstract protected void markAllRead();

/**
 * 獲得“全部”消息。這個函數實際上就定義了查詢條件
 * 查詢條件的不同影響到列表數據的不同,
 * 這個函數的定義是不同頁面最重要的區別
 */
abstract protected Cursor fetchMessages();

/**
 * 獲得數據類型。
 * 目前我們把全部的消息數據都存放在一張表(StatusTable)中,
 * 通過StatusType來區分。這個函數就要求指定所操作的StatusType
 * 這裏有兩點,第一是名字應該考慮改成getStatusType
 * 第二是要考慮有沒有混合type操作的可能。(目前爲止還沒有)
 */
public abstract int getDatabaseType();

/**
 * 獲得頁面操作的數據的所有者。大部分頁面的所有者都是自己(myself)
 * 但是因爲引入了user profile頁面,因此有些頁面,例如user timeline、
 * fav timeline等,它的所有者可能是其他user,這時就需要用這個函數來指定,
 * 以方便操作。BTW,這個名字似乎也應該修改成更加明確的名字。
 * (目前user timeline沒有從CursorBase繼承,因此只有fav timeline實際用到了)
 */
public abstract String getUserId();

/**
 * 獲得最新一條消息的ID
 */
public abstract String fetchMaxId();

/**
 * 獲得最早一條消息的ID
 */
public abstract String fetchMinId();

/**
 * 把Tweet列表信息存入數據庫
 * Tweet列表信息通常是通過API獲得的
 */
public abstract int addMessages(ArrayList<Tweet> tweets,
                boolean isUnread);

/**
 * 獲得比指定消息更新的消息
 * 主要用於“刷新”操作
 */
public abstract List<Status> getMessageSinceId(String maxId)


/**
 * 獲得比指定消息更早的消息
 * 主要用於“更多”操作
 */
public abstract List<Status> getMoreMessageFromId(String minId)

API的封裝

twitta的API封裝是非常簡陋的。三日坊主給項目帶來了全新的、功能更加完善的API。API在接下來的分離包的工作中被歸到了com.ch_linghu.fanfoudroid.weibocom.ch_linghu.fanfoudroid.http兩個包中。API的封裝工作完全是由三日坊主完成的,使用的實例也可以在代碼中隨處可見。如果有什麼問題可以直接跟三日坊主交流。也希望三日坊主能在這裏補充一下相關的資料及注意點之類。尤其是異常部分的設計,相信接下來會非常用得到。

API和HTTP

com.cn_linghu.fanfoudroid.weibo 的所有API方法都是在使用 com.cn_linghu.fanfoudroid.http 的 httpClient 進行 POST 或 GET 請求.

理想狀態是使用者只需要使用 weibo 類的接口即可, 而無需關心 httpClient 的具體實現.

API異常

所有 httpClient 進行的請求都有可能拋出 HttpException , 其中分兩種:

  • 底層異常: 由底層函數拋出的異常, 可以通過 getCause() 查看具體爲哪種異常, 但一般情況並不需要關注此類注底層異常, 可統一視其爲請求內部異常. 具體原因:
    • URISyntaxException, 由new URI 引發.
    • IOException, 由createMultipartEntity 或 UrlEncodedFormEntity 引起.
    • IOException和ClientProtocolException, 由HttpClient.execute 引發.

#

  • 子類異常: HTTP CODE非200所導致的異常, 此類異常都是 HttpException 子類, 可統一捕捉.
    • HttpRequestException, 通常發生在請求的錯誤,如請求錯誤了 網址導致404等, 拋出此異常, 首先檢查request log, 確認不是人爲錯誤導致請求失敗.
    • HttpAuthException, 通常發生在Auth失敗, 檢查用於驗證登錄的用戶名/密碼/KEY等.
    • HttpRefusedException, 通常發生在服務器接受到請求, 但拒絕請求, 可是多種原因, 具體原因服務器會返回拒絕理由, 調用HttpRefusedException#getError#getMessage查看.
    • HttpServerException, 通常發生在服務器發生錯誤時, 檢查服務器端是否在正常提供服務.
    • HttpException, 其他未知錯誤.

捕捉說明

一般情況下, 使用API方法的時候只會特別關心拋出的少數的幾種異常, 比如是否登錄成功的 HttpAuthException , 是否有權限查看某個用戶信息的HttpRefusedException . 而對於其他的所有異常都並不關係, 不需要傳遞給用戶特別的消息, 只需要告之 "請求失敗" 之類的即可, 然後將錯誤寫入日誌, 由開發人員進行分析.

因此, 默認的異常捕捉基本都是:

  try {
      status = getApi().showStatus(reply_id);
  } catch (HttpException e) {
    Log.e(TAG, e.getMessage(), e);
    return TaskResult.IO_ERROR;
  }

如果需要單獨關注某個異常, 則提前捕捉你所關心的異常:

  try {
    status = getApi().showStatus(reply_id);
  } catch (HttpAuthException auth_e) {
    // 用戶身份驗證失敗

  } catch (HttpRefusedException auth_e) {
    // 可能沒有權限查看該條信息

  } catch (HttpException e) {
    // 其他情況引起的異常

    Log.e(TAG, e.getMessage(), e);
    return TaskResult.IO_ERROR;
  }

數據封裝

系統裏對於數據的封裝,分爲幾層:通過API獲得的json數據,在API封裝層中被直接包裝成Entity結構。這些Entity結構的實現都在com.ch_linghu.fanfoudroid.weibo中,主要包括以下類:

  • Status: 對消息的封裝
  • DirectMessage: 對私信的封裝
  • User: 對用戶信息的封裝
  • Photo: 對消息中照片信息的封裝
  • Trend: 對單個熱門話題的封裝
  • SavedSearch: 對保存搜索結果的封裝
  • Trends: 對熱門話題的封裝(Trend的列表)
  • IDs: 用戶列表的封裝

以上這些數據類封裝是直接針對json數據的,是隻讀的。實際使用中經常會受到限制。因此我們定義了一套純粹的、與數據來源無關的data結構。這些結構存放在com.ch_linghu.fanfoudroid.data中。主要包括:

  • Tweet: 對消息的封裝
  • Dm: 對私信的封裝
  • User: 對用戶信息的封裝

我們從API獲得數據之後,立刻將它們轉換爲data結構,之後在程序中主要是以data結構做操作。有部分較簡單的數據,如Trends、SavedSearch等,是直接用Entity結構進行處理的。

我們可以看到,在程序中對數據的處理來自兩個不同的層次:entity層和data層,並且兩個層次的邏輯意義並沒有明顯區別,完全是實現上的限制。因此,我認爲,在以後的改進中,可以考慮增強entity層,使之可以完全承擔data層的工作,然後消除data層,這樣可以避免不必要的轉換工作,並使得數據處理的邏輯更加清晰。

爲了緩存和持久化,我們還定義了數據庫層,數據庫定義都在com.ch_linghu.fanfoudroid.data.db中,目前我們主要定義了以下表:

  • StatusTable: 消息表。所有的消息都存放在這個表中,除了Status本身的字段外,我們還定義了一些其他的字段以方便操作:
    • status_type: 用於標示消息的類型。注意消息類型是不重疊的,因此同一個ID的消息,在表中可能存在多條,分別具有不同的status_type。
    • is_unread: 用於標示消息的已讀/未讀狀態。目前該字段並沒有被實際使用。
    • owner: 用於標示消息的所有者,以便處理不同用戶的同一個status_type的消息,如favourite timeline。
  • MessageTable: 私信表。用於存放私信。
  • UserInfoTable: 用戶信息表。用於存放用戶信息。
  • StatusDatabase: 提供了DatabaseHelper及Table的操作接口。

並且我們提供了數據庫和data結構相互操作的接口。這些接口都定義在StatusDatabase中。

現在各個表本身的定義已經分離到各個單獨的類中。但是所有表的操作接口仍然都放在StatusDatabase裏,這是不合理的做法,未來應該要把各個接口也分別放到各自的類中。

目前全部的數據表我們都認爲是緩存表,裏面的數據都不是關鍵信息,都是可以消失的。目前Status表的策略是同一個owner、同一個status_type的消息,在無法保證數據連續的情況下,僅保留最新20條。其他表目前沒有采用這一策略,但是也可以這樣做。

目前系統仍然缺乏一個統一的全局數據的封裝,例如登錄用戶的相關擴展信息、全局調試開關、全局配置信息等。目前這些信息分別存放在Preference、TwitterApplication、weibo.Configuration等處,我覺得應該有一個更加統一的處理機制。可以考慮在Preference上進行再次擴展。

Task

AsyncTask是Android提供的異步執行機制,可以方便的將一些工作轉移到新的線程裏去執行,執行過程中或完畢後調用回調函數進行界面更新及其他處理工作。(因爲UI的操作是不能在線程中做的,這不僅僅是Android的限制,也是幾乎所有GUI系統的限制)

在本項目中,需要異步執行的地方很多,基本的Task模式是這樣:

  1. 在doXXX函數中首先判斷是否當前任務是否正在運行。如果是則直接退出,否則新建一個任務並用execute方法啓動之。 順便說一句,現有的邏輯會造成Task被重複new,這裏需要修改。
  2. Task啓動前先執行onPreExecute,然後後臺執行doBackground,並在執行過程中通過publishProgress向主線程報告進度,主線程在onProgressUpdate回調中進行處理。
  3. 執行結束視情況調用onPostExecute或onCancelled。
  4. 在Activity被Destory時,我們要cancel所有正在運行的Task,以防止Task的重入。

爲了統一起見,我們對AsyncTask做了一點薄封裝(GenericTask)。我們主要做了以下兩點工作:

  • 簡化了重載函數。AsyncTask需要重載4個函數,GenericTask只需要重載一個(doBackground)。
  • 將主線程回調函數分離到TaskListener中以便複用。一個Activity中可能需要實現好幾個Task,但是他們調用結束後可能是做相同的界面刷新動作,這樣我們只需要一個TaskListener,掛到不同的Task去,就可以了,無需每個Task寫重複的代碼。關於這一點目前做得並不算好,幾乎每一個Task仍然都有自己獨立的Listener,雖然這樣也可以,但是我覺得一定有可以精簡合併的,可以考慮精簡一下。

現有的封裝並沒有能解決doXXX函數中的重複代碼(判斷當前任務是否正在運行),及Destory時對Task的退出處理。這裏是需要改進的。

【注】實際上Destory時對Task的退出處理是有考慮到的。三日坊主曾經寫過一個TaskManager來嘗試對Task的cancel進行統一處理。不過我認爲那個方案還有可商榷之處,因此目前還沒有大規模使用。

此外,關於Task還有幾個輔助類:

  • TaskParam: 用於Task的信息傳入。
  • TaskResult: 用於Task的返回值。
  • TaskAdapter: 對TaskListener的進一步簡化,爲TaskListener的接口方法做了默認實現,這樣派生自TaskAdapter的類就只重載需要的方法,不需要去實現每一個方法了。
  • TweetCommonTask: 對於單條消息的一些操作實在太常用(目前有Delete和Favourite兩個操作),因此我們做了一個Common實現,免得在各個Activity裏重複實現。

Service

對於Service部分我沒有做仔細的閱讀,這裏基本是直接沿用了twitta的代碼。如果有誰覺得對這塊比較瞭解可以來解釋一下代碼的請幫助更新這一部分。

輔助工具類

這裏定義了項目中大量被使用的工具方法和類。

圖像緩存

圖像緩存由一系列相互關聯的類構成。主要完成對頭像、照片的緩存和處理工作。

首先我們看ImageCache,這是一個圖像緩存的接口,主要用於將URL和Bitmap對應起來。它提供了兩個方法:get和put。put將一個url和一個bitmap關聯起來,get則是獲取指定url對應的bitmap。

ImageCache有兩個派生類:MemoryImageCache和ImageManager。其中MemoryImageCache較爲簡單,它直接使用了HashMap在內存中保存url和bitmap的關聯關係。這裏就不多做解釋了。

ImageManager本身也是一個ImageCache,它也實現了put和get接口,也可以用作圖像緩存,只不過它的圖像是保存成文件的(目前沒有保存到SD卡,而是保存在手機的應用程序私有目錄中)。但是它做的工作比這個要多很多。

ImageCache的put帶有兩個參數:url和bitmap。而ImageManager額外實現了更多的put,有單個url參數的,這個函數可以自動從網絡獲取指定url的圖片然後保存,有指定圖像質量的,這可以壓縮保存圖片。還有指定file、bitmap和quality的,可以用於將bitmap直接以指定quality存入指定文件。它還提供了compressImage和resizeImage兩個輔助函數,用於壓縮和縮放圖像(在WriteActivity的照片上傳部分會用到)。

另外,爲了方便頭像的管理,我們還另外實現了一個ProfileImageCacheManager,這個類專門用於管理頭像。它只提供了一個對外接口:get,這個方法接受兩個參數:url和callback。作用是得到指定url的頭像bitmap。ProfileImageCacheManager的內部使用ImageManager做圖片管理。get方法首先會去ImageManager嘗試獲得對應的圖片。如果這個圖片存在,則直接返回對應的Bitmap,callback被忽略。如果圖片不存在,則會啓動一個Task,從網絡獲取指定URL的圖片,並存入ImageManager中。當Task執行完畢,圖片被正確獲得之後,將調用callback的refresh函數刷新界面。至於如何刷新,由callback自己實現。

Utils

Utils中存放了各種輔助函數。最初是twitta實現的,當初它的作者可能沒有想那麼多,因此Utils裏堆了很多實際上用途各異的東西。後來因爲我們項目的需要,又增加了一些,我認爲這些可以在接下來做一次重構,把它們細分到各個意義更明確的類裏去。這個難度應該不會太大。

Utils中的輔助函數大體可以分成:

  • 字符串輔助函數:
    • isEmpty 判斷字符串是否爲空
  • 日期轉換類函數:
    • parseDateTime -
    • parseDateTimeFromSqlite |-將字符串轉換成日期類
    • parseSearchApiDateTime -
    • getRelativeDate 得到類似“大約xx分鐘前”這樣的字符串
    • getNowTime 獲得當前時間
  • 消息文本處理函數
    • setSimpleTweetText 設置控件顯示純文本的消息
    • setTweetText 設置控件顯示帶格式和跳轉鏈接的消息
    • linkfyxxx函數簇 用於自定義linkfy過濾器,產生需要的鏈接內容。如搜索、用戶名、URL等
  • 圖像處理函數
    • drawableToBitmap 將drawable資源的內容轉換成Bitmap
  • 照片預覽相關處理函數
    • getPhotoPageLink 獲得消息中的照片鏈接
    • getPhotoURL 獲得照片頁面中的圖片地址

總體運作流程


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章