1. 程式人生 > >編寫高效Android程式碼

編寫高效Android程式碼

雖然如此說,但似乎並沒有什麼好的辦法:Android裝置是嵌入式裝置。現代的手持裝置,與其說是電話,更像一臺拿在手中的電腦。但是,即使是“最快”的手持裝置,其效能也趕不上一臺普通的臺式電腦。

這就是為什麼我們在書寫Android應用程式的時候要格外關注效率。這些裝置並沒有那麼快,並且受電池電量的制約。這意味著,裝置沒有更多的能力,我們必須把程式寫的儘量有效。

本章討論了很多能讓開發者使他們的程式執行更有效的方法,遵照這些方法,你可以使你的程式發揮最大的效力。

簡介

對於佔用資源的系統,有兩條基本原則:

  • 不要做不必要的事
  • 不要分配不必要的記憶體

所有下面的內容都遵照這兩個原則。

有些人可能馬上會跳出來,把本節的大部分內容歸於“草率的優化”(xing:參見[

The Root of All Evil]),不可否認微優化(micro-optimization。xing:程式碼優化,相對於結構優化)的確會帶來很多問題,諸如無法使用更有效的資料結構和演算法。但是在手持裝置上,你別無選擇。假如你認為Android虛擬機器的效能與桌上型電腦相當,你的程式很有可能一開始就佔用了系統的全部記憶體(xing:記憶體很小),這會讓你的程式慢得像蝸牛一樣,更遑論做其他的操作了。

Android的成功依賴於你的程式提供的使用者體驗。而這種使用者體驗,部 分依賴於你的程式是響應快速而靈活的,還是響應緩慢而僵化的。因為所有的程式都執行在同一個裝置之上,都在一起,這就如果在同一條路上行駛的汽車。而這篇 文件就相當於你在取得駕照之前必須要學習的交通規則。如果大家都按照這些規則去做,駕駛就會很順暢,但是如果你不這樣做,你可能會車毀人亡。這就是為什麼 這些原則十分重要。

當我們開門見山、直擊主題之前,還必須要提醒大家一點:不管VM是否支援 實時(JIT)編譯器(xing:它允許實時地將Java解釋型程式自動編譯成本機機器語言,以使程式執行的速度更快。有些JVM包含JIT編譯器。), 下面提到的這些原則都是成立的。假如我們有目標完全相同的兩個方法,在解釋執行時foo()比bar()快,那麼編譯之後,foo()依然會比bar() 快。所以不要寄希望於編譯器可以拯救你的程式。

避免建立物件

世界上沒有免費的物件。雖然GC為每個執行緒都建立了臨時物件池,可以使建立物件的代價變得小一些,但是分配記憶體永遠都比不分配記憶體的代價大。

如果你在使用者介面迴圈中分配物件記憶體,就會引發週期性的垃圾回收,使用者就會覺得介面像打嗝一樣一頓一頓的。

所以,除非必要,應儘量避免盡力物件的例項。下面的例子將幫助你理解這條原則:

  • 當你從使用者輸入的資料中擷取一段字串時,儘量使用substring函式取得原始資料的一個子串,而不是為子串另外建立一份拷貝。這樣你就有一個新的String物件,它與原始資料共享一個char陣列。
  • 如果你有一個函式返回一個String物件,而你確切的知道這個字串會被附加到一個StringBuffer,那麼,請改變這個函式的引數和實現方式,直接把結果附加到StringBuffer中,而不要再建立一個短命的臨時物件。

一個更極端的例子是,把多維陣列分成多個一維陣列。

  • int陣列比Integer陣列好,這也概括了一個基本事實,兩個平行的int陣列比(int,int)物件陣列效能要好很多。同理,這試用於所有基本型別的組合。
  • 如 果你想用一種容器儲存(Foo,Bar)元組,嘗試使用兩個單獨的 Foo[]陣列和Bar[]陣列,一定比(Foo,Bar)陣列效率更高。(也有例外的情況,就是當你建立一個API,讓別人呼叫它的時候。這時候你要注 重對API藉口的設計而犧牲一點兒速度。當然在API的內部,你仍要儘可能的提高程式碼的效率)

