最佳實踐(1):安卓開發

Jeffery發表於2015-03-28

這篇文章主要為Futurice公司Android開發者總結的經驗教訓。遵循這些規範可以避免無謂的重複勞動。如果對iOS或Windows Phone平臺的開發感興趣,請檢視《iOS開發最佳實踐》和《Windows客戶端最佳實踐》。

歡迎反饋,但請先閱讀反饋規範

摘要

  • 使用Gradle和Gradle預設的專案結構
  • 將密碼和敏感資料放在gradle.properties中
  • 不要實現自己的HTTP客戶端,使用Volley或者OkHttp庫
  • 使用Jackson庫解析JSON資料
  • 由於65K的方法空間限制,避免使用Guava並使用盡可能少的庫
  • 用Fragment來顯示UI
  • Activity只用來管理Fragment
  • XML也是程式碼,管理好XML程式碼
  • 使用樣式來減少佈局XML程式碼中重複屬性
  • 將樣式寫在多個檔案中,避免把樣式全部寫在單一的大檔案當中
  • 保持colors.xml檔案的簡短乾淨,只定義調色盤
  • 同樣也保持dimens.xml簡短乾淨,只定義通用的常量
  • 避免深層級的ViewGroup
  • 避免客戶端處理WebView要顯示的內容,並且注意記憶體洩露
  • 使用Robolectric進行單元測試,使用Robotium進行連線裝置(UI)的測試
  • 使用Genymotion模擬器
  • 一直使用ProGuard或者DexGuard

Android SDK

Android SDK存放在home目錄或者其他跟應用開發無關的位置。一些IDE在安裝時包含了SDK,這時SDK可能存放在IDE的安裝目錄下。而這是很不好的做法,特別是當你需要升級(或者重新安裝)或更換IDE時。同時也要避免把SDK存放在系統目錄下,否則,當普通使用者(不是root)使用IDE時就需要獲取sudo許可權。

編譯系統

編譯系統首選Gradle。相比於Gradle,Ant更加的侷限並且更加繁瑣。使用Gradle編譯系統可以很簡單的做到:

  • 將應用編譯成不同的版本
  • 完成簡單的類似指令碼的任務
  • 管理和下載依賴
  • 自定義祕鑰倉庫
  • 其他…

Google正積極的開發安卓Gradle外掛,作為新的標準編譯系統。

專案結構

主要有兩個主流的專案結構:舊的Ant專案結構和Eclipse ADT專案結構,較新的Gradle和Android Studio專案結構。當然選擇新的專案結構。如果你的專案正在用舊的專案結構,考慮放棄舊的結構,轉移到新的專案結構下吧。

舊專案結構:

 

新的專案結構:

新舊專案結構最大的不同點是新專案結構更加合理的分開了程式碼集(main, androidTest)。例如,你可以在程式碼集src資料夾下新增’paid’和’free’資料夾,分別用於存放付費版應用程式碼和免費版應用的程式碼。

頂層app資料夾用於將你的應用和其他庫(例如:library-foobar)區分開來。Settings.gradle中儲存了app/build.gradle需要用到的庫的引用。

Gradle配置

普通專案結構。遵循Google安卓Gradle規範
簡單任務。可以用Gradle完成一些簡單任務,而不用特地去寫(shell, Python, Perl等)指令碼。具體參考Gradle文件
密碼。你需要在build.gradle中配置應用發行版本的簽名配置。以下這些情況是需要避免的:

不要這樣做。也許你會在版本控制系統中這樣做。

換一種方式,新建一個gradle.properties檔案,檔案內容如下。注意,不要把Gradle.properties新增到版本控制系統中。

KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789

Gradle會自動匯入gradle.properties檔案,所以你可以在build.gradle中這樣寫:

使用Maven管理專案依賴,而不是直接匯入jar檔案。如果你顯式的匯入jar檔案到專案中,那這些依賴的jar檔案只會是某個固定的版本,例如2.1.1。下載jar檔案並管理更新這種方式笨拙不堪,而Maven完全解決了這個問題,並且,Maven可以整合在安卓Gradle編譯系統中。你可以指定版本的範圍,例如2.2.+,然後Maven就會自動更新到版本範圍內的最新版本。例如:

IDE和文字編輯器

不管用什麼編輯器,它都必須要能夠很好的顯示專案結構。編譯器的選擇看個人喜好,但是編輯器必須要能夠顯示專案結構和編譯。

