Related demo source code;

Based on macOs: 10.13 / AS: 3.3.2 rainfall distribution on 10-12 / Android build – tools: 28.0.0 / JDK: 1.8

1. Why

I happened to see that the String concatenation in the log statement has been optimized to StringBuilder.

// MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";
    private void methodBoolean(boolean showLog) {
        Log.d(TAG, "methodBoolean: "+ showLog); }}Copy the code
# corresponding smali code
.method private methodBoolean(Z)V
    .locals 3
    .param p1, "showLog"    # Z

    .line 51
    const-string v0, "MainActivity" # define the TAG variable value
    new-instance v1, Ljava/lang/StringBuilder; # create a StringBuilder
    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V

    # define the first part of the string literal in the Log MSG argument
    const-string v2, "methodBoolean: "

    # concatenate and print String into v1 register
    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v1

    Call the Log method to print the Log
    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    .line 52
    return-void
.end method
Copy the code

Remembering the old “StringBuilder performs better than String when concatenating lots of strings,” I wondered if this was true, and if it was true in all scenarios, so I wanted to explore. For simplicity’s sake, I wrote the source code in Java instead of Kotlin.

2. The test

Will concatenation be efficient since the bottom layer is optimized to StringBuilder? Under test

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    /** * String Loop stitching test **@paramLoop Number of cycles *@paramBase concatenation string *@returnTime spent, in ms */
    private long methodForStr(int loop, String base) {
        long startTs = System.currentTimeMillis();
        String result = "";
        for (int i = 0; i < loop; i++) {
            result += base;
        }
        return System.currentTimeMillis() - startTs;
    }

    /** * StringBuilder loop stitching test */
    @Keep
    private long methodForSb(int loop, String base) {
        long startTs = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < loop; i++) {
            sb.append(base);
        }
        String result = sb.toString();
        returnSystem.currentTimeMillis() - startTs; }}Copy the code

It takes about 460ms:1ms to get the smALI string by cyclic stitching 5000 times on Samsung S8 +, showing an obvious efficiency gap.

3. Smali loop splicing code analysis

MethodForStr (int loop, String Base) (String base, String base);

.method private methodForStr(ILjava/lang/String;)J
    .locals 5
    .param p1, "loop"    # I denotes the argument loop
    .param p2, "base"    # Ljava/lang/String;

    .line 73
    invoke-static {}, Ljava/lang/System;->currentTimeMillis()J Get the loop start timestamp

    move-result-wide v0

    .line 74
    .local v0, "startTs":J # v0 represents the local variable startTs of type long
    const-string v2, ""

    .line 75
    .local v2, "result":Ljava/lang/String; # v2 represents the local variable result
    const/4 v3, 0x0 # define the initialization of the for loop variable I

    .local v3, "i":I
    :goto_0  # for at the beginning of the body of the loop
    if-ge v3, p1, :cond_0  If I >= loop, jump to cond_0 and exit the loop, otherwise continue with the following code

    # this is the body of the for loop:
    # 1. Create a StringBuilder object
    # 2. Concatenate the result + base string, then pass toString() to get the concatenate result
    # 3. Reassign the result to the result variable
    # 4. Enter the next cycle
    .line 76
    new-instance v4, Ljava/lang/StringBuilder;
    invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v4, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v4, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v2

    The # for loop variable I increments itself by 1 and proceeds to the next loop
    .line 75
    add-int/lit8 v3, v3, 0x1 Add the value 0x1 to the second register v3, and then add it to the first register v3

    goto :goto_0 # jump to the goto_0 tag, i.e., recalculate the loop condition and execute the loop body

    .line 78
    .end local v3    # "i":I
    :cond_0 # define the tag cond_0

    When the loop ends, get the current timestamp and calculate the elapsed time
    invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
    move-result-wide v3
    sub-long/2addr v3, v0

    return-wide v3
.end method
Copy the code

Based on the smali code above, the source code can be derived inversely as follows:

private long methodForStr(int loop, String base) {
    long startTs = System.currentTimeMillis();
    String result = "";
    for (int i = 0; i < loop; i++) {
        // Change the concatenation of String to StringBuilder each time in the body of the loop
        // Is this negative optimization?
        StringBuilder sb = new StringBuilder();
        sb.append(result);
        sb.append(base);
        result = sb.toString();
    }
    return System.currentTimeMillis() - startTs;
}
Copy the code

