性能優化專題十--高效java編碼

一、對常量使用靜態 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 的訪問將使用成本相對較低的“字符串常量”指令,而非字段查詢

注意:此優化僅適用於原語類型和 String 常量,不適用於任意引用類型。儘管如此,最好還是儘可能聲明常量 static final

二、增強型 for 循環使用場所

對於實現 Iterable 接口的集合以及數組,可以使用增強型 for 循環(有時也稱爲“for-each”循環)。對於集合,系統會分配迭代器以對 hasNext() 和 next() 進行接口調用。對於 ArrayList,手寫計數循環的速度快約 3 倍(有或沒有 JIT),但對於其他集合,增強型 for 循環語法與使用顯式迭代器完全等效。

遍歷數組有以下幾種替代方案:

    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() 速度最慢,因爲 JIT 還無法消除每次循環迭代都要獲取數組長度這項成本。

one() 速度更快。它會將所有內容都提取到局部變量中,避免查詢。只有數組長度方面具有性能優勢。

對於沒有 JIT 的設備,two() 速度最快;對於具有 JIT 的設備,two() 與 one() 速度難以區分。two() 使用了在 1.5 版 Java 編程語言中引入的增強型 for 循環語法。

因此,應默認使用增強型 for 循環,但對於性能關鍵型 ArrayList 迭代,不妨考慮使用手寫計數循環。

提示:另請參閱 Josh Bloch 的《Effective Java》第 46 條。

三、對於私有內部類,考慮使用包訪問權限,而非私有訪問權限

請查看以下類定義:

    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),它會直接訪問外部類中的私有方法和私有實例字段。這是合乎規則的,並且代碼會按預期輸出“Value is 27”。

問題在於,虛擬機認爲從 Foo$Inner 直接訪問 Foo 的私有成員不符合規則,因爲 Foo 和 Foo$Inner 屬於不同的類,雖然 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);
    }

javac Foo.java後,在執行javap -verbose  Foo.class,可以看到在Foo類中確實由編譯器幫助生成了輔助方法,以訪問到外部類中的私有成員變量MValue,以及私有方法doStuff()

每當需要訪問外部類中的 mValue 字段或調用外部類中的 doStuff() 方法時,內部類代碼就會調用這些靜態方法。這意味着以上代碼實際上可以歸結爲一種情況,那就是您通過訪問器方法訪問成員字段。之前我們討論了訪問器的速度比直接訪問字段要慢,因此這是一個特定習慣用語會對性能產生“不可見”影響的示例。

如果您在性能關鍵位置 (hotspot) 使用這樣的代碼,則可以將內部類訪問的字段和方法聲明爲擁有包訪問權限(而非私有訪問權限),從而避免產生相關開銷。遺憾的是,這意味着同一軟件包中的其他類可以直接訪問這些字段,因此不應在公共 API 中使用此方法。

四、避免枚舉,浮點數的使用。

  • 使用自定義註解代替枚舉

單個枚舉會使應用的 classes.dex 文件增加大約 1.0 到 1.4KB 的大小。這些增加的大小會快速累積,產生複雜的系統或共享庫。如果可能,請考慮使用 @IntDef 註釋和代碼縮減移除枚舉並將它們轉換爲整數。此類型轉換可保留枚舉的各種安全優勢。

日常我們使用枚舉來定義一些常量的取值,使用枚舉能夠確保參數的安全性。但是Android開發文檔上指出,使用枚舉會比使用靜態變量多消耗兩倍的內存,應該儘量避免在Android中使用枚舉,那麼枚舉爲什麼會更消耗內存呢?下面一起分析一下。

public enum  Sex {
    MAN, WOMAN;
}

從反編譯的代碼來看,我們定義的枚舉,編譯器會將其轉換成一個類,這個類繼承自java.lang.Enum類,除此之外,編譯器還會幫我們生成多個枚舉類的實例,賦值給我們定義的枚舉類型常量,並且還聲明瞭一個枚舉對象的數組,保存了所有的枚舉對象。下面我們分別來計算一下采用靜態變量和枚舉佔用內存的大小對比。
下面是反編譯後的枚舉類文件,可以看到明顯比我們想象中的要佔用更多內存空間:

public final class Sex extends Enum {
    public static Sex[] values()
    {
        return (Sex[])$VALUES.clone();
    }
 
    public static Sex valueOf(String s)
    {
        return (Sex)Enum.valueOf(com/liunian/androidbasic/enumtest/Sex, s);
    }
 
    private Sex(String s, int i)
    {
        super(s, i);
    }
 
    public static final Sex MAN;
    public static final Sex WOMAN;
    private static final Sex $VALUES[];
 
    static 
    {
        MAN = new Sex("MAN", 0);
        WOMAN = new Sex("WOMAN", 1);
        $VALUES = (new Sex[] {
            MAN, WOMAN
        });
    }
}

枚舉佔用內存的大小比靜態變量多得多,枚舉類型數據的內存優化,使用註解的方案

  • 避免使用浮點數

一般來講,在 Android 設備上,浮點數要比整數慢約 2 倍。

在速度方面,float 和 double 在更現代的硬件上沒有區別。在空間方面,double 所佔空間大 2 倍。對於臺式機,假定空間不是問題,您應該優先使用 double,而非 float

此外,即使對於整數,某些處理器擁有硬件乘法器,卻缺少硬件除法器。在這種情況下,整數的除法和取模運算會在軟件中執行;如果您要設計哈希表或要進行大量數學運算,則需要考慮這一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章