تحديد المواصفات الشخصية استنادًا إلى التطبيق

في نظام التشغيل Android 15 (المستوى 35 من واجهة برمجة التطبيقات) والإصدارات الأحدث، يتيح لك إنشاء الملفات الشخصية المستند إلى التطبيق بدء تسجيلات الملفات الشخصية المتوافقة وإيقافها بشكل صريح في الرمز البرمجي باستخدام واجهة برمجة التطبيقات ProfilingManager أو أدوات إنشاء AndroidX Core (androidx.core:core 1.15.0 أو الإصدارات الأحدث). استخدِم ميزة "إنشاء الملفات الشخصية المباشرة" عندما تريد التحكّم في توقيت الملف الشخصي أو مَعلمات الجلسة، مثل المدد وسياسات التخزين المؤقت ومعدّلات البيانات في الملف الصوتي، أو استهداف تجربة مستخدم معيّنة أو حالة وقت تشغيل معيّنة.

إضافة التبعيات

للحصول على أفضل تجربة مع واجهة برمجة التطبيقات ProfilingManager، أضِف مكتبات Jetpack التالية إلى ملف build.gradle.kts.

Kotlin

   dependencies {
       implementation("androidx.tracing:tracing-ktx:2.0.5")
       implementation("androidx.core:core:1.19.1")
   }
   

أنيق

   dependencies {
       implementation 'androidx.tracing:tracing-ktx:2.0.5'
       implementation 'androidx.core:core:1.19.1'
   }
   

تسجيل تتبُّع النظام

بعد إضافة التبعيات المطلوبة، استخدِم الرمز التالي لتسجيل سجلّ تتبّع النظام. يوضّح هذا المثال كيفية بدء جلسة تحليل أداء مع إدارة العمليات الثقيلة بأمان خارج سلسلة التعليمات الرئيسية.

Kotlin

@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
fun ProfiledScreen(modifier: Modifier = Modifier) {
    // Use the application context: requestProfiling resolves the ProfilingManager
    // system service from it, so there's no reason to hand it a short-lived Activity.
    val appContext = LocalContext.current.applicationContext
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            // Run the orchestration off the main thread. Profiling a heavy operation
            // on the UI thread would freeze the UI (ANR) and distort the very metrics
            // you're trying to capture.
            //
            // Note: this scope is tied to composition. If the user leaves this screen
            // mid-session, the coroutine is cancelled and stopSignal.cancel() might not
            // run, but setDurationMs() acts as a safety net and ends the profile.
            scope.launch(Dispatchers.Default) {
                val callbackExecutor = Dispatchers.IO.asExecutor()
                val resultCallback = Consumer<ProfilingResult> { profilingResult ->
                    if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
                        Log.d("ProfileTest", "Result file: ${profilingResult.resultFilePath}")
                    } else {
                        // errorMessage explains the failure (e.g., rate limiting); keep it.
                        Log.e(
                            "ProfileTest",
                            "Profiling failed errorCode=${profilingResult.errorCode} " +
                                "errorMessage=${profilingResult.errorMessage}"
                        )
                    }
                }

                val stopSignal = CancellationSignal()
                val requestBuilder = SystemTraceRequestBuilder().apply {
                    setCancellationSignal(stopSignal)
                    setTag("FOO") // Caller-supplied tag for identification.
                    setDurationMs(60000) // Hard cap: ends the session if cancel() never fires.
                    setBufferFillPolicy(BufferFillPolicy.RING_BUFFER)
                    setBufferSizeKb(32768)
                }

                // 1. Start the session. This is asynchronous system IPC. The tracing
                //    engine takes a moment to start and allocate buffers.
                requestProfiling(appContext, requestBuilder.build(), callbackExecutor, resultCallback)

                // 2. The API exposes no "profiling started" signal, so pad with a short,
                //    best-effort delay before running the code you care about. This is
                //    approximate. Increase it on slower or heavily loaded devices.
                delay(STARTUP_PADDING_MS)

                // 3. The session is already recording every thread in your app. This slice
                //    doesn't scope what's captured. It just labels this region of the
                //    timeline so heavyOperation() is easier to find. trace { } closes the
                //    section even if the block throws.

                trace("MyApp:HeavyOperation") {
                    heavyOperation()
                }

                // 4. Stop recording. Until this fires or the setDurationMs() cap is
                //    reached (whichever comes first), the session keeps capturing app-wide
                //    activity.

                stopSignal.cancel()
            }
        }
    ) {
        Text("Run & Profile Heavy Operation")
    }
}

// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private const val STARTUP_PADDING_MS = 100L

fun heavyOperation() {
    // Background computations to profile.
}

Java

void sampleRecordSystemTrace(Context context) {
  // Use the application context: requestProfiling resolves the ProfilingManager
  // system service from it, so there's no reason to hand it a short-lived Activity.
  Context appContext = context.getApplicationContext();

  // Run the orchestration off the main thread. Profiling a heavy operation
  // on the UI thread would freeze the UI (ANR) and distort the very metrics
  // you're trying to capture.
  profilingExecutor.execute(() -> {
    Executor callbackExecutor = Executors.newSingleThreadExecutor();
    Consumer<ProfilingResult> resultCallback = profilingResult -> {
      if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
        Log.d("ProfileTest", "Result file: " + profilingResult.getResultFilePath());
      } else {
        // errorMessage explains the failure (e.g., rate limiting); keep it.
        Log.e(
            "ProfileTest",
            "Profiling failed errorCode=" + profilingResult.getErrorCode()
                + " errorMessage=" + profilingResult.getErrorMessage());
      }
    };

    CancellationSignal stopSignal = new CancellationSignal();
    SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder();
    requestBuilder.setCancellationSignal(stopSignal);
    requestBuilder.setTag("FOO"); // Caller-supplied tag for identification.
    requestBuilder.setDurationMs(60000); // Hard cap: ends the session if cancel() never fires.
    requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER);
    requestBuilder.setBufferSizeKb(32768);

    // 1. Start the session. This is asynchronous system IPC. The tracing
    //    engine takes a moment to start and allocate buffers.
    Profiling.requestProfiling(
        appContext, requestBuilder.build(), callbackExecutor, resultCallback);

    // 2. The API exposes no "profiling started" signal, so pad with a short,
    //    best-effort delay before running the code you care about. This is
    //    approximate. Increase it on slower or heavily loaded devices.
    try {
      Thread.sleep(STARTUP_PADDING_MS);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      stopSignal.cancel();
      return;
    }

    // 3. The session is already recording every thread in your app. This slice
    //    doesn't scope what's captured. It just labels this region of the
    //    timeline so heavyOperation() is easier to find. try/finally closes the
    //    section even if heavyOperation() throws.
    Trace.beginSection("MyApp:HeavyOperation");
    try {
      heavyOperation();
    } finally {
      Trace.endSection();
    }

    // 4. Stop recording. Until this fires or the setDurationMs() cap is
    //    reached (whichever comes first), the session keeps capturing app-wide
    //    activity.
    stopSignal.cancel();
  });
}

// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private static final long STARTUP_PADDING_MS = 100L;

// Single background thread that runs the profiling orchestration.
private final ExecutorService profilingExecutor = Executors.newSingleThreadExecutor();

void heavyOperation() {
  // Background computations to profile.
}

