Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 性能優化之String篇

Android 性能優化之String篇

編輯:關於Android編程

關於String相關知識都是老掉牙的東西了,但我們經常可能在不經意的String 字符串拼接的情況下浪費內存,影響性能,也常常會成為觸發內存OOM的最後一步。所以本文對String字符串進行深度解析,有助於我們日常開發中提高程序的性能,解決因String 而導致的性能問題。

首先我們先回顧一下String類型的本質

String類型的本質

先看一下String的頭部源碼

/** Strings are constant; their values cannot be changed after they
 * are created. String buffers support mutable strings.
 * Because String objects are immutable they can be shared. 
 * @see StringBuffer
 * @see StringBuilder
 * @see Charset
 * @since 1.0
 */
public final class String implements Serializable, Comparable, CharSequence {

    private static final long serialVersionUID = -6849794470754667710L;

    private static final char REPLACEMENT_CHAR = (char) 0xfffd;

打開String的源碼,類注釋中有這麼一段話“Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.”。

這句話總結歸納了String的一個最重要的特點:

String是值不可變(immutable)的常量,是線程安全的(can be shared)。
接下來,String類使用了final修飾符,表明了String類的第二個特點:String類是不可繼承的。

String類表示字符串。java程序中的所有字符串,如“ABC”,是實現這個類的實例

字符串是常量,它們的值不能被創建後改變。支持可變字符串字符串緩沖區。因為字符串對象是不可改變的,所以它們可以被共享。例如:

String str = "abc";

相當於

String s = new String("abc");

這裡實際上創建了兩個String對象,一個是”abc”對象,存儲在常量空間中,一個是使用new關鍵字為對象s申請的空間,存儲引用地址。

在執行到雙引號包含字符串的語句時,JVM會先到常量池裡查找,如果有的話返回常量池裡的這個實例的引用,否則的話創建一個新實例並置入常量池裡,如上面所示,str 和 s 指向同一個引用.

String的定義方法歸納起來總共為以下四種方式:

直接使用”“引號創建; 使用new String()創建; 使用new String(“abcd”)創建以及其他的一些重載構造函數創建; 使用重載的字符串連接操作符+創建。

常量池

在討論String的一些本質,先了解一下常量池的概念java中的常量池(constant pool)技術,是為了方便快捷地創建某些對象而出現的,當需要一個對象時,就可以從池中取一個出來(如果池中沒有則創建一個),則在需要重復重復創建相等變量時節省了很多時間。常量池其實也就是一個內存空間,不同於使用new關鍵字創建的對象所在的堆空間。
在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量。常量池還具備動態性(java.lang.String.intern()),運行期間可以將新的常量放入池中。

常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共享。

java中基本類型的包裝類的大部分都實現了常量池技術,
即Byte,Short,Integer,Long,Character,Boolean;

Java String對象和字符串常量的關系?

JAVA中所有的對象都存放在堆裡面,包括String對象。字符串常量保存在JAVA的.class文件的常量池中,在編譯期就確定好了。

比如我們通過以下代碼塊:

String s = new String( "myString" );

其中字符串常量是”myString”,在編譯時被存儲在常量池的某個位置。在運行階段,虛擬機發現字符串常量”myString”,它會在一個內部字符串常量列表中查找,如果沒有找到,那麼會在堆裡面創建一個包含字符序列[myString]的String對象s1,然後把這個字符序列和對應的String對象作為名值對( [myString], s1 )保存到內部字符串常量列表中。如下圖所示:

imagevc/gzay1xNfWt/vQ8sHQo6zIu7rzt7W72LbU06a1xFN0cmluZ7bUz/O1xNL908Oho86su6TV4rj2xNqyv8HQse21xLnYvPzKx8jOus7M2LaotcTX1rf70PLB0NTa1eK49sHQse3Jz9a7s/bP1tK7tM6hozwvcD4NCjxwPsD9yOejrFN0cmluZyBzMiA9ICZsZHF1bztteVN0cmluZyZyZHF1bzujrNTL0NDKsXMyu+G008Tasr/X1rf7tK6zo8G/wdCx7cTatcO1vXMxtcS3tbvY1rWjrMv50tRzMrrNczG2vNa4z/LNrNK7uPZTdHJpbme21M/zoaO1q8rHU3RyaW5nttTP83PU2rbRwO+1xNK7uPayu82szrvWw6Osy/nS1LrNczGyu8/gzayhozwvcD4NCjxwPkpBVkHW0LXE19a3+7Sus6PBv7/J0tTX986qU3RyaW5nttTP88q508OjrNfWt/u0rrOjwb+1xNfWt/vQ8sHQsb7J7crHtOa3xdTas6PBv7PY1tCjrNTa19a3+7SuxNqyv8HQse3W0MO/uPbX1rf7tK6zo8G/tcTX1rf70PLB0LbU06bSu7j2U3RyaW5nttTP86OsyrW8ysq508O1xL7NysfV4rj2ttTP86GjPC9wPg0KPHA+1eK49sS/x7DN+MnPsvvK9rXE1+624LnY09rV4rj2U3RyaW5nttTP87rN19a3+7Sus6PBv7XEudjPtaOszfjJz7j309DLtbeoPC9wPg0KPGgyIGlkPQ=="string-在-jvm-的存儲結構">String 在 JVM 的存儲結構

String 在 JVM 的存儲結構
一般而言,Java 對象在虛擬機的結構如下:
對象頭(object header):8 個字節
Java 原始類型數據:如 int, float, char 等類型的數據,各類型數據占內存如 表 1. Java 各數據類型所占內存.
引用(reference):4 個字節
填充符(padding)

如果對於 String(JDK 6)的成員變量聲明如下:

private final char value[]; 
  private final int offset; 
  private final int count; 
  private int hash;

JDK6字符串內存占用的計算方式:

首先計算一個空的 char 數組所占空間,在 Java 裡數組也是對象,因而數組也有對象頭,故一個數組所占的空間為對象頭所占的空間加上數組長度,即 8 + 4 = 12 字節 , 經過填充後為 16 字節。

那麼一個空 String 所占空間為:

對象頭(8 字節)+ char 數組(16 字節)+ 3 個 int(3 × 4 = 12 字節)+1 個 char 數組的引用 (4 字節 ) = 40 字節。

因此一個實際的 String 所占空間的計算公式如下:

8*( ( 8+12+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

其中,n 為字符串長度。

String 方法很多時候我們移動客戶端常用於文本分析及大量字符串處理,
比如高頻率的拼接字符串,Log日志輸出,會對內存性能造成一些影響。可能導致內存占用太大甚至OOM。
頻繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和內存抖動。

String 一些提高性能方法

String的contact()方法

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(0, count, buf, 0);
    str.getChars(0, otherLen, buf, count);
    return new String(0, count + otherLen, buf);
    }

這是concat()的源碼,它看上去就是一個數字拷貝形式,我們知道數組的處理速度是非常快的,但是由於該方法最後是這樣的:return new String(0, count + otherLen, buf);這同樣也創建了10W個字符串對象,這是它變慢的根本原因。

String的intern()方法

當調用 intern 方法時,如果池已經包含一個等於此 String 對象的字符串(該對象由 equals(Object) 方法確定),則返回池中的字符串。否則,將此 String 對象添加到池中,並且返回此 String 對象的引用。

例如:

“abc”.intern()方法的返回值還是字符串”abc”,表面上看起來好像這個方法沒什麼用處。但實際上,它做了個小動作:
檢查字符串池裡是否存在”abc”這麼一個字符串,如果存在,就返回池裡的字符串;如果不存在,該方法會把”abc”添加到字符串池中,然後再返回它的引用。

String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true

過多得使用 intern()將導致 PermGen 過度增長而最後返回 OutOfMemoryError,因為垃圾收集器不會對被緩存的 String 做垃圾回收,所以如果使用不當會造成內存洩露。

關於截取字符串方法的性能比較

對於從大文本中截取少量字符串的應用,String.substring()將會導致內存的過度浪費。 對於從一般文本中截取一定數量的字符串,截取的字符串長度總和與原始文本長度相差不大,現有的 String.substring()設計恰好可以共享原始文本從而達到節省內存的目的。

使用StringBuilder 提高性能

在拼接動態字符串時,盡量用 StringBuffer 或 StringBuilder的 append,這樣可以減少構造過多的臨時 String 對象。但是如何正確的使用StringBuilder呢?

初始合適的長度

StringBuilder繼承AbstractStringBuilder,打開AbstractStringBuilder的源碼

/**
 * A modifiable {@link CharSequence sequence of characters} for use in creating
 * and modifying Strings. This class is intended as a base class for
 * {@link StringBuffer} and {@link StringBuilder}.
 *
 * @see StringBuffer
 * @see StringBuilder
 * @since 1.5
 */
abstract class AbstractStringBuilder {

    static final int INITIAL_CAPACITY = 16;

    private char[] value;

    private int count;

    private boolean shared;

我們可以看到
StringBuilder的內部有一個char[], 不斷的append()就是不斷的往char[]裡填東西的過程。
new StringBuilder(),並且 時char[]的默認長度是16,

private void enlargeBuffer(int min) {
        int newCount = ((value.length >> 1) + value.length) + 2;
        char[] newData = new char[min > newCount ? min : newCount];
        System.arraycopy(value, 0, newData, 0, count);
        value = newData;
        shared = false;
    }

然後如果StringBuilder的剩余容量,無法添加全部內容,如果要append第17個字符,怎麼辦?可以看到enlargeBuffer函數,用System.arraycopy成倍復制擴容!導致內存的消耗,增加GC的壓力。
這要是在高頻率的回調或循環下,對內存和性能影響非常大,或者引發OOM。

同時StringBuilder的toString方法,也會造成char數組的浪費。

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

我們的優化方法是StringBuilder在append()的時候,不是直接往char[]裡塞東西,而是先拿一個String[]把它們都存起來,到了最後才把所有String的length加起來,構造一個合理長度的StringBuilder。

重用的StringBuilder

/**
     * 參考BigDecimal, 可重用的StringBuilder, 節約StringBuilder內部的char[]
     *
     * 參考下面的示例代碼將其保存為ThreadLocal.
     *
     * 
* private static final ThreadLocal threadLocalStringBuilderHolder = new ThreadLocal() { * @Override * protected StringBuilderHelper initialValue() { * return new StringBuilderHelper(256); * } * }; * * StringBuilder sb = threadLocalStringBuilderHolder.get().resetAndGetStringBuilder(); * * 
*/ public class StringBuilderHolder { private final StringBuilder sb; public StringBuilderHolder(int capacity) { sb = new StringBuilder(capacity); } /** * 重置StringBuilder內部的writerIndex, 而char[]保留不動. */ public StringBuilder resetAndGetStringBuilder() { sb.setLength(0); return sb; } }

這個做法來源於JDK裡的BigDecimal類

Log真正需要時候做拼接

對於那些需要高頻率拼接打印Log的場景,封裝一個LogUtil,來控制日志在真正需要輸出時候才去做拼接。比如:

public void log(String  msg ){
        if (BuildConfig.DEBUG){
            Log.e("TAG","Explicit concurrent mark sweep " +
                    "GC freed 10477(686KB) AllocSpace objects, 0(0B) " +
                    "LOS objects, 39% free, 9MB/15MB, paused 915us total 28.320ms"+msg);
        }
    }

總結幾個簡單題目

String s1 = new String("s1") ; 
String s2 = new String("s1") ;

上面創建了幾個String對象?

答案:3個 ,編譯期Constant Pool中創建1個,運行期heap中創建2個.

String s1 = "s1";  
String s2 = s1;  
s2 = "s2";

s1指向的對象中的字符串是什麼?

答案: “s1”

總結

關於String 性能優化,了解String 在 JVM 中的存儲結構,String 的 API 使用可能造成的性能問題以及解決方法,就總結到這。若有錯漏,歡迎補充。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved