دراسة حالة: تصحيح أخطاء تعطُّل بسبب نفاد الذاكرة

تحدث الأعطال بسبب نفاد الذاكرة (OutOfMemoryError) عندما يستنفد تطبيقك كومة Java المخصّصة له ولا يمكنه تخصيص ذاكرة جديدة. نادرًا ما تكشف عمليات تتبُّع تسلسل استدعاء الدوال البرمجية من تقارير الأعطال في OutOfMemoryError عن السبب الأساسي لأنّها تعرض فقط طلب التخصيص الذي تعذّر إكماله عند نفاد الذاكرة، وليس العناصر التي تحتفظ ببقية الذاكرة الديناميكية.

تتيح لك أداة ProfilingManager التقاط لقطات لأجزاء من ذاكرة Java من الأجهزة في الميدان ومحليًا حتى تتمكّن من فحص الرسم البياني للاحتفاظ بالعناصر في واجهة مستخدم Perfetto وتحديد ما يحتفظ بالذاكرة.

السيناريو

لنفترض أنّ أداة إعداد تقارير الأعطال تشير إلى مجموعة متزايدة من OutOfMemoryError الأعطال في مرحلة الإنتاج عندما يتفاعل المستخدمون مع OomSampleActivity.

للتحقيق في المشكلة، يمكنك تسجيل مشغّل TRIGGER_TYPE_OOM باستخدام ProfilingManager لكي يلتقط النظام تلقائيًا لقطة لأجزاء من الذاكرة في Java عند حدوث OutOfMemoryError.

التقاط لقطة لأجزاء من الذاكرة

في الإصدار 17 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 37) والإصدارات الأحدث، يمكنك ضبط الملفات المستندة إلى المشغّلات باستخدام ProfilingTrigger.TRIGGER_TYPE_OOM لأخذ لقطة لأجزاء من الذاكرة عند حدوث عطل في OutOfMemoryError. عندما يطرح تطبيقك استثناءً غير معالَج OutOfMemoryError، يبدأ النظام تلقائيًا في التقاط PROFILING_TYPE_JAVA_HEAP_DUMP جديد.

بما أنّ عملية التطبيق تنتهي عند حدوث العطل، ينهي النظام حفظ لقطة لأجزاء من الذاكرة في الخلفية ويضع النتيجة في قائمة الانتظار (يُرجى الاطّلاع على دورة حياة المشغّل). سجِّل مستمعًا عامًا للنتائج في الخلفية Executor بعد اكتمال بدء تشغيل التطبيق (حتى لا تبطئ بدء تشغيل التطبيق) باستخدام registerForAllProfilingResults حتى يتلقّى تطبيقك ProfilingResult في قائمة الانتظار في المرة التالية التي يشغّله فيها المستخدم. على عكس addProfilingTriggers (التي تستمر في العمل عند إعادة تشغيل التطبيق ولا يلزم استدعاؤها إلا مرة واحدة أو عند تعديل المشغّلات)، يجب استدعاء registerForAllProfilingResults عند كل عملية بدء تشغيل للتطبيق:

Kotlin

@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN)
fun registerOomTrigger(context: Context) {
    val profilingManager =
        context.getSystemService(ProfilingManager::class.java) ?: return
    val callbackExecutor: Executor = Executors.newSingleThreadExecutor()

    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (
            profilingResult.errorCode == ProfilingResult.ERROR_NONE &&
            profilingResult.triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM
        ) {
            val heapDumpPath = profilingResult.resultFilePath
            // Schedule a background job to compress and upload heapDumpPath
        }
    }

    profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback)

    val oomTrigger = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM)
        .build()
    profilingManager.addProfilingTriggers(listOf(oomTrigger))
}

Java

@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN)
public static void registerOomTrigger(Context context) {
    ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class);
    if (profilingManager == null) return;
    Executor callbackExecutor = Executors.newSingleThreadExecutor();

    Consumer<ProfilingResult> resultCallback =
        profilingResult -> {
            if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE
                && profilingResult.getTriggerType() == ProfilingTrigger.TRIGGER_TYPE_OOM) {
                String heapDumpPath = profilingResult.getResultFilePath();
                // Schedule a background job to compress and upload heapDumpPath
            }
        };

    profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback);

    ProfilingTrigger oomTrigger =
        new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM)
            .build();
    profilingManager.addProfilingTriggers(Collections.singletonList(oomTrigger));
}