現在最為推薦的IDE時Android Studio,因為Android Studio由Google開發,最為接近Gradle,預設使用新的專案結構,也終於釋出了beta版,可以說是為Android開發量身定做的IDE。

當然你也可以使用Eclipse ADT,但是需要重新配置,因為Ecplise ADT預設使用舊的專案結構和使用Ant編譯。甚至,可以使用純文字編輯器,比如Vim, Sublime Text, 或者Emacs。如果使用純文字編輯器,就需要在命令列中使用Gradle和adb。如果Eclipse整合Gradle後仍舊不能工作,你可以選擇在命令列中編譯,或者遷移至Android Studio。

不管使用什麼IDE和文字編輯器,確保使用Gradle和新的專案結構來編譯應用程式,同時避免把編譯器的配置檔案新增到版本控制系統當中。例如,避免新增Ant的配置檔案build.xml。還有需要強調的一點,如果你在Ant中更改了編譯配置,不要忘記更新build.gradle,使其能夠完成編譯。另外,對其他的開發者友好一點,不要強迫他們去改變他們的工具的偏好設定。

Jackson是一個用於將物件轉換成JSON或者將JSON轉換成物件的Java庫。為了解決JSON和物件相互轉換的問題,Gson是一個受歡迎的選擇。但是我們發現,自從Jackson支援多種JSON處理方式:流,記憶體中的樹模型和傳統的JSON-POJO資料繫結,Jackson更加高效。請記住,Jackson是一個比GSON大的庫,所以請根據你自己的實際情況做出選擇。考慮到65K的方法空間限制,你可能會偏向於選擇GSON。其他選擇:Json-smartBoon JSON

網路,快取和圖片。現在已經有許多經過實踐證明的向後端伺服器請求資料的解決方案。你應該考慮使用這些解決方案來實現自己的客戶端。使用Volley或者Retrofit。Volley也提供了載入和快取圖片的幫助類。如果你選擇Retrofit,考慮使用Picasso來載入和快取圖片,使用OkHttp來實現高效的HTTP請求。Retrofit,Picasso和OkHttp都由同一個公司實現,所以這三者契合的特別好。OkHttp也可以和Volley配套使用。

RxJava是一個用於響應式程式設計的庫,也即是,處理非同步事件的庫。RxJava這個範例非常強大,而且前途光明。RxJava非常與眾不同,因此使用RxJava時可能會令人迷惑。我們推薦在把RxJava部署到整個應用前先花一些時間瞭解RxJava。現在已經有一些專案是利用RxJava來完成的,如果你需要幫助,請向這些人詢問:Timo Tuominen, Olli Salonen, Andre Medeiros, Mark Voit, Antti Lammi, Vera Izrailit, Juha Ristolainen。另外,我們也寫了一些部落格:[1], [2], [3], [4].

如果你沒有使用Rx的經驗,請從應用Rx的響應API開始。或者,從應用Rx的UI事件處理開始,比如點選事件或者在搜尋框中的鍵盤事件。如果你對使用Rx很有信心,想要把Rx應用到整個應用程式當中,請在比較難處理、容易令人迷惑的部分寫明Javadocs。記住,其他不熟悉RxJava的程式設計師維護專案時可能會非常困難。請盡力去幫助他去理解你的程式碼和Rx。

Retrolambda是一個在Android平臺或者其他低於JDK8的平臺上處理Lambda表示式語法的Java庫。利用這個庫,可以保持你的程式碼的整潔嚴謹並且具有可讀性,特別是當你使用了函數語言程式設計風格(functional style),例如使用了RxJava。使用前,先安裝JDK8,在Android Studio專案結構對話方塊中將它設定為你的SDK路徑,設定JAVA8_HOME和JAVA7_HOME環境變數,然後在專案根目錄下build.gradle中增加以下內容:

然後在每一個模組下的build.gradle中,增加以下內容:

Android Studio支援對Java8的lambda智慧提示。如果你是第一次使用lambda,從以下兩條規則開始:

  • 所有隻有一個方法的介面都是「lambda友好」的,能夠被轉換成更加整潔嚴謹的語法。
  • 如果你不確定引數或者其他資訊,寫一個普通的匿名內部類,然後讓Android Studio將它轉換成一個lamdba表示式。

