严格模式活动实例计数冲突(2 个实例,1 个预期)在轮换完全为空的活动时

相关之处仅在于它促使从严格模式中消除任何误报,因为任何误报的持续存在使得死刑不切实际

在过去的几天里,我一直在追逐并修复应用程序中的内存泄漏。在达到我相信我已经修复了所有问题的程度时,我实现了一个类似于Android StrictMode和堆转储中描述的故障响亮机制(启用具有死刑的实例跟踪,拦截关闭错误消息,转储堆,下次应用程序启动时发送求救信号)。当然,所有这些都只是在调试版本中。

切中要害

在相信我已经修复了所有活动泄漏之后,某些活动仍然会导致屏幕旋转时出现严格的模式实例违规警告。奇怪的是,只有一些,而不是所有应用程序的活动来做到这一点。

我已经审查了发生此类违规行为时采取的堆转储,并审查了相关活动的代码以查找泄漏,但没有得到任何结果。

因此,在这一点上,我试图制作尽可能小的测试用例。我创建了一个完全空白的活动(甚至没有布局),看起来像这样:

package com.example.app;

import android.app.Activity;
import android.os.Bundle;
import android.os.StrictMode;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        StrictMode.setVmPolicy(
                new StrictMode.VmPolicy.Builder()
                        .detectAll()
                        .penaltyLog()
                        .build());
        StrictMode.setThreadPolicy(
                new StrictMode.ThreadPolicy.Builder()
                        .detectAll()
                        .penaltyDeath()
                        .penaltyLog()
                        .build());
        super.onCreate(savedInstanceState);
    }

}

为了完整起见,清单:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app" >

    <application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
        android:name="com.example.app.MainActivity"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    </application>

</manifest>

我打开活动(设备保持肖像)。我旋转到横向,然后回到纵向,此时我从LogCat中的StrictMode中看到:

01-15 17:24:23.248: E/StrictMode(13867): class com.example.app.MainActivity;实例数 = 2;limit=1 01-15 17:24:23.248: E/StrictMode(13867): android.os.StrictMode$InstanceCountViolation: class com.example.app.MainActivity;实例数 = 2;limit=1 01-15 17:24:23.248: E/StrictMode(13867): at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

堆转储

此时,我使用 DDMS 手动获取堆转储。以下是 MAT 中的两个实例:

1

这是泄露的那个,从它到GC根的路径的第一部分:

2

请注意,无论我在纵向和横向之间旋转多少次,实际实例数永远不会超过 2,并且预期实例数永远不会超过 1。

任何人都可以理解泄漏吗?这甚至是真正的泄漏吗?如果是,则可能一定是Android错误。如果不是,我能看到的唯一另一件事可能是StrictMode中的一个错误。

好的答案可能包括

  1. 如果这是一个真正的泄漏,解释它是如何发生的,以及是否可以采取任何行动来修复它或禁止StrictMode对此类案件启动死刑(回想一下,误报/中性使死刑不切实际)。
  2. 如果这不是一个真正的泄漏,解释为什么StrictMode不这么认为,以及如果可以采取任何行动来修复它或禁止StrictMode对此类案件启动死刑(回想一下,误报/中性使死刑不切实际)。
  3. 在任何一种情况下,假设为什么所有活动都不会发生这种情况(我正在处理的应用程序中的大多数活动不会在轮换时产生此类冲突)。

到目前为止,我已经查看了StrictMode源代码,并且没有看到任何明显的错误 - 正如预期的那样,在考虑将额外的实例视为违规之前,它会强制GC。

阅读StrictMode源代码的良好切入点是:

  1. http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/os/StrictMode.java#StrictMode.trackActivity%28java.lang.Object%29
  2. http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/os/StrictMode.java#StrictMode.InstanceTracker.finalize%28%29
  3. http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/os/StrictMode.java#StrictMode.incrementExpectedActivityCount%28java.lang.Class%29
  4. http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/os/StrictMode.java#StrictMode.decrementExpectedActivityCount(java.lang.Class)

全面披露