إزالة التشويش من الملف الشخصي

بعد أن يؤدي OutOfMemoryError إلى بدء عملية تسجيل ويتلقّى ردّ الاتصال ProfilingResult عند تشغيل التطبيق في المرة التالية، حمِّل ملف الملف الشخصي إلى خادمك أو اسحبه إلى جهاز التطوير باستخدام adb. إذا سجّلت الملف الشخصي من بنية إصدار تستخدم تقليص حجم الرموز وإخفائها باستخدام R8، عليك إزالة الإخفاء بلا إنترنت على محطة العمل أو الخادم قبل التحليل. بدون إزالة الغموض، يُخفي R8 أسماء فئات Java وKotlin والحقول والطُرق في لقطة أجزاء من الذاكرة الأولية (على سبيل المثال، تحويل أسماء الفئات الأصلية إلى a.b).

استخدِم الأمر trace_processor bundle في Perfetto مع علامة --proguard-map لتجميع الملف الشخصي الذي تم تسجيله مع ملف mapping.txt في R8 الذي تم إنشاؤه لبنية الإصدار المحددة:

trace_processor bundle \
  --proguard-map /path/to/mapping.txt \
  profile_trigger-type-7_2026-05-06-14-12-40-748_uid-10316.perfetto-java-heap-dump \
  deobfuscated_oom.tar

ينشئ هذا الأمر ملف deobfuscated_oom.tar، وهو أرشيف غني يجمع الملف الشخصي الأصلي مع عمليات ربط إزالة التشويش. عند فتح هذا الأرشيف في واجهة مستخدم Perfetto أو إرسال طلب بحث عنه باستخدام trace_processor، يستعيد Perfetto تلقائيًا جميع أسماء الفئات والحقول والطرق الأصلية. لمزيد من المعلومات، يُرجى الاطّلاع على مستندات الترميز وإزالة التشويش في Perfetto.

تحليل لقطة لأجزاء من الذاكرة

يمكنك فحص لقطة أجزاء من الذاكرة التي تم إلغاء تشويشها يدويًا في واجهة مستخدم Perfetto كما هو موضّح في الخطوات التالية، أو إدارة عملية التحقيق في مشكلة نفاد الذاكرة آليًا باستخدام وكيل ذكاء اصطناعي.

افتح لقطة لأجزاء من الذاكرة التي تم إلغاء تشويشها في واجهة مستخدم Perfetto واختَر Heapdump Explorer في شريط التنقّل الأيمن. في علامة التبويب Flamegraph مع ضبط المقياس على حجم العنصر، يبرز التسلسل الهرمي للاحتفاظ بالبيانات في الشكل 1 على الفور.

رسم بياني لهيكل هرمي في أداة Heapdump Explorer في واجهة مستخدم Perfetto يعرض StatsReporter يحتفظ بـ 8 نُسخ من OomSampleActivity بإجمالي 240.28 ميغابايت.
الشكل 1. رسم بياني مفصّل لأداء الرموز في لقطة لأجزاء من الذاكرة يعرض 8 نُسخ محفوظة من OomSampleActivity.

يكشف فحص الرسم البياني اللهبي في الشكل 1 عن عدة نتائج رئيسية:

  • الاحتفاظ بكمية هائلة من الذاكرة: من إجمالي الذاكرة الديناميكية التي تبلغ 254.79 ميغابايت (root)، يحتفظ com.example.sampleapp.OomSampleActivity بحجم تراكمي يبلغ 240.28 ميغابايت (%94.31 من إجمالي الذاكرة الديناميكية للتطبيق)، ويقع معظمها في مصفوفة byte[] فرعية.
  • مثيلات متعددة للنشاط المباشر: يؤدي اختيار الإطار OomSampleActivity إلى عرض العدد الذاتي: 8. على الرغم من أنه يجب أن يكون هناك مثيل واحد فقط من OomSampleActivity على الشاشة في أي وقت، إلا أن هناك 8 مثيلات منفصلة موجودة في الذاكرة في وقت واحد.
  • مسار الكائن المحتفظ به: تشير قراءة الإطارات الرئيسية لـ OomSampleActivity في الرسم البياني اللهبي إلى سلسلة المراجع التي تُبقي هذه النُسخ نشطة: يحتوي com.example.sampleapp.StatsReporter على java.util.concurrent.CopyOnWriteArrayList (مدعوم من java.lang.Object[])، والذي يشير مباشرةً إلى جميع نُسخ OomSampleActivity البالغ عددها 8.

