前语

内存走漏是程序界永恒的论题,关于Android开发来说尤为重要,想让你的App体现得更高雅,了解并治理内存走漏问题势在必行。
经过本篇文章,你将了解到:

  1. 何为内存走漏?
  2. Android 常见内存走漏场景
  3. Java匿名内部类会导致走漏吗?
  4. Java的Lambda是否会走漏?
  5. Kotlin匿名内部类会导致走漏吗?
  6. Kotlin的Lambda是否会走漏?
  7. Kotlin高阶函数的会走漏吗?
  8. 内存走漏总结

1. 何为内存走漏?

简略内存散布

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

如上图,体系在分配内存的时分,会寻觅闲暇的内存块进行分配(有些需求连续的存储空间)。
分配成功,则符号该内存块被占用,当内存块不再被运用时,则置为闲暇。

占用和被占用触及到内存的分配和开释,在不同的程序语言里有不同的封装。

C 分配/开释内存函数:

分配:malloc函数
开释:free函数

C++ 分配/开释内存函数:

分配:new函数
开释:delete函数

C/C++ 需求程序员手动分配和开释内存,而咱们知道手动的东西容易遗漏。

假如一块内存永远不再被运用,可是又没有被收回,那么这段内存一直无法被复用,这便是内存走漏

Java内存走漏

鉴于C/C++ 需求手动开释内存容易遗漏终究形成内存走漏的问题,Java在内存收回机制上做了改进:
不需求程序员手动开释内存,JVM体系有GC机制,定期扫描不再被引证的目标,将目标所占的的内存空间开释。

你可能会有疑惑:已然都有GC机制了,为啥还会有走漏呢?
由于GC是根据可达性来判别目标是否还在运用,当GC动作产生时,假如一个目标被gc root目标持有,那么它是无法被收回的。

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

如上图,obj1obj5被gc root 直接或直接持有,它们是不会被收回的,而obj6obj10 没有被gc root持有,它们是能够被收回的。

常见的作为gc root的目标

JVM在建议GC 动作的时分,需求从gc root动身判别目标的可达性,常见的gc root目标:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

开发里排查内存走漏触及比较多的gc root是:

JNI 变量、静态引证、活动的线程

假如不触及到JNI开发,咱们更多关注的是后两者。

到此,咱们知道了Java内存走漏的缘由:

不再被运用的目标,由于一些不妥的操作导致其被gc root持有无法被收回,终究内存走漏

2. Android 常见内存走漏场景

经典走漏问题

Handler运用不妥走漏

先看耳熟能详的Demo:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
    }
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d("fish", "hello world");
        }
    };
}

上面有个匿名内部类,承继自Handler。
咱们知道在Java里,匿名内部类默许持有外部类引证,而且此处编译器会有提示:

This Handler class should be static or leaks might occur (anonymous android.os.Handler)

意思是:

引荐运用静态类来承继Handler,由于运用匿名内部类可能会有内存走漏风险

咱们做个试验,操作步骤:打开Activity,封闭Activity,观察内存运用情况,是否产生内存走漏。

问题来了:以上代码会有内存走漏吗?
答案当然是否定的,由于咱们并没有运用handler目标。

将代码改造一下,onCreate里新增如下代码:

        handler.sendEmptyMessageDelayed(2, 5000);

此刻会产生内存走漏吗?
当然肉眼是无法证明是否走漏的,咱们经过运用Android Studio自带的性能剖析东西:Profiler 进行剖析:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

公然Activity产生走漏了。

怎么躲避此种场景下的内存走漏呢?

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new MyHandler().sendEmptyMessageDelayed(2, 5000);
    }
    static class MyHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Log.d("fish", "hello world");
        }
    }
}

运用静态内部类完成Handler功用,静态内部类默许没有持有外部类引证。
检测成果,没有产生内存走漏。

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