總體來說,就是避免建立短命的臨時物件。減少物件的建立就能減少垃圾收集,進而減少對使用者體驗的影響。

使用本地方法

當你在處理字串的時候,不要吝惜使用String.indexOf(), String.lastIndexOf()等特殊實現的方法(specialty methods)。這些方法都是使用C/C++實現的,比起Java迴圈快10到100倍。

使用實類比介面好

假設你有一個HashMap物件,你可以將它宣告為HashMap或者Map:

Map myMap1 = new HashMap();
HashMap myMap2 = new HashMap();

哪個更好呢?

按照傳統的觀點Map會更好些,因為這樣你可以改變他的具體實現類,只要這個類繼承自Map介面。傳統的觀點對於傳統的程式是正確的,但是它並不適合嵌入式系統。呼叫一個介面的引用會比呼叫實體類的引用多花費一倍的時間。

如果HashMap完全適合你的程式,那麼使用Map就沒有什麼價值。如果有些地方你不能確定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(當然公共API是一個例外:一個好的API常常會犧牲一些效能)

用靜態方法比虛方法好

如果你不需要訪問一個物件的成員變數,那麼請把方法宣告成static。虛方法執行的更快,因為它可以被直接呼叫而不需要一個虛擬函式表。另外你也可以通過宣告體現出這個函式的呼叫不會改變物件的狀態。

不用getter和setter

在很多本地語言如C++中,都會使用getter(比如:i = getCount())來避免直接訪問成員變數(i = mCount)。在C++中這是一個非常好的習慣,因為編譯器能夠內聯訪問,如果你需要約束或除錯變數,你可以在任何時候新增程式碼。

在Android上,這就不是個好主意了。虛方法的開銷比直接訪問成員變數大得多。在通用的介面定義中,可以依照OO的方式定義getters和setters,但是在一般的類中,你應該直接訪問變數。

將成員變數快取到本地

訪問成員變數比訪問本地變數慢得多,下面一段程式碼:

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()方法,這樣做比你在一個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);
}
}

這裡有4次訪問成員變數mScrollBar,如果將它快取到本地,4次成員變數訪問就會變成4次效率更高的棧變數訪問。

另外就是方法的引數與本地變數的效率相同。

使用常量

讓我們來看看這兩段在類前面的宣告:

static int intVal = 42;
static String strVal = "Hello, world!";

必以其會生成一個叫做<clinit>的初始化類的方法,當 類第一次被使用的時候這個方法會被執行。方法會將42賦給intVal,然後把一個指向類中常量表的引用賦給strVal。當以後要用到這些值的時候,會 在成員變量表中查詢到他們。下面我們做些改進,使用“final"關鍵字:

static final int intVal = 42;
static final String strVal = "Hello, world!";

現在,類不再需要<clinit>方法,因為在成員變數初始化的時候,會將常量直接儲存到類檔案中。用到intVal的程式碼被直接替換成42,而使用strVal的會指向一個字串常量,而不是使用成員變數。

將一個方法或類宣告為"final"不會帶來效能的提升,但是會幫助編譯器優化程式碼。舉例說,如果編譯器知道一個"getter"方法不會被過載,那麼編譯器會對其採用內聯呼叫。

你也可以將本地變數宣告為"final",同樣,這也不會帶來效能的提 升。使用"final"只能使本地變數看起來更清晰些(但是也有些時候這是必須的,比如在使用匿名內部類的時候)(xing:原文是 or you have to, e.g. for use in an anonymous inner class)

謹慎使用foreach

foreach可以用在實現了Iterable介面的集合型別上。 foreach會給這些物件分配一個iterator,然後呼叫 hasNext()和next()方法。你最好使用foreach處理ArrayList物件,但是對其他集合物件,foreach相當於使用 iterator。

