文檔當前狀態:**beta0.2**
* [x] 選題收集:2017/10/15
* [x] 初稿整理:
* [ ] 補充校對:
* [ ] 入庫存檔:
---
>大部分內容整理自:[Exploring Kotlin’s hidden costs](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62)
,更多見文末的 [參考引文](#參考引文)。
[TOC]
---
>通過對[《Kotlin 官方參考文檔 中文》](https://www.kotlincn.net/docs/reference/) 的學習,已經能夠在項目,使用kotlin進行開發了。kotlin提供的一些 “語法糖”特性,能很大程度上改善java 沉冗的語法,提高編碼效率,但這些的背后是如何實現的、對代碼運行性能又有怎么樣的影響,所知并不多,下文會通過對部分kotli特性的進一步說明,來幫助我們在 更有效的使用kotlin。
需要說明的是,相較于java,文中提及到kotlin 語法糖會帶來的一些額外成本:
* 基本類型的裝箱、拆箱
* 分配短期對象
* 生成額外的方法
這些在大部分場景下,并不會對性能造成很大的影響,但涉及到一些高頻調用的場景下,就需要我們慎重對待。
### **空值安全**
Kotlin 語言中最好的特性之一就是明確區分了可空與不可空類型。這可以使編譯器在運行時通過禁止任何代碼將 null 或者可空值分配給不可空變量來有效地阻止意想不到的 NullPointerException。
#### **不可空參數運行時檢查**
>**Tips: 在生產環境下,可以在混淆規則中 優化kotlin的 空值檢查**
讓我們聲明一個公共的接收一個不可空 String 做為參數的函數:
```
fun sayHello(who: String) {
println("Hello $who")
}
```
現在看一下編譯之后的等同的 Java 代碼:
```
public static final void sayHello(@NotNull String who) {
Intrinsics.checkParameterIsNotNull(who, "who");
String var1 = "Hello " + who;
System.out.println(var1);
}
```
注意,Kotlin 編譯器是 Java 的好公民,它在參數上添加了一個 @NotNull 注解,因此當一個 null 值傳過來的時候 Java 工具可以據此來顯示一個警告。
但對于外部調用者,一個注解并不足以實現空值安全。這就是為什么編譯器在函數的剛開始處還添加了一個可以檢測參數并且如果參數為 null 就拋出 IllegalArgumentException 的靜態方法調用。為了使不安全的調用代碼更容易修復,這個函數會快速失敗而不是在后期隨機地拋出 NullPointerException。
在實踐中,每一個公共的函數都會在每一個不可空引用參數上添加一個 Intrinsics.checkParameterIsNotNull() 靜態調用。私有函數不會有這些檢查,因為編譯器會保證 Kotlin 類中的代碼是空值安全的。
這些靜態調用對性能的影響可以忽略不計,并且他們在調試或者測試一個 app 時確實很有用。話雖這么說,但你還是可能將他們視為一種正式版本中不必要的額外成本。在這種情況下,可以通過使用編譯器選項中的 -Xno-param-assertions 或者添加以下的混淆規則來禁用運行時空值檢查:
```
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
```
注意,這條混淆規則只有在優化功能開啟的時候有效。優化功能在默認的安卓混淆配置中是禁用的。
### **裝箱、拆箱**
#### **可空的基本類型**
> **Tips: 為了更好的可讀性和更佳的性能盡量使用不可空基礎類型。**
可空類型都是引用類型。將基礎類型變量聲明為 可空的話,會阻止 Kotlin 使用 Java 中類似 int或者 float 那樣的基礎類型,相應的類似 Integer 或者 Float 那樣的裝箱引用類型會被使用,這就引起了額外的裝箱或拆箱成本。
此外,當使用可空類型,Kotlin編譯器會迫使你編寫安全的代碼:
```
fun add(a: Int, b: Int): Int {
return a + b
}
fun add(a: Int?, b: Int?): Int {
return (a ?: 0) + (b ?: 0)
}
```
為了更好的可讀性和更佳的性能,盡量使用不可空基礎類型。
#### **基礎類型的 泛型使用**
>**Tips: 涉及基本類型相關的操作時,優先使用指定類型的方法,而不是泛型。**
Kotlin 中有三種數組類型:
* IntArray, FloatArray 還有其他的:基礎類型數組。編譯為 int[], float[] 和其他的類型。
* Array<T>:不可空對象引用類型化數組,這涉及到對基礎類型的裝箱。
* Array<T?>:可空對象引用類型化數組。很明顯,這也涉及到基礎類型的裝箱。
如果你需要一個不可空的基礎類型數組,最好用 IntArray 而不是 Array<Int> 來避免隱式的拆/裝箱。
### **可變參數**
>Tips: 如果 頻繁調用的代碼中 含有 可變參數,考慮使用 一個真正的數組 替代它
Kotlin 允許聲明具有數量可變的參數的函數,就像 Java 那樣。聲明語法有點不一樣:
```
fun printDouble(vararg values: Int) {
values.forEach { println(it * 2) }
}
```
就像 Java 中那樣,vararg 參數實際上被編譯為一個給定類型的 數組 參數。你可以用三種不同的方式來調用這些函數:
#### 1. 傳入多個參數
```
printDouble(1, 2, 3)
```
Kotlin 編譯器會將這行代碼轉化為創建并初始化一個新的數組,和 Java 編譯器做的完全一樣:
```
printDouble(new int[]{1, 2, 3});
```
因此有創建一個新數組的開銷,但與 Java 相比這并不是什么新鮮事。
#### 2. 傳入一個單獨的數組
這就是不同之處。在 Java 中,你可以直接傳入一個現有的數組引用作為可變參數。但是在 Kotlin 中你需要使用 分布操作符:
```
val values = intArrayOf(1, 2, 3)
printDouble(*values)
```
在 Java 中,數組引用被“原樣”傳入函數,而無需分配額外的數組內存。然而,分布操作符編譯的方式不同,正如你在(等同的)Java 代碼中看到的:
```
int[] values = new int[]{1, 2, 3};
printDouble(Arrays.copyOf(values, values.length));
```
每當調用這個函數時,現在的數組總會被復制。好處是代碼更安全:允許函數在不影響調用者代碼的情況下修改這個數組。但是會分配額外的內存。
注意,在 Kotlin 代碼中調用一個有可變參數的 Java 方法會產生相同的效果。
#### 3. 傳入混合的數組和參數
分布操作符主要的好處是,它還允許在同一個調用中數組參數和其他參數混合在一起進行傳遞。
```
val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)
```
這是如何編譯的呢?生成的代碼十分有意思:
```
int[] values = new int[]{1, 2, 3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());
```
除了創建新數組外,一個臨時的 builder 對象被用來計算最終的數組大小并填充它。這就使得這個方法調用又增加了另一個小的成本。
> **在 Kotlin 中調用一個具有可變參數的函數時會增加創建一個新臨時數組的成本,即使是使用已有數組的值。對方法被反復調用的性能關鍵性的代碼來說,考慮添加一個以真正的數組而不是 可變數組 為參數的方法。**
### **Lambda 表達式**
#### **捕獲、非捕獲的Lambda**
>**Tips:使用lambda表達式時,在保證簡潔的前提下,優先考慮寫成非捕獲**
什么是“捕獲和非捕獲的Lambda表達式”?(參考自[Java 8 的新特性和改進總覽](http://www.oschina.net/translate/everything-about-java-8) )?
當Lambda表達式訪問一個定義在Lambda表達式體外的非靜態變量或者對象時,這個Lambda表達式稱為“捕獲的”。比如,下面這個lambda表達式捕捉了變量x:
```
int x = 5; return y -> x + y;
```
為了保證這個lambda表達式聲明是正確的,被它捕獲的變量必須是“有效final”的。所以要么它們需要用final修飾符號標記,要么保證它們在賦值后不能被改變。Lambda表達式是否是捕獲的和性能悄然相關。一個非不捕獲的lambda通常比捕獲的更高效,雖然這一點沒有書面的規范說明(據我所知),而且也不能為了程序的正確性指望它做什么,**非捕獲的lambda只需要計算一次. 然后每次使用到它都會返回一個唯一的實例。而捕獲的lambda表達式每次使用時都需要重新計算一次**,而且從目前實現來看,它很像實例化一個匿名內部類的實例。,
當然,我們并不說 一定要寫成 非捕獲的,在某些場景,相比“刻意”非捕獲,捕獲式的性能 也不是那么難看。
在這個工程中,我們使用了大量的Lambda表達式來實現回調處理。然而在這些使用Lambda實現的回調中很多并沒有捕獲局部變量,而是需要引用當前類的變量或者調用當前類的方法。然而目前仍需要對象分配。下面就是我們提到的例子的代碼:
```
public MessageProcessor() {}
public int processMessages() {
return queue.read(obj -> {
if (obj instanceof NewClient) {
this.processNewClient((NewClient) obj);
}
...
});
}
```
有一個簡單的辦法解決這個問題,我們將Lambda表達式的代碼提前到構造方法中,并將其賦值給一個成員屬性。在調用點我們直接引用這個屬性即可。下面就是修改后的代碼:
```
private final Consumer<Msg> handler;
public MessageProcessor() {
handler = obj -> {
if (obj instanceof NewClient) {
this.processNewClient((NewClient) obj);
}
...
};
}
public int processMessages() {
return queue.read(handler);
}
```
然而上面的“優化后”后代碼給卻給整個工程帶來了一個嚴重的問題:性能分析表明,這種修改產生很大的對象申請,其產生的內存申請在總應用的60%以上。
類似這種無關上下文的優化可能帶來其他問題。
1. 純粹為了優化的目的,使用了非慣用的代碼寫法,可讀性會稍差一些。
2. 內存分配方面的問題,示例中為MessageProcessor增加了一個成員屬性,使得MessageProcessor對象需要申請更大的內存空間。Lambda表達式的創建和捕獲位于構造方式中,使得MessageProcessor的構造方法調用緩慢一些。
我們遇到這種情況,需要進行內存分析,結合合理的業務用例來進行優化。有些情況下,我們使用成員屬性確保為經常調用的Lambda表達式只申請一個對象,這樣的緩存策略大有裨益。任何性能調優的科學的方法都可以進行嘗試。
上述的方法也是其他程序員對Lambda表達式進行優化應該使用的。書寫整潔,簡單,函數式的代碼永遠是第一步。任何優化,如上面的提前代碼作為成員屬性,都必須結合真實的具體問題進行處理。變量捕獲并申請對象的Lambda表達式并非不好,就像我們我們寫出new Foo()代碼并非一無是處一樣。
鏈接中有這方面更詳細的說明—— [深入探索Java 8 Lambda表達式](http://www.infoq.com/cn/articles/Java-8-Lambdas-A-Peek-Under-the-Hood)
### **伴生對象**
Kotlin 類沒有靜態變量和方法。相應的,類中與實例無關的字段和方法可以通過**伴生對象**或**包級屬性**來聲明。
#### **kotlin中的 “static 全局變量(伴生對象實現)**
>**Tips: 始終用 const 關鍵字來聲明基本類型和字符串常量**
在 Kotlin 中你通常在一個伴生對象中聲明在類中使用的“靜態”常量。
```
class MyClass {
companion object {
private val TAG = "TAG"
}
fun helloWorld() {
println(TAG)
}
}
```
這段代碼看起來干凈整潔,但是幕后發生的事情卻十分不堪。
基于上述原因,訪問一個在伴生對象中聲明為私有的常量實際上會在這個伴生對象的實現類中生成一個額外的、合成的 getter 方法。
```
GETSTATIC be/myapplication/MyClass.Companion : Lbe/myapplication/MyClass$Companion;
INVOKESTATIC be/myapplication/MyClass$Companion.access$getTAG$p (Lbe/myapplication/MyClass$Companion;)Ljava/lang/String;
ASTORE 1
```
但是更糟的是,這個合成方法實際上并沒有返回值;它調用了 Kotlin 生成的實例 getter 方法:
```
ALOAD 0
INVOKESPECIAL be/myapplication/MyClass$Companion.getTAG ()Ljava/lang/String;
ARETURN
```
當常量被聲明為 public 而不是 private 時,getter 方法是公共的并且可以被直接調用,因此不需要上一步的方法。但是 Kotlin 仍然必須通過調用 getter 方法來訪問常量。
所以我們真的解決了問題嗎?并沒有!事實證明,為了存儲常量值,Kotlin 編譯器實際上在主類級別上而不是伴生對象中生成了一個 private static final 字段。但是,因為在類中靜態字段被聲明為私有的,在伴生對象中需要有另外一個合成方法來訪問它
```
INVOKESTATIC be/myapplication/MyClass.access$getTAG$cp ()Ljava/lang/String;
ARETURN
```
最終,那個合成方法讀取實際值:
```
GETSTATIC be/myapplication/MyClass.TAG : Ljava/lang/String;
ARETURN
```
換句話說,當你從一個 Kotlin 類來訪問一個伴生對象中的私有常量字段的時候,與 Java 直接讀取一個靜態字段不同,你的代碼實際上會:
* 在伴生對象上調用一個靜態方法,
* 然后在伴生對象上調用實例方法,
* 然后在類中調用靜態方法,
* 讀取靜態字段然后返回它的值。
這是等同的 Java 代碼:
```
public final class MyClass {
private static final String TAG = "TAG";
public static final Companion companion = new Companion();
// synthetic
public static final String access$getTAG$cp() {
return TAG;
}
public static final class Companion {
private final String getTAG() {
return MyClass.access$getTAG$cp();
}
// synthetic
public static final String access$getTAG$p(Companion c) {
return c.getTAG();
}
}
public final void helloWorld() {
System.out.println(Companion.access$getTAG$p(companion));
}
}
```
如何改善?
首先,通過 const 關鍵字聲明值為編譯時常量來完全避免任何的方法調用是有可能的。這將有效地在調用代碼中直接內聯這個值,但是只有基本類型和字符串才能如此使用。
```
class MyClass {
companion object {
private const val TAG = "TAG"
}
fun helloWorld() {
println(TAG)
}
}
```
第二,你可以在伴生對象的公共字段上使用 @JvmField 注解來告訴編譯器不要生成任何的 getter 和 setter 方法,就像純 Java 中的常量一樣做為類的一個靜態變量暴露出來。實際上,這個注解只是單獨為了兼容 Java 而創建的,如果你的常量不需要從 Java 代碼中訪問的話,我是一點也不推薦你用一個晦澀的交互注解來弄亂你漂亮 Kotlin 代碼的。此外,它只能用于公共字段。在 Android 的開發環境中,你可能只在實現 Parcelable 對象的時候才會使用這個注解:
```
class MyClass() : Parcelable {
companion object {
@JvmField
val CREATOR = creator { MyClass(it) }
}
private constructor(parcel: Parcel) : this()
override fun writeToParcel(dest: Parcel, flags: Int) {}
override fun describeContents() = 0
}
```
最后,你也可以用 ProGuard 工具來優化字節碼,希望通過這種方式來合并這些鏈式方法調用,但是絕對不保證這是有效的。
>* 與 Java 相比,在 Kotlin 中從伴生對象里讀取一個 static 常量會增加 2 到 3 個額外的間接級別并且每一個常量都會生成 2 到 3個方法。
>* 始終用 const 關鍵字來聲明基本類型和字符串常量從而避免這些(成本)
>* 對其他類型的常量來說,你不能使用const,因此如果你需要反復訪問這個常量的話,你或許可以把它的值緩存在一個本地變量中。同時,最好在它們自己的對象而不是伴生對象中來存儲公共的全局常量。
#### kotlin中的**static屬性**:使用**伴生對象** 還是 **包級屬性**?
這里并不打算給出選擇 哪個更好的建議,你需要了解兩者的幕后實現后,結合具體的代碼進行選擇。
這里我們使用相同的 例子來說明:
在java中 ,代碼是這樣:
```
class MyClass {
private static final String TAG = "TAG";
public void helloWorld(){
System.out.println(TAG );
}
}
```
再來看看其他寫法
寫法1:包級屬性的方法:
```
private const val TAG = "TAG"
class MyClass {
fun helloWorld() {
println(TAG)
}
}
```
寫法1:decompile后像這樣
```
//額外的一個類
public final class MyClassKt {
private static final String TAG = "TAG";
}
public final class MyClass {
public final void helloWorld() {
String var1 = "TAG";
System.out.println(var1);
}
}
```
寫法2:寫成包級屬性,但不寫成 const
```
private val TAG = "TAG"
class MyClass {
fun helloWorld() {
println(TAG)
}
}
```
寫法2:decompile后像這樣
```
//額外的一個類
public final class MyClassKt {
private static final String TAG = "TAG";
// $FF: synthetic method
@NotNull
public static final String access$getTAG$p() {
return TAG;
}
}
public final class MyClass {
public final void helloWorld() {
String var1 = MyClassKt.access$getTAG$p();
System.out.println(var1);
}
}
```
寫法3:將全局常量值 寫成自己類的成員屬性
```
class MyClass {
//無法使用const
private val TAG = "TAG"
fun helloWorld() {
println(TAG)
}
}
```
寫法3:decompile后像這樣
```
public final class MyClass {
private final String TAG = "TAG";
public final void helloWorld() {
String var1 = this.TAG;
System.out.println(var1);
}
}
```
了解多種寫法的幕后實現后,根據實際情況選擇不用的 寫法
### **委托屬性**
#### **泛型委托**
>Tips: **對應基本類型的委托,最好使用具體類型 來避免 裝/拆箱**
委托方法也可以被聲明成泛型的,這樣一來不同類型的屬性就可以復用同一個委托類了。
```
private var maxDelay: Long by SharedPreferencesDelegate<Long>()
```
然而,如果像上例那樣對基本類型使用泛型委托的話,即便聲明的基本類型非空,也會在每次讀寫屬性的時候觸發裝箱和拆箱的操作。
對于非空基本類型的委托屬性來說,最好使用給定類型的特定委托類而不是泛型委托來避免每次訪問屬性時增加裝箱的額外開銷。
#### **lazy的線程安全**
>**Tips: 使用lazy()時,考慮指定LazyThreadSafetyMode.NONE**
針對常見情形,Kotlin 提供了一些標準委托,如 Delegates.notNull()、 Delegates.observable() 和 lazy()。
lazy() 是一個在第一次讀取時通過給定的 lambda 值來計算屬性的初值,并返回只讀屬性的委托。
```
private val dateFormat: DateFormat by lazy {
SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
```
這是一種簡潔的延遲高消耗的初始化至其真正需要時的方式,在保留代碼可讀性的同時提升了性能。
需要注意的是,lazy() 并不是內聯函數,傳入的 lambda 參數也會被編譯成一個額外的 Function 類,并且不會被內聯到返回的委托對象中。
經常被忽略的一點是 lazy() 有可選的 mode 參數 來決定應該返回 3 種委托的哪一種:
```
public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
```
默認模式 LazyThreadSafetyMode.SYNCHRONIZED 將提供相對耗費昂貴的 雙重檢查鎖 來保證一旦屬性可以從多線程讀取時初始化塊可以安全地執行。
如果你確信屬性只會在單線程(如主線程)被訪問,那么可以選擇 LazyThreadSafetyMode.NONE 來代替,從而避免使用鎖的額外開銷。
```
val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {
SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
```
>**使用 lazy() 委托來延遲初始化時的大量開銷以及指定模式來避免不必要的鎖。**
### **區間**
#### **基本類型區間—if 包含**
>**Tips: if中 使用區間表達式(基本類型),盡量顯式聲明而不是使用 間接方法返回的區間;**
區間表達式的主要作用是使用 in 和 !in 操作符實現包含和不包含。
```
if (i in 1..10) {
println(i)
}
```
該實現針對非空基本類型的區間(包括 Int、Long、Byte、Short、Float、Double 以及 Char 的值)實現了優化,所以上面的代碼可以被優化成這樣:
```
if(1 <= i && i <= 10) {
System.out.println(i);
}
```
零額外支出并且沒有額外對象開銷。區間也可以被包含在 when 表達式中:
```
val message = when (statusCode) {
in 200..299 -> "OK"
in 300..399 -> "Find it somewhere else"
else -> "Oops"
}
```
相比一系列的 if{...} else if{...} 代碼塊,這段代碼在不降低效率的同時提高了代碼的可讀性。
然而,如果在聲明和使用之間有至少一次間接調用的話,range 會有一些微小的額外開銷。比如下面的代碼:
```
private val myRange get() = 1..10
fun rangeTest(i: Int) {
if (i in myRange) {
println(i)
}
}
```
在編譯后會創建一個額外的 IntRange 對象:
```
private final IntRange getMyRange() {
return new IntRange(1, 10);
}
public final void rangeTest(int i) {
if(this.getMyRange().contains(i)) {
System.out.println(i);
}
}
```
將屬性的 getter 聲明為 inline 的方法也無法避免這個對象的創建。
#### **基本類型區間—for遍歷**
>**Tips: for 中使用基本類型的區間的 優先考慮..或downTo()**
整型區間 (除了 Float 和 Double之外其他的基本類型)也是 級數:它們可以被迭代。這就可以將經典 Java 的 for 循環用一個更短的表達式替代。
```
for (i in 1..10) {
println(i)
}
```
經過編譯器優化后的代碼實現了零額外開銷:
```
int i = 1;
byte var3 = 10;
if(i <= var3) {
while(true) {
System.out.println(i);
if(i == var3) {
break;
}
++i;
}
}
```
如果要反向迭代,可以使用 downTo() 中綴方法來代替 ..:
```
for (i in 10 downTo 1) {
println(i)
}
```
編譯之后,這也實現了零額外開銷:
```
int i = 10;
byte var3 = 1;
if(i >= var3) {
while(true) {
System.out.println(i);
if(i == var3) {
break;
}
--i;
}
}
```
然而,其他迭代器參數并沒有如此好的優化。
反向迭代還有一種結果相同的方式,使用 reversed() 方法結合區間:
```
for (i in (1..10).reversed()) {
println(i)
}
```
編譯后的代碼并沒有看起來那么少:
```
IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10)));
int i = var10000.getFirst();
int var3 = var10000.getLast();
int var4 = var10000.getStep();
if(var4 > 0) {
if(i > var3) {
return;
}
} else if(i < var3) {
return;
}
while(true) {
System.out.println(i);
if(i == var3) {
return;
}
i += var4;
}
```
會創建一個臨時的 IntRange 對象來代表區間,然后創建另一個 IntProgression 對象來反轉前者的值。
事實上,任何結合不止一個方法來創建遞進都會生成類似的至少創建兩個微小遞進對象的代碼。
這個規則也適用于使用 step() 中綴方法來操作遞進的步驟,即使只有一步:
```
for (i in 1..10 step 2) {
println(i)
}
```
一個次要提示,當生成的代碼讀取 IntProgression 的 last 屬性時會通過對邊界和步長的小小計算來決定準確的最后值。在上面的代碼中,最終值是 9。
最后,until() 中綴函數對于迭代也很有用,該函數(執行結果)不包含最大值。
```
for (i in 0 until size) {
println(i)
}
```
遺憾的是,編譯器并沒有針對這個經典的包含區間圍優化,迭代器依然會創建區間對象:
```
IntRange var10000 = RangesKt.until(0, size);
int i = var10000.getFirst();
int var1 = var10000.getLast();
if(i <= var1) {
while(true) {
System.out.println(i);
if(i == var1) {
break;
}
++i;
}
}
```
不知道后續版本的kotlin編譯器會不會對此進行優化。當前可以通過這樣寫來優化代碼:
```
for (i in 0..size - 1) {
println(i)
}
```
#### **非基本類型 區間-if**
>**Tips:對實現了 Comparable 的非基本類型的區間進行頻繁的if包含判斷,考慮這個區間聲明為常量來避免重復創建區間類**
除了基本類型外,區間也可以用于其他實現了 Comparable 的非基本類型。
如下:
```
if (name in "Alfred".."Alicia") {
println(name)
}
```
在這種情況下,最終實現并不會優化,而且總是會創建一個 ClosedRange 對象,如下面編譯后的代碼所示:
```
if(RangesKt.rangeTo((Comparable)"Alfred", (Comparable)"Alicia")
.contains((Comparable)name)) {
System.out.println(name);
}
```
#### **迭代區間for 還是 forEach()**
>**Tips:迭代區間時,最好只使用 for 循環而不是區間上的 forEach() 方法。**
作為 for 循環的替代,使用區間內聯的擴展方法 forEach() 來實現相似的效果可能更吸引人。
```
(1..10).forEach {
println(it)
}
```
但如果仔細觀察這里使用的 forEach() 方法簽名的話,你就會注意到并沒有優化區間,而只是優化了 Iterable,所以需要創建一個 iterator。下面是編譯后代碼的 Java 形式:
```
Iterable $receiver$iv = (Iterable)(new IntRange(1, 10));
Iterator var1 = $receiver$iv.iterator();
while(var1.hasNext()) {
int element$iv = ((IntIterator)var1).nextInt();
System.out.println(element$iv);
}
```
這段代碼相比前者更為低效,原因是為了創建一個 IntRange 對象,還需要額外創建 IntIterator。但至少它還是生成了基本類型的值。
#### **迭代集合:indices**
>**Tips: 除了迭代 數組和標準集合時,可以使用indices,其他類迭代 考慮 使用常規區間**
Kotlin 標準庫提供了內置的 indices 擴展屬性來生成數組和 Collection 的區間。
```
val list = listOf("A", "B", "C")
for (i in list.indices) {
println(list[i])
}
```
令人驚訝的是,對這個 indices 的迭代得到了編譯器的優化:
```
List list = CollectionsKt.listOf(new String[]{"A", "B", "C"});
int i = 0;
int var2 = ((Collection)list).size() - 1;
if(i <= var2) {
while(true) {
Object var3 = list.get(i);
System.out.println(var3);
if(i == var2) {
break;
}
++i;
}
}
```
從上面的代碼中我們可以看到沒有創建 IntRange 對象,列表的迭代是以最高效率的方式運行的,這適用于數組和實現了 Collection 的類。
看到這里 你可能會嘗試在特定的類上使用自己的 indices 擴展屬性,來獲得相同的迭代器性能的。
```
inline val SparseArray<*>.indices: IntRange
get() = 0..size() - 1
fun printValues(map: SparseArray<String>) {
for (i in map.indices) {
println(map.valueAt(i))
}
}
```
但編譯之后,我們可以發現這并沒有那么高效率,因為編譯器無法足夠智能地避免區間對象的產生:
```
public static final void printValues(@NotNull SparseArray map) {
Intrinsics.checkParameterIsNotNull(map, "map");
IntRange var10002 = new IntRange(0, map.size() - 1);
int i = var10002.getFirst();
int var2 = var10002.getLast();
if(i <= var2) {
while(true) {
Object $receiver$iv = map.valueAt(i);
System.out.println($receiver$iv);
if(i == var2) {
break;
}
++i;
}
}
}
```
所以,可以結合實際場景 進行編碼,比如這里 推薦 常規寫法 進行遍歷:
(注:原文使用until,但基于上面的論述,使用.. 生成迭代區間更好)
```
fun printValues(map: SparseArray<String>) {
for (i in 0 .. map.size()-1) {
println(map.valueAt(i))
}
}
```
### **高階函數**
>**Tips: 如果一個 含有捕獲lamada高階函數 調用頻繁,考慮內聯它。**
Kotlin 支持將函數賦值給變量并將他們做為參數傳給其他函數。接收其他函數做為參數的函數被稱為高階函數。Kotlin 函數可以通過在他的名字前面加 :: 前綴來引用,或者在代碼中中直接聲明為一個匿名函數,或者使用最簡潔的 lambda 表達式語法 來描述一個函數。
Kotlin 是為 Java 6/7 JVM 和 Android 提供 lambda 支持的最好方法之一。
考慮下面的工具函數,在一個數據庫事務中執行任意操作并返回受影響的行數:
```
fun transaction(db: Database, body: (Database) -> Int): Int {
db.beginTransaction()
try {
val result = body(db)
db.setTransactionSuccessful()
return result
} finally {
db.endTransaction()
}
}
```
我們可以通過傳遞一個 lambda 表達式做為最后的參數來調用這個函數:
```
val deletedRows = transaction(db) {
it.delete("Customers", null, null)
}
```
他們是如何轉化為字節碼的呢?,lambdas 和匿名函數被編譯成 Function 對象。
這是上面的的 lamdba 表達式編譯之后的 Java 表現形式。
```
class MyClass$myMethod$1 implements Function1 {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke((Database)var1));
}
public final int invoke(@NotNull Database it) {
Intrinsics.checkParameterIsNotNull(it, "it");
return db.delete("Customers", null, null);
}
}
```
在你的 Android dex 文件中,每一個 lambda 表達式都被編譯成一個 Function,這將最終增加3到4個方法。
好消息是,這些 Function 對象的新實例只在必要的時候才創建。在實踐中,這意味著:
* 對捕獲表達式來說,每當一個 lambda 做為參數傳遞的時候都會生成一個新的 Function 實例,執行完后就會進行垃圾回收。
* 對非捕獲表達式(純函數)來說,會創建一個單例的 Function 實例并且在下次調用的時候重用。
由于我們示例中的調用代碼使用了一個非捕獲的 lambda,因此它被編譯為一個單例而不是內部類:
```
this.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);
```
避免反復調用那些正在調用捕獲 lambdas的標準的(非內聯)高階函數以減少垃圾回收器的壓力。
此外,還需要考慮裝箱的開銷
與 Java8 大約有43個不同的專業方法接口來盡可能地避免裝箱和拆箱相反,Kotnlin 編譯出來的 Function 對象只實現了完全通用的接口,有效地使用任何輸入輸出值的 Object 類型。
```
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
```
這意味著調用一個做為參數傳遞給高階函數的方法時,如果輸入值或者返回值涉及到基本類型(如 Int 或 Long),實際上調用了系統的裝箱和拆箱。這在性能上可能有著不容忽視的影響,特別是在 Android 上。
在上面編譯好的 lambda 中,你可以看到結果被裝箱成了 Integer 對象。然后調用者代碼馬上將其拆箱。
>當寫一個標準(非內聯)的高階函數(涉及到以基本類型做為輸入或輸出值的函數做為參數)時要小心一點。反復調用這個參數函數會由于裝箱和拆箱的操作對垃圾回收器造成更多壓力。
幸好,使用 lambda 表達式時,Kotlin 有一個非常棒的技巧來避免這些成本:將高階函數聲明為內聯。這將會使編譯器將函數體直接內聯到調用代碼內,完全避免了方法調用。對高階函數來說好處更大,因為作為參數傳遞的 lambda 表達式的函數體也會被內聯起來。實際的影響有:
* 聲明 lambda 時不會有 Function 對象被實例化;
* 不需要針對 lambda 輸入輸出的基本類型值進行裝箱和拆箱;
* 方法總數不會增加;
* 不會執行真正的函數調用。對那些多次被使用的注重 CPU (計算)的方法來說可以提高性能。
*
將我們的 transaction() 函數聲明為內聯后,調用代碼變成了:
```
db.beginTransaction();
int var5;
try {
int result$iv = db.delete("Customers", null, null);
db.setTransactionSuccessful();
var5 = result$iv;
} finally {
db.endTransaction();
}
```
關于這個殺手锏特性的一些限制:
* 內聯函數不能直接調用自己,也不能通過其他內聯函數來調用;
* 一個類中被聲明為公共的內聯函數只能訪問這個類中公共的方法和成員變量;
* 代碼量會增加。多次內聯一個長函數會使生成的代碼量明顯增多,尤其這個長方法又引用了另外一個長的內聯方法。
>**如果可能的話,就將一個高階函數聲明為內聯。保持簡短,如有必要可以將大段的代碼塊移至非內聯的方法中。你還可以將調用自代碼中影響性能的關鍵部分的函數內聯起來。
### 更多內容
局部函數 (暫時沒有寫過,待后續跟進)
### 參考引文
[](https://github.com/xitu/gold-miner/blob/master/TODO/exploring-kotlins-hidden-costs-part-3.md)
* [Exploring Kotlin’s hidden costs?—?Part 1](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62)
* [Exploring Kotlin’s hidden costs?—?Part 2](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70)
* [Exploring Kotlin’s hidden costs?—?Part 3](https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4)
* [深入探索Java 8 Lambda表達式](http://www.infoq.com/cn/articles/Java-8-Lambdas-A-Peek-Under-the-Hood)
* [Java 8 的新特性和改進總覽](http://www.oschina.net/translate/everything-about-java-8)
- 0-發現
- AndroidInterview-Q-A
- Android能讓你少走彎路的干貨整理
- LearningNotes
- temp
- temp11
- 部分地址
- 0-待辦任務
- 待補充列表
- 0-未分類
- AndroidView事件分發與滑動沖突處理
- Spannable
- 事件分發機制詳解
- 1-Java
- 1-Java-01基礎
- 未歸檔
- 你應該知道的JDK知識
- 集合框架
- 1-Java-04合集
- Java之旅0
- Java之旅
- JAVA之旅01
- JAVA之旅02
- JAVA之旅03
- JAVA之旅04
- JAVA之旅05
- JAVA之旅06
- JAVA之旅07
- JAVA之旅08
- JAVA之旅09
- java之旅1
- JAVA之旅10
- JAVA之旅11
- JAVA之旅12
- JAVA之旅13
- JAVA之旅14
- JAVA之旅15
- JAVA之旅16
- JAVA之旅17
- JAVA之旅18
- JAVA之旅19
- java之旅2
- JAVA之旅20
- JAVA之旅21
- JAVA之旅22
- JAVA之旅23
- JAVA之旅24
- JAVA之旅25
- JAVA之旅26
- JAVA之旅27
- JAVA之旅28
- JAVA之旅29
- java之旅3
- JAVA之旅30
- JAVA之旅31
- JAVA之旅32
- JAVA之旅33
- JAVA之旅34
- JAVA之旅35
- 1-Java-05辨析
- HashMapArrayMap
- Java8新特性
- Java8接口默認方法
- 圖解HashMap(1)
- 圖解HashMap(2)
- 2-Android
- 2-Android-1-基礎
- View繪制流程
- 事件分發
- AndroidView的事件分發機制和滑動沖突解決
- 自定義View基礎
- 1-安卓自定義View基礎-坐標系
- 2-安卓自定義View基礎-角度弧度
- 3-安卓自定義View基礎-顏色
- 自定義View進階
- 1-安卓自定義View進階-分類和流程
- 10-安卓自定義View進階-Matrix詳解
- 11-安卓自定義View進階-MatrixCamera
- 12-安卓自定義View進階-事件分發機制原理
- 13-安卓自定義View進階-事件分發機制詳解
- 14-安卓自定義View進階-MotionEvent詳解
- 15-安卓自定義View進階-特殊形狀控件事件處理方案
- 16-安卓自定義View進階-多點觸控詳解
- 17-安卓自定義View進階-手勢檢測GestureDetector
- 2-安卓自定義View進階-繪制基本圖形
- 3-安卓自定義View進階-畫布操作
- 4-安卓自定義View進階-圖片文字
- 5-安卓自定義View進階-Path基本操作
- 6-安卓自定義View進階-貝塞爾曲線
- 7-安卓自定義View進階-Path完結篇偽
- 8-安卓自定義View進階-Path玩出花樣PathMeasure
- 9-安卓自定義View進階-Matrix原理
- 通用類介紹
- Application
- 2-Android-2-使用
- 2-Android-02控件
- ViewGroup
- ConstraintLayout
- CoordinatorLayout
- 2-Android-03三方使用
- Dagger2
- Dagger2圖文完全教程
- Dagger2最清晰的使用教程
- Dagger2讓你愛不釋手-終結篇
- Dagger2讓你愛不釋手-重點概念講解、融合篇
- dagger2讓你愛不釋手:基礎依賴注入框架篇
- 閱讀筆記
- Glide
- Google推薦的圖片加載庫Glide:最新版使用指南(含新特性)
- rxjava
- 這可能是最好的RxJava2.x入門教程完結版
- 這可能是最好的RxJava2.x入門教程(一)
- 這可能是最好的RxJava2.x入門教程(三)
- 這可能是最好的RxJava2.x入門教程(二)
- 這可能是最好的RxJava2.x入門教程(五)
- 這可能是最好的RxJava2.x入門教程(四)
- 2-Android-3-優化
- 優化概況
- 各種優化
- Android端秒開優化
- apk大小優化
- 內存分析
- 混淆
- 2-Android-4-工具
- adb命令
- 一鍵分析Android的BugReport
- 版本控制
- git
- git章節簡述
- 2-Android-5-源碼
- HandlerThread 源碼分析
- IntentService的使用和源碼分析
- 2-Android-9-辨析
- LRU算法
- 什么是Bitmap
- 常見圖片壓縮方式
- 3-Kotlin
- Kotlin使用筆記1-草稿
- Kotlin使用筆記2
- kotlin特性草稿
- Kotlin草稿-Delegation
- Kotlin草稿-Field
- Kotlin草稿-object
- 4-JavaScript
- 5-Python
- 6-Other
- Git
- Gradle
- Android中ProGuard配置和總結
- gradle使用筆記
- Nexus私服搭建
- 編譯提速最佳實踐
- 7-設計模式與架構
- 組件化
- 組件化探索(OKR)
- 1-參考列表
- 2-1-組件化概述
- 2-2-gradle配置
- 2-3-代碼編寫
- 2-4-常見問題
- 2-9-值得一讀
- 8-數據結構與算法
- 0臨時文件
- 漢諾塔
- 8-數據-1數據結構
- HashMap
- HashMap、Hashtable、HashSet 和 ConcurrentHashMap 的比較
- 遲到一年HashMap解讀
- 8-數據-2算法
- 1個就夠了
- Java常用排序算法(必須掌握的8大排序算法)
- 常用排序算法總結(性能+代碼)
- 必須知道的八大種排序算法(java實現)
- 9-職業
- 閱讀
- 書單
- 面試
- 面試-01-java
- Java面試題全集駱昊(上)
- Java面試題全集駱昊(下)
- Java面試題全集駱昊(中)
- 面試-02-android
- 40道Android面試題
- 面試-03-開源源碼
- Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程
- 面試-07-設計模式
- 面試-08-算法
- 面試-09-其他
- SUMMARY
- 版權說明
- temp111