无论是匿名内部类仍是静态内部类,都没有显式地持有外部类引证,已然匿名内部类会产生走漏,那为啥还需求匿名内部类呢?
匿名内部类优点:

  1. 无需重新界说新的具名类
  2. 符合条件的匿名内部类能够转为Lambda表达式,简练
  3. 匿名内部类能够直接访问外部类引证

假若现在需求在收到message时弹出个Toast。
关于匿名内部类的完成很简略:

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Toast.makeText(ThirdActivity.this, "hello world", Toast.LENGTH_SHORT).show();
        }
    };

由于它默许持有外部类引证。

而关于静态内部类,则提示无法访问外部类目标。

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

需求给它独自传递外部类引证,相较于匿名内部类比较繁琐。

Handler 走漏的本质原因

关于当时的Demo来说,匿名内部类隐式持有外部类引证,咱们需求需求找到匿名内部类被哪个gc root直接/直接地持有了。

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

由图可知,终究Activity被Thread持有了。
简略回忆源码流程:

  1. 结构Handler目标时会绑定当时线程的Looper,Looper里持有MessageQueue引证
  2. 当时线程的Looper存储在Thread里的ThreadLocal
  3. 当Handler发送消息的时分,结构Message目标,而该Message目标持有Handler引证
  4. Message目标将会被放置在MessageQueue里
  5. 由此推断,Thread将会直接持有Handler,而Handler又持有外部类引证,终究Thread将会直接持有外部类引证,导致了走漏

线程运用不妥走漏

先看简略Demo:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}

问:上述代码会产生内存走漏吗?
答:当然不会,由于线程并没有敞开。

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(200000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
}

再剖析剖析,会有内存走漏吗?
与之前的Handler共同,匿名内部类会持有外部类的引证,而匿名内部类自身又被线程持有,因此会产生走漏。

怎么躲避此种场景下的内存走漏呢?

有两种方法:
第一种:运用静态内部类替换匿名内部类
此种方法同Handler处理相似。

第二种:运用Lambda替换匿名内部类

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(() -> {
            try {
                Thread.sleep(200000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
}

Lambda表达式没有隐式持有外部类,因此此种场景下不会有内存走漏风险。

注册不妥内存走漏

模仿一个简略下载进程,首先界说一个下载管理类:

public class DownloadManager {
   private DownloadManager() {
   }
   static class Inner {
      private static final DownloadManager ins = new DownloadManager();
   }
   public static DownloadManager getIns() {
      return Inner.ins;
   }
   private HashMap<String, DownloadListener> map = new HashMap();
   //模仿注册
   public void download(DownloadListener listener, String path) {
      map.put(path, listener);
      new Thread(() -> {
         //模仿下载
         listener.onSuc();
      }).start();
   }
}
interface DownloadListener {
   void onSuc();
   void onFail();
}

外部传入下载路径,下载成功后告诉外界调用者:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        DownloadManager.getIns().download(new DownloadListener() {
            @Override
            public void onSuc() {
                //更新UI
            }
            @Override
            public void onFail() {
            }
        }, "hello test");
    }
}

由于需求在下载回调时更新UI,因此选择匿名内部类接收回调,而由于该匿名内部类被静态变量: DownloadManager.ins 持有。
也便是说:

静态变量作为gc root,直接持有匿名内部类,终究持有Activity导致了走漏

怎么躲避此种场景下的内存走漏呢?

有两种方法:

  1. 静态内部类持有Activity弱引证
  2. DownloadManager供给反注册方法,当Activity毁掉时反注册从Map里移除回调

3. Java匿名内部类会导致走漏吗?

线程持有匿名内部类目标

内存走漏的一些前置常识现已过了一遍,接下来咱们从字节码的视点分别剖析匿名内部类、Lambda表达式、高阶函数是否存在走漏问题。
先看Demo:

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("fish", "hello world");
            }
        }).start();
    }
}