يُعدّ الرمز النموذجي جلسة تحليل الملفات الشخصية ويديرها من خلال الخطوات التالية:

  1. إعداد المنفِّذ: أنشئ Executor بدون واجهة مستخدم لتلقّي نتائج تحليل الأداء خارج سلسلة التعليمات الرئيسية.
  2. التعامل مع نتائج تحليل الملفات الشخصية: يمكنك إنشاء دالة رد اتصال Consumer<ProfilingResult> لتلقّي النتيجة المكتملة أو رسالة الخطأ من ProfilingManager.
  3. إنشاء طلب إنشاء الملف الشخصي: أنشئ SystemTraceRequestBuilder لتخصيص إعدادات الجلسة (أو تجاهل التخصيصات لاستخدام الإعدادات التلقائية للنظام):

    • ‫setTag(): لإرفاق علامة يوفّرها المتصل وتعرضها الدالة ProfilingResult.getTag وتُلحق باسم ملف الإخراج (ما يصل إلى أول 20 حرفًا أبجديًا رقميًا صغيرًا وواصلات).
    • ‫setDurationMs(): لضبط الحد الأقصى لمدة التسجيل بالمللي ثانية (مثلاً، 60000 لمدة 60 ثانية).
    • ‫setBufferFillPolicy(): يحدّد هذا الإعداد كيفية تعامل المخزن المؤقت مع حدود السعة: يؤدي الخيار BufferFillPolicy.RING_BUFFER إلى استبدال البيانات الأقدم عندما يمتلئ المخزن المؤقت، بينما يؤدي الخيار BufferFillPolicy.DISCARD إلى إيقاف تسجيل البيانات الجديدة بمجرد امتلاء المخزن المؤقت.
    • ‫setBufferSizeKb(): لضبط حجم ذاكرة التخزين المؤقت للتسجيل بالكيلوبايت
  4. اختياري: إيقاف التسجيل مبكرًا: يمكنك تمرير CancellationSignal (stopSignal) لإيقاف الجلسة فور انتهاء العملية المستهدَفة، وحفظ البيانات المسجَّلة حتى الآن وإرسال ProfilingResult إلى دالة الاستدعاء. يُرجى الاطّلاع على إيقاف تسجيل نشط لمعرفة التفاصيل.

  5. بدء التسجيل وتلقّي النتائج: يمكنك الاتصال بـ "requestProfiling()" لبدء التسجيل في الخلفية. عند انتهاء الجلسة، يتلقّى "resultCallback.accept()" ProfilingResult:

    • في حال نجاح العملية (ERROR_NONE)، يمكنك استدعاء getResultFilePath() للحصول على المسار على الجهاز فقط. للحصول على تعليمات حول سحب الملفات باستخدام adb أو تحميلها باستخدام WorkManager، يُرجى الاطّلاع على استرداد الملفات الشخصية وتحميلها.
    • في حال تعذّر ذلك، افحص getErrorCode() وgetErrorMessage(). يمكن أن يتعذّر تنفيذ الطلبات بسبب الحدّ من معدّل الزحف بالإضافة إلى عدة أسباب أخرى.
    • إذا تم إنهاء تطبيقك قبل تسليم النتيجة، يضع النظام النتيجة في قائمة انتظار ويسلّمها في المرة التالية التي يسجّل فيها تطبيقك مستمعًا عامًا باستخدام registerForAllProfilingResults.
  6. إضافة نقاط تتبُّع مخصّصة: يمكنك تضمين العمليات في trace("...") { ... } (أو Trace.beginSection() / Trace.endSection()) لتصنيف مناطق المخطط الزمني.

إيقاف تسجيل نشط

لإيقاف عملية التسجيل فور اكتمال التفاعل بدلاً من انتظار انتهاء صلاحية setDurationMs()، يمكنك تمرير CancellationSignal إلى setCancellationSignal() أو requestProfiling(). في حال تحديد كل من CancellationSignal وsetDurationMs()، تنتهي الجلسة عند حدوث أي منهما أولاً، وفي حال عدم تحديد أي منهما، يطبّق النظام مدة تلقائية.

لن يؤدي الاتصال بالرقم cancel() إلى تجاهل التسجيل. يوقف النظام عملية التسجيل، ويحجب البيانات التي تم جمعها حتى تلك اللحظة ويحفظها، ثم يرسل ProfilingResult إلى المستمع مع ERROR_NONE.

تسجيل سجلّات تتبُّع النظام باستخدام عيّنات حزمة التنفيذ

على الأجهزة التي تعمل بنظام التشغيل Android 17 (مستوى واجهة برمجة التطبيقات 37) والإصدارات الأحدث (أو Android 16 مع وحدة Profiling رئيسية محدّثة)، تتيح أداة ProfilingManager جمع حزم التنفيذ التي تم أخذ عيّنات منها إلى جانب سجلّ تتبّع النظام في جلسة واحدة حتى تتمكّن من الاطّلاع على أحداث المخطط الزمني، مثل حالات سلاسل التعليمات والإطارات وشرائح التتبّع المخصّصة، بالإضافة إلى حزم التنفيذ في وحدة المعالجة المركزية (CPU) للطرق التي لا تتضمّن أدوات قياس حالة التطبيق الصريحة.

