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