تحديد السبب الأساسي في الرمز البرمجي

يشير مسار الاحتفاظ من StatsReporter إلى OomSampleActivity، لذا عليك فحص كلا الفئتين في قاعدة الرموز البرمجية.

‫StatsReporter هو عنصر مفرد يحتفظ بقائمة من عمليات الاستدعاء StatsListener وينشر التحديثات بشكل دوري من سلسلة استطلاع في الخلفية:

Kotlin

class StatsReporter private constructor() {
    fun interface StatsListener {
        fun onReportStats(stats: String)
    }

    private val listeners: MutableList<StatsListener> = CopyOnWriteArrayList()
    private var isReporting = false

    init {
        startReporting()
    }

    fun registerListener(listener: StatsListener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener)
        }
    }

    fun unregisterListener(listener: StatsListener) {
        listeners.remove(listener)
    }

    private fun startReporting() {
        if (isReporting) return
        isReporting = true

        Thread({
            while (true) {
                try {
                    // Poll every 10 seconds
                    Thread.sleep(10000)
                } catch (e: InterruptedException) {
                    Thread.currentThread().interrupt()
                    break
                }

                for (listener in listeners) {
                    // Send periodic mock stats update from the background thread
                    listener.onReportStats("Update from StatsReporter background polling thread")
                }
            }
        }, "StatsReporterPollingThread").start()
    }

    companion object {
        private var instance: StatsReporter? = null

        @JvmStatic
        fun getInstance(): StatsReporter {
            return instance ?: StatsReporter().also { instance = it }
        }
    }
}

Java

public class StatsReporter {
    public interface StatsListener {
        void onReportStats(String stats);
    }

    private static StatsReporter sInstance;

    private final List<StatsListener> mListeners = new CopyOnWriteArrayList<>();
    private boolean mIsReporting = false;

    private StatsReporter() {
        startReporting();
    }

    public static StatsReporter getInstance() {
        if (sInstance == null) {
            sInstance = new StatsReporter();
        }
        return sInstance;
    }

    public void registerListener(StatsListener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
    }

    public void unregisterListener(StatsListener listener) {
        mListeners.remove(listener);
    }

    private void startReporting() {
        if (mIsReporting) return;
        mIsReporting = true;

        new Thread(() -> {
            while (true) {
                try {
                    // Poll every 10 seconds
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }

                for (StatsListener listener : mListeners) {
                    // Send periodic mock stats update from the background thread
                    listener.onReportStats("Update from StatsReporter background polling thread");
                }
            }
        }, "StatsReporterPollingThread").start();
    }
}

بعد ذلك، افحص OomSampleActivity:

Kotlin

class OomSampleActivity : Activity(), StatsReporter.StatsListener {
    // Allocate some memory.
    private val heavyData = ByteArray(30 * 1024 * 1024)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_oom_sample)

        StatsReporter.getInstance().registerListener(this)
    }

    override fun onReportStats(stats: String) {
        // Read from heavyData so R8 doesn't strip it out as dead code during release builds
        Log.d(TAG, "Received stats in heavy leaked activity: $stats | Data size: ${heavyData.size}")
    }

    companion object {
        private const val TAG = "OomSampleActivity"
    }
}

Java

public class OomSampleActivity extends Activity implements StatsReporter.StatsListener {
    private static final String TAG = "OomSampleActivity";

    // Allocate some memory.
    private final byte[] mHeavyData = new byte[30 * 1024 * 1024];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_oom_sample);

        StatsReporter.getInstance().registerListener(this);
    }

    @Override
    public void onReportStats(String stats) {
        // Read from mHeavyData so R8 does not strip it out as dead code during release builds
        Log.d(TAG, "Received stats in heavy leaked activity: " + stats + " | Data size: " + mHeavyData.length);
    }
}

