Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีตรวจสอบขนาด Heap Memory ของแอป Android

ภาพรวม

บทความนี้สาธิตวิธีตรวจสอบขนาด Heap Memory (หน่วยความจำที่จัดสรรให้แอป) ใน Android โดยใช้ ActivityManager.getMemoryClass() และ Debug.getNativeHeapAllocatedSize() พร้อมอธิบาย Dalvik/ART Heap, Memory Limit, และวิธีป้องกัน OutOfMemoryError

ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่

สร้างโปรเจกต์ Android ใหม่ใน Android Studio

ขั้นตอนที่ 2: Layout (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnCheckHeap"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="🔍 ตรวจสอบ Heap Size" />

    <TextView
        android:id="@+id/tvHeapInfo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:textSize="16sp"
        android:lineSpacingExtra="8dp"
        android:gravity="center"
        android:text="กดปุ่มเพื่อตรวจสอบ" />

</LinearLayout>

ขั้นตอนที่ 3: MainActivity.java - ครอบคลุมทุก Metric

import androidx.appcompat.app.AppCompatActivity;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.Formatter;

public class MainActivity extends AppCompatActivity {

    private TextView tvHeapInfo;
    private Button btnCheckHeap;

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

        tvHeapInfo = findViewById(R.id.tvHeapInfo);
        btnCheckHeap = findViewById(R.id.btnCheckHeap);

        btnCheckHeap.setOnClickListener(v -> showHeapInfo());
    }

    private void showHeapInfo() {
        StringBuilder info = new StringBuilder();
        
        // 1. Dalvik/ART Heap Limit (จาก ActivityManager) - API 1+
        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = activityManager.getMemoryClass(); // MB
        int largeMemoryClass = activityManager.getLargeMemoryClass(); // MB (สำหรับ largeHeap=true)
        
        info.append("📱 Heap Limits (System):\n");
        info.append(String.format("  • Normal Heap Limit: %d MB\n", memoryClass));
        info.append(String.format("  • Large Heap Limit: %d MB\n\n", largeMemoryClass));

        // 2. Current Heap Usage (Runtime) - API 1+
        Runtime runtime = Runtime.getRuntime();
        long maxMemory = runtime.maxMemory(); // Heap Max (bytes)
        long totalMemory = runtime.totalMemory(); // Heap Current Allocated (bytes)
        long freeMemory = runtime.freeMemory(); // Free ใน Heap ปัจจุบัน (bytes)
        long usedMemory = totalMemory - freeMemory; // Used จริง (bytes)
        
        info.append("📊 Current Heap (Runtime):\n");
        info.append(String.format("  • Max Heap: %s\n", formatBytes(maxMemory)));
        info.append(String.format("  • Allocated: %s\n", formatBytes(totalMemory)));
        info.append(String.format("  • Used: %s\n", formatBytes(usedMemory)));
        info.append(String.format("  • Free: %s\n\n", formatBytes(freeMemory)));

        // 3. Native Heap (C/C++ allocations, Bitmaps pre-API 8) - API 1+
        long nativeAllocated = Debug.getNativeHeapAllocatedSize();
        long nativeFree = Debug.getNativeHeapFreeSize();
        long nativeSize = Debug.getNativeHeapSize();
        
        info.append("🔧 Native Heap:\n");
        info.append(String.format("  • Allocated: %s\n", formatBytes(nativeAllocated)));
        info.append(String.format("  • Free: %s\n", formatBytes(nativeFree)));
        info.append(String.format("  • Total: %s\n\n", formatBytes(nativeSize)));

        // 4. Java Heap (MemoryMXBean) - API 9+
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
        MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();
        
        info.append("☕ Java Memory MXBean:\n");
        info.append(String.format("  • Heap Used: %s / Max: %s\n", 
            formatBytes(heapUsage.getUsed()), formatBytes(heapUsage.getMax())));
        info.append(String.format("  • Non-Heap Used: %s / Max: %s\n\n", 
            formatBytes(nonHeapUsage.getUsed()), formatBytes(nonHeapUsage.getMax())));

        // 5. PSS (Proportional Set Size) - API 16+ (Accurate System View)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            android.os.Debug.MemoryInfo[] memInfo = activityManager.getProcessMemoryInfo(new int[]{android.os.Process.myPid()});
            if (memInfo.length > 0) {
                long pssKb = memInfo[0].getTotalPss(); // KB
                long privateDirtyKb = memInfo[0].getTotalPrivateDirty(); // KB
                info.append("📈 Process Memory (PSS):\n");
                info.append(String.format("  • Total PSS: %s\n", formatBytes(pssKb * 1024)));
                info.append(String.format("  • Private Dirty: %s\n", formatBytes(privateDirtyKb * 1024)));
            }
        }

        // 6. App Memory Class (API 16+)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
            activityManager.getMemoryInfo(memoryInfo);
            info.append("\n💾 System Memory:\n");
            info.append(String.format("  • Available: %s\n", formatBytes(memoryInfo.availMem)));
            info.append(String.format("  • Total: %s\n", formatBytes(memoryInfo.totalMem)));
            info.append(String.format("  • Low Memory: %s\n", memoryInfo.lowMemory));
            info.append(String.format("  • Threshold: %s\n", formatBytes(memoryInfo.threshold)));
        }

        tvHeapInfo.setText(info.toString());
    }

    private String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        int exp = (int) (Math.log(bytes) / Math.log(1024));
        String unit = "KMGTPE".charAt(exp - 1) + "B";
        return String.format(Locale.getDefault(), "%.2f %s", bytes / Math.pow(1024, exp), unit);
    }
}

