Android 開發(fā)者指南之性能提示

翻譯自 Android 開發(fā)者訓(xùn)練課程望抽,原文鏈接:Performance tips


這篇文檔主要涵蓋了一些微小的優(yōu)化翼闹,組合它們能夠提升應(yīng)用的整體性能,但是這些變化不會帶來戲劇性的效果。你應(yīng)該優(yōu)選選擇正確的算法和數(shù)據(jù)結(jié)構(gòu),但是它超出了本文檔要說明的范圍尿扯。在一般的開發(fā)練習(xí)中,你應(yīng)該使用本文檔中的提示焰雕,這樣才能把提高代碼效率當(dāng)成一種習(xí)慣衷笋。

編寫高效代碼的兩個基本原則:

  • 不要做不該做的事
  • 盡量避免分配內(nèi)存

當(dāng)你微優(yōu)化安卓應(yīng)用時,面對最棘手的問題之一就是矩屁,你的應(yīng)用會運行在各種不同類型的硬件上辟宗。不同版本的虛擬機跑在不同處理器上,運行速度也不同吝秕。通常你不能簡單地說泊脐,設(shè)備 X 是比設(shè)備 Y 運行快/慢的因素,將結(jié)果從一個設(shè)備擴(kuò)展到其他設(shè)備烁峭。特別是容客,關(guān)于在其他設(shè)備上的性能,模擬器上的測量結(jié)果不全面约郁。有沒有 JIT 的設(shè)備也有非常大的差異:具有 JIT 的設(shè)備的最佳代碼并不總是沒有設(shè)備的最佳代碼缩挑。

為確保你的應(yīng)用在各種設(shè)備上都能正常運行,確保你的代碼在各個級別都高效鬓梅,并積極優(yōu)化你的性能供置。

避免創(chuàng)建不必要的對象

創(chuàng)建對象并不是沒有開銷的。分代垃圾收集器具有用于臨時對象的每個線程分配池绽快,這可以使分配更便宜芥丧,但是分配內(nèi)存總是比不分配代價要大。

當(dāng)你在應(yīng)用中創(chuàng)建更多的對象時坊罢,你將被迫進(jìn)行垃圾收集续担,對于用戶體驗來說,它就像「打嗝」一樣的活孩。在安卓 2.3 之后引入了并發(fā)垃圾收集器物遇,但是也應(yīng)該避免不必要的工作。

因此诱鞠,你要避免創(chuàng)建不必要的對象挎挖。下面是一些例子:

  • 如果你的方法返回一個字符串这敬,你知道它的結(jié)果總會拼接到 StringBuffer航夺,這時你就該更改簽名和實現(xiàn),這樣函數(shù)會直接追加崔涂,而不是創(chuàng)建存活期短的臨時對象阳掐。
  • 當(dāng)從輸入數(shù)據(jù)提取字符串時,嘗試返回原始數(shù)據(jù)到子字符串,而不是創(chuàng)建一個拷貝缭保。你會創(chuàng)建一個新的 String 對象汛闸,但是它會和原始數(shù)據(jù)共享 char[]。(需要考慮的是艺骂,如果你只使用原始輸入的一小部分诸老,那么無論如何,如果你用這個方法钳恕,你都會在內(nèi)存中保留它别伏。))

一個激進(jìn)的想法是,把多維數(shù)組切片變成并行的一維數(shù)組忧额。

  • int 數(shù)組比 Integer 對象數(shù)組好多了厘肮。但是概括來說,兩個并行的 int 數(shù)組同樣比二維數(shù)組 (int,int)高效睦番。對于其他的基本數(shù)據(jù)類型的組合也是如此类茂。
  • 如果你需要實現(xiàn)一個容器,用來存儲二元組 (Foo,Bar) 對象托嚣,記住兩個并行的 Foo[]Bar[] 數(shù)組通常比一個常規(guī)的 (Foo,Bar) 對象數(shù)組要好得多巩检。(當(dāng)然例外情況是,你為其他代碼設(shè)計 API 以進(jìn)行訪問注益。在這些情況下碴巾,為了實現(xiàn)良好的 API 設(shè)計,通常最好對速度進(jìn)行小的折衷丑搔。但是在你自己的內(nèi)部代碼中厦瓢,你應(yīng)該嘗試盡可能高效。)

一般來說啤月,盡量避免創(chuàng)建短期的臨時對象煮仇。更少地創(chuàng)建對象意味著更低頻率的垃圾回收,這對用戶體驗有直接影響谎仲。

首選靜態(tài)虛擬

如果你不需要訪問對象的字段浙垫,請將方法設(shè)為靜態(tài),調(diào)用速度就會提高 15%-20%郑诺。這也是很好的做法夹姥,因為你可以從方法簽名中看出,調(diào)用方法不能改變對象的狀態(tài)辙诞。

