kotlin的bylazy会不会导致内存泄漏-创新互联
最新分析内存泄漏问题的时候,发现引用链里有一个SynchronizedLazyImpl,搜了一下发现是by lazy相关的,而这个是实现单例懒加载的语法糖,所以猜测可能是这里引起的泄漏,于是研究了一下by lazy会不会引起泄漏。
创新互联建站是一家专业提供象山企业网站建设,专注与做网站、网站制作、H5响应式网站、小程序制作等业务。10年已为象山众多企业、政府机构等服务。创新互联专业网站设计公司优惠进行中。本篇文章会通过一个Demo来一探究竟。
一、by lazy原理 1、by lazy是干嘛的by lazy是懒加载,是实现单例的一个方法,这样加载的变量会在第一次用到的时候才会进行初始化。
2、探究by lazy的原理先写一个test的类
class TestClass(context: Context) {init {Log.d("TestClass", "init()!")
}
}
然后在Activity里通过by lazy来初始化一个变量。
class TestActivity : AppCompatActivity() {val testClx by lazy {val context = this
TestClass(context)
}
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
}
}
想要一探by lazy的究竟,最好是通过字节码,但是字节码太难懂了,那就再将字节码Decompile成.java文件。
方法:Tools->kotlin->Show Kotlin Bytecode
然后再点一下Decomile,就会生成.java文件了。
public final class TestActivity extends AppCompatActivity {@NotNull
private final Lazy testClx$delegate = LazyKt.lazy((Function0)(new Function0() { // $FF: synthetic method
// $FF: bridge method
public Object invoke() { return this.invoke();
}
@NotNull
public final TestClass invoke() { TestActivity context = TestActivity.this;
return new TestClass((Context)context);
}
}));
@NotNull
public final TestClass getTestClx() { Lazy var1 = this.testClx$delegate;
Object var3 = null;
return (TestClass)var1.getValue();
}
protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
this.setContentView(1300001);
}
}
从.java文件可以看到,TestActivity里并没有testClx这个成员变量,而是testClx$delegate。
当要使用testClx这个变量的时候,其实是通过getTestClx()这个方法暴露给了外界。而getTestClx()这个方法内部,其实是通过testClx$delegate.getValue()方法来获取值的。
那我们的分析重点就来到了testClx$delegate这个东西。这个东西在TestActivity创建的时候就进行初始化了,我们进入LazyKt.lazy方法看一下。
public actual funlazy(initializer: () ->T): Lazy= SynchronizedLazyImpl(initializer)
这里实际是走了SynchronizedLazyImpl,那我们继续深入
private class SynchronizedLazyImpl(initializer: () ->T, lock: Any? = null) : Lazy, Serializable {private var initializer: (() ->T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
...
}
这个类里其实也并不复杂,构建函数接受一个lamda表达式,这里的表达式就是by lazy 代码块里的代码。
内部有一个value,就是外部testClx$delegate.getValue()这里获取的,那重点就在get()里了。
然后就会发现,内部的实现完全就是一个Java式的双重校验单例呀。
如果为value为null,会先锁住,再进行一次判断,如果还未null,就进行初始化,这里的初始化就是通过lamda表达式来进行初始化。
然后进行初始化之后,会把initializer置空,这一步是个重点,我们后面再说。
那到这里by lazy的原理我们也搞清楚了,利用double check来保证单例。
可以在TestActivity里去多次调用testClx试一下,TestClass里init的log只会打印一次,并且在第一次调用的时候才会打印。
二、会不会泄漏在探究之前,我先去网上搜索了一下相关的问题。发现有好几篇文章说会泄漏,stack overflow上也有这样的回答:
https://stackoverflow.com/questions/51718733/why-kotlin-by-lazy-can-cause-memory-leak-in-android
大致的意思就是,by lazy会持有lambda表达式中会持有context的引用,这里的引用一直到变量初始化之后才会被释放,如果变量访问较晚或者没有访问就可能会导致内存泄漏。
这么一听好像还挺有道理的,于是准备验证一下。
1、验证会不会泄漏我们从Main Activity跳转到TestActivity,但是TestActivity里的testClx变量从未被访问,也就不会初始化。
class MainActivity : AppCompatActivity() {lateinit var path: String
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btn: Button = findViewById(R.id.btn_jump)
btn.setOnClickListener {val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
}
val dumpBtn: Button = findViewById(R.id.btn_dump)
dumpBtn.setOnClickListener {dump()
}
path = externalCacheDir!!.path
}
fun dump() {Debug.dumpHprofData("$path/test.hprof")
}
}
跳转过后回到MainActivity,并将hprof文件dump下来导入profiler查看。
此时的预期应该是TestActivity会发生泄漏,但实际情况却并没有:
那就和文章里说的不对了,我们回到.java文件里深究一下。
2、深究public final class TestActivity extends AppCompatActivity {@NotNull
private final Lazy testClx$delegate = LazyKt.lazy((Function0)(new Function0() { // $FF: synthetic method
// $FF: bridge method
public Object invoke() { return this.invoke();
}
@NotNull
public final TestClass invoke() { TestActivity context = TestActivity.this;
return new TestClass((Context)context);
}
}));
...
}
可以看到,这里LazyKt.lazy方法里,写了一个匿名内部类Function0。
function0里的invoke()方法,就是我们进行初始化的内容。可以看到这个匿名内部类Function0是持有了TestAcivity的引用的。
如果按照前面的说法会泄漏的话,那初始化里将initializer置空就很重要,初始化之后会释放掉
那这里会不会泄漏呢?我们画个图分析一下:
当TestActivity在前台的时候,肯定是不会被回收的,从GCRoot出发是可达的。
当TestActivity销毁之后,原本的引用链断了
虽然Function0持有了TestActivity的实例,但是他们都是从GCRoot不可达的,当发生GC时他们都是会被回收的。那都会被回收,从从哪里来的内存泄漏呢?
所以结论就是,by lazy初始化的变量,是不会引起内存泄漏的!
3、对比大家可能都听说过,Activity里的匿名内部类handler可能会造成内存泄漏,和这里by lazy有什么不一样呢?
我们就要明白handler泄漏的真正原因:
通过handler发送了一条message,此时的message是持有handler引用的。如果这条handler在消息队列里没有被发出,此时Activity销毁了,那么就会存在这样一跳引用链:
主线程 —>threadlocal —>Looper —>MessageQueue —>Message —>Handler —>Activity
这里是因为threadlocal是常驻的,不会被回收,所以才导致了Activity不能被回收而泄漏。
**而我们前面的情况,并没有这样一条引用链。**所以,要搞清楚,并不是匿名内部类都会造成内存泄漏!
在判断有没有内存泄漏时,我们还是要通过本质去判断,到底有没有一条从GCRoot的引用链,导致已经销毁的类无法被回收。
三、总结通过实践、深究源码、与handler泄漏对比,我们可以知道正常使用by lazy初始化的变量并不会导致内存泄漏。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
网页标题:kotlin的bylazy会不会导致内存泄漏-创新互联
本文来源:http://scyanting.com/article/cciojp.html