請注意dex方法限制,避免使用過多的庫。被打包成dex檔案的安卓應用,都有一個硬性的限制:最多能有65536個方法引用[1] [2] [3]。如果你超出了這個限制,在編譯的時候你就會看到一個嚴重的編譯錯誤。因此,使用盡量少的庫,並使用dex-method-counts工具來決定在保證不超出限制的前提下,有哪些庫可以使用。特別要避免使用Guava庫,因為它包含了超過13k個方法。

在Android應用開發中,首選Fragment來顯示UI。Fragment是可重用的使用者互動介面,並且可以將Fragment組合在一起。我們推薦使用Fragment來顯示使用者互動介面,而不是使用Activity。以下是一些理由:

  •  實現多檢視佈局。將手機應用擴充套件至平板的主要方法便是利用Fragment。利用Fragment,可以讓檢視A和B都顯示在一個平板螢幕上,而在手機螢幕上,檢視A和B都佔一整塊螢幕。如果你的應用從一開始就用Frament來實現,那麼你很容易就能將你的應用適配到螢幕大小不同的裝置上。
  • 屏與屏之間的通訊。 安卓API並沒有提供一個恰當的方法將複雜的資料(例如,一些Java物件)從一個Activity傳送到另外一個Activity中。但是利用Fragment時,以activity例項為通訊管道,可以實現該activity下的子fragment之間的通訊。即使這種方法優於Activity之間的通訊,你可能仍舊需要一個事件匯流排的架構,考慮使用Otto或者greenrobot EventBus
  • Fragment有更好的普適性,而不僅僅只是實現UI。你可以實現一個沒有UI的fragment,作為activity後臺執行的「工人」。你也可以將這個點子發揮的更淋漓盡致一點,比如建立一個fragment專門用於實現fragment的改變邏輯,而不是將這些邏輯寫在activity中。
  • 甚至ActionBar也可以在fragment中管理。你可以建立一個沒有UI的fragment,只用於管理ActionBar,或者在每一個當前可見的fragment中把自己需要的action項新增到父activity的ActionBar上。閱讀更多內容

雖然我們建議使用fragment,但是我們不建議大量使用巢狀的fragment,因為可能會引起“套娃式bug”(matryoshka bugs)。只在合理的情況下(例如,水平滑動的ViewPager中的fragment巢狀在一個模擬螢幕的fragment中)或者經過深思熟慮時,才使用巢狀的fragment。

從架構層面來講,你的應用應該有一個頂層的activity,其中activity中包含了大部分的業務相關的fragment。你也可以有其他的輔助activity,只要這些activity和主activity的通訊足夠簡單,能夠通過Intent.setData()或者Intent.setAction()或者其他簡單的方式實現即可。

Java包結構

Android應用程式的Java包結構可以用基本上近似於模型-檢視-控制器結構。對於Android,Fragment和Activity實際上就是控制類。同時,這兩者也是使用者互動介面的一部分,因此,這兩者也是檢視。

由於上述原因,將fragment(或者activity)嚴格的歸類為控制器或者是檢視是非常困難,不合理的。所以,更合理的做法是把fragment存放在專有的fragment包內。如果你遵循了前一部分的建議,那麼可以將activity存放在最頂層的包下。如果你計劃建立多於2個或3個activity,那麼建立一個activities包。

否則(譯者注:如果沒有fragment和activity),包結構看起來就是一個典型的MVC結構。有一個models包,存放主要用於JSON解析時API返回值的POJO物件;一個views包,存放你自定義的檢視,通知,action bar檢視和小部件等。Adapter的歸類比較模糊,是處於資料和檢視之間的位置。但是,一般情況下,adapter需要在getView()函式中引入一些檢視,所以可以在views包下建一個adapters包來存放adpater。

一些控制類是整個應用程式都需要使用到的,也更加接近安卓系統底層。這些控制類存放在managers包下。各種資料處理類,例如「DateUtils」,存放在utils包下。負責與後端伺服器進行互動的類存放在network包下。

總之,按靠近後端伺服器到靠近使用者的順序排列,包結構如下:

 資源

命名。遵循以型別作為字首的習慣,像type_foo_bar.xml。例如:fragment_contact_details.xml,view_primary_button.xml,activity_main.xml。

管理好佈局XML程式碼。如果你不確定如何按照一定的格式來管理XML,可以參考以下幾個習慣:

  • 一個屬性佔單獨的一行,縮排4個空格
  • android:id總是第一個屬性
  • android:layout_****屬性放在頂部
  • style屬性放在底部
  • 標籤關閉/>獨佔一行,便於調整屬性的順序和增加屬性
  • 不要在android:text中硬編碼字串,考慮使用Android Studio中提供的Designtime attributes功能

