旋转完全空活动时StrictMode活动实例计数违规(2个实例,1个预期)

dhp*_*ott 34 java android memory-leaks android-activity

相关的只是因为它促使从严格模式中消除任何误报,因为任何人的继续存在使死刑变得不切实际

在过去的几天里,我一直在追逐并修复应用程序中的内存泄漏.在达到我认为已经修复它们的点之后,我实现了一个类似于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);
    }

}
Run Code Online (Sandbox Code Playgroud)

为了完整,清单:

<?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>
Run Code Online (Sandbox Code Playgroud)

我打开活动(设备持有肖像).我旋转到横向,然后回到肖像,此时我在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):在android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

堆转储

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

1

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

2

请注意,无论我在纵向和横向之间旋转多少次,实际实例的数量永远不会超过两个,并且预期实例的数量永远不会超过一个.

谁能理解泄漏?它甚至是真正的泄漏吗?如果是,它可能必须是一个Android bug.如果不是,我唯一可以看到它可能是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.

其他StrictMode来源:

  1. Activity的instanceTracker实例只是一个最终实例字段: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

mat*_*ash 20

我带着#2门走:这不是真正的泄漏.

更具体地说,它只与垃圾收集有关.三条线索:

  1. GC根的路径结束于FinalizerReference,与GC密切相关.它基本上处理调用finalize()符合GC条件的对象的方法 - 这里有一个,即ViewRootImpl.WindowInputEventReceiver扩展的实例,它InputEventReceiver有一个finalize()方法.如果这是一个"真正的"内存泄漏,那么该对象将符合GC的条件,并且至少应该有一个到GC根目录的其他路径.

  2. 至少在我的测试用例中,如果我获取堆快照之前强制执行GC ,那么只有一个引用MainActivity(如果我不执行快照,则有两个引用).看起来像从DDMS强制GC实际上包括调用所有终结器(最有可能通过调用FinalizerReference.finalizeAllEnqueued()哪个应该释放所有这些引用.

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

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

为什么StrictMode会这样想?

StrictMode关于这种情况有广泛的意见,请查看源代码decrementExpectedActivityCount().尽管如此,它看起来并没有像预期的那样完全正常.

    // 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);
    }
Run Code Online (Sandbox Code Playgroud)

更新

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

从此我得出结论,调用Runtime.getRuntime().gc()是实际释放这些可终结对象的内容,并且该代码仅在超出设置限制后运行.

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

虽然它不是100%万无一失,但调用System.gc()Activity onCreate()可能会让这个问题消失(而且在我的测试中确实如此).然而,Java的规范明确指出不能强制进行垃圾收集(这只是一个暗示)所以我不确定我是否会相信死刑,即使这个"修复"......

您可以通过调用反射手动增加活动实例计数的限制来组合它.但它似乎是一个非常粗糙的黑客:

Method m = StrictMode.class.getMethod("incrementExpectedActivityCount", Class.class);
m.invoke(null, MainActivity.class);
Run Code Online (Sandbox Code Playgroud)

(注意:确保在应用程序启动时只执行一次).

  • 赏金:因为这个问题最终得到了所需的关注,所以我们会对严格的模式资源进行检查并提供变通方法,以便更好地理解潜在的问题.:-) (5认同)