下面展示了foreach一種可接受的用法:

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()中,每次迴圈都會訪問兩次靜態成員變數,取得一次陣列的長 度。 retrieves the static field twice and gets the array length once for every iteration through the loop.

在one()中,將所有成員變數儲存到本地變數。 pulls everything out into local variables, avoiding the lookups.

two()使用了在java1.5中引入的foreach語法。編譯器會 將對陣列的引用和陣列的長度儲存到本地變數中,這對訪問陣列元素非常好。但是編譯器還會在每次迴圈中產生一個額外的對本地變數的儲存操作(對變數a的存 取)這樣會比one()多出4個位元組,速度要稍微慢一些。

綜上所述:foreach語法在運用於array時效能很好,但是運用於其他集合物件時要小心,因為它會產生額外的物件。

避免使用列舉

列舉變數非常方便,但不幸的是它會犧牲執行的速度和並大幅增加檔案體積。例如:

public class Foo {
public enum Shrubbery { GROUND, CRAWLING, HANGING }
}

會產生一個900位元組的.class檔案 (Foo$Shubbery.class)。在它被首次呼叫時,這個類會呼叫初始化方法來準備每個列舉變數。每個列舉項都會被宣告成一個靜態變數,並被賦 值。然後將這些靜態變數放在一個名為"$VALUES"的靜態陣列變數中。而這麼一大堆程式碼,僅僅是為了使用三個整數。

這樣:

Shrubbery shrub = Shrubbery.GROUND;會引起一個對靜態變數的引用,如果這個靜態變數是final int,那麼編譯器會直接內聯這個常數。

一方面說,使用列舉變數可以讓你的API更出色,並能提供編譯時的檢查。所以在通常的時候你毫無疑問應該為公共API選擇列舉變數。但是當效能方面有所限制的時候,你就應該避免這種做法了。

有些情況下,使用ordinal()方法獲取列舉變數的整數值會更好一些,舉例來說,將:

for (int n = 0; n < list.size(); n++) {
if (list.items[n].e == MyEnum.VAL_X)
// do stuff 1
else if (list.items[n].e == MyEnum.VAL_Y)
// do stuff 2
}

替換為:

int valX = MyEnum.VAL_X.ordinal();
int valY = MyEnum.VAL_Y.ordinal();
int count = list.size();
MyItem items = list.items();
 
for (int n = 0; n < count; n++)
{
int valItem = items[n].e.ordinal();
 
if (valItem == valX)
// do stuff 1
else if (valItem == valY)
// do stuff 2
}

會使效能得到一些改善,但這並不是最終的解決之道。

將與內部類一同使用的變數宣告在包範圍內

請看下面的類定義:

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是一個完全獨立的類,它要直接訪問Foo的私有成員是非法的。要跨越這個鴻溝,編譯器需要生成一組方法:

static int Foo.access$100(Foo foo) {
return foo.mValue;
}
static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}

內部類在每次訪問"mValue"和"doStuff"方法時,都會呼叫 這些靜態方法。就是說,上面的程式碼說明了一個問題,你是在通過介面方法訪問這些成員變數和函式而不是直接呼叫它們。在前面我們已經說過,使用介面方法 (getter、setter)比直接訪問速度要慢。所以這個例子就是在特定語法下面產生的一個“隱性的”效能障礙。

通過將內部類訪問的變數和函式宣告由私有範圍改為包範圍,我們可以避免這 個問題。這樣做可以讓程式碼執行更快,並且避免產生額外的靜態方法。(遺憾的是,這些域和方法可以被同一個包內的其他類直接訪問,這與經典的OO原則相違 背。因此當你設計公共API的時候應該謹慎使用這條優化原則)

避免使用浮點數

在奔騰CPU出現之前,遊戲設計者做得最多的就是整數運算。隨著奔騰的到來,浮點運算處理器成為了CPU內建的特性,浮點和整數配合使用,能夠讓你的遊戲執行得更順暢。通常在桌面電腦上,你可以隨意的使用浮點運算。

