https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4
Delegated properties
delegated property 指的是一个 property 的 getter 或可选的 setter 是由一个额外的对象实现的,这个对象被称为 delegate。我们可以使用这个特性复用自定义的属性实现。
1 | class Example { |
delegate 对象需要实现 operator
, getValue()
函数和getValue()
函数提供读写操作。函数执行时收到 所代理对象实例 和 属性相关的 metadata (比如 name)。
delegate property 背后的代码如下:
1 | public final class Example { |
类中加入了一些静态属性元数据,delegate 在构造函数初始化,读写属性时调用 delegate 的方法。
Delegate instances
上面的例子中,我们实例化了一个新的 delegate 对象。当 delegate 的内部实现是有状态的时需要这步,比如下面这个例子,一个缓存 property 计算后的值:1
2
3
4
5
6
7
8
9
10
11
12class StringDelegate {
private var cache: String? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
var result = cache
if (result == null) {
result = someOperation()
cache = result
}
return result
}
}
当 delegate 实例需要构造函数提供额外的参数时,也需要创建新的实例:
1 | class Example { |
当然也有某些场景只需一个 delegate 实例代理所有 property:delegate 是状态无关的,工作过程只与所代理对象实例和 property name 相关。这时你可以把 delegate 写成单例的,用 object
替换 class
.
比如这个例子:1
2
3
4
5object FragmentDelegate {
operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {
return thisRef.fragmentManager.findFragmentByTag(property.name)
}
}
同样,一个对象也可以扩展为 delegate。getValue()
and setValue()
也可以声明为扩展函数的形式。kotlin 内部还提供了一组扩展函数,允许将 Map/MutableMap 实例作为代理,使用 property name 做 key。
如果你打算在一个类内复用某个代理实例,记得在构造函数里给他初始化。
Note: Kotlin 1.1 之后,函数的局部变量也可以被 delegate ,此时可以不必在构造函数立即初始化 delegate,在函数使用前实例化即可。
使用 delegate property 引入 delegate 对象的开销,还会在类中加入一些 metadata。尽可能复用 delegate。 大量使用 delegate 时要考虑这样做是必须的吗?有没有更好地解决方式。
Generic delegates
delegate 函数还可以以泛型形式声明,用于代理不同的类型。
1 | private var maxDelay: Long by SharedPreferencesDelegate<Long>() |
如果代理的是原始类型,每次读写都将会产生装箱拆箱开销,即使将原始类型声明为 non-null 也没用。
对于非空原始类型,不用泛型,使用指定类型的 delegate 就可以避免装箱拆箱操作。
Standard delegates: lazy()
kotlin 提供了几个标准 delegate,like Delegates.notNull(), Delegates.observable() and lazy()。
lazy()
函数返回一个只读属性 delegate,使用 lambda 负责在初次使用时初始化。
1 | private val dateFormat: DateFormat by lazy { |
在保持可读性的同时,提供了一个简洁的方式将耗时的初始化过程延迟执行,提升性能。
需要注意,lazy()
并不是内联函数,传入的 lambda 被编译为单独的 Function
,而不是内联到返回的 delegate 对象内。
lazy()
有个不起眼的 optional 参数 mode
,可以以 3 种形式返回 delegate :1
2
3
4
5
6
7public 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)
}
默认 mode 是 LazyThreadSafetyMode.SYNCHRONIZED
,内部会进行 double-checked lock,在多线程访问时会检查锁,相对来说开销较高。
如果你确定只有单线程的访问,设置 mode 为 LazyThreadSafetyMode.NONE
可以规避访问锁引发的额外开销。
1 | val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) { |
使用
lazy()
将耗时初始化延迟执行,指定 mode 避免锁带来的开销。
Ranges
kotlin 使用 ranges 表示有限集的 value,value 可以是任何 Comparable
类型。这个表达式创建一个实现了 ClosedRange
接口的对象,操作符是 ..
。
Inclusion tests
range 主要是为了使用 in
和 !in
操作符书写 inclusion or exclusion tests。
1 | if (i in 1..10) { |
内部实现对 非空原始类型的 range 进行了优化 (bounded byInt, Long, Byte, Short, Float, Double or Char values),上例会编译为:
1 | if(1 <= i && i <= 10) { |
没有额外开销,也不会有对象生成。也可以在 when 语句内使用 range:
1 | val message = when (statusCode) { |
和 if{...} else if{...}
功能相同,但可读性更高。
不过,当 range 实例化和实际使用位置并不在同一层级时,会有一个小的开销:
1 | private val myRange get() = 1..10 |
这导致编译后额外生成了一个 IntRange
对象:1
2
3
4
5
6
7
8
9private final IntRange getMyRange() {
return new IntRange(1, 10);
}
public final void rangeTest(int i) {
if(this.getMyRange().contains(i)) {
System.out.println(i);
}
}
将 getter 声明为内联也不能避免。kotlin 1.1 编译器可以考虑优化一下这里。不过至少没有装箱什么的,感谢 balabala。
不要瞎折腾,直接用,或者声明成 constants 复用。
range 也可用于其他实现了 Comparable
接口的非原始类型。
1 | if (name in "Alfred".."Alicia") { |
像这种就没啥特殊处理了,肯定会创建一个 ClosedRange
对象:
1 | if(RangesKt.rangeTo((Comparable)"Alfred", (Comparable)"Alicia") |
需要反复使用时考虑设置为 constant。
Iterations: for loops
整型 range 是连续的:可被遍历。可以用更简短的代码替代 java 的 for
循环。
1 | for (i in 1..10) { |
也不会产生额外的开销:
1 | int i = 1; |
向后遍历可以用 downTo()
infix 函数替换 ..
1 | for (i in 10 downTo 1) { |
这也不会产生额外的开销:
1 | int i = 10; |
还有 until()
1 | for (i in 0 until size) { |
文章完成时的 kotlin 版本对这个函数生成的代码并不完美。但 Kotlin 1.1.4 有很大的提升,现在生成的代码更高效:
1 | int i = 0; |
However, other iteration variants are not as well optimized.
除了 downto() 以外另一种方式:
1 | for (i in (1..10).reversed()) { |
不要用
1 | IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10))); |
产生了一个临时 IntRange
对象对应 range,然后创建一个 IntProgression
对象将第一个对象的值逆序。
任何将两个以上函数结合来创建 progression 的代码都会产生相似的结果,至少两个轻量 progression object 被生成。
对 step()
infix function(修饰函数?) 也一样,就算 step = 1
:
1 | for (i in 1..10 step 2) { |
提示,生成的代码读取 IntProgression
的 last
属性时会进行一个计算,确定世界边界,比如上例中,这个值是 9。
遍历就用一个表达式,别整太多中间步骤
Iterations: forEach()
用 range 的 内联扩展函数 forEach()
替代 for
1 | (1..10).forEach { |
他不会进行优化,只是使用 Iterable
遍历:
1 | Iterable $receiver$iv = (Iterable)(new IntRange(1, 10)); |
这段代码比上面的性能还低,它除了创建 IntRange
对象还有一个 IntIterator
的开销。At least, this one generates primitive values.
forEach()
有额外开销,不如用for
循环。
Iterations: collection indices
kotlin 标准库为 Collection 提供了扩展函数获取 rande 引索
1 | val list = listOf("A", "B", "C") |
这部分代码也获得了优化:
1 | List list = CollectionsKt.listOf(new String[]{"A", "B", "C"}); |
没有多余的东西生成,对数组和实现 Collection 的类兼容很好。也许你想自己实现个扩展函数实现相同功能:
1 | inline val SparseArray<*>.indices: IntRange |
但你的实现可不会被编译器优化,它还没那么叼:
1 | public static final void printValues( SparseArray map) { |
我建议你就在 for
循环里用until()
:
1 | fun printValues(map: SparseArray<String>) { |
如果… 你要写一个不实现
Collection
接口的 collection …最好用 for 循环写引索 range ,避免生成多余对象。
balabala over