性能小貼士

性能小貼士

本文主要介紹一些代碼優化方面的小貼士,結合起來使用能總體性的提高應用性能。可是,這些技巧不可能帶來戲劇性的性能改變。合適的算法和數據結構是解決性能的首選考慮(還有程序的執行流程優化),但這已經脫離了本文的範疇。java

本文介紹的小貼士是每一個有追求的程序員應有的編碼習慣。程序員

關於如何寫出高效的代碼,這裏有兩個基本的原則:算法

  • Don't do work that you don't need to do編程

  • Don't allocate memory if you can avoid it數組

面臨的現狀

一個很是棘手的問題,你的應用運行在不一樣類型的硬件設備上的。不一樣版本的虛擬機以不一樣的運行速度運行在不一樣的處理器上。一般,不能輕易的下結論說"設備x比設備y快或者慢了多少倍"。尤爲是,模擬器和真機的差異很是大,模擬器上的性能測試不能充分反應真機的狀況,何況,一個有JIT和一個沒有JIT的設備之間也存在巨大的差異。數據結構

爲了保證你的應用性能在絕大多數的設備上有一個好的表現,確保你的代碼在各個方面的高效性並不遺餘力去優化你的性能。架構

避免建立沒必要要的對象

對象的建立毫不是免費的。如今主流的垃圾收集器中每一個線程的臨時對象分配池使得對象的建立更加低廉,可是分配內存的代價仍是要比不分配要高昂的多。 正如你在程序裏建立了大量的對象,垃圾回收會按期的觸發,會形成一點點卡頓的感受。雖然Android在2.3引入了併發的垃圾回收機制,但沒必要要的工做仍是應該避免。併發

所以,不要建立那些非必需的實例對象。如下的幾個例子可能具備參考意義:函數

  • 若是你有一個方法返回一個string,並且你知道這個返回結果是要被加到一個StringBuffer中去的,那麼改變你的方法實現,變成直接添加而不是建立一個短暫的臨時對象。oop

  • 當從一個輸入數據中提取字符串,嘗試去返回原數據的子串,而不是建立原數據的一個拷貝。採用子串雖然也會建立一個新的string對象,可是數據的內容仍是和原數據共用的。

  • 儘量的使用一維數組,而不是多維的數組。

關於減小沒必要要的對象建立,咱們還能夠有如下的實踐:

  • 使用Message.obtain()或者handler.ontainMessage()去獲取一個Message對象,而不是直接new。

  • 使用優化過的數據結構,SparseArray來替代Map。

  • 使用線程池去重用線程而不是每次新建線程。

  • Handler隨意建立,只爲把內容放到主線程執行,這個時候能夠公用一個Handler。

  • 在那些被高頻調用的方法裏,避免建立臨時對象,好比View的onDraw(),在循環裏等等。

  • 在API23如下,當作圖片清理設置ImageView.setImageBitmap(null)時,方法內部會建立一個BitmapDrawable對象。因此,咱們 使用ImageView.setImageDrawable(null)來代替。

總的來講,盡你所能的避免建立短暫的臨時對象。垃圾回收對體驗有較大的影響,越少的對象建立意味着越低頻的垃圾回收。

Prefer Static Over Virtual

把一個方法定義成static的,相比於對象方法大概會有15~20%的速度提高。這也是一個好的實踐,由於你能夠從方法聲明上分辨出這個方法調用不會改動對象的狀態。

使用Static Final聲明常量

如下是一個類在頂部的聲明:

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>方法就不會被調用,由於這兩個常量直接存放進dex文件的靜態字段初始化器中。指向intVal的引用直接使用42,訪問strVal也會使用一個相對廉價的字符串常量而不是經過字段查找的方式。

注意:這個優化只適用於基本類型以及字符串常量,而非任意的引用類型。可是,爲常量加上static final的聲明是一個好習慣。

避免內部的Getters/Setters

在像C++這樣的本地語言中,經過getters方法的形式替代直接的字段訪問會比較日常。這在C++裏是一個優秀的習慣,在C#或者Java這樣的面向對象的語言裏也是被常用,由於大部分的編譯器都作了內聯優化。

然而,這在Android上並非一個好的實踐。方法的調用比實例字段的查找要昂貴的多。遵循面向對象的編程習慣提供setter和getter方法是好的,可是在類的內部應該直接使用字段訪問。

