kotlin的by lazy会不会导致内存泄漏

最新分析内存泄漏问题的时候,发现引用链里有一个SynchronizedLazyImpl,搜了一下发现是by lazy相关的,而这个是实现单例懒加载的语法糖,所以猜测可能是这里引起的泄漏,于是研究了一下by lazy会不会引起泄漏。

在这里插入图片描述

本篇文章会通过一个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 fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

这里实际是走了SynchronizedLazyImpl,那我们继续深入

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, 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初始化的变量并不会导致内存泄漏。

猜你喜欢

转载自blog.csdn.net/qq_43478882/article/details/128179146
今日推荐