考慮下面的在類首部的聲明辙售。

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

編譯器生成一個類的初始化方法,叫做 <clint>飞涂,當(dāng)?shù)谝淮问褂妙惖臅r候旦部,該方法會被執(zhí)行祈搜。這個方法把值 42 存在 intVal 變量中,從類文件字符串常量表中提取一個引用指向 strVal士八。當(dāng)稍后引用這些值時容燕,通過字段可以訪問它們。

我們可以使用 final 關(guān)鍵字改善這一步:

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

這樣婚度,類就不需要 <clinit> 方法了蘸秘,因為常量進(jìn)入 dex 文件中的靜態(tài)字段初始值設(shè)定項。引用 intVal 的代碼會直接使用整數(shù)值 42蝗茁,訪問 strVal 會使用相對劃算的「字符串常量」指令秘血,而不是字段查找。

注意:此優(yōu)化僅適用于基本類型和字符串常量评甜,而不適用于任意引用類型灰粮。盡管如此,最好盡可能地聲明常量 static final 值忍坷。

使用增強型 for 循環(huán)

增強型 for 循環(huán)(也就是 for-each 循環(huán))可以遍歷實現(xiàn)了 Iterable 接口的集合和數(shù)組粘舟。對于集合,迭代器被分配用于創(chuàng)建叫做 hasNext()next() 的接口佩研。對于 ArrayList柑肴,一個手寫的計數(shù)循環(huán)比 for-each 快約 3 倍,但是對于其他集合旬薯,增強型 for 循環(huán)完全等同于顯式迭代器用法晰骑。

這里有幾個遍歷數(shù)組的方案:

static class Foo {
    int splat;
}

Foo[] array = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < array.length; ++i) {
        sum += array[i].splat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = array;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].splat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : array) {
        sum += a.splat;
    }
}

zero() 最慢,因為每次通過循環(huán)迭代獲得數(shù)組長度是有成本的绊序,JIT 還不會優(yōu)化硕舆。

one() 快一些,它將所有內(nèi)容都拉到局部變量中骤公,從而避免了查找抚官。只有數(shù)組的長度才能提供性能優(yōu)勢。

two() 在沒有 JIT 的設(shè)備上是最快的阶捆,與具有 JIT 的設(shè)備的 one() 無法區(qū)分凌节。它使用了 Java 語言 1.5 版本后引入的增強型 for 循環(huán)語法。

所以洒试,你應(yīng)該默認(rèn)使用增強型 for 循環(huán)倍奢,但是考慮一個手寫的計數(shù)循環(huán),用于性能關(guān)鍵的 ArrayList 迭代垒棋。

考慮包而不是私有內(nèi)部類的私有訪問

來看下面的類的定義:

public class Foo { 
    private class Inner { 
        void stuff() { 
            Foo.this.doStuff(Foo.this.mValue);
        } 
    } 
 
    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);
    } 
} 

重要的是卒煞,我們定義了一個私有的內(nèi)部類 Foo$Inner,它可以直接訪問外部類的私有方法和私有成員變量捕犬。這是合法的跷坝,代碼會打印 「Value is27」。

問題是碉碉,虛擬機認(rèn)為從 Foo$Inner 直接訪問 Foo 的私有成員是非法的柴钻,因為 FooFoo$Inner 是不同的類,即使 Java 語言允許內(nèi)部類訪問外部類的私有成員垢粮。為了彌合差距贴届,編譯器會生成一對合成方法:

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

當(dāng)內(nèi)部類要訪問外部類的 mValue 字段或者調(diào)用 doStuff() 方法時,它會調(diào)用這些靜態(tài)方法蜡吧。這意味著上面的代碼實際上歸結(jié)為毫蚓,你通過訪問器方法訪問成員字段的情況。之前我們討論到訪問器如何比直接訪問字段更慢昔善。所以這是一個特定語言習(xí)語的例子元潘,導(dǎo)致「看不見」的表演。

避免使用浮點型

根據(jù)經(jīng)驗君仆,浮點數(shù) 比Android 設(shè)備上的整數(shù)慢約 2 倍翩概。

在速度方面,現(xiàn)代硬件上的 floatdouble 沒有區(qū)別返咱。在空間方面钥庇,double 大 2 倍。與桌面計算機一樣咖摹,假設(shè)空間不是問題评姨,您應(yīng)該更喜歡 double

此外萤晴,即使對于整數(shù)吐句,一些處理器也有硬件乘法但缺乏硬件除法。在這種情況下店读,整數(shù)除法和模數(shù)運算在軟件中執(zhí)行 - 如果您正在設(shè)計哈希表或進(jìn)行大量數(shù)學(xué)運算蕴侧,則需要考慮。

了解并使用庫

