Android performance optimization -- memory optimization

Memory is the lifeblood of Android applications. Once there is a problem with memory, memory leaks in the light, and crashes in the serious. Therefore, an application remains robust. The work of memory is a protracted war, and you need to pay attention to reasonableness from writing code. Therefore, if you want to understand how memory optimization is done, you must start with the basics.

1 JVM memory principle

This part is really boring, but it is very important for us to understand the memory model, and this part is also a frequent interviewee

image.png

From the above figure, I divided the memory module of JVM into two parts, left and right. The left belongs to the shared area (method area, heap area), which can be accessed by all threads, but it will also bring synchronization problems, so I won't go into details here. The right side belongs to the private area, and each thread has its own independent area.

1.1 Method execution flow

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
}
复制代码

We see that in the onCreate method of MainActivity, the execute method is executed, because it is currently a UI thread, and each thread has a Java virtual machine stack. As you can see from the above figure, then every time a method is executed, in the Java virtual machine Each stack corresponds to a stack frame.

image.png

Each time a method is called, it means that a stack frame is pushed onto the stack. When the onCreate method is executed, the execute method will be executed, so let's take a look at the execute method.

The execute method represents a stack frame in the Java virtual machine stack, and the stack frame consists of four parts:

(1) Local variable table : Local variables are declared in the method body, such as a, b, c, and will also be recycled after the method is executed; (2) Operand stack : In any method, any variable is involved. Operations such as inter-operation are performed in the operand stack; for example, in the execute method:

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对象则是存在堆区,看下图

image.png

因为方法区属于常驻内存,那么其中的instance引用会一直跟堆区的Observable连接,导致这个单例对象会存在很长的时间

btnRegister.setOnClickListener {
    Observable.instance.register(this)
}
btnSend.setOnClickListener {
    Observable.instance.send("发送消息")
}
复制代码

在MainActivity中,点击注册按钮,注意这里传入的值,是当前Activity,那么这个时候退出,会发生什么?我们先从profile工具里看一下,退出之后,有2个内存泄漏的地方,如果使用的leakcannary(后面会介绍)就应该会明白

image.png

那么在MainActivity中,哪个地方发生的了内存泄漏呢?我们紧跟一下看看GcRoot的引用,发现有这样一条引用链,MainActivity在一个list数组中,而且这个数组是Observable中的observers,而且是被instance持有,前面我们说到,instance的生命周期很长,所以当Activity准备被销毁时,发现被instance持有导致回收失败,发生了内存泄漏。

image.png

那么这种情况,我们该怎么处理呢?一般来说,有注册就有解注册,所以我们在封装的时候一定要注意单例中传入的参数

override fun onDestroy() {
    super.onDestroy()
    Observable.instance.unregister(this)
}
复制代码

再次运行我们发现,已经不存在内存泄漏了

image.png

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就是用来监控内存泄漏的工具

image.png 那我们执行之前的应用,打开leaks看一下gcroot的引用,是不是跟我们在as的profiler中看到的是一样的

image.png

如果使用过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. 单例引发的内存泄漏

image.png

We can see from gcroot that LifeCycleOwner is passed in TeachAidsCaptureImpl. LifeCycleOwner should be familiar to everyone and can monitor the life cycle of Activity or Fragment. Then CaptureModeManager is a singleton, and the incoming mode is TeachAidsCaptureImpl, which will cause a problem , the life cycle of a singleton is very long. When the Fragment is destroyed, it cannot be destroyed because TeachAidsCaptureImpl holds a reference to the Fragment.

fun clear() {
    if (mode != null) {
        mode = null
    }
}
复制代码

Therefore, before the Activity or Fragment is destroyed, set the model to empty, then the memory leak will be solved, until we see this interface, then our application is safe

image.png

2. Memory leaks caused by using Toast

image.png

When we use Toast, we need to pass in a context. We usually pass in Activity, so who is this context for? There is also View in Toast. If we have customized Toast, we should know, then if the View in Toast Holding a reference to Activity will cause a memory leak

Toast.makeText(this,"Toast内存泄漏",Toast.LENGTH_SHORT).show()
复制代码

So how to avoid it? Passing in the context of the Application will not cause the Activity to not be recycled.

Guess you like

Origin juejin.im/post/7134252767379456014