创作不易,希望能一键三连哦~点赞👍、收藏🌟、加关注➕,随用随学不迷路~
接上篇 Android Perfiler 性能分析工具(三)CPU Trace抓取与分析-- Android APP 性能追踪与分析工具 - 掘金 (juejin.cn) 在学习了如何使用Perfiler分析CPU的性能后接下来我们就要对内存 MEMORY 进行分析,来确定APP的优化方向。
- Perfetto 官方源码链接地址 github.com/google/perf…
- Perfetto UI 在线分析工具 ui.perfetto.dev/
- 也可用Android Studio自带工具Profiler进行分析(有🤏一点点卡,但方便不依赖网络)
- 本文案例源码:MartinDong/PerfettoDemo: Android 性能优化 (github.com)
如何找到工具,参照: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++ 中分配的对象。
- 容易发生内存泄漏的大多数是 Java、Native 这两个,我们可以重点关注这两个的波动情况。如出现了持续增长,且点击了强制回收都不降下来,即说明内存出现了泄漏。
- Capture heap dump:捕获堆转储
- Record native allocations
- Record Java / Kotlin allocations
Capture heap dump
场景交互
对要分析的页面、函数、过程等,进行反复调用,然后在内存检测视图右击鼠标,选择 Force garbage collection 让内存回收,然后进行下一步。
1、启动录制
如需捕获堆转储,请点击 Capture heap dump,然后选择 Record。在转储堆期间,Java 内存量可能会暂时增加。 这很正常,因为堆转储与您的应用发生在同一进程中,并需要一些内存以收集数据。 无需手动停止收集,程序将自动停止收集,并转跳到详细分析页面进行展示。如下图:
演示代码如下
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、转跳到循环引用位置进行修复
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销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。 解决方案: 使用完资源后及时进行关闭回收。
创作不易,希望能一键三连哦~点赞👍、收藏🌟、加关注➕,随用随学不迷路~