android - 如何使用 Android Profiler 在 Android 中查找不需要的引用
问题描述
我正在努力更好地寻找 Android 中的内存泄漏。
我发现了 Android Profiler 并学习了如何执行堆转储以及如何确定内存中给定对象的实例是否过多。
我已经读过,找到一个不需要的对象仍然存在的根源的方法之一是突出显示它并“查看哪些对象仍然持有对它的引用,并追溯到最初的原因。”
所以......在这个屏幕截图中,您可以看到不希望的情况:我有三个 MainActivity 实例......并且所有三个实例在“深度”列中都有一个数字,表明它们确实是泄漏。
如果有问题的对象是我自己创建的一个类,那么这个过程会更直接,但是由于我们在这里处理的是一个实际的 Activity,当我突出显示这三个中的任何一个时,就会有一个庞大的列表引用它的对象(列表远远超出屏幕截图)。
当然,其中大部分都是正常/良性的参考——我应该如何判断哪些值得研究?
有什么线索?是“ this$0 ”吗?还是保留列中的海量数字?与相关对象匹配的深度数?我只是在猜测这一点。
当然,我不希望自己仔细考虑整个列表,“不……不可能是那个……这是 Android 框架 X、Y 和 Z 的正常部分……”
解决方案
我正在努力更好地寻找 Android 中的内存泄漏。
我将举几个例子,但首先,由于您提出的问题与您的主题无关,我将首先解释您在“实例视图”中看到的内容;
深度:从任何 GC 根到所选实例的最短跃点数。
- 0 用于“私有最终类”或静态成员
- 如果它是空白的,它将被回收 - 这对您无害
Shallow Size:此实例在 Java 内存中的大小
保留大小:此实例支配的内存大小(根据支配树)。
是这个$0吗?
this$0
是编译器生成的合成变量,表示“超出我的范围”,它是非静态内部类的父对象。
我应该如何判断哪些值得调查?
这取决于,如果depth
是 0 并且它是你的代码 - 调查,也许这是一个长期运行的任务,结束条件不好。
在 Memory Profiler 中分析堆转储时,您可以过滤 Android Studio 认为可能表明应用中的 Activity 和 Fragment 实例存在内存泄漏的分析数据。
过滤器显示的数据类型包括:
已销毁但仍被引用的活动实例。
没有有效 FragmentManager 但仍被引用的片段实例。
在某些情况下,例如以下情况,过滤器可能会产生误报:
Fragment 已创建但尚未使用。
Fragment 正在被缓存,但不是 FragmentTransaction 的一部分。
分析记忆的技巧
在使用 Memory Profiler 时,您应该强调您的应用程序代码并尝试强制内存泄漏。
在您的应用程序中引发内存泄漏的一种方法是在检查堆之前让它运行一段时间。
泄漏可能会蔓延到堆中分配的顶部。
但是,泄漏越小,您需要运行应用程序才能看到它的时间越长。
您还可以通过以下方式之一触发内存泄漏:
- 在不同的活动状态下,将设备从纵向旋转到横向并再次旋转多次。旋转设备通常会导致应用程序泄漏 Activity、Context 或 View 对象,因为系统会重新创建 Activity,并且如果您的应用程序在其他地方持有对这些对象之一的引用,则系统无法对其进行垃圾收集。
- 在不同的活动状态下在您的应用程序和另一个应用程序之间切换(导航到主屏幕,然后返回到您的应用程序)。
不断增长的图表是一个重要指标
如果您观察到一条仅持续上升而很少下降的趋势线,则可能是由于内存泄漏,这意味着无法释放某些内存。或者根本没有足够的内存来处理应用程序。当应用程序达到其内存限制并且 Android 操作系统无法为应用程序分配更多内存时,将抛出 OutOfMemoryError。
短时间内出现震荡
湍流是不稳定的指标,这也适用于 Android 内存使用情况。当我们观察到这种模式时,通常会在其短暂的生命周期中创建并丢弃大量昂贵的对象。
CPU 在执行垃圾收集时浪费了很多周期,而没有为应用程序执行实际工作。用户可能会遇到缓慢的 UI,在这种情况下我们绝对应该优化我们的内存使用。
public class ThreadActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
new DownloadTask().start();
}
private class DownloadTask extends Thread {
@Override
public void run() {
SystemClock.sleep(2000 * 10);
}
}
}
内部类包含对其封闭类的隐式引用,它将自动生成一个构造函数并将活动作为对它的引用传递。
上面的代码其实是
public class ThreadActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
new DownloadTask(this).start();
}
private class DownloadTask extends Thread {
Activity activity;
public DownloadTask(Activity activity) {
this.activity = activity;
}
@Override
public void run() {
SystemClock.sleep(2000 * 10);
}
}
}
在正常情况下,用户打开活动等待 20 秒,直到下载任务完成。
任务完成后,堆栈释放所有对象。
然后下次垃圾收集器工作时,他将从堆中释放对象。
当用户关闭活动时,主方法将从堆栈中释放,线程活动也从堆中回收,一切都按需要工作而不会泄漏。
在用户在 10 秒后关闭/旋转活动的情况下。
任务仍在工作,这意味着活动的引用仍然存在,我们有内存泄漏
注意:当下载 run() 任务完成时,堆栈释放对象。所以当垃圾收集器下一次工作时,对象将从堆中回收,因为它上面没有引用对象。
推荐阅读
- continuous-integration - 构建过程完成后如何解决 Gitlab-runner gitlab-ci.yml 文件作业失败?
- android - java.lang.UnsatisfiedLinkError:dlopen 失败:找不到库
- javascript - 使用云功能的推送通知不起作用
- python - pyspark Hive Context -- 使用 UTF-8 编码读取表
- python - Python - 如何平滑 xarray 图?
- google-bigquery - 使用 CLI 将数据加载到列类型分区:不兼容的表分区规范
- security - Docker Image 编译会有副作用吗?
- hibernate - EJB 事务回滚导致不需要的数据库更改/对象持久性
- c++ - 模板接受 const 但不接受字面量
- sql - PL/SQL 中的 SQL 注入 - 神话还是事实?