يوضّح هذا المثال أيضًا كيفية استدعاء ProfilingManager.requestProfiling مباشرةً بدون أدوات إنشاء Jetpack من خلال تحديد أحد أنواع الملفات الشخصية المتوافقة (PROFILING_TYPE_SYSTEM_TRACE) وإدخال إعدادات Bundle:

Kotlin

val profilingManager = context.getSystemService(ProfilingManager::class.java)

// Pass a Bundle to customize the capture, or pass null for default settings.
val config = Bundle().apply {
    putInt("KEY_DURATION_MS", 15000) // 15 seconds
    putInt("KEY_SIZE_KB", 20480) // 20 MB buffer size

    // Collect sampled CPU call stacks alongside the system trace.
    putBoolean("KEY_COLLECT_STACK_SAMPLING", true)
    putInt("KEY_FREQUENCY_HZ", 100) // Sampling frequency in Hz
    // Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls.
}
profilingManager?.requestProfiling(
    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
    config,
    "trace-with-callstacks",
    null,
    callbackExecutor,
    resultCallback
)

Java

ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class);
if (profilingManager == null) return;

// Pass a Bundle to customize the capture, or pass null for default settings.
Bundle config = new Bundle();
config.putInt("KEY_DURATION_MS", 15000); // 15 seconds
config.putInt("KEY_SIZE_KB", 20480); // 20 MB buffer size

// Collect sampled CPU call stacks alongside the system trace.
config.putBoolean("KEY_COLLECT_STACK_SAMPLING", true);
config.putInt("KEY_FREQUENCY_HZ", 100); // Sampling frequency in Hz
// Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls.

profilingManager.requestProfiling(
    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
    config,
    "trace-with-callstacks",
    null,
    callbackExecutor,
    resultCallback
);

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

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

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

يمكنك طلب لقطة من ذاكرة Java في تطبيقك باستخدام JavaHeapDumpRequestBuilder (أو PROFILING_TYPE_JAVA_HEAP_DUMP). على عكس لقطات أجزاء من الذاكرة العادية في .hprof التي يتم التقاطها باستخدام Debug.dumpHprofData، تم تصميم لقطات أجزاء من الذاكرة في ProfilingManager لجمع البيانات بشكل آمن وبأقل قدر من النفقات العامة في مرحلة الإنتاج:

  • جمع البيانات بدون تجميد: ينشئ النظام نسخة طبق الأصل من عملية تطبيقك، ويجمع لقطة لعناصر متعدّدة من العملية المستنسخة، وبذلك لا يتجمّد تطبيقك أثناء كتابة ملف تفريغ الذاكرة على القرص.
  • الرسم البياني المرجعي فقط (بدون بيانات العناصر الأولية): بدلاً من تفريغ محتوى الذاكرة الأولية مثل ملف .hprof، يسجّل ProfilingManager رسمًا بيانيًا لـ .perfetto-java-heap-dump protobuf يحتوي على الفئات وأحجام العناصر وسلاسل المراجع وجذور تجميع المهملات (GC) مع حذف القيم الأولية ومحتوى السلاسل وحمولات الصفائف. يحمي هذا خصوصية المستخدم ويقلل من حجم الملف. للاطّلاع على تفاصيل كاملة، يُرجى مراجعة أوجه الاختلاف بين لقطات أجزاء من الذاكرة في ProfilingManager ولقطات HPROF.

باستخدام إعدادات Context وExecutor وresultCallback نفسها الموضّحة في تسجيل تتبُّع النظام، أنشئ طلبًا باستخدام JavaHeapDumpRequestBuilder:

Kotlin

val heapDumpRequest = JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build()

requestProfiling(
    appContext,
    heapDumpRequest,
    callbackExecutor,
    resultCallback
)

Java

ProfilingRequest heapDumpRequest = new JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build();

Profiling.requestProfiling(
    getApplicationContext(),
    heapDumpRequest,
    callbackExecutor,
    resultCallback
);

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

الخطوات التالية

بعد تسجيل الملفات الشخصية، يمكنك استردادها محليًا باستخدام adb أو تحميلها إلى الخلفية باستخدام WorkManager.