يؤكّد الرمز سبب احتفاظ "StatsReporter" بـ 8 نُسخ من "OomSampleActivity":

  1. يخصّص كل مثيل من OomSampleActivity مخزنًا مؤقتًا بسعة 30 ميغابايت byte[] (heavyData في Kotlin وmHeavyData في Java) ويسجّل نفسه كمستمع على العنصر الفردي StatsReporter في onCreate.
  2. لا يستدعي OomSampleActivity أبدًا unregisterListener عندما يدمره النظام. بما أنّ StatsReporter يستمر بعد انتهاء النشاط ويحتفظ بمرجع قوي لكل أداة معالجة حدث مسجَّلة، لا يمكن لأداة تجميع الموارد المهملة استعادة النُسخ التي تم تدميرها أو مصفوفات byte[] التي تبلغ سعتها 30 ميغابايت.
  3. في كل مرة يتلف فيها النظام OomSampleActivity ويعيد إنشاءه، مثل أثناء تدوير الشاشة في مراحل النشاط في Android، يتسرّب مثيل آخر بحجم 30 ميبيبايت في StatsReporter. بعد 7 عمليات تدوير (8 نُسخ × 30 ميغابايت ≈ 240 ميغابايت)، تمتلئ الذاكرة المؤقتة ويتعطّل التطبيق مع ظهور OutOfMemoryError.

إصلاح التسرّب

لإصلاح تسرّب الذاكرة، يمكنك إلغاء onDestroy في النشاط واستدعاء StatsReporter.getInstance().unregisterListener(this) لكي يحرّر النمط الفردي مرجعه عندما يتلف النظام النشاط:

Kotlin

class OomSampleActivityFixed : Activity(), StatsReporter.StatsListener {
    // Allocate some memory.
    private val heavyData = ByteArray(30 * 1024 * 1024)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_oom_sample)

        // Register the activity as a listener
        StatsReporter.getInstance().registerListener(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        // FIX: Unregister the listener to prevent the memory leak
        // when the Activity is destroyed (for example, during screen rotation).
        StatsReporter.getInstance().unregisterListener(this)
    }

    override fun onReportStats(stats: String) {
        // Read from heavyData so R8 doesn't strip it out as dead code during release builds
        Log.d(TAG, "Received stats in fixed heavy activity: $stats | Data size: ${heavyData.size}")
    }

    companion object {
        private const val TAG = "OomSampleFixed"
    }
}

Java

public class OomSampleActivityFixed extends Activity implements StatsReporter.StatsListener {
    private static final String TAG = "OomSampleFixed";

    // Allocate some memory.
    private final byte[] mHeavyData = new byte[30 * 1024 * 1024];

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_oom_sample);

        // Register the activity as a listener
        StatsReporter.getInstance().registerListener(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // FIX: Properly unregister the listener to prevent the memory leak
        // when the Activity is destroyed (e.g., during screen rotation).
        StatsReporter.getInstance().unregisterListener(this);
    }

    @Override
    public void onReportStats(String stats) {
        // Read from mHeavyData so R8 does not strip it out as dead code during release builds
        Log.d(TAG, "Received stats in fixed heavy activity: " + stats + " | Data size: " + mHeavyData.length);
    }
}

أتمِت التحقيق في حالات نفاد الذاكرة باستخدام وكيل ذكاء اصطناعي

بدلاً من فحص الرسم البياني الشعلة يدويًا وتتبُّع المراجع في الملفات، يمكنك استخدام وكيل ترميز مستنِد إلى الذكاء الاصطناعي تم ضبطه باستخدام مهارة Perfetto AI لتحليل لقطة أجزاء من الذاكرة وربطها بقاعدة الرموز البرمجية:

Analyze the Perfetto OOM heap dump at </path/to/profile>, which corresponds to
the codebase located at </path/to/codebase> using the Perfetto AI skill and find
the root cause of the OutOfMemoryError.

