1. 程式人生 > >Android最佳實踐

Android最佳實踐

為效能設計: 1)避免建立物件 物件的建立從來不是免費的。雖然GC使得記憶體申請代價不再高昂,但是申請總是比不申請來得昂貴。如果你在一個使用者介面迴圈中申請物件,你將會強行執行週期性的GC,在使用者體驗上出現一些小的“打嗝”,因此除非不得已,你應該避免建立物件例項,下面是一些例子可以幫助理解: 當你在一組輸入資料中抽取字串時,嘗試返回源資料的子串,而非建立一個副本。你將會建立一個新的String物件,但是它會和資料共享字元陣列char[]。 如果你有一個返回String的方法,而且你知道它的結果將會一直被追加到StringBuffer,改變你的簽名和實現,使這個函式裡面直接追加,避免建立臨時物件。 一個更激進的主意是將多維陣列切成與之平行的一維陣列: 一個int陣列比Integer陣列要好,但也有一個公認的事實就是兩個平行的int陣列要比一個(int,int)物件陣列要高效很多。對於其它原始資料型別亦如是。 如果你需要實現一個儲存一組物件(Foo,Bar)的容器,請記住兩個平等的Foo[]和Bar[]陣列通常元比一個定製物件陣列要好(當然,對於此有個例外,就是當你設計一個API供其它程式碼訪問時;在那樣的情況下,通常最好是為保證API的正確性而犧牲一點速度。但是在你的內部程式碼,你應該儘可能保持高效)。 通常來說,避免建立臨時物件,如果你可以的話。更少的物件建立意味著更小頻率的GC,這對使用者體驗有直接的影響。 2)用Native方法 當處理字串時,要毫不猶豫地使用諸如String.indexOf()、String.lastIndexOf()之類的專門方法,這些是典型的用C/C++程式碼實現的方法,它們可以輕易地比實現同樣功能的Java迴圈快10-100倍。 對此建議的一反面是呼叫一個native方法要比呼叫一個解析的方法,不要將native方法用於瑣碎的計算,如果可以避免的話。 優先使用Virtual而非Interface 假如你有一個HashMap物件,你可以宣告它為一個HashMap或一個通用的Map: Map myMap1 = new HashMap(); HashMap myMap2 = new HashMap(); 哪一個更好? 一般的會說你該選擇Map,因為它允許你改變其實現,對於通常的程式設計來說這是對的,但是對於嵌入式系統來說這並不是太妙。通過介面的引用來呼叫一個方法要比通過一個具體型別的引用呼叫virtual方法花多2倍的時間。 如果你已經選擇了一個HashMap,因為它正好適用你正在做的事情,那通過Map來呼叫就沒有什麼價值了。考慮到IDE可以為你重構程式碼,用Map來呼叫就沒有太大價值了,即使你不知道你程式碼將去向何方(但是,再一次的,公共的API是又是一個例外:好的API較少考慮效能)。 3)優先選擇static而非virtual 如果你不必訪問一個物件的欄位,使你的方法成為static方法。它可以被更快地呼叫,因為它不需要一個虛擬方法表間接呼叫。同時它也是一個好的做法,因為從方法的簽名可以看出呼叫這個方法不會改變物件的狀態。 4)避免內部的Getter/Setter 在一些像C++的語言中,通常的做法是用getter(如:i=getCount())代替直接地訪問欄位(i=mCount),在C++這是一個很好的習慣,因為編譯器通常能夠內聯這個訪問,並且你需要限制或debug欄位訪問,你可以在任何時候增加程式碼。 在Android,這是一個壞主意。虛擬方法呼叫代價是昂貴的,例項欄位查詢代價更高。沿用面一般向物件程式設計實踐在公開介面中提供gettter和setter是合理的,但在一個類中你應該直接訪問欄位。 Cache欄位查詢 訪問物件欄位要比訪問本地變數慢得多,如下面這段: for (int i = 0; i < this.mCount; i++) dumpItem(this.mItems[i]); 應該寫成這樣: int count = this.mCount; Item[] items = this.mItems; for (int i = 0; i < count; i++) dumpItems(items[i]); (我們用一個顯式的"this"來表明這是一個成員變數。) 有一個相似的指引就是,不要在for語句中的第二個從句中呼叫方法。例如下面這段程式碼將會在每次迭代中都會執行一次getCount(),這是一個巨大的浪費,你可以將它的值cache為一個int。 for (int i = 0; i < this.getCount(); i++) dumpItems(this.getItem(i)); 通常,如果你將要訪問一個例項欄位多次,一個好的習慣就是建立一個臨時變數。例如: protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) { if (isHorizontalScrollBarEnabled()) { int size = mScrollBar.getSize(false); if (size <= 0) { size = mScrollBarSize; } mScrollBar.setBounds(0, height - size, width, height); mScrollBar.setParams( computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(), false); mScrollBar.draw(canvas); } } 這是對成員欄位mScrollBar的四次分開查詢,通過將mScrollBar快取到本地變數,四次成員欄位查詢變成四次本地變數引用,這樣更為高效。 同樣地,方法引數作為本地變數擁有相同的效能特徵。 宣告常量為final 考慮在類開頭的如下宣告: static int intVal = 42; static String strVal = "Hello, world!"; 編譯器產生一個叫的類初始化器方法,它在類首次使用時被執行。這個方法將42存到intVal,併為intVal從類檔案字串常量表中抽出一個引用,當這些值在後面被引用到時,它們以欄位查詢的方式被訪問。 我們可以用final關鍵字改進之: static final int intVal = 42; static final String strVal = "Hello, world!"; 這個類不再需要一個方法,因為常量存到直接由VM處理的類檔案靜態欄位初始化器,程式碼訪問intVal將會直接使用integer值42,而對intVal的訪問會用一個相對廉價的“字串常量”指令來代替一個欄位查詢。 宣告一個方法或類為final並不能直接獲得效能上的好處,但它確實能起到某些優化作用。例如,假如編譯器知道一個"getter"不能被一個子類重寫,它能夠內聯這個方法呼叫。 你也可以將本地變數宣告為final,然而這並無真正意義上的效能提升。對於要地變數,只有在使程式碼更清晰(或你不得不,如為了在匿名內部類中使用)時使用final。 小心使用增強的For迴圈語句 增強的For語句可以用於實現了Iterable介面的Collection,對於這些物件,一個iterator被申請用來進行介面呼叫hasNext()和next()。對於ArrayList,你最好直接遍歷它,但對於其它collections,增強的For迴圈語句將會等同於顯式的迭代用法。 儘管如此,下面的程式碼展示了增強的For語句的可為人接受的用法: public class Foo { int mSplat; static Foo mArray[] = new Foo[27]; public static void zero() { int sum = 0; for (int i = 0; i < mArray.length; i++) { sum += mArray[i].mSplat; } } public static void one() { int sum = 0; Foo[] localArray = mArray; int len = localArray.length; for (int i = 0; i < len; i++) { sum += localArray[i].mSplat; } } public static void two() { int sum = 0; for (Foo a: mArray) { sum += a.mSplat; } } } zero()在迴圈中每次迭代獲取靜態欄位兩次計算陣列長度一次。 one()將所有東西存到本地變數,避免查詢。 two()用到了增強的For迴圈語句,由編譯器產生的程式碼拷貝陣列引用和陣列長度到本地變數,使之成為一個遍歷陣列元素的一個很好的選擇。它確實在主迴圈中產生了一個額外的本地載入/儲存,使得它比起one()有點慢並且長了4bytes。 總之,增強的For語句對於陣列表現良好,但對iterable物件要小心使用,因為有額外的物件建立。 避免Enum型別 Enum非常方便,但不幸的是當考慮到時間和速度時就讓人痛苦。例如這個: public class Foo { public enum Shrubbery { GROUND, CRAWLING, HANGING } } 將編譯成一個900byte的.class檔案,在首次使用是時,類初始化器在代表每個被列舉的值物件上啟用方法。每個物件都有其靜態欄位,並且整個集合就儲存在一個數組(一個稱為“$values”的靜態欄位)上,對於僅僅的三個integer來說,那是太多的程式碼和資料了。 這個: Shrubbery shrub = Shrubbery.GROUND; 導致了一次靜態欄位查詢。如果“GROUND”是一個static final int編譯器將會將它看作是一個常量並內聯它。 相反地,當然,是運用enum你可以得到一個更優雅的API和一些編譯時的值檢查。因此,通常折衷的辦法是:為API,你應該千方百計地使用enum,但是當考慮到效能時嘗試避免使用它。 利用內部類使用包作用方域 考慮下面的類定義: public class Foo { private int mValue; public void run() { Inner in = new Inner(); mValue = 27; in.stuff(); } private void doStuff(int value) { System.out.println("Value is " + value); } private class Inner { void stuff() { Foo.this.doStuff(Foo.this.mValue); } } } 在這裡我們要特別指出的是這裡定義了一個內部類(Foo$Inner),它可以直接訪問外部類的私有方法和私有例項欄位,這是合法的,程式碼的執行的結果是如預期般的“Value is 27”。 問題在於,Foo$Inner是一個完全獨立的類,這使得直接訪問其私有方法是非法的,為了架起橋樑,編譯器會產生如下兩個虛擬方法 /*package*/ static int Foo.access$100(Foo foo) { return foo.mValue; } /*package*/ static void Foo.access$200(Foo foo, int value) { foo.doStuff(value); } 當內部類程式碼需要訪問外部類的mValue變數或啟用doStuff方法時就會呼叫這些方法。這就意味著上面的程式碼清楚表明了你是通過訪問器來訪問成員欄位的,而非直接訪問。前面我們討論過訪問器是比直接訪問是要慢的,所以這是一個由於某種特定語言方言所導致的隱性效能打擊。 我們可以通過宣告由內部類訪問的欄位和方法為具有包作用域而非私有作用域來解決這個問題。這樣執行得更快並且移除了額外產生的方法(不幸的是,這也意味著這些欄位可以被同包下的其它類所訪問,這個是違反了使所有的欄位成為私有的標準OO做法的,再一次的,如果你是在設計一個公共的API的話,你可能要慎重地考慮這一優化策略)。 9)避免使用Float型別 在Pentium CPU釋出之前,對於遊戲作者來說做很多整型計算是很正常的事。有了Pentium之後,浮點計算聯合處理器成了內建功能,你的遊戲通過交錯整型和浮點操作比只有整型計算時執行起來要快得多。在桌面系統上通常的可以自由的使用浮點數。 不幸的是,嵌入式處理器很少具有硬體浮點支援,所以所有的"float"和"double"操作都是在軟體上進行。某些基本的浮點操作可能會花費數微秒。 還有,甚至對於整型數,一些晶片支援硬體乘法但缺少硬體除法,在這種情況下,整型除法和取模運算是在軟體上執行的——如果你是在設計一個雜湊表或做很多數學運算這就是你需要考慮的事情。 為響應靈敏性設計 程式碼可能通過各種效能測試,但是當用戶使用時還是會需要漫長的等待,這些就是那種響應不夠靈敏的應用——它們反應遲鈍,掛起或凍住週期很長,或者要花很長時間來處理輸入。 在Android上,系統通過向用戶顯示一個稱為應用無響應(ANR:Application Not Responding)的對話方塊來防止在一段時間內響應不夠快。使用者可以選擇讓應用繼續,但是使用者並不會想要每次都來處理這個對話方塊。因此應把你的應用設計得響應靈敏,使得系統不必顯示ANR給使用者。 通常地,當不能響應使用者輸入時系統顯示一個ANR。例如,如果一個應用在IO操作(經常是網路訪問)上阻塞了,那麼主應用執行緒就會無法處理正在進行的使用者輸入事件。經過一段時間,系統認為應用已經掛起,向用戶顯示一個ANR,讓使用者可以選擇關閉。 相同地,如果你的應用花太多的時間在構建詳細的記憶體結構上,又或者在計算遊戲的下一個動作上,系統會認為你的應用已經掛起。用上面的技術來保證這些計算是高效的一直都是很重要的,但是即使是最高效的程式碼執行也是需要花費時間的。 在這兩種情況下,解決的辦法通常就是建立一個子執行緒,在這個子執行緒上做你的大部分工作。這樣讓你的主執行緒(驅動使用者介面事件迴圈)保持執行,並讓你的程式碼免於被系統認為已經凍住。因為這樣的執行緒化通常都是在類級別上來完成的,所以你可以認為響應效能問題是一個類問題(與上面描述的方法級別的效能問題)。 這個文件討論了Android系統是如何決定一個應用沒有響應的,並提供了指引來保障你的應用是響應靈敏的。 1)是什麼引發了ANR? 在Android系統上,應用的響應靈敏性由Activity Manager和Window Manager system services所監控,當它監測到如下的其中一個條件時,Android就會為特定的應用顯示一個ANR: 5秒內對輸入事件無響應。 一個BroadCastReceiver在10秒內沒有執行完畢。 怎樣避免ANR? 考慮到上面對ANR的定義,讓我們來研究一下這是為什麼會發生以及怎樣最好的組織你的應用以避免ANR。 Android應用正常是執行在一個單獨的(如main)執行緒中的,這就意味著在你應用主執行緒中正在做的需要花很長時間來完成的事情都能夠啟用ANR對話方塊。因為你的應用並沒有給自己一個機會來處理輸入事件或Intent廣播。 因此任何執行在主執行緒中的方法應該做盡可能少的事情。特別地Activitiy在關鍵生命週期方法中如onCreate()和onResume()應當做盡可能少的設定。潛在地的耗時長的操作(如網路或資料庫操作,或高耗費數學計算如改變點陣圖大小)應該在子執行緒裡面完成(或以資料庫操作為例,可以通過非同步請求)。儘管如此,這並不是說當等待子執行緒完成的過程中你的主執行緒必須被阻塞——你不必呼叫Thread.wait()或Thread.sleep(),恰恰相反,你的主執行緒應該為子執行緒提供一個Handler,以便子執行緒完成時可以提交回給主執行緒。以這種方式來設計你的應用,將會允許你的主執行緒一直可以響應輸入,以避免由5秒鐘的輸入事件超時導致的ANR對話。這些做法同樣應該被其它任何顯示UI的執行緒所效仿,因為它們屬於同樣型別的超時。 IntentReciever執行時間的特定限制限制了它們應該做什麼:在後臺執行的一些瑣碎的工作如儲存設定或註冊通知。至於其它在主執行緒裡被呼叫的方法,在BroadcastReceiver中,應用應該避免潛在的長耗時操作或計算,而是應該用子執行緒來完成密集任務(因為BroadcastReceiver的生命週期是短暫的)。對Intent broadcast作出響應,你的應用應該啟動一個Service來執行長耗時的動作。同樣,你也應該避免從Intent Receiver中啟動Activity,因為它會產生一個新的屏,偷走任何使用者正在執行的應用的焦點。對Intent broadcast作出的響應,假如你的應用需要向用戶顯示什麼東西,應該用Notification Manager來完成。 增強響應靈敏性 通常,在一個應用中,100到200微秒是一個讓使用者感覺到阻滯的閾值,因此這裡有些小技巧讓你用來使你的應用看起來響應更靈敏。 如果你的應用正在後臺對使用者輸入作出響應,顯示正在進行的進度(ProgressBar和ProgressDialog對此很有用)。 特別是對於遊戲,在子執行緒中做移動的計算。 如果你的應用有一個耗時的初始化過程,考慮用閃屏或儘可能快地渲染主介面並非同步地填充資訊。在這兩種情況下你都應該表明進度正在進行,以免使用者覺得你的應用被凍住了。 為無縫設計: 即使你的應用是快速且響應靈敏的,一些設計仍然能句對使用者造成問題——因為與其它應用未計劃的互動或者對話,意外的資料丟失,無意識的阻塞等等。為了避免這些問題,有助於理解你的應用執行的環境和可以影響你的應用的系統互動。總之,你應該倔盡全力地開發一個與系統和其它應用無縫互動的應用。 一個常見的無縫問題就是一個應用的後臺程序(如service或broadcast receiver)對某事件作出響應而彈出對話方塊,這看起來彷彿並無大礙,特別是當你在模擬器上單獨地構建和測試你的應用時。然而,當你的應用在真正的裝置上執行,後臺執行緒顯示對話方塊時,你的應用當時可能沒有獲得使用者焦點。這就會出現你的應用會在活動的應用後面顯示對話方塊,或者從當前應用中獲得焦點並顯示對話方塊的情況,而管論當時使用者正在做什麼(如正在打電話等)。那樣的行為可能對你的應用或使用者不起作用。 為了避免這些問題,你的應用應該利用適當的系統資源——Notification類,來通知使用者。利用通知,你的應用可以通過在狀態條上顯示一個圖示來通知使用者事件已經發生,而非獲得焦點和打斷使用者。 另外一個無縫問題的例子就是,Activity由於未能正確實現onPause()及其它生命週期方法而無意中丟失了狀態或使用者資料。又或者,你的應用要暴露供其它應用使用的資料,你應該通過ContentProvider來實現,而非通過一個全世界都可讀的原始檔案或資料庫。 這些例子的共同特點就是,它們都是關於如何跟系統和其它應用協作得更好,Android系統的設計就是將所有的應用看作是一個鬆散耦合的元件聯邦,不是一堆墨盒程式碼。這就使你作為一個開發者可以將整個系統視為只是一個更大一點的元件聯邦。這樣有得於你將應用與其它應用清晰和無縫的整合,所以作為回報,你應該更好的設計你的程式碼。 這個文件討論了常見的無縫整合問題和怎樣去避免它們。它包含了如下主題: 別丟棄資料 一定要記住Android是一個移動平臺。看起來很顯然,但是記住這個事實很重要,就是任何Activity(如"正在打進來的電話"應用)在任何時候都有可能彈出來覆蓋你的Activity,這會啟用onSaveInstanceState()和onPause()方法,並導致你的應用被殺死。如果當其它Activity出現時,使用者正好在你的應用上編輯資料,你的應用被殺死的同時那些資料也很可能會丟失。當然了,除非你先儲存了進行中的工作。“Android方式”是這麼做的:能接收和編輯使用者輸入的應用需要重寫onSaveInstanceState()方法並以恰當的方式儲存它們的狀態,當用戶重新訪問應用時,就能重新獲得資料。 一個運用這個行為經典的例子就是郵件應用,當用戶正在撰寫郵件時另一人Activity開始了,應用應該將正在編輯的郵件儲存為草稿。 不要暴露原始資料 如果你不想穿著內衣在大街上散步,同樣你的資料也不應如此。儘管可能暴露某些應用可以方便其它應用讀取,但這通常不是最好的主意。暴露原始資料要求其它的應用能夠理解你的資料格式;如果你改變了格式,你將會破壞其它沒有同時更新的應用。 “Android方式”就是建立一個ContentProvider通過一個清晰的、深思熟慮的、可維護的API來暴露你的資料給其它應用。使用ContentProvider就像一個Java介面來分離和元件化兩段緊密耦合的程式碼,這就意味著你能夠修改你資料的內部格式而不用修改由ContentProvider暴露的介面,這樣就不會影響其它應用。 別打斷使用者 如果能確定一個使用者是帶有目的性的執行一個應用才是安全的。那就是為什麼你除非是直接響應當前活動的使用者輸入,不然就要避免產生Activity的原因。 那就是說,不要從後臺執行的BroadcastReceiver和Service中呼叫startActivity()。如果這樣做將會打斷任何正在執行的應用,並使使用者惱怒。甚至你的Activity可能成為一個“擊鍵強盜”接收一些使用者正在為上一個Activity提供的輸入,視乎你的應用所做的,這是這可能是個壞訊息。 取代直接從後臺直接產生Activity UIs,你應該用NotificationManager來設定通知,這將會出現在狀態條上,當用戶空閒時可以點選它們來看你的應用向他們顯示了什麼。 (注意,當你的Activity已經在前臺時所有這些都沒適用:這時,對於輸入的響應,使用者期望看到你的下一個Activity。) 有太多事要做?在一個執行緒裡做 如果你的應用需要做一個代價高昂或長耗時的計算,你可能要將它移到一個執行緒裡。這個將會防止顯示“Application Not Responding”對話方塊給使用者,最終導致你的應用完全終止。 預設地,在一個Activity中的程式碼和其所有的View執行在同一個執行緒上。這與處理UI事件的執行緒是同一個。例如,當一個鍵被按下時,一個key-down事件被新增到Activity主執行緒的佇列。事件處理系統需要很快地讓這個事件出列並處理這個事件。不然,系統數秒後將會認為應用已經掛起並替使用者殺死這個應用。 如果你有長耗時的程式碼,讓它在你的Activity上內聯執行將會在使它執行在事件處理執行緒上,這很大程度上阻塞了了事件處理控制代碼。這會延緩輸入處理並導致ANR對話方塊。為了避免之,將你的計算移到一個執行緒中。在為響應靈敏性設計中已經討論瞭如何做。 5)不要過載一個單一的Activity屏 任何值得使用的應用都可能會有幾個不同的螢幕。當設計你的UI螢幕時,請一定要運用多個Activity物件例項。依賴於你的開發背景,你可能像解釋某些類似Java Applet的東西一樣來解釋一個Activity,Activity是你應用的入口點。然而,那並不是準確:一個Applet的子類是一個Java Applet的單一入口點,而一個Activity應該被看作一個潛在的進入你的應用的多個入口點。在你的”main”Activity和任何其它你可能有的Activity之間的唯一不同就是,那“miain”Activity碰巧是那個唯一在你的AndroidManifest.xml檔案中對“android.intent.action.MAIN”動作有興趣的一個而已。 所以,當設計你的應用時,把你的應用看成一個Activity物件的聯邦。從長遠來看,這會使得你的程式碼更具可維護性。 6)擴充套件系統主題 當提到使用者介面的觀感時,協調是很重要的。使用者為那些與他們所期望的使用者介面相反的應用所震動。當設計你的UI時,你應當儘量避免出現太多你自己主題,相反地,用同一個主題。你可以重寫或擴充套件那部分你必須的主題,但是至少你是基於與其他應用相同的UI基礎上的。詳細可以參閱“應用風格和主題”部分。 7)設計你的UI可以與多屏解析度一起工作 不同的的基於Android的裝置可能會支援不同的解析度。甚至一些可能支援隨時更改解析度。保證你的佈局和圖片足夠靈活對於在不同裝置螢幕上正常顯示是非常重要的。 幸運的是,這是很容易辦到的。簡單講,你需要做的就是為你的關鍵解析度提供不同版本的作品,然後設計你的佈局適應各種不同的維度。(例如,避免作用硬編碼位置而用相對佈局。)如果那樣做的話,剩下的系統會處理,你的應用在任何裝置上看起來都很棒。 8)假定網路是很慢的 Android裝置會有多種網路連結選項。所有的都會提供資料訪問,雖然有一些會比另一些更快。其中速度最慢的就是GPRS(GSM網路的非3G資料服務)。即便具備3G能力的裝置在非3G網路上也會花很多的時間,所以網速低將會是一個長期存在的事實。 那就是為什麼你應該針對最小化的網路訪問和頻寬編寫你的應用。你不能假設網路是快速的,所以你應該一直計劃它是慢的。如果你的使用者碰巧是在一個快速的網路上,那很好——他們的體驗只會提升。你要避免相反的情況:應用有時可用,但有時慢得令人沮喪,得看使用者是在哪在什麼時間,這樣的應用可能不會受歡迎。 別假定觸控式螢幕和鍵盤 Android可能支援多種外觀形狀。那就是說一些Android裝置將會有完整的“QWERTY"鍵盤,而其它的可能會有40鍵、12鍵或其它鍵盤設定。同樣地,一些裝置會有觸控式螢幕,但很多會沒有。 當構建你的應用時,一定要記住,不要假定特定的鍵盤佈局——當然了,除非你真的喜歡限制你的應用以到它只能在某些裝置上執行。 一定要節省裝置電池 如果移動裝置經常侷限於屋內,那就不是很“移動”。移動裝置是電池供電的,而我們如果能讓電池每充一次的電量使用得更持久一些,每個人都會更開心——特別是使用者。其中的兩個用電大戶就是處理器和音訊,那就是為什麼你寫的應用應儘量做少的工作的同時儘可能頻繁地使用網路的原因。 最小化你的應用使用的處理器時間就歸結為書寫高效的程式碼。從音訊上最小化功耗,要確保優雅地處理錯誤條件,並僅獲取你需要的東西。例如,如果連線網路失敗不要一直重試連線網路,如果失敗了一次,很可能是使用者沒有接收訊號。如果你立即重試,那麼你所做的一切只是在浪費電池能量。 使用者是相當聰明的:如果你的程式是高耗電的,你可以相信他們會發覺的。在那一點上,唯一可以確定的是你的程式將不會保持安裝非常久。