内存,是Android运用的生命线,一旦在内存上呈现问题,轻者内存走漏,重者直接crash,因此一个运用坚持强健,内存这块的工作是持久战,并且从写代码这块就需求留意合理性,所以想要了解内存优化怎么去做,要先从基础知识开端。
1 JVM内存原理
这一部分确实很枯燥,可是对于咱们理解内存模型十分重要,这一块也是面试的常客
从上图中,我将JVM的内存模块分成了左右两大部分,左边归于同享区域(办法区、堆区),所有的线程都能够拜访,但也会带来同步问题,这儿就不细说了;右边归于私有区域,每个线程都有自己独立的区域。
1.1 办法履行流程
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
execute()
}
private fun execute(){
val a = 2.5f
val b = 2.5f
val c = a + b
val method = Method()
val d = getD()
}
private fun getD(): Int {
return 0
}
}
class Method{
private var a:Int = 0
}
咱们看到在MainActivity的onCreate办法中,履行了execute办法,由于当时是UI线程,每个线程都有一个Java虚拟机栈,从上图中能够看到,那么每履行一个办法,在Java虚拟机栈中都对应一个栈帧。
每次调用一个办法,都代表一个栈帧入栈,当onCreate办法履行完结之后,会履行execute办法,那么咱们看下execute办法。
execute办法在Java虚拟机栈中代表一个栈帧,栈帧是由四部分组成:
(1)局部变量表:局部变量是声明在办法体内的,例如a,b,c,在办法履行完结之后,也会被收回; (2)操作数栈:在恣意办法中,涉及到变量之间运算等操作都是在操作数栈中进行;例如execute办法中:
val a = 2.5f
当履行这句代码时,首要会将 2.5f压入操作数栈,然后给a赋值,依次类推
(3)回来地址:例如在execute调用了getD办法,那么这个办法在履行到到return的时分就完毕了,当一个办法完毕之后,就要回来到该办法的被调用途,那么该办法就带着一个回来地址,告诉JVM给谁赋值,然后经过操作数栈给d赋值
(4)动态链接:在execute办法中,实例化了Method类,在这儿,首要会给Method中的一些静态变量或许办法进行内存分配,这个进程能够理解为动态链接。
1.2 从单例形式了解目标生命周期
单例形式,或许是很多设计形式中,咱们运用最频繁的一个,可是单例真是就这么简单吗,运用不小心就会形成内存走漏!
interface IObserver {
fun send(msg:String)
}
class Observable : IObserver {
private val observers: MutableList<IObserver> by lazy {
mutableListOf()
}
fun register(observer: IObserver) {
observers.add(observer)
}
fun unregister(observer: IObserver) {
observers.remove(observer)
}
override fun send(msg: String) {
observers.forEach {
it.send(msg)
}
}
companion object {
val instance: Observable by lazy {
Observable()
}
}
}
这儿是写了一个观察者,这个被观察者是一个单例,instance是存放在办法区中,而创立的Observable目标则是存在堆区,看下图
由于办法区归于常驻内存,那么其中的instance引证会一向跟堆区的Observable衔接,导致这个单例目标会存在很长的时刻
btnRegister.setOnClickListener {
Observable.instance.register(this)
}
btnSend.setOnClickListener {
Observable.instance.send("发送消息")
}
在MainActivity中,点击注册按钮,留意这儿传入的值,是当时Activity,那么这个时分退出,会发生什么?咱们先从profile东西里看一下,退出之后,有2个内存走漏的当地,假如运用的leakcannary(后面会介绍)就应该会理解
那么在MainActivity中,哪个当地发生的了内存走漏呢?咱们紧跟一下看看GcRoot的引证,发现有这样一条引证链,MainActivity在一个list数组中,并且这个数组是Observable中的observers,并且是被instance持有,前面咱们提到,instance的生命周期很长,所以当Activity预备被毁掉时,发现被instance持有导致收回失利,发生了内存走漏。
那么这种情况,咱们该怎样处理呢?一般来说,有注册就有解注册,所以咱们在封装的时分必定要留意单例中传入的参数
override fun onDestroy() {
super.onDestroy()
Observable.instance.unregister(this)
}
再次运转咱们发现,已经不存在内存走漏了
1.3 GcRoot
前面咱们提到了,由于instance是Gcroot,导致其引证了observers,observers引证了MainActivity,MainActivity退出的时分没有被收回,那么什么样的目标能被看做是GcRoot呢?
(1)静态变量、常量:例如instance,其内存是在办法区的,在办法区一般存储的都是静态的常量或许变量,其生命周期十分长;
(2)局部变量表:在Java虚拟机栈的栈帧中,存在局部变量表,为什么局部变量表能作为gcroot,原因很简单,咱们看下面这个办法
private fun execute() {
val a = 2.5f
val method = Method()
val d = getD()
}
a变量便是一个局部变量表中的成员,咱们想一下,假如a不是gcroot,那么废物收回时就有或许被收回,那么这个办法还有什么含义呢?所以当这个办法履行完结之后,gcroot被收回,其引证也会被收回。
2 OOM
在之前咱们简单介绍了内存走漏的场景,那么内存走漏一旦发生,就会导致OOM吗?其实并不是,内存走漏一开端并不会导致OOM,而是逐渐累计的,当内存空间不足时,会形成卡顿、耗电等不良体会,最终就会导致OOM,app溃散
那么什么情况下会导致OOM呢?
(1)Java堆内存不足
(2)没有连续的内存空间
(3)线程数超出限制
其实以上3种情况,前两种都有或许是内存走漏导致的,所以怎么防止内存走漏,是咱们内存优化的要点
2.1 leakcanary运用
首要在module中引进leakcanary的依赖,关于leakcanary的原理,之后会独自写一篇博客介绍,这儿咱们的主要工作是剖析内存走漏
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
配置依赖之后,重新运转项目,会看到一个leaks app,这个app便是用来监控内存走漏的东西
假如运用过leakcanary的伙伴们应该知道,leakcanary会生成一个hprof文件,那么经过MAT东西,能够剖析这个hprof文件,查找内存走漏的位置,下面的链接能够下载MAT东西 www.eclipse.org/mat/downloa…
2.2 内存走漏的场景
1. 资源性的目标没有关闭
例如,咱们在做一个相机模块,经过camera拿到了一帧图片,通常咱们会将其转换为bitmap,在运用完结之后,假如没有将其收回,那么就会形成内存走漏,具体运用完该怎样办呢?
if(bitmap != null){
bitmap?.recycle()
bitmap = null
}
调用bitmap的recycle办法,然后将bitmap置为null
2. 注册的目标没有刊出
这种场景其实咱们已经很常见了,在之前也提到过,便是注册跟反注册要成对呈现,例如咱们在注册广播接收器的时分,必定要记得,在Activity毁掉的时分去解注册,具体运用办法就不做过多的赘述。
3. 类的静态变量持有大数据量目标
由于咱们知道,类的静态变量是存储在办法区的,办法区空间有限并且生命周期长,假如持有大数据量目标,那么很难被gc收回,假如再次向办法区分配内存,会导致没有足够的空间分配,从而导致OOM
4. 单例形成的内存走漏
这个咱们在前面已经有一个具体的介绍,由于咱们在运用单例的时分,经常会传入context或许activity目标,由于有上下文的存在,导致单例持有不能被毁掉;
因此在传入context的时分,能够传入Application的context,那么单例就不会持有activity的上下文能够正常被收回;
假如不能传入Application的context,那么能够经过弱引证包装context,运用的时分从弱引证中取出,但这样会存在危险,由于弱引证或许随时被体系收回,假如在某个时刻有必要要运用context,或许会带来额定的问题,因此根据不同的场景谨慎运用。
object ToastUtils {
private var context:Context? = null
fun setText(context: Context) {
this.context = context
Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
}
}
咱们看下上面的代码,ToastUtils是一个单例,咱们在外边写了一个context:Context? 的引证,这种写法是十分危险的,由于ToastUtils会持有context的引证导致内存走漏
object ToastUtils {
fun setText(context: Context) {
Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
}
}
5. 非静态内部类的静态实例
咱们先了解下什么是静态内部类和非静态内部类,首要只要内部类才能设置为静态类,例如
class MainActivity : AppCompatActivity() {
private var a = 10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
}
inner class InnerClass {
fun setA(code: Int) {
a = code
}
}
}
InnerClass是一个非静态内部类,那么在MainActivity声明了一个变量a,其实InnerClass是能够拿到这个变量,也便是说,非静态内部类其实是对外部类有一个隐式持有,那么它的静态实例目标是存储在办法区,并且该目标持有MainActivity的引证,导致退出时无法被开释。
处理办法便是:将InnerClass设置为静态类
class InnerClass {
fun setA(code: Int) {
a = code //这儿就无法运用外部类的目标或许办法
}
}
大家假如对于kotlin不了解的话,就简单介绍一下,inner class在java中便是非静态的内部类;而直接用class润饰,那么就相当于Java中的 public static 静态内部类。
6. Handler
这个可便是老生常谈了,假如运用过Handler的话都知道,它十分简单发生内存走漏,具体的原理就不说了,感觉现在用Handler真的越来越少了
其实说了这么多,真正在写代码的时分,不能真正的防止,接下来我就运用leakcanary来检测某个项目中存在的内存走漏问题,并处理
3 从实践项目动身,根除内存走漏
1. 单例引发的内存走漏
咱们从gcroot中能够看到,在TeachAidsCaptureImpl中传入了LifeCycleOwner,LifeCycleOwner大家应该了解,能够监听Activity或许Fragment的生命周期,然后CaptureModeManager是一个单例,传入的mode便是TeachAidsCaptureImpl,这样就会导致一个问题,单例的生命周期很长,Fragment被毁掉的时分由于TeachAidsCaptureImpl持有了Fragment的引证,导致无法毁掉
fun clear() {
if (mode != null) {
mode = null
}
}
所以,在Activity或许Fragment毁掉前,将model置为空,那么内存走漏就会处理了,直到看到这个界面,那么咱们的运用便是安全的了
2.运用Toast引发的内存走漏
在咱们运用Toast的时分,需求传入一个上下文,咱们通常会传入Activity,那么这个上下文给谁用的呢,在Toast中也有View,假如咱们自定过Toast应该知道,那么假如Toast中的View持有了Activity的引证,那么就会导致内存走漏
Toast.makeText(this,"Toast内存走漏",Toast.LENGTH_SHORT).show()
那么怎样防止呢?传入Application的上下文,就不会导致Activity不被收回。