Подтвердить что ты не робот

Нарушение счетчика активности экземпляра StrictMode (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>

Я открываю действие (портрет, удерживаемый устройством). Я поворачиваюсь к пейзажу и возвращаюсь к портрету, и в этот момент я вижу из StrictMode это в LogCat:

01-15 17: 24: 23.248: E/StrictMode (13867): класс com.example.app.MainActivity; экземпляры = 2; предел = 1 01-15 17: 24: 23.248: E/StrictMode (13867): android.os.StrictMode $InstanceCountViolation: класс com.example.app.MainActivity; экземпляры = 2; предел = 1 01-15 17: 24: 23.248: E/StrictMode (13867): at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

Куча кучи

В этот момент я вручную получаю кучу кучи, используя DDMS. Вот два примера в MAT:

1

И вот тот, который просочился, с первой частью пути от него до корня GC:

2

Обратите внимание, что независимо от того, сколько раз я вращаюсь между портретом и ландшафтом, количество фактических экземпляров никогда не превышает двух, а количество ожидаемых экземпляров никогда не превышает один.

Может ли кто-нибудь понять утечку? Это даже настоящая утечка? Если это так, вероятно, это ошибка Android. Если это не так, единственное, что я вижу, это может быть ошибка в StrictMode.

Хорошие ответы могут включать

  • Если это настоящая утечка, объяснение того, как это происходит, и что, если можно предпринять какие-либо действия для ее исправления или подавить StrictMode от начала смертной казни за такие случаи (напомните, что ложные срабатывания/нейтралитеты делают смертную казнь непрактично).
  • Если это не настоящая утечка, объяснение того, почему StrictMode думает иначе, и что, если можно предпринять какие-либо действия для его устранения или подавить StrictMode от начала смертной казни для таких случаев (напомните, что ложные срабатывания/нейтрали делают смертная казнь непрактична).
  • В любом случае, гипотезы о том, почему это не происходит со всеми действиями (большинство действий в приложении, над которым я работаю, не порождают таких нарушений при ротации).

Я дошел до источника StrictMode и не вижу ничего явно неправильного - как и ожидалось, он заставляет GC, прежде чем рассматривать дополнительный экземпляр как нарушение.

Хорошие точки входа для чтения источника StrictMode:

Полное раскрытие информации

Я только делал эти тесты на одном устройстве, Galaxy S4 запускал CyanogenMod 11 моментальный снимок M2 (http://download.cyanogenmod.org/get/jenkins/53833/cm-11-20140104-SNAPSHOT-M2-jfltexx.zip), но я не может представить, что CyanogenMod будет отклоняться от KitKat с точки зрения управления деятельностью.

Дополнительные источники StrictMode:

4b9b3361

Ответ 1

Я иду с дверью № 2: Это не настоящая утечка.

В частности, это просто связано с сборкой мусора. Три подсказки:

  • Путь к корню GC заканчивается на FinalizerReference, который тесно связан с GC. Он в основном обрабатывает вызовы методов finalize() для объектов, которые имеют право на GC, для которых он существует, а именно экземпляр ViewRootImpl.WindowInputEventReceiver, который расширяет InputEventReceiver, который имеет метод finalize(). Если это была "настоящая" утечка памяти, тогда объект не будет иметь права на GC, и должен существовать хотя бы один путь к корню GC.

  • По крайней мере, в моем тестовом примере, если я заставляю GC до делать снимок кучи, тогда есть только одна ссылка на MainActivity (тогда как есть два, если я беру снимок без этого). Похоже, принудительное включение GC из DDMS включает вызов всех финализаторов (скорее всего, вызывая FinalizerReference.finalizeAllEnqueued(), который должен выпустить все эти ссылки.

  • Я мог бы воспроизвести его на устройстве с Android 4.4.4, но не в Android L, у которого есть новый алгоритм GC (по общему признанию, это самое косвенное доказательство, но оно согласуется с остальными).

Почему это происходит для некоторых видов деятельности, а не для других? Хотя я не могу точно сказать, вероятно, что создание "более сложной" активности запускает 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);
    }

Обновление

На самом деле, я выполнил больше тестов, вызвав StrictMode.incrementExpectedActivityCount() с помощью отражения, и я нашел очень любопытный результат, который не меняет ответа (он все еще # 2), но я думаю, что дает дополнительную подсказку. Если вы увеличите количество "ожидаемых" экземпляров Activity (скажем, до 4), тогда будет нарушено строгое нарушение режима (при наличии 5 экземпляров) на каждом 4-м повороте.

Из этого я пришел к выводу, что вызов Runtime.getRuntime().gc() - это то, что фактически выпускает эти финализированные объекты, и этот код работает только после выхода за пределы установленного предела.

Что делать, если можно исправить это действие?

Хотя это не на 100% надежнее, вызов System.gc() в Activity onCreate(), скорее всего, заставит эту проблему уйти (и это было в моих тестах). Однако спецификация Java четко говорит о том, что сбор мусора не может быть принудительно (и это всего лишь намек), поэтому я не уверен, что буду доверять смертной казни даже с этим "исправлением"...

Вы могли бы объединить его с ручным увеличением предела для экземпляра экземпляра активности, вызвав отражение. Но это кажется действительно грубым взломом:

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

(Примечание: обязательно выполняйте это только один раз при запуске приложения).

Ответ 2

Я хочу обобщить и дать окончательный ответ безопасному времени для других разработчиков.

Является ли android.os.StrictMode $InstanceCountViolation проблемой или нет

Это может быть реальной проблемой. Чтобы определить, является ли это проблемой, я могу порекомендовать следующее сообщение: Обнаружение утечки активности в Android. Если вы увидите, что есть объекты, содержащие ссылку на это действие, которые не связаны с Android Framework, тогда у вас есть проблема, которая должна быть исправлена ​​вами.

Если нет объектов, содержащих ссылку на это действие, которые не связаны с Android Framework, это означает, что вы столкнулись с проблемой, связанной с проверкой checkActivityLeaks. Также, как было указано, сбор мусора должен помочь в этом случае

Почему detectActivityLeaks работает неправильно и воспроизводит не на всех устройствах

Если мы посмотрим на источники обнаруженияActivityLeaks:

// 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 не может быть достаточно на Java, если вы хотите быть уверенным, что все объекты, которые готовы к выпуску, собираются GC. В большинстве случаев достаточно вызвать System.gc() дважды или использовать что-то похожее на методы, реализованные в этой jlibs библиотеке.

Вот почему эта проблема воспроизводится на некоторых устройствах (версии ОС) и не воспроизводится на других. Все это зависит от того, как выполняется GC, и иногда достаточно одного вызова, чтобы убедиться, что объект будет собран GC.

Как избежать этой проблемы в случае отсутствия утечки Я просто запускаю System.gc() перед началом работы в конфигурации отладки, как в следующем примере. Это поможет избежать этой проблемы и даст вам возможность продолжить проверку detectActivityLeaks.

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

Это также гарантирует, что в выпуске сборка мусора не будет принудительно, поскольку это не рекомендуется.

Ответ 3

Я не разработчик Android, так что это немного выстрел в темноте.

Предположительно, поскольку поведение несовместимо для других действий, но, тем не менее, запускается совершенно тривиально, проблема сводится к времени, затраченному на завершение onCreate.

Если функция onCreate может завершиться до завершения последовательности паузы/остановки/уничтожения (и в конечном итоге сбор мусора, хотя она не должна доходить до этой точки afaics) для старой активности, тогда действительно будет два экземпляра.

Простое решение для предотвращения этого состоит в том, чтобы активность имела статический атомный логический "готовый" (изначально ложный), который проверяет перед тем, как делать что-либо еще в onCreate, где вы будете цитировать

while(!ready.compareAndSet(false, true)) {
    //sleep a bit
}

затем переопределите обратный вызов жизненного цикла ondestroy и вызовите ready.compareAndSet(true, false)

Извините, если этот подход абсолютно наивен.