当咱们进入Activity,而后又退出时,猜猜会产生走漏吗?
有些小伙伴会说:当然了,线程持有匿名内部类目标,而匿名内部类目标又持有外部类(Activity)引证。
实际上是此处的线程并没有执行耗时任务,很快就完毕了,体系收回Activity目标时线程现已完毕了,不会再持有匿名内部类目标了。

怎么确定匿名内部类持有外部类引证呢?
一个很直观的体现:

在匿名内部类里访问外部类实例变量,若是编译器没有提示过错,则能够认为匿名内部类持有外部类引证

当然,想要看到石锤就得从字节码动身了。

Java匿名内部类Class文件

build一下并查找Javac的产物:在/build/intermediates/javac 开始的目录下

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

这里是看不到匿名内部类的,需求到文件浏览器里查找。

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

能够看出,咱们仅仅声明晰一个ThirdActivity类,可是生成了两个Class文件,其中一个是匿名内部类生成的,一般命名方法为:外部类名+”$”+”第几个内部类”+”.class”。
拖到Studio里检查内容:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

显然匿名内部类结构函数形参里有外部类的类型,当结构匿名内部类时会传递进去并赋值给匿名内部类的成员变量。

Java匿名内部类字节码

检查字节码方法有多种,能够用javap指令:

javap -c ThirdActivity$1.class

也能够在Android Studio里下载字节码插件:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

在源文件上右键选择检查字节码:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

能够看出:

  1. New 指令创建匿名内部类目标并复制到操作数栈顶
  2. 加载外部类目标到操作数栈顶
  3. 调用匿名内部类结构函数,并将第2步的栈顶目标传入

如此一来,匿名内部类创建了,而且持有了外部类引证。

回到开始问题,Java匿名内部类是否会走漏呢?

当外部类毁掉的时分,假如匿名内部类被gc root 持有(直接/直接),那么将会产生内存走漏

4. Java的Lambda是否会走漏?

线程持有Lambda目标

将上小结的匿名内部类改造为Lambda(注:不是所有的匿名内部类都能够转为Lambda表达式)

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(() -> {
            Log.d("fish", "hello world");
            Log.d("fish", "hello world2");
        }).start();
    }
}

Java Lambda生成的Class文件

Java Lambda并没有生成Class文件。

Java Lambda字节码

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

Java Lambda并没有生成Class文件,而是经过INVOKEDYNAMIC 指令动态生成Runnable目标,终究传入Thread里。
能够看出,此刻生成的Lambda并没有持有外部类引证。

Java Lambda显式持有外部类引证

public class ThirdActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        new Thread(() -> {
            //显式持有外部类引证
            Log.d("fish", ThirdActivity.class.getName());
        }).start();
    }
}

再检查字节码:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

能够看出,传入了外部类引证。
回到开始问题,Java Lambda是否会走漏呢?

  1. Lambda没有隐式持有外部类引证,
  2. 若在Lambda内显式持有外部类引证,那么此刻和Java 匿名内部类相似的,当外部类毁掉的时分,假如Lambda被gc root 持有(直接/直接),那么将会产生内存走漏

5. Kotlin匿名内部类会导致走漏吗?

线程持有匿名内部类目标

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread(object : Runnable {
            override fun run() {
                println("hello world")
            }
        }).start()
    }
}

此刻匿名内部类会持有外部类引证吗?
先从生成的Class文件下手。

Kotlin 匿名内部类生成的Class文件

Kotlin编译生成的Class目录:build/tmp/kotlin-classes/ 查找生成的Class文件:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

咱们发现生成了Class文件,命名规则:外部类名+方法名+第几个匿名内部类+”.class”

Kotlin 匿名内部类字节码

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

能够看出,并没有持有外部类引证。

Kotlin 匿名内部类显式持有外部类引证

class FourActivity : AppCompatActivity() {
    val name = "fish"
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread(object : Runnable {
            override fun run() {
                println("hello world $name")
            }
        }).start()
    }
}

检查字节码:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

由此可见,结构函数携带了外部类引证。