يستعلم الوكيل عن لقطة لأجزاء من الذاكرة باستخدام trace_processor، ويتتبّع مسار المسيطر من StatsReporter إلى مثيلات OomSampleActivity التي تم تسريبها، ويفحص كلا الفئتين في قاعدة الرموز البرمجية. بدون تدخّل يدوي، يصل إلى النتيجة نفسها، وهي أنّ OomSampleActivity يسجّل مستمعًا في onCreate بدون إلغاء تسجيله، وينشئ رمز تصحيح يضيف unregisterListener(this) في onDestroy (تم إنشاء التقرير التالي من لقطة منفصلة تضمنت 7 نُسخ محفوظة في وقت التعطّل):

# Perfetto OOM Heap Dump Analysis Report

## 1. Orienting Context & First-Pass Triage

| Property | Value |
| :--- | :--- |
| **Process Name** | `com.example.sampleapp` |
| **Sample Timestamp (`graph_sample_ts`)** | `3433221241585` |
| **Leaf Retained Class** | `byte[]` (7 instances) |
| **Leaf Self Size** | `220,201,044` bytes (~210 MiB) |

### Dominator Tree Retention Path

Querying the `android.memory.heap_graph.dominator_tree` standard library module identifies the following largest dominated class path from GC root to leaf allocation:

    [ROOT_JAVA_FRAME] com.example.sampleapp.StatsReporter [1]
      -> java.util.concurrent.CopyOnWriteArrayList [1]
      -> java.lang.Object[] [1]
      -> com.example.sampleapp.OomSampleActivity [7]
      -> byte[] [7] (220,201,044 bytes)
## 2. Root Cause Analysis

> **Important:** The `OutOfMemoryError` is caused by an **Activity listener leak**: `OomSampleActivity` (`app/src/main/java/com/example/sampleapp/OomSampleActivity.java#L8-L27`) registers itself as a listener on the long-lived `StatsReporter` (`app/src/main/java/com/example/sampleapp/StatsReporter.java#L6-L61`) singleton in `onCreate()`, but never unregisters itself in `onDestroy()`.

### Step-by-Step Retention Mechanism

1. **Per-Activity 30 MiB Allocation:**
   Each instance of `OomSampleActivity` (`OomSampleActivity.java#L11-L12`) allocates a 30 MiB byte array (`30 * 1024 * 1024 = 31,457,280` bytes):
   ```java
   // OomSampleActivity.java:12
   private final byte[] mHeavyData = new byte[30 * 1024 * 1024];
   ```

2. **Strong Reference Registration in `onCreate()`:**
   When created, `OomSampleActivity.onCreate()` (`OomSampleActivity.java#L15-L20`) registers `this` with `StatsReporter` (`StatsReporter.java#L29-L33`):
   ```java
   // OomSampleActivity.java:19
   StatsReporter.getInstance().registerListener(this);
   ```

3. **GC Root Pinning via Background Thread & Singleton:**
   `StatsReporter` (`StatsReporter.java#L11-L60`) is both a static singleton (`sInstance`) and the owner of an active background thread (`StatsReporterPollingThread`, which holds a `[ROOT_JAVA_FRAME]` GC root reference while sleeping/looping in `startReporting()`). It stores listeners in `mListeners` (`CopyOnWriteArrayList<StatsListener>`).

4. **Leak Accumulation Across Activity Recreations:**
   Because `OomSampleActivity` does not override `onDestroy()` to call `StatsReporter.unregisterListener(this)`, every destroyed instance of `OomSampleActivity` remains strongly reachable in `mListeners`. After 7 Activity creations (7 * 30 MiB = 220,200,960 bytes plus array headers = `220,201,044` bytes), the Java heap is exhausted.

---

## 3. Actionable Implementation Plan

### 1. Unregister the Listener in `OomSampleActivity.onDestroy()`

Update `OomSampleActivity.java` to unregister the listener when the `Activity` is destroyed (matching the fix in `OomSampleActivityFixed.java`):

```diff
--- a/app/src/main/java/com/example/sampleapp/OomSampleActivity.java
+++ b/app/src/main/java/com/example/sampleapp/OomSampleActivity.java
@@ -19,4 +19,10 @@
         StatsReporter.getInstance().registerListener(this);
     }

+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        StatsReporter.getInstance().unregisterListener(this);
+    }
+
     @Override
```