除了喜歡庫代碼而不是自己編寫代碼两入,請記住系統(tǒng)可以自由地用手動編譯匯編程序替換對庫方法的調(diào)用净宵,這可能比 JIT 可以生成的等效的 Java 最佳代碼更好。這里典型的例子是 String.indexOf() 和相關(guān)的 API裹纳,Dalvik 用內(nèi)聯(lián)的內(nèi)在代替择葡。類似地,System.arraycopy() 方法比帶有 JIT 的 Nexus One 上的手動編碼循環(huán)快約 9 倍剃氧。

小心使用原生方法

使用 Android NDK 的原生代碼開發(fā)應(yīng)用敏储,不一定比用 Java 語言開發(fā)的更高效。一方面朋鞍,Java 和 原生之間傳遞有損耗已添,JIT 不會跨越這些邊界優(yōu)化妥箕。如果你分配了原生資源(原生堆上的內(nèi)存,文件描述符更舞,或其他內(nèi)容)畦幢,安排及時收集這些資源可能要困難得多。你還需要為要運行的每個體系結(jié)構(gòu)編譯代碼(而不是依賴于具有 JIT 的體系結(jié)構(gòu))缆蝉。你可能甚至需要為相同的架構(gòu)編譯多個版本:為 G1 中的 ARM 處理器編譯的原生代碼無法充分利用 Nexus One 中的 ARM宇葱,以及為 Nexus One 中的 ARM 編譯的代碼不會在 G1 中的 ARM 上運行。

性能神話

在沒有 JIT 的設(shè)備上刊头,通過具有精確類型而不是接口的變量調(diào)用方法確實更有效黍瞧。(因此例如,調(diào)用 HashMap 映射上的方法比使用 Map 映射更便宜原杂,即使在這兩種情況下映射都是 HashMap印颤。)情況并非如此慢 2 倍,實際差異更像是慢了 6%穿肄。此外膀哲,JIT 使兩者有效地難以區(qū)分。

在沒有 JIT 的設(shè)備上被碗,緩存字段訪問比重復(fù)訪問字段快約 20%某宪。使用 JIT,字段訪問的成本與本地訪問大致相同锐朴,因此除非您覺得它使代碼更易于閱讀兴喂,否則這不值得進(jìn)行優(yōu)化。(對于 final焚志,static 和 static final 字段也是如此衣迷。)

總是測量

在開始優(yōu)化之前,請確保你遇到需要解決的問題酱酬。確保你可以準(zhǔn)確衡量現(xiàn)有的績效壶谒,否則你將無法衡量嘗試的替代方案的好處。

你可能還會發(fā)現(xiàn) Traceview 對于分析很有用膳沽,但重要的是要知道當(dāng)前會禁用 JIT汗菜,這可能會導(dǎo)致它錯誤地將時間錯誤歸結(jié)為 JIT 可能能夠贏回的代碼。在 Traceview 數(shù)據(jù)建議進(jìn)行更改以確保在沒有 Traceview 的情況下運行時生成的代碼實際運行得更快時挑社,這一點尤其重要陨界。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市痛阻,隨后出現(xiàn)的幾起案子菌瘪,更是在濱河造成了極大的恐慌,老刑警劉巖阱当,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俏扩,死亡現(xiàn)場離奇詭異糜工,居然都是意外死亡,警方通過查閱死者的電腦和手機录淡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門捌木,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赁咙,你說我怎么就攤上這事∶庾辏” “怎么了彼水?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長极舔。 經(jīng)常有香客問我凤覆,道長,這世上最難降的妖魔是什么拆魏? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任盯桦,我火速辦了婚禮,結(jié)果婚禮上渤刃,老公的妹妹穿的比我還像新娘拥峦。我一直安慰自己,他們只是感情好卖子,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布略号。 她就那樣靜靜地躺著,像睡著了一般洋闽。 火紅的嫁衣襯著肌膚如雪玄柠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天诫舅,我揣著相機與錄音羽利,去河邊找鬼。 笑死刊懈,一個胖子當(dāng)著我的面吹牛这弧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虚汛,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼当宴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泽疆?” 一聲冷哼從身側(cè)響起户矢,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎殉疼,沒想到半個月后梯浪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捌年,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年挂洛,在試婚紗的時候發(fā)現(xiàn)自己被綠了礼预。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡虏劲,死狀恐怖托酸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柒巫,我是刑警寧澤励堡,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站堡掏,受9級特大地震影響应结,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泉唁,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一鹅龄、第九天 我趴在偏房一處隱蔽的房頂上張望夕晓。 院中可真熱鬧署浩,春花似錦、人聲如沸厢拭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宝踪,卻和暖如春侨糟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘩燥。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工秕重, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厉膀。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓溶耘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親服鹅。 傳聞我的和親對象是個殘疾皇子凳兵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容