但是非常遺憾,嵌入式處理器通常沒有支援浮點運算的硬體,所有對"float"和"double"的運算都是通過軟體實現的。一些基本的浮點運算,甚至需要毫秒級的時間才能完成。

甚至是整數,一些晶片有對乘法的硬體支援而缺少對除法的支援。這種情況下,整數的除法和取模運算也是有軟體來完成的。所以當你在使用雜湊表或者做大量數學運算時一定要小心謹慎。

相關推薦

編寫高效Android程式碼

雖然如此說,但似乎並沒有什麼好的辦法:Android裝置是嵌入式裝置。現代的手持裝置,與其說是電話,更像一臺拿在手中的電腦。但是,即使是“最快”的手持裝置,其效能也趕不上一臺普通的臺式電腦。 這就是為什麼我們在書寫Android應用程式的時候要格外關注效率。這些裝置並沒有那麼快,並且受電池電量的制約。這意味

如何編寫高效程式碼(2014/6/1)

編寫高效的程式碼有兩個條件:選擇好的演算法和資料結構,編寫編譯器能夠優化以轉換成高效可執行的程式碼。前者是基礎和前提,即使後者做的足夠好,但是選用了錯誤的演算法和資料結構,優化也不起作用,這個一點要搞清楚。本文的內容的側重於後者。1 計算機系統架構L1和L2位於CPU晶片上,

如何編寫高效Android程式碼

現代的手持裝置,與其說是電話,更像一臺拿在手中的電腦。但是,即使是“最快”的手持裝置,其效能也趕不上一臺普通的臺式電腦。 這就是為什麼我們在書寫Android應用程式的時候要格外關注效率。這些裝置並沒有那麼快,並且受電池電量的制約。這意味著,裝置沒有更多的能力,我們必須把程式寫的儘量有效。

編寫高效android程式碼

雖然這篇文章已經有了幾個譯本,不過參詳過發現其中很多字句並非原文字意,下面是我自己翻譯的版本,若有不妥之處,請指正。 無論怎樣,基於android的裝置也是嵌入式裝置。現代的手持裝置,與其說是電話,更像一臺拿在手中的電腦。但是,即使是“最快”的手持裝置,其效能也達不到一臺普

高效編寫Android程式碼

from eoe.cn 編寫高效的android程式碼 http://my.eoe.cn/1119557/archive/5612.html 無論怎樣,基於android的裝置也是嵌入式裝置。現代的手持裝置,與其說是電話,更像一臺拿在手中的電腦。但是,即使是“最

android studio引入一個自定義的佈局,自定義控制元件,避免每一個活動中都編寫一樣佈局程式碼的問題

本次演示的是標題欄上建立按鈕,即 引入自定義佈局和自定義控制元件的應用十分的廣泛,它的形成的效果很多的應用程式都有,我們可以自定義標題欄,因為普通的標題欄就是一行文字,但是,我們可以發現,很多手機軟體的標題欄上都有返回,或者 進入的按鈕,尤其是全面屏的手機。而且它還能解

