做Android的,或者做Java开发的同学,通常都会听到一句话,内部类持有外部类的引用,可能会导致外部类内存泄漏。果真是这样吗?
知道大家很忙,那就先把结论抛出来
- Java中内部类会持有外部类的引用,可能引发内存泄漏
- Kotlin存在编译优化,会将内部类编译成普通的类,如果内部类中没有实际引用外部类,则不会造成内存泄漏;如果内部类中引用了外部类,则会将外部类的作为参数传递给“内部类”,进而可能引发内存泄漏。
- Kotlin协程中,GlobalScope与上条相同,如果协程内部没有实际引用外部类,则不会造成内存泄漏;如果实际引用了外部类,则可能造成内存泄漏;
- Kotlin协程中,lifecycleScope等会在onDestroy前cancel掉挂起状态协程的执行,所以在上条的基础上,会进一步缩小内存泄漏的场景。
- 综上,Kotlin较Java而言,优化了编译,减少了内存泄漏的场景;如果使用协程,尽量避免使用GlobalScope。
具体的实践过程如下
2. Java中匿名内部类持有父类的引用首先我们要定义一个baseActivity,重写他的finalize,让我们更方便的看到Activity的回收过程
public class baseActivity extends AppCompatActivity {
@Override
protected void finalize() throws Throwable {
Log.e("yanlog", "baseActivity finalize:" + this);
super.finalize();
}
}
我们用Android Studio的Profile来发起回收
先来一个最普普通通的用法
public class TmpJavaActivity extends baseActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
}
我们经过测试会发现,TmpJavaActivity始终无法被回收。
参看下编译后的smail文件
从上面的编译结果来看,TmpJavaActivity确实作为一个内部类持有TmpJavaActivity的应用所以我们有了以下结论
Java中匿名内部类确实会持有父类的应用,进而有可能导致内存泄漏
3. Kotlin中匿名内部类持有外部类的引用将2中的代码翻译为Kotlin,如下
class TmpActivity : baseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tmp)
val thread = Thread {
while (true) {
Thread.sleep(1000)
}
}
thread.start()
}
}
然后经过测试,**我们竟然神奇的发现TmpActivity被回收了!**有如下的日志打印
11-02 20:06:29.469 13127 13142 E yanlog : baseActivity finalize:com.cmri.myapplication.TmpActivity@b9d9408
我们惯例反编译apk看下原因
这里,编译生成了一个TmpActivity$$ExternalSyntheticLambda0类,这个类是一个有一个静态对象,该静态对象不持有Activity的引用,所以导致没有内存泄漏。
那么如果我们在run方法中引用外部类呢? 难道也不持有引用吗?
改下代码看看
class TmpActivity : baseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tmp)
val thread = Thread {
while (true) {
Log.e("yanlog","thread"+this@TmpActivity)
java.lang.Thread.sleep(1000)
}
}
thread.start()
}
}
可以很清楚的看到,如果内部类中确实引用外部类的情况下,会将外部类作为参数传入到外面。相当于”内部类“持有外部类的引用。
综上,我们得到以下结论
1. kotlin编译后,会将内部类编译为普通的类A,一般情况下不持有外部类的引用。
2. 如果内部类代码中确实引用了外部类,那么编译后,会将外部类作为参数传入到类A中,相当于”内部类“持有外部类的应用。
3. Kotlin相对于Java类来说,内部类除非必要,否则不持有外部类的引用,所以会减少内存泄漏的场景。
也要分两种情况,协程中不引用外部类
class TmpActivity : baseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tmp)
GlobalScope.launch {
while (true) {
delay(1000)
}
}
}
}
结果
11-03 09:21:00.490 21426 21441 E yanlog : baseActivity finalize:com.cmri.myapplication.TmpActivity@d229f01
可以看到不会造成内存泄漏
但是如果协程中确实引用了外部类,如下
class TmpActivity : baseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tmp)
GlobalScope.launch {
while (true) {
Log.e("yanlog","TempActivity:"+this@TmpActivity)
delay(1000)
}
}
}
}
会发现TmpActivity一直无法被回收,看下反编译后的结果
可以看到TmpActivity被作为参数传递了其他类,所以会造成内存泄漏。
所以可以得出结论
1. kotlin协程中,如果没有引用外部类,则不会造成内存泄漏
2. kotlin协程中,如果确实引用了外部类,则可能会造成内存泄漏
如果我们把上述代码中的GlobalScope改成lifecycleScope,那还会造成内存泄漏吗?
class TmpActivity : baseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tmp)
lifecycleScope.launch {
while (true) {
Log.e("yanlog","TempActivity:"+this@TmpActivity)
delay(1000)
}
}
}
override fun onDestroy() {
Log.e("yanlog","onDestory")
super.onDestroy()
}
}
答案是不会,并且,会在onDestory执行前,cancel掉协程的执行,看日志输出
11-03 09:35:31.587 23749 23910 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:35:32.589 23749 23910 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:35:33.591 23749 23910 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:35:34.595 23749 23910 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:35:35.598 23749 23910 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:36:06.277 26053 26053 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:36:07.294 26053 26053 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:36:08.296 26053 26053 E yanlog : TempActivity:com.cmri.myapplication.TmpActivity@d229f01
11-03 09:36:08.499 26053 26053 E yanlog : onDestory
11-03 09:36:09.927 26053 26069 E yanlog : baseActivity finalize:com.cmri.myapplication.TmpActivity@d229f01
有人说我不走寻常路,用Thread.sleep代替delay后,会造成内存泄漏吗?
class TmpActivity : baseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tmp)
lifecycleScope.launch(Dispatchers.IO) {
while (true) {
Log.e("yanlog","TempActivity:"+this@TmpActivity)
Thread.sleep(1000)
}
}
}
override fun onDestroy() {
Log.e("yanlog","onDestory")
super.onDestroy()
}
}
答案是会造成内存泄漏的。原因也是比较显而易见的,协程一直处于一个运行状态,没有挂起状态,导致没有一个“时机”去取消协程的执行。



