Android Perfiler 性能分析工具(四)内存泄漏 抓取与分析-- Android APP 性能追踪与分析工具

924 阅读8分钟

创作不易,希望能一键三连哦~点赞👍、收藏🌟、加关注➕,随用随学不迷路~

接上篇 Android Perfiler 性能分析工具(三)CPU Trace抓取与分析-- Android APP 性能追踪与分析工具 - 掘金 (juejin.cn) 在学习了如何使用Perfiler分析CPU的性能后接下来我们就要对内存 MEMORY 进行分析,来确定APP的优化方向。

如何找到工具,参照:Android Perfiler 性能分析工具(一)如何使用-- Android APP 性能追踪与分析工具 - 掘金 (juejin.cn)

案例教学

MEMERY 性能分析与优化(一)内存泄漏

进入 MEMERY 性能追踪详情页

可以进行以下几项追踪,同时可以在右侧看到事实内存占用波动情况,细分为:

  • Other:您的应用使用的系统不确定如何分类的内存。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
  • Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
  • Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Native:从 C 或 C++ 代码分配的对象的内存。
  • Java:从 Java 或 Kotlin 代码分配的对象的内存。
  • Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。
  • 容易发生内存泄漏的大多数是 JavaNative 这两个,我们可以重点关注这两个的波动情况。如出现了持续增长,且点击了强制回收都不降下来,即说明内存出现了泄漏。
  • Capture heap dump:捕获堆转储
  • Record native allocations
  • Record Java / Kotlin allocations image.png

Capture heap dump

场景交互

对要分析的页面、函数、过程等,进行反复调用,然后在内存检测视图右击鼠标,选择 Force garbage collection 让内存回收,然后进行下一步。

image.png

1、启动录制

如需捕获堆转储,请点击 Capture heap dump,然后选择 Record。在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与您的应用发生在同一进程中,并需要一些内存以收集数据。 无需手动停止收集,程序将自动停止收集,并转跳到详细分析页面进行展示。如下图:

image.png

演示代码如下

class LeakActivity : AppCompatActivity() {
    private var view: View? = null

    private var context: Context? = null
    private var textView: TextView? = null
    private var testResource: TestResource? = null
    private var objectAnimator: ObjectAnimator? = null

    private lateinit var binding: ActivityLeakBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLeakBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 静态Activity(Activity上下文Context)和View
        context = this
        textView =  TextView(this)
        // 单例造成的内存泄漏
        TestManager.getInstance(this)
        // 线程造成的内存泄漏
        anonymousInnerClass()
        // 非静态内部类创建静态实例造成的内存泄漏
        testResource = TestResource()

        // 在属性动画中有一类无限循环动画
        objectAnimator = ObjectAnimator.ofFloat(binding.btnAddView, "rotation", 0f, 360f)
        objectAnimator!!.repeatCount = ValueAnimator.INFINITE
        objectAnimator!!.start()

        binding.btnAddView.setOnClickListener {
            TaskExecutors.doTask(10, object : (Int) -> Unit {
                override fun invoke(result: Int) {
                    // 这可能会导致内存泄漏,如果在活动被终止后任务仍在运行,甚至会导致崩溃
                    runOnUiThread {
                        binding.btnAddView.text = result.toString()
                    }
                }
            })
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 由于 neglect 将view置空导致内存泄漏
        // 如果不再需要,是否应该取消正在运行的任务
        //TaskExecutors.cancelTask()
    }


    //匿名内部类持有MemoryTestActivity实例引用,当耗时匿名线程内部类执行完成以后MemoryTestActivity实例才会回收;
    fun anonymousInnerClass() {
        object : AsyncTask<Void?, Void?, Void?>() {
            override fun doInBackground(vararg params: Void?): Void? {
                //执行异步处理
                SystemClock.sleep(120000)
                return null
            }
        }.execute()
    }

    class TestResource{
        //资源类
    }


}

object TaskExecutors {

    private var job: Job? = null

    fun doTask(input: Int, callback: (Int) -> Unit) {
        job = GlobalScope.launch {
            val result = doHeavyCalculation(input)
            callback.invoke(result)
        }
    }

    private suspend fun doHeavyCalculation(input: Int): Int {
        delay(10000L)
        return input * 10
    }

    fun cancelTask() {
        // find a way to cancel long-running task
    }
}


