Android パフォーマンスの最適化 -- メモリの最適化

メモリは Android アプリケーションの生命線です. メモリに問題が発生すると, 軽いメモリ リークが発生し, 深刻な場合にはクラッシュします. したがって, アプリケーションは堅牢なままです. メモリの仕事は長引く戦争です, 注意を払う必要があります.したがって、メモリの最適化がどのように行われるかを理解したい場合は、基本から始める必要があります。

1 JVM メモリの原則

この部分は非常に退屈ですが、記憶モデルを理解することは非常に重要であり、この部分も頻繁にインタビューを受けています

画像.png

上の図から、JVMのメモリモジュールを左右2つに分けてみると、左側はすべてのスレッドがアクセスできる共有領域(メソッド領域、ヒープ領域)ですが、同期の問題も発生しますということで、ここでは詳しく説明しませんが、右側はプライベート領域に属し、各スレッドには独自の独立した領域があります。

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 仮想マシンでは、各スタックはスタック フレームに対応します。

画像.png

メソッドが呼び出されるたびに、スタックフレームがスタックにプッシュされることを意味し、onCreate メソッドが実行されると、execute メソッドが実行されるので、execute メソッドを見てみましょう。

execute メソッドは、Java 仮想マシン スタックのスタック フレームを表し、スタック フレームは次の 4 つの部分で構成されます。

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

画像.png

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

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

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

画像.png

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

画像.png

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

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

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

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

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

画像.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. 单例引发的内存泄漏

画像.png

gcroot を見ると、LifeCycleOwner が TeachAidsCaptureImpl に渡されていることがわかります. LifeCycleOwner は誰もが知っているはずで、Activity や Fragment のライフサイクルを監視できます. 次に、CaptureModeManager はシングルトンであり、受信モードは TeachAidsCaptureImpl であり、問​​題が発生します.シングルトンのサイクルは非常に長く、Fragment が破棄されると、TeachAidsCaptureImpl が Fragment への参照を保持しているため、Fragment を破棄することはできません。

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

したがって、アクティビティまたはフラグメントが破棄される前に、モデルを空に設定すると、メモリ リークが解決され、このインターフェイスが表示されるまで、アプリケーションは安全になります。

画像.png

2. Toast の使用によるメモリ リーク

画像.png

Toast を使用するときは、コンテキストを渡す必要があります. 通常は Activity を渡しますが、このコンテキストは誰のためのものでしょうか? Toast には View もあります. Toast をカスタマイズした場合は、Toast Holding の View がActivity への参照はメモリリークを引き起こします

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

では、それを回避する方法は?Application のコンテキストで渡しても、Activity がリサイクルされないことはありません。

おすすめ

転載: juejin.im/post/7134252767379456014