### 2. Optional Architectural Hardening in `StatsReporter`

* **Lifecycle-Aware Registration:** Have `StatsReporter.registerListener()` accept a `LifecycleOwner` and attach a `DefaultLifecycleObserver` that automatically invokes `unregisterListener()` on `ON_DESTROY`.
* **Weak References:** Store `WeakReference<StatsListener>` entries in `StatsReporter` and evict cleared references during iteration so forgotten `unregisterListener()` calls cannot retain entire `Activity` hierarchies.

التحقّق من الإصلاح

تحقَّق من حلّ مشكلة تسرُّب الذاكرة إما محليًا على جهاز اختباري أو في الإصدار العلني.

إذا كانت المشكلة قابلة للتكرار على الجهاز

بما أنّ TRIGGER_TYPE_OOM لا يسجّل لقطة لأجزاء من الذاكرة إلا عند حدوث OutOfMemoryError، فإنّ الإصلاح الناجح يمنع تفعيل المشغّل. لفحص الذاكرة والتأكّد من حلّ مشكلة تسرُّب الذاكرة، عليك تدوير الشاشة ثم التقاط لقطة لأجزاء من الذاكرة في Java عند الطلب من سطر الأوامر:

  1. أوقِف محدِّد المعدّل (rate_limiter.disabled true) واحتفِظ بالنتائج المؤقتة (delete_temporary_results.disabled true) لكي يحتفظ النظام بلقطة سطر الأوامر في /data/misc/perfetto-traces/profiling/.
  2. افتح تطبيق "OomSampleActivityFixed" على جهاز الاختبار، ثم قم بتدوير الشاشة 8 مرات أو أكثر للتأكّد من أنّ التطبيق لم يعد يتعطّل، واحتفظ به في المقدّمة.
  3. يمكنك بدء لقطة لأجزاء من الذاكرة في Java عند الطلب (profiling_type 1) باستخدام أمر واجهة الأوامر cmd profiling_service request-profiling:

    adb shell cmd profiling_service request-profiling 1 com.example.sampleapp
    
  4. اسحب الملف الشخصي المحفوظ من /data/misc/perfetto-traces/profiling/ وافتحه في واجهة مستخدم Perfetto.

الرسم البياني اللهبي في Heapdump Explorer ضمن واجهة مستخدم Perfetto بعد الإصلاح، والذي يعرض مثيلاً واحدًا من OomSampleActivityFixed يحتفظ بـ 30.01 ميغابايت
الشكل 2. رسم بياني لهيكل لقطة لأجزاء من الذاكرة بعد الإصلاح يعرض مثيلاً واحدًا من OomSampleActivityFixed.

كما هو موضّح في الشكل 2، بعد تدوير الشاشة عدة مرات:

  • يحتوي com.example.sampleapp.OomSampleActivityFixed على عدد ذاتي يبلغ 1 بدلاً من تجميع مثيل في كل تدوير.
  • تظل المساحة المستعادة في الذاكرة التراكمية ثابتة عند 30.01 ميغابايت (من إجمالي حجم الذاكرة الديناميكية البالغ 44.27 ميغابايت)، ما يؤكد أنّ أداة تجميع المهملات تستعيد مثيلات الأنشطة السابقة.

إذا كان من الصعب إعادة إظهار المشكلة محليًا

عندما يؤدي التنقّل المعقّد بين الشاشات المتعددة أو الجلسات الطويلة أو بيانات مستخدم معيّنة إلى حدوث OutOfMemoryError لا يمكنك إعادة إنتاجه بشكل موثوق على جهاز اختبار، يمكنك التحقّق من صحة الإصلاح في قناة الإصدار العلني بعد طرحه:

  • يمكنك مراقبة مجموعة الأعطال "OutOfMemoryError" في أداة إعداد تقارير الأعطال كمقياس بديل في جميع الإصدارات للتأكّد من انخفاض الأعطال التي تتضمّن "OomSampleActivity".
  • افحص أي لقطات جديدة لأجزاء من الذاكرة في TRIGGER_TYPE_OOM تم جمعها من الإصدار المحدَّث للتأكّد من أنّ StatsReporter لم يعُد يحتفظ بنُسخ OomSampleActivity التي تم تدميرها.