我只在一台设备上进行了这些测试,即运行CyanogenMod 11快照M2的Galaxy S4(http://download.cyanogenmod.org/get/jenkins/53833/cm-11-20140104-SNAPSHOT-M2-jfltexx.zip),但我无法想象CyanogenMod会在活动管理方面偏离KitKat。

其他严格模式来源:

  1. 活动的实例跟踪器实例只是一个最终实例字段:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/app/Activity.java#Activity.0mInstanceTracker
  2. 预期活动实例计数递增的位置:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/app/ActivityThread.java#2095
  3. 减少预期活动实例计数的位置:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/app/ActivityThread.java#3485

答案 1

我选择2号门:这不是一个真正的泄漏。

更具体地说,它只是与垃圾回收有关。三条线索:

  1. GC 根目录的路径在 FinalizerReference 结束,这与 GC 密切相关。它基本上处理调用符合GC条件的对象上的方法 - 这里有一个,即扩展的实例,它确实有一个方法。如果这是“真正的”内存泄漏,则该对象将不符合 GC 的条件,并且至少应该有一个指向 GC 根目录的其他路径。finalize()ViewRootImpl.WindowInputEventReceiverInputEventReceiverfinalize()

  2. 至少在我的测试用例中,如果我在拍摄堆快照之前强制GC,那么只有一个引用(而如果我不这样做就拍摄快照,则有两个)。看起来从 DDMS 强制 GC 实际上包括调用所有终结器(最有可能通过调用应该释放所有这些引用)。MainActivityFinalizerReference.finalizeAllEnqueued()

  3. 我可以在Android 4.4.4的设备中重现它,但在具有新GC算法的Android L中则不然(诚然,这最多是间接证据,但它与其他算法一致)。

为什么某些活动会发生这种情况,而其他活动则不会发生这种情况?虽然我不能肯定地说,但构建一个“更复杂”的活动可能会触发GC(仅仅是因为它需要分配更多的内存),而像这样的简单活动“通常”不会。但这应该是可变的。

为什么严格模式不这么认为?

关于这种情况有广泛的评论,请检查的源代码。尽管如此,它看起来并没有完全按照他们的预期工作。StrictModedecrementExpectedActivityCount()

    // Note: adding 1 here to give some breathing room during
    // orientation changes.  (shouldn't be necessary, though?)
    limit = newExpected + 1;

    ...

    // Quick check.
    int actual = InstanceTracker.getInstanceCount(klass);
    if (actual <= limit) {
        return;
    }

    // Do a GC and explicit count to double-check.
    // This is the work that we are trying to avoid by tracking the object instances
    // explicity.  Running an explicit GC can be expensive (80ms) and so can walking
    // the heap to count instance (30ms).  This extra work can make the system feel
    // noticeably less responsive during orientation changes when activities are
    // being restarted.  Granted, it is only a problem when StrictMode is enabled
    // but it is annoying.
    Runtime.getRuntime().gc();

    long instances = VMDebug.countInstancesOfClass(klass, false);
    if (instances > limit) {
        Throwable tr = new InstanceCountViolation(klass, instances, limit);
        onVmPolicyViolation(tr.getMessage(), tr);
    }

更新

实际上,我已经进行了更多的测试,使用反射进行调用,我发现了一个非常奇怪的结果,这并没有改变答案(它仍然是#2),但我认为提供了额外的线索。如果将活动的“预期”实例数增加(例如,增加到 4 个),则每 4 次轮换一次,仍会发生严格模式冲突(声称存在 5 个实例)。StrictMode.incrementExpectedActivityCount()

由此,我得出的结论是,对的调用是实际发布这些可最终确定对象的内容,并且代码仅在超出设置的限制后运行。Runtime.getRuntime().gc()

如果可以采取任何措施来修复它怎么办?

虽然它不是100%万无一失的,但调用活动可能会使这个问题消失(在我的测试中确实如此)。但是,Java的规范清楚地表明不能强制垃圾收集(这只是一个提示),所以我不确定我是否会相信死刑,即使有这个“修复”......System.gc()onCreate()

您可以将其与通过调用反射来手动增加活动实例计数的限制相结合。但这似乎是一个非常粗糙的黑客攻击:

Method m = StrictMode.class.getMethod("incrementExpectedActivityCount", Class.class);
m.invoke(null, MainActivity.class);

(注意:请确保在应用程序启动时仅执行此操作一次)。


答案 2

我想总结一下,并为其他开发人员提供安全时间的最终答案。

android.os.StrictMode$InstanceCountViolation 是否是一个问题

这可能是一个真正的问题。要确定这是否是一个问题,我可以推荐以下帖子:检测Android中泄露的活动。如果您看到有对象包含对此活动的引用,而这些对象与Android框架无关,那么您就有一个问题应该由您来修复。

如果没有对象包含与此活动无关的与此活动的引用,则意味着您遇到了与如何实现expatchActivityLeaks检查相关的问题。另外,正如所指出的,在这种情况下,垃圾回收应该会有所帮助

为什么 detectActivityLeaks 工作不正常,并且不能在所有设备上重现

如果我们来看看 detectActivityLeaks 的来源:

// Note: adding 1 here to give some breathing room during
// orientation changes.  (shouldn't be necessary, though?)
limit = newExpected + 1;

...

// Quick check.
int actual = InstanceTracker.getInstanceCount(klass);
if (actual <= limit) {
    return;
}

// Do a GC and explicit count to double-check.
// This is the work that we are trying to avoid by tracking the object instances
// explicity.  Running an explicit GC can be expensive (80ms) and so can walking
// the heap to count instance (30ms).  This extra work can make the system feel
// noticeably less responsive during orientation changes when activities are
// being restarted.  Granted, it is only a problem when StrictMode is enabled
// but it is annoying.
Runtime.getRuntime().gc();

long instances = VMDebug.countInstancesOfClass(klass, false);
if (instances > limit) {
    Throwable tr = new InstanceCountViolation(klass, instances, limit);
    onVmPolicyViolation(tr.getMessage(), tr);
}

这里的问题是,要正确计算实例,所有以前的实例都应该被垃圾回收(至少逻辑是中继的)。这是此实现的主要问题。原因是,如果你想确保GC收集了所有准备释放的对象,那么GC的一次往返行程在Java中是不够的。在大多数情况下,调用 System.gc() 两次或使用与此 jlibs 库中实现的方法类似的方法就足够了。

这就是为什么此问题在某些设备(操作系统版本)上重现而在另一些设备上不会重现的原因。这完全取决于GC的实现方式,有时只有一个调用就足以确保GC将收集该对象。

如果没有泄漏的活动,如何避免此问题在调试配置中启动活动之前,我只需运行 System.gc(),如以下示例所示。这将有助于避免此问题,并使您能够继续使用expatchActivityLeaks检查。

 if (BuildConfig.DEBUG)
 {         
     System.gc();
 }
 Intent intent = new Intent(context, SomeActivity.class);
 this.startActivity(intent);

这还可以确保在发布版本中不会强制进行垃圾回收,因为不建议这样做。


推荐