最重要的規則是,在佈局XML中定義android:layout_****屬性,而其他的android:****屬性則在樣式XML中定義。這個規則有例外的情況,但是大部分情況下是適用的。這個規則保證只有layout屬性(positioning, margin, sizing)和內容屬性在佈局檔案中,其他的外觀屬性(colors, padding, font)則定義在樣式檔案中。

例外的情況有:

  • android:id顯然應該在佈局檔案中定義
  • LinearLayout的android:orientation屬性在佈局檔案中定義更為合理
  • android:text應該在佈局檔案中定義,因為它定義了特定的內容(譯者注:屬於內容屬性)
  • 有時候建立通用的樣式檔案來定義android:layout_width和android:layout_height更加合理,但是一般情況下這兩個屬性應該在佈局檔案中定義。

使用樣式。在專案中,重複的view的外觀(譯者注:重複的view屬性)是很常見的,因此,基本上每個專案都需要恰當的使用樣式。在一個應用程式中,至少應該有一個通用的文字內容的樣式。例如:

應用到TextView當中如下:

你也可能需要給button按鈕寫一個通用的樣式, 不過不要只停留在給文字內容和按鈕寫通用樣式上。繼續的深入應用這個思想,把View的相關的重複的屬性寫成通用的樣式。

把大的樣式檔案分成多個小樣式檔案。你不一定非得只有一個styles.xml檔案。Android SDK支援以非傳統方式命名的樣式檔案。檔名styles並沒有特別的作用,起作用的只是檔案中的XML標籤<style>。因此,一個專案中可以同時有這些樣式檔案styles.xml, styles_home.xml, styles_item_details.xml, styles_forms.xml。不像資源目錄名那樣在編譯時有特殊意義,在res/values下的檔名是任意的。

colors.xml是顏色調色盤。colors.xml中應該只包含一些顏色名字到RGBA顏色值的對映。不要在colors.xml中為不同的按鈕定義不同的顏色。

不要像下面這樣做:

如果你像上面這種形式來定義顏色,你很快便開始定義重複的RGBA顏色值。這種情況下,需要改變基礎色值時,工作將會變得非常複雜。並且,這些顏色定義跟上下文有關,像”button”和”comment”這些,應該在按鈕的樣式檔案中定義,而不是在colors.xml中定義。

你可以這樣做:

像應用程式的設計者要這份顏色調色盤。名字不一定非得是顏色的名字,例如”green”, “blue”等。像”brand_primary”, “brand_secondary”, “brand_negative”這中型別的名字也是完全可以接受的。以這種格式來管理顏色,在改變顏色值的時候會很方便,同時也可以很直觀的看到使用了多少個不同的顏色。如果要展現一個漂亮的UI介面,減少顏色種類的使用是很重要的一點。

像管理colors.xml那樣來管理dimens.xml。同樣,你可以定義間隔,字型大小等屬性的”調色盤”,理由和管理顏色的理由一樣。下面是dimens檔案的一個好樣例:

你應該使用(譯者注:dimens檔案中定義的)spacing_****尺寸來實現檢視佈局的margin和padding屬性,而不是在佈局檔案中硬編碼屬性值,這一點很像字串的一般處理方式。這會使應用保持一致的觀感,同時在管理和更改樣式和佈局時也更加方便。

避免深層級檢視。有時候,你想要在原有的檢視xml中新增一個新的LinearLayout,以此來實現一個新的檢視。那麼,很有可能發生下面的情況:

即使你沒有直接在一個佈局檔案中看到上述的情況,當你在把一個檢視填充(在Java程式碼中)到另一個檢視中時,上述的情況也有可能發生。

這可能引發一系列的問題。可能會有效能問題,因為在這種情況下,處理器需要處理非常複雜的UI樹。另外一個更嚴重的錯誤是棧溢位錯誤

因此,儘可能的減少檢視的層級:學習如何使用RelativeLayout,如何優化佈局和如如何使用<merge>標籤

謹慎處理與WebView相關的問題。當你必須顯示一個網頁時,例如一篇新聞,不要在客戶端中處理HTML,更好的做法是向後端程式設計師請求”純淨”的HTML程式碼。當你把WebView繫結到activity上,而不是繫結到ApplicationContext上時,WebView也可能會洩露記憶體。不要使用WebView來展現簡單文字或者按鈕,用TextView和Button來實現。

 測試框架