4. Source code analysis

4.1 String.java

/* * 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 * */
public final class String
    implements java.io.Serializable.Comparable<String>, CharSequence {
        // String is actually a char array, but because it is private final, it is immutable.
        private final char value[];
    }
Copy the code

The class annotation describes it as immutable. Each literal is an object. When you modify a string, you do not modify it in the original memory, but redirect it to a new object:

String str = "a"; // String object "a"
str = "a" + "a"; // String object "aa"
Copy the code

Each time + is performed, a new String is generated:

// Combined with the smali analysis in Part 3, it can be found that:
// Each time in the body of the for loop, a 'StringBuilder' object is created and a 'String' object is generated to concatenate the result;
private long methodForStr(int loop, String base) {
    long startTs = System.currentTimeMillis();
    String result = "";
    for (int i = 0; i < loop; i++) {
        result += base;
    }
    return System.currentTimeMillis() - startTs;
}
Copy the code

Frequent creation of objects in the loop body will also lead to a large number of objects being abandoned, triggering GC, and frequently stopping the world will naturally lead to longer splicing time, as shown in the following figure:

4.2 StringBuilder.java

/**
 * A mutable sequence of characters.  This class provides an API compatible
 * with {@code StringBuffer}, but with no guarantee of synchronization.
 * */
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable.CharSequence{}

AbstractStringBuilder class comments indicate that it is actually an array of mutable characters and that the core logic is actually implemented in AbstractStringBuilder
// StringBuilder.append (" STR"
abstract class AbstractStringBuilder implements Appendable.CharSequence {
    char[] value; // Is used to actually store the sequence of characters corresponding to the string
    int count; // The number of characters stored

    AbstractStringBuilder() {
    }

    // Provide a reasonable initial capacity to reduce capacity expansion times and improve efficiency
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

    @Override
    public AbstractStringBuilder append(CharSequence s) {
        if (s == null)
            return appendNull();
        if (s instanceof String)
            return this.append((String)s);
        if (s instanceof AbstractStringBuilder)
            return this.append((AbstractStringBuilder)s);

        return this.append(s, 0, s.length());
    }

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len); // Make sure the value array has enough space to store all the characters of the variable STR
        str.getChars(0, len, value, count); // Extract all characters from the variable STR and append them to the end of the value array
        count += len;
        return this;
    }

    // If the current value array capacity is insufficient, automatic expansion: create a new array and copy the original array data
    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); }}}// String.java
public final String{
    // Copies the specified range of characters from the current string into the array after the DST dstBegin bit
    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        // omit some judgment code
        getCharsNoCheck(srcBegin, srcEnd, dst, dstBegin);
    }

    @FastNative
    native void getCharsNoCheck(int start, int end, char[] buffer, int index);
}
Copy the code

StringBuilder operates on the same char[] array every time the string is append.

5. Is all string concatenation scenarios preferredStringBuilder ?

Not really. For example, if you’re using compile-time constants, you can just use String. If you’re using StringBuilder, AS will tell you to change to String.

For non-circular String concatenation scenarios, it makes no difference whether the source code is String or StringBuilder, the bytecode is converted to StringBuilder;

    // Compile-time constant tests
    private String methodFixStr(a) {
        return "a" + "a" + "a" + "a" + "a" + "a";
    }

    private String methodFixSb(a) {
        StringBuilder sb = new StringBuilder();
        sb.append("a");
        sb.append("a");
        sb.append("a");
        sb.append("a");
        sb.append("a");
        return sb.toString();
    }
Copy the code

Corresponding SMali code:

.method private methodFixStr()Ljava/lang/String;
    .locals 1

    .line 100
    const-string v0, "aaaaaa" The compiler optimizes directly to the final result

    return-object v0
.end method

# stringBuilder is not optimized and is still pieced together step by step
This is why the IDE prompts for String
.method private methodFixSb()Ljava/lang/String;
    .locals 2

    .line 108
    new-instance v0, Ljava/lang/StringBuilder;
    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V

    .line 109
    .local v0, "sb":Ljava/lang/StringBuilder;
    const-string v1, "a"

    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 110
    const-string v1, "a"
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 111
    const-string v1, "a"
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 112
    const-string v1, "a"
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 113
    const-string v1, "a"
    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 114
    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v1
    return-object v1
.end method
Copy the code