連載:編寫高效程式碼(11) 儘量減少分支

         我們在介紹處理器時,已經知道了,現在的處理器都是流水線結構,if和switch等語句會帶來跳轉,而跳轉會打亂流水線的正常執行,影響程式的執行效率。          下面這段程式碼,把奇數賦一個值,把偶數賦一個值,可以用這種方式實現: for(i=0; i

連載:編寫高效程式碼(8) 空間換時間——我們總是在走,卻忘了停留

         時間和空間的關係,是霍金這種智商的人要研究的東西,我們只需要知道,在程式設計時,空間是可以換時間的,時間也是可以換空間的。          李開復在他的自傳《世界因你不同》中描述了他小時候在美國學校裡的一個故事,老師出了道題:“誰知道1/7等於多少?”小

uni-app 是一個使用 Vue.js 開發跨平臺應用的前端框架,開發者編寫一套程式碼,可編譯到iOS、Android、微信小程式等多個平臺。

uni-app 是一個使用 Vue.js 開發跨平臺應用的前端框架,開發者編寫一套程式碼,可編譯到iOS、Android、微信小程式等多個平臺。 uni-app在跨端數量、擴充套件能力、效能體

android 程式碼編寫selector--StateListDrawable使用

在專案開發中肯定會遇到各種點選效果,一般都是建立一個xml檔案然後放在drawable資料夾下面,最後控制元件設定background引用。如下: <selector xmlns:android="http://schemas.android.com

selenium-python編寫unittest執行程式碼時候不執行

使用python+ selenium 編寫簡單的自動化指令碼的時候,自己寫出簡單的程式碼如下: import unittest from selenium import webdriver import time class LoginCase(unittest.TestCase): de

讀讀《編寫高質量程式碼:改善Java程式的151條建議》

這本書可以作為平時寫程式碼的一個參考書,這本書以我個人讀的經驗看來,最好是通過平時程式碼驅動的方式來讀,這樣吸收的快,也讀的快。 這本書主要講什麼,我自己用了個思維導圖概述: 根據這種導圖可知,主要講的就是Java語法、JDK API、程式效能、開源工具和框架、程式設計風格和程式設計思

值得細讀!如何系統有效地提升Android程式碼的安全性?

眾所周知,程式碼安全是Android開發工作中的一大核心要素。 11月3日,安卓巴士全球開發者論壇線下系列沙龍第七站在成都順利舉辦。作為中國領先的安卓開發者社群,安卓巴士近年來一直致力於在全國各大城市舉辦線下技術大會,為Android開發者提供最為全面深入的安全技術解讀。網易雲易盾移動安全專家尹彬彬指出,安

編寫高質量程式碼改善C#程式的157個建議——導航開篇

為什麼要來看這本書    寫此書的作者在書中也有明確的記錄。作者一直在思考一個問題:就是到底什麼樣的程式設計書籍能夠幫助入門者快速進階?所謂“入門者”指的是已經可以使用一門語言來編寫程式,但是不太明白如何編寫高質量程式碼的人。作者回憶自己開發生涯的入門階段發現,那時候常常被以下三類問題所困擾。

Android程式碼報錯彙總

本帖子是個人開發中遇到的錯誤,會在此處做一個記錄,會持續更新。 1. 匯入依賴時,有時會報錯(類似下列錯誤) More than one file was found with OS independent path ‘META-INF/DEPENDENCIES’

編寫一個Android程式

  首先,我們先編寫一個apk,後面用這個apk來進行逆向。用Android Studio建立一個新的Android專案,命名為Jhm,一路Next直到Finish。 一  修改UI介面   開啟app\src\main\res\layout 目錄下的activity_main.xml,   

Android程式碼設定屬性

/設定佈局檔案的高度,控制元件為FrameLayout FrameLayout mFrameLayout = holder.getView(R.id.fl_bill_or_image); ViewGroup.LayoutParams lp = mFrameLayout.getLayoutPara

使用android-SerialPort-api時候出現問題(android程式碼執行shell命令)

最近在搞移動端串列埠通訊,使用的是官方的介面 android-SerialPort-api,這個接口裡面需要對/dev這個資料夾下面的串列埠檔案進行操作 所以demo裡面要執行su命令對dev資料夾下面的ttySN檔案進行許可權更改,改為666,所以重點來了 我們先看下面的相

repo搭建android程式碼倉庫

        OEM提供的rk3188+Android4.4.2原始碼包是把全部的android原始碼做成了一整個git倉,看著都嚇人。於是我打算改造一下。      &nbs

C++:編寫異常安全程式碼

在C++的使用當中,最令人頭疼的地方莫非是記憶體管理或者異常的使用。 想寫出一個真正異常安全的程式碼是非常難得,需要考慮的因素有非常多。 在現代C++當中也有很多人提倡不使用異常,但是要完全杜絕使用C++異常 也是很難的,除非打算不使用任何一個標準庫,重寫所有需要用的資料結構演算法等等。 在一般情況下