class TestManager private constructor(private val context: Context) {
    companion object {
        private var manager: TestManager? = null

        /**
         * 如果传入的context是activity,service的上下文,会导致内存泄漏
         * 原因是我们的manger是一个static的静态对象,这个对象的生命周期和整个app的生命周期一样长
         * 当activity销毁的时候,我们的这个manger仍然持有者这个activity的context,就会导致activity对象无法被释放回收,就导致了内存泄漏
         */
        fun getInstance(context: Context): TestManager? {
            if (manager == null) {
                manager = TestManager(context)
            }
            return manager
        }
    }
}
2、开始分析

警号⚠️标记的数量为工具检测到的内存泄漏的个数。点击即可进入详细页面查看具体信息。

  • a、点击Leaks,过滤出泄漏点
  • b、点击泄漏点进一步查看详情
  • c、点击详情,查看循环引用在哪里
  • d、转跳到循环引用位置进行修复
image.png
3、解决问题
  • 活动实例没有正确释放

例如: 在Activity的onDestroy()方法中没有将与Activity关联的视图等资源释放,导致Activity无法被垃圾回收。
修复: 在Activity的onDestroy()中释放所有的资源,如设置滑动监听器的View为null。

@Override
protected void onDestroy() {
    super.onDestroy();
    view = null;  // 释放View引用,避免泄漏
}
  • 匿名内部类导致的内存泄漏

例如: 给View设置点击监听器时使用匿名内部类,但没有正确移除监听器。
修复: 使用WeakReference或在适当位置(Activity/Fragment停止时)移除监听器。

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});
// 移除监听器 
view.setOnClickListener(null); 
  • 线程导致的内存泄漏

例如: 启动Thread但没有正确终止线程,线程中有对Context等资源的引用。
修复: 调用Thread.interrupt()终止线程,或者在线程run方法结束时释放资源。

  • 单例导致的内存泄漏

例如: 单例持有Context等资源的引用,但没有正确释放。
修复: 在单例的onDestory()等方法中释放资源。

  • 资源未关闭

例如: 打开输入流、输出流等资源但没有关闭,导致资源无法释放。 
修复: 确保及时调用close()方法关闭相关资源。

  • 资源未注销

例如: 注册广播接收者或服务但未调用unregisterReceiver()或stopService()注销,导致资源无法释放。
修复: 确保在适当位置注销广播接收者和停止服务。

  • 非静态内部类创建静态实例造成的内存泄漏

例如: 这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免重复创建,不过这种写法会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态实例,该实例的生命周期和应用一样长,这就导致了该静态实例一直持有该Activity的引用,导致Activity的内存资源不能正常回收;
解决方法: 将该内部类设为静态内部类或将内部类抽象出来封装一个单例,如果需要使用Context,请使用ApplicationContext;

  • Handler造成的内存泄漏

例如: 直接在变量声明的时候创建Handler的方式可能造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时,消息队列还有未处理的消息或者正在处理的消息(例如上面的例子,子线程中处理耗时任务,还没有执行完毕,activity就退出销毁),而消息队列中Message持有mHandler实例引用,mHander又持有Activity的引用,所以导致Activity的内存无法及时回收,引发内存泄漏。 解决方案总结:

  • 通过程序逻辑来进行维护

    • 在关闭Activity的时候停掉后台线程;线程停掉相当于切断了Handler和外部连接线,Activity自然会被在合适的时候回收;
    • 如果Handler被delay延迟的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行;
  • 将Handler声明为静态类

    • 在Java中,非静态的内部类和匿名内部类都会隐式持有其外部类的引用,静态内部类不会持有外部类的引用。静态类不持有外部类的对象,所以你的Activity可以随意被回收;由于Handler不在持有外部类的对象的引用,导致程序不允许你在Handler中操作Activity中的对象了,所以你需要在Handler中增加一个对Activity的弱引用(WeakReference);
  • 动画

例如: 在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy()中没有去停止动画,那么动画会一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。解决此类问题要在onDestroy()方法中去调用objectAnimator.cancel()来停止动画;

解决办法: 在onDestroy()方法中去调用objectAnimator.cancel()来停止动画。

  • 第三方库使用不当

例如: 对于EventBus,RxJava等一些第三方开源 框架 的使用,若是Activity销毁之前没有进行解除订阅会导致内存泄漏; 解决方案: 需要在生命周期相对注册与注销(onCreate->onDestory | onResume->onPause … )

  • 资源未关闭造成的内存泄漏 例如: 对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。 解决方案: 使用完资源后及时进行关闭回收。

创作不易,希望能一键三连哦~点赞👍、收藏🌟、加关注➕,随用随学不迷路~