回到开始问题,Kotlin 匿名内部类是否会走漏呢?

  1. Kotlin 匿名内部类没有隐式持有外部类引证,
  2. 若在Kotlin 匿名内部类内显式持有外部类引证,那么此刻和Java 匿名内部类相似的,当外部类毁掉的时分,假如Lambda被gc root 持有(直接/直接),那么将会产生内存走漏

6. Kotlin的Lambda是否会走漏?

线程持有Lambda目标

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread { println("hello world ") }
    }
}

此刻Lambda会持有外部类引证吗?
先从生成的Class文件下手。

Kotlin Lambda生成的Class文件

Kotlin Lambda 并没有生成Class文件。

Kotlin Lambda字节码

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

能够看出,并没有隐式持有外部类引证。

Kotlin Lambda显式持有外部类引证

class FourActivity : AppCompatActivity() {
    val name = "fish"
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Thread { println("hello world $name") }
    }
}

检查字节码:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

由此可见,结构函数携带了外部类引证。

回到开始问题,Kotlin Lambda是否会走漏呢?

和Java Lambda表述共同

7. Kotlin高阶函数的会走漏吗?

什么是高阶函数?

将函数类型当做形参或返回值的函数称为高阶函数。
高阶函数在Kotlin里无处不在,是Kotlin简练写法的一大利器。

高阶函数生成的Class文件

class FourActivity : AppCompatActivity() {
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        test {
            println("$it")
        }
    }
    //高阶函数作为形参
    private fun test(block:(String) -> Unit) {
        block.invoke("fish")
    }
}

很简略的一个高阶函数,检查生成的Class文件:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

检查Kotlin Bytecode内容:
final class com/fish/perform/FourActivity$onCreate$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function1 {

承继自Lambda,并完成了Function1接口。
它的结构函数并没有形参,说明不会传入外部类引证。

高阶函数的字节码

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

和之前剖析的匿名内部类和Lambda不同的是(尽管高阶函数也能够用Lambda简化表达):触及到了GETSTATIC指令。
该指令意思是从静态变量里获取高阶函数的引证,在高阶函数的字节码加载的时分就现已将静态变量初始化:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

能够这么了解:

  1. 高阶函数的Class加载的时分会初始化实例,并将该实例存储在静态变量里
  2. 当外部调用高阶函数时,从静态变量里获取高阶函数实例

高阶函数显式持有外部类引证

class FourActivity : AppCompatActivity() {
    val name="fish"
    private lateinit var binding: ActivityFourBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFourBinding.inflate(layoutInflater)
        setContentView(binding.root)
        test {
            println("$it:$name")
        }
    }
    //高阶函数作为形参
    private fun test(block:(String) -> Unit) {
        block.invoke("fish")
    }
}

检查字节码:

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

结构函数持有了外部类引证,此刻并没有生成静态变量(没必要生成,若生成了便是妥妥的内存走漏了)

回到开始问题,高阶函数是否会走漏呢?

  1. 高阶函数没有隐式持有外部类引证
  2. 若在高阶函数内显式持有外部类引证,那么此刻和Java 匿名内部类相似的,当外部类毁掉的时分,假如高阶函数被gc root 持有(直接/直接),那么将会产生内存走漏

8. 内存走漏总结

匿名内部类/Lambda Java和Kotlin谁会导致内存泄漏?

简略了解内存走漏:

  1. 长生命周期的目标持有短生命周期的目标,导致短生命周期的目标在生命周期完毕后没有被及时收回,导致内存无法复用,终究走漏
  2. 合理地开释对短生命周期目标的引证

代码根本都在正文里,此处就不贴github链接了,有疑惑请评论/私信。
后续将会继续输出APT/AGP等全系列流程文章,敬请期待。

您若喜爱,请点赞、关注、保藏,您的鼓舞是我前进的动力

继续更新中,和我一同稳扎稳打体系、深化学习Android/Kotlin

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。