在沒有JIT的狀況下,直接的字段訪問要比調用一個無用的getter方法快大約3倍。在有JIT(直接的字段方法跟本地訪問同樣低廉)的狀況下,直接的字段訪問要比方法調用快大約7倍。(特意作了驗證,在我6.0系統的motox設備上大約是10倍)

使用加強的for循環

加強的for循環也叫"for-each"循環,一般用於遍歷那些實現了Iterable接口的集合。在集合中,一個迭代器通常會調用hasNext()next()。在使用ArrayList時,一個手寫的計數的循環在有JIT的狀況下性能大約是沒有時的3倍,可是在使用其它的集合時使用加強的for循環的性能跟顯示的調用迭代器的性能基本接近。

下面是迭代一個數組的幾種可選方案:

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

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

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

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

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

zero()是最慢的,由於JIT不能優化掉每次循環中的數組長度的查找。

one()要快一點。它把全部的都放到本地變量,避免了查找。但其實這裏只有數組長度的定義能提高性能。

two()在沒有JIT的狀況下是最快的,在有JIT的狀況下跟one()基本同樣。

因此,默認狀況下咱們應該使用加強的for循環,但在處理ArrayList時,若是對性能特別敏感的狀況下,咱們考慮使用手寫的計數循環。(依然是motox手機,100萬的數據量遍歷,手寫的帶計數的循環的性能要比for loop快15倍左右)

對於私有的內部類,考慮使用包訪問權限代替私有訪問權限

以下的類定義:

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);
    }
}

上述代碼中,咱們定義了一個私有的內部類(Foo$Inner),這個私有類直接訪問了外部類的一個私有方法doStuff(), 同時訪問了外部對象的私有的字段。這是合法的,代碼也如預期打印出了"Value is 27"。 問題在於虛擬機認爲直接從Foo$Inner訪問Foo的私有方法是不合法的,由於這兩個是不一樣的類,然而Java的語法又容許這樣的訪問。爲了解決這個問題,編譯器生成了一系列的合成方法:

/*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()方法時會調用這些靜態方法。這就意味着上述的代碼歸根結底到了這樣一種場景,經過方法的存取來訪問字段。在前面有講到過,存取要比直接的字段方法要慢。因此,這個例子展現了一個特定的語言特性所引發的無形的性能損耗。

多使用系統提供的函數

除了常規的理由外,咱們更願意選擇系統函數的緣由是由於系統可能會使用匯編的方式去替換方法的實現,這比JIT生成的代碼有着更好的性能。典型的有String.indexOf()和相關的APIS。相似的,在有JIT的Nexus One手機上,System.arraycopy()方法要比你手寫的循環快9倍左右。

慎用本地方法

使用NDK經過C++等本地代碼開發出來的應用的性能並不必定比用Java代碼來的高效。至少,Java-Native是有傳輸消耗的,JIT也沒法跨越這個邊界進行優化。並且你還會面臨如下的問題:

  • 若是你分配了本地的資源,就須要考慮如何及時的回收這些資源

  • 你須要爲要支持的CPU架構編譯不一樣的動態庫

  • 就算是相同的CPU架構,你可能須要編譯不一樣的版本。舉個例子,爲G1手機的ARM架構的處理器編譯的動態庫在Nexus One上不能發揮出應有的效果,而爲Nexus One編譯的動態庫,在G1上根本就跑不起來。

本地代碼最主要的使用場景是複用已有C/C++代碼把它們使用到Android裏來,而不是爲了提高速度。

多測量

開始優化前,首先要確認問題是否存在。其次,你要能精確的測量出當前問題的性能數據,不然就不能很好的量化提高效果。(對於性能問題都須要用數聽說話)

總結

今天講的內容有什麼用,能減少多少內存,提高多少程序性能呢?

做爲一個有追求的程序員,本文的小技巧都應該歸入我的的編碼習慣。

如今的設備硬件性能這麼強了,還有必要作這些細節優化嗎?

雖然,如今的設備的硬件性能很是強了,可是應用程序面臨的內存、性能等問題仍然很是嚴峻。好比說,加載幾張高清圖,
不作任何限制就能讓程序崩潰。
    又好比,上面提到過的語言層面的一些性能損耗。面向對象的java語言在建立對象時,會先建立它   所繼承的父類的對象。
建立一個繼承關係比較複雜的類的實例時,會順帶建立不少父類實例。這就是無形的性能損耗。然而面向對象能帶來的好處讓
咱們只能選擇妥協。因此,相比於這些咱們沒法解決或者很難解決的問題,本文所介紹的技巧能夠算是性價比很是高的實踐。
相關文章
相關標籤/搜索