Android SDK提供的測試框架仍舊不夠完善,特別是UI測試。Android Gradle現在利用一個為安卓定製的JUnit幫助工具外掛,實現了一個測試框架connectedAndroidTest來執行你建立的JUnit測試。也就是說,在進行測試時,你需要連線裝置或者模擬器。請根據官方的測試指南[1] [2]來操作。

只用Robolectric來單元測試,不用於檢視UI測試。為了保證開發速度,Robolectric這個測試框架致力於提供不連線裝置時的測試,也即是適合於對模型和檢視模型的單元測試。但是,在Robolectric的框架下測試UI是不準確,不完全的。在測試和動畫,對話方塊相關的UI元素時,你可能會遇到一些問題。由此,你”墜入了深淵”(測試過程中看不到控制螢幕),這使測試變得非常複雜。

Robotium讓寫UI測試變得非常容易。在Robotium測試框架下測試UI,你不需要進行連線裝置的測試,但是利用Robotium提供的大量的幫助工具,你可以非常方便的分析檢視UI和控制螢幕。測試用例也非常簡單,以下是一個例子:

模擬器

如果你以開發安卓應用為職業,那麼買一個正版的Genymotion模擬器吧。相比於AVD模擬器,Genymotion模擬器具有更高的幀率。它提供了一些工具來演示你的應用,模擬網路連線質量,GPS定位等。當然,Genymotion也適合於進行連線裝置的測試。(譯者注:為了全面的測試)你需要買很多(但不是全部)不同的裝置,因此花錢買一個正版的Genymotion模擬器會比買很多物理裝置便宜很多。

注意:Genymotion模擬器不會實現所有的谷歌服務,例如Google Play商店和地圖。如果你需要測試三星獨有的API,那還是有必要買一個三星的裝置。

Proguard配置

一般情況下,ProGuard用於縮減和混淆安卓專案的打包程式碼。

是否使用Proguard取決於你的專案配置。大部分情況下,當你編譯一個發行版本的apk時,你需要配置gradle來執行ProGuard。

為了判斷要保留哪些程式碼,忽略或者混淆哪些程式碼,你必須明確的指出一個或者多個程式碼入口。 這些程式碼入口一般為包含有main函式的類,Java小程式(applet),移動資訊裝置小程式(Midlet),activity等。你在SDK_HOME/tools/proguard/proguard-android.txt可以找到安卓框架提供的預設配置。每個專案在my-project/app/proguard-rules.pro中自定義的proguard規則,(譯者注:執行proguard時)會被附加到預設配置上。

有一個跟ProGuard相關的常見問題,在應用程式啟動時因為ClassNotFoundException或者NoSuchFieldException或者類似的異常而崩潰,即使你在編譯命令列(例如,assmbleRelease)中成功的完成編譯並且沒有warning提示。不外乎以下兩種情況:

  1. ProGuard認為一些類,列舉,方法,變數或者註解不需要,將其移除了。
  2. ProGuard混淆了類,列舉,或者變數,但是這些類可能被通過它原來的名字間接地呼叫了,例如,通過Java的反射機制呼叫。

檢視app/build/outputs/proguard/release/usage.txt,看造成崩潰問題的物件是否被移除了。檢視app/build/outputs/proguard/release/mapping.txt,看造成崩潰問題的物件是否被混淆了。

為了防止ProGuard剔除需要用到的類或者類成員,在你的proguard配置中新增一個keep項:

為了防止ProGuard混淆一些類或者類成員,新增一個keepnames項:

在這份ProGuard配置模板中有一些例子。在ProGuard文件中有更多的例子。

提示:把每一個發行版本的mapping.txt檔案都儲存下來。這樣,當使用者遇到一個bug,提交了一個混淆的呼叫棧時,便可以根據儲存的mapping.txt來除錯,找到問題所在。

DexGuard。如果你需要一個不錯的工具來優化程式碼,特別是經過混淆的發行版程式碼,考慮使用DexGuard。DexGuard是有ProGuard團隊做的一個商業軟體。利用DexGuard,可以很容易的分割Dex檔案,解決了65k方法空間限制的問題。

感謝

感謝Antti Lammi, Joni Karppinen, Peter Tackage, Timo Tuominen, Vera Izrailit, Vihtori Mäntylä, Mark Voit, Andre Medeiros, Paul Houghton和其他Futurice開發者分享關於安卓開發的知識。

許可

Futurice Oy Creative Commons Attribution 4.0 International (CC BY 4.0)

相關文章