ขั้นตอนที่ 4: AndroidManifest.xml - Large Heap Option

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:largeHeap="true"> <!-- ขอ Heap ใหญ่กว่าปกติ (ใช้ด้วยความระมัดระวัง) -->

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

ความเข้าใจ Heap Memory ใน Android

1. Dalvik vs ART

  • Dalvik (API < 21): JIT Compilation, Heap แยก Process, Garbage Collection แบบ Stop-the-world
  • ART (API 21+): AOT Compilation, Heap แบบ Generational, GC แบบ Concurrent/Incremental, ใช้ Memory มากกว่า Dalvik

2. Memory Class (ต่อ Device)

Device CategoryNormal HeapLarge Heap
Low-end (Go Edition)16-32 MB64-128 MB
Mid-range64-128 MB256-512 MB
High-end / Flagship192-512 MB512 MB - 1.5 GB
Tablet / Foldable256-512 MB1-2 GB

3. android:largeHeap="true"

  • ขอ Heap Size = getLargeMemoryClass() แทน getMemoryClass()
  • ใช้เมื่อ: Image Processing, Large Bitmap, Video Editing, ML Models
  • ข้อเสีย: GC ช้ากว่า, ใช้ System Memory มาก, อาจโดน Kill โดย LMK (Low Memory Killer) ก่อน
  • Best Practice: หลีกเลี่ยงถ้าเป็นไปได้ ใช้ Bitmap Pooling, inSampleSize, LruCache แทน

การ Monitor Memory ใน Development

1. Android Studio Profiler (แนะนำ)

  • View → Tool Windows → Profiler
  • เลือก Process → Memory Tab
  • ดู: Java Heap, Native Heap, Graphics, Stack, Code, Others
  • Capture Heap Dump → Analyze Leak (LeakCanary Integration)

2. ADB Commands

# ดู Memory Info ของ Package
adb shell dumpsys meminfo com.package.name

# ดู Heap Dump (ต้อง Debuggable)
adb shell am dumpheap com.package.name /data/local/tmp/heap.hprof
# Pull มา analyze ด้วย MAT (Memory Analyzer Tool) หรือ Android Studio

3. LeakCanary (Library Detect Leak อัตโนมัติ)

dependencies {
    debugImplementation "com.squareup.leakcanary:leakcanary-android:2.12"
    releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:2.12"
}

OutOfMemoryError - สาเหตุและการแก้

สาเหตุอาการแก้ไข
Bitmap ใหญ่เกิน HeapOOM บน decodeBitmapinSampleSize, inBitmap, Glide/Coil, WebP
Memory Leak (Context, Listener, Static Ref)Heap เพิ่มขึ้นเรื่อยๆLeakCanary, WeakReference, Lifecycle-Aware
Large Heap แต่อุปกรณ์ Low-endCrash บนเครื่องเก่าไม่ใช้ largeHeap, Optimize Bitmap
Native Memory (Bitmap pre-API 8, OpenGL, NDK)Java Heap OK แต่ Native เต็มrecycle(), close(), explicit free

Best Practices

  • ใช้ LruCache สำหรับ Cache Bitmap/Object
  • โหลดรูปด้วย inSampleSize หรือ Library (Glide, Coil, Picasso)
  • หลีกเลี่ยง Static Reference ของ Context/View/Bitmap
  • Unregister Listener/Callback ใน onDestroy/onStop
  • ใช้ WeakReference สำหรับ Callback/Listener
  • Profile เรียบร้อยก่อน Release