# Kotlin使用優化(四)

小小小小小粽子-發表於2019-03-26

上回我們聊到Kotlin的屬性訪問,編譯器確實默默幫我們做了很多事情,隱藏的事情越多,潛在開銷的可能性就越大,好在,我們通過分析知道了可能的優化點,這一節我們以此為基礎做一些延申。

相信大家在使用第三方SDK的時候,都需要在Application裡面初始化,它們需要一個Context物件,我們通常宣告一個成員變數然後在onCreate方法裡面初始它們,但是Kotlin裡面物件有Nullable跟NonNull的區別,我們明知道下次在使用這個物件的時候它不可能為空,還是得一遍遍的檢查它是否賦值,否則編譯器就會發出警告。

Kotlin提供了一個便利的語法糖lateinit,使用它,我們依然可以在onCreate裡面初始化物件,同時把物件宣告成NonNull。來看個例子:

class Main {
    private lateinit var name: String
    
    fun onCreate() {
        name = "王小明"
  }
}
複製程式碼

從位元組碼來看,這被編譯成一個普通的Java欄位:

public final class Main {


  // access flags 0x2
  private Ljava/lang/String; name

  // access flags 0x11
  public final onCreate()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    LDC "\u738b\u5c0f\u660e"
    PUTFIELD Main.name : Ljava/lang/String;
   L1
    LINENUMBER 7 L1
    RETURN
   L2
    LOCALVARIABLE this LMain; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LMain; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
}
複製程式碼

但是,當我們使用這個name欄位的時候:

fun onCreate() {
    name = "王小明"
  print(name)
}
複製程式碼

編譯器會做額外的檢查:

 LINENUMBER 4 L0
    ALOAD 0
    LDC "\u738b\u5c0f\u660e"
    PUTFIELD Main.name : Ljava/lang/String;
   L1
    LINENUMBER 5 L1
    ALOAD 0
    GETFIELD Main.name : Ljava/lang/String;
    DUP
    IFNONNULL L2
    LDC "name"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException (Ljava/lang/String;)V
   L2
    ASTORE 1
   L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L4
   L5
    LINENUMBER 6 L5
    RETURN
   L6
    LOCALVARIABLE this LMain; L0 L6 0
    MAXSTACK = 2
    MAXLOCALS = 2
複製程式碼

主要是這裡:

 LINENUMBER 5 L1
    ALOAD 0
    GETFIELD Main.name : Ljava/lang/String;
    DUP
    IFNONNULL L2
    LDC "name"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException (Ljava/lang/String;)V
複製程式碼

我們可以看到,每次我們訪問這個欄位,編譯器會檢查這個欄位是否已經初始化,我們可以減少檢查的次數,笨方法就是在使用前把它賦值給一個本地變數:

fun onCreate() {
    name = "王小明"
  val value = name
  print(value)
    print(value)
}
複製程式碼

這樣編譯器只會在我們給value賦值時檢查name是否已初始化,我們後來再使用本地變數value也不會有額外的開銷:

// access flags 0x11
  public final onCreate()V
   L0
    LINENUMBER 4 L0
    ALOAD 0
    LDC "\u738b\u5c0f\u660e"
    PUTFIELD Main.name : Ljava/lang/String;
   L1
    LINENUMBER 5 L1
    ALOAD 0
    GETFIELD Main.name : Ljava/lang/String;
    DUP
    IFNONNULL L2
    LDC "name"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwUninitializedPropertyAccessException (Ljava/lang/String;)V
   L2
    ASTORE 1
   L3
    LINENUMBER 6 L3
   L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 7 L6
   L7
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L8
   L9
    LINENUMBER 8 L9
    RETURN
   L10
    LOCALVARIABLE value Ljava/lang/String; L3 L10 1
    LOCALVARIABLE this LMain; L0 L10 0
    MAXSTACK = 2
    MAXLOCALS = 2
複製程式碼

從位元組碼裡就可以看出這一點。

要更優雅一些,我們可以使用also擴充套件方法這樣:

fun onCreate() {
    name = "王小明"
    name.also {  
    
     } 
   }
複製程式碼

這樣編譯器也只會做一次檢查,原因不再贅述。

這種使用外部定義的成員的場景比較常見,我們甚至經常會在內部類裡面訪問外部類的成員。

class Main {
    private var name = "王小明"    
    inner class Inner {
        fun printName() {
            print(name)
        }
    }
}
複製程式碼

來看看反編譯的Java程式碼:

public final class Main {
   private String name = "王小明";    // $FF: synthetic method
  public static final void access$setName$p(Main $this, String var1) {
      $this.name = var1;
  }

   public final class Inner {
      public final void printName() {
         String var1 = Main.this.name;
         System.out.print(var1);
  }
   }
}
複製程式碼

編譯器生成了兩個類,還生成了一些特殊的方法讓內部類來訪問Main類的私有成員,我們也可以看到printName方法最終就是呼叫這個生成的方法來獲取name的值的:

// access flags 0x11
  public final printName()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    GETFIELD Main$Inner.this$0 : LMain;
    INVOKESTATIC Main.access$getName$p (LMain;)Ljava/lang/String;
    ASTORE 1
   L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L2
   L3
    LINENUMBER 7 L3
    RETURN
   L4
    LOCALVARIABLE this LMain$Inner; L0 L4 0
    MAXSTACK = 2
    MAXLOCALS = 2

複製程式碼

如果我們把name宣告成public呢?

 // access flags 0x2
  private Ljava/lang/String; name
  @Lorg/jetbrains/annotations/NotNull;() // invisible

複製程式碼

很遺憾,編譯器還是幫我們編譯成私有成員,然後生成get,set方法,然後還是呼叫方法去獲取name的值。

 L0
    LINENUMBER 6 L0
    ALOAD 0
    GETFIELD Main$Inner.this$0 : LMain;
    INVOKEVIRTUAL Main.getName ()Ljava/lang/String;
    ASTORE 1
複製程式碼

我還不信了,宣告成public你還給我直接訪問,還要給我生成get,set方法?

於是我又給name加上了@JvmField註解,沒錯,我總是想搞事情,哈哈

public final class Main {
   @JvmField
   @NotNull  
 public String name = "王小明";   
   public final class Inner {
      public final void printName() {
         String var1 = Main.this.name;
  System.out.print(var1);
  }
   }
}
複製程式碼

就結果而言,我是滿意的,這下編譯器終於不會悄咪咪地給我生成額外的方法了。

今天的故事,到這裡差不多就要結束了,下回,我們來說說讓Kotlin看起來不那麼OOP的伴生物件的坑,期待吧?

快來關注我吧!

相關文章