Résolution d'une NullPointerException dans un processus secondaire Android

Symptômes du problème

Une NullPointerException se manifeste après l'exécution d'une opération au sein de l'application.

Extrait du journal d'erreurs


FATAL EXCEPTION: main
Process: com.test.test:remote, PID: 23919
java.lang.RuntimeException: Error receiving broadcast Intent { act=android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT ... } in com.xupin.poclibrary.tool.bluetooth.BluetoothManager$2@256db6d
    at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1712)
    // ... traces de pile ommises ...
Caused by: java.lang.NullPointerException: Attempt to read from field 'com.test.test.tool.entity.SessionEntity com.test.test.services.TestServices.callingSession' on a null object reference
    at com.test.test.tool.bluetooth.BluetoothManager.handlePttBtnDown(BluetoothManager.java:469)
    at com.test.test.tool.bluetooth.BluetoothManager.access$300(BluetoothManager.java:42)
    at com.test.test.tool.bluetooth.BluetoothManager$2.onReceive(BluetoothManager.java:521)
    // ... traces de pile ommises ...

Analyse de la cause première

L'examen du journal des erreurs indique que l'exception fatale survient dans le processus nommé com.test.test:remote. Cette désignation, incluant un côlon et un suffixe, signale qu'il s'agit d'un processus secondaire de l'application, distinct du processus principal. La NullPointerException est explicitement liée à une tentative d'accès au champ callingSession d'une instance de com.test.test.services.TestServices, qui est nulle.

Normalement, TestServices est conçu comme un singleton et devrait être correctement initialisé dans le processus principal de l'application. La présence de cette exception dans un processus secondaire suggère une défaillance dans la gestion des états partagés ou des initialisations entre les différents processus. Un indice supplémentaire est le déclenchement répété (deux fois) d'un écouteur dynamique (BluetoothManager$2), ce qui renforce l'idée d'initialisations dupliquées ou mal gérées à travers les frontières des processus.

Le problème fondamental est qu'un objet singleton ou un état global initialisé dans le processus principal n'est pas automatiquement disponible ou initialisé pour les processus secondaires de l'application. Chaque processus Android est une machine virtuelle Java distincte avec sa propre mémoire et ses propres instances d'objets. Si un code dans un processus secondaire tente d'accéder à un objet qui n'a été initialisé que dans le processus principal, il se retrouvera face à une référence nulle, menant à une NullPointerException.

Mise en œuvre de la solution

Pour prévenir de telles erreurs, il est crucial d'adapter l'initialisation des composants critiques en fonction du processus d'exécution. Les opérations qui dépendent d'un état global ou de singletons spécifiques au processus principal doivent être conditionnées pour ne s'exécuter que dans ce dernier.

Voici un utilitaire Java qui permet de déterminer si le code s'exécute dans le processus principal de l'application ou dans un processus secondaire :


import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;

import java.lang.reflect.Method;
import java.util.List;

/**
 * Fournit des méthodes pour identifier le processus d'exécution actuel de l'application.
 * Cela permet de distinguer le processus principal des processus secondaires.
 */
public final class ApplicationProcessResolver {

    private static final String TAG_LOG = "ProcessResolver";

    // Empêche l'instanciation de cette classe utilitaire.
    private ApplicationProcessResolver() {}

    /**
     * Vérifie si le processus en cours d'exécution est le processus principal de l'application.
     * Le processus principal partage généralement le même nom que le package de l'application.
     *
     * @param appCtx Le contexte de l'application.
     * @return {@code true} si le processus est le principal, {@code false} sinon.
     */
    public static boolean isMainAppProcess(Context appCtx) {
        String appPackageId = appCtx.getPackageName();
        String currentExecutionProcess = fetchCurrentProcessName(appCtx);
        Log.d(TAG_LOG, "ID du paquet: " + appPackageId + ", Nom du processus: " + currentExecutionProcess);
        return TextUtils.equals(appPackageId, currentExecutionProcess);
    }

    /**
     * Récupère le nom du processus Android actuellement actif.
     * Cette méthode utilise différentes stratégies en fonction de la version de l'API Android.
     *
     * @param context Le contexte nécessaire pour accéder aux services système.
     * @return Le nom du processus en cours, ou {@code null} si une erreur survient.
     */
    @Nullable
    public static String fetchCurrentProcessName(Context context) {
        // Méthode privilégiée pour Android P (API 28) et versions ultérieures.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            return Application.getProcessName();
        }

        // Utilisation de la réflexion pour les versions d'API antérieures (inspiré de WorkManager).
        try {
            Class> activityThreadCls = Class.forName("android.app.ActivityThread", false, context.getClassLoader());
            Object processNameResult;

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { // API 18+
                Method getProcessNameMethod = activityThreadCls.getDeclaredMethod("currentProcessName");
                getProcessNameMethod.setAccessible(true);
                processNameResult = getProcessNameMethod.invoke(null);
            } else { // API < 18
                Method getCurrentActivityThreadMethod = activityThreadCls.getDeclaredMethod("currentActivityThread");
                getCurrentActivityThreadMethod.setAccessible(true);
                Object activityThreadInstance = getCurrentActivityThreadMethod.invoke(null);

                Method getProcessNameMethod = activityThreadCls.getDeclaredMethod("getProcessName");
                getProcessNameMethod.setAccessible(true);
                processNameResult = getProcessNameMethod.invoke(activityThreadInstance);
            }

            if (processNameResult instanceof String) {
                return (String) processNameResult;
            }
        } catch (Throwable exc) {
            Log.e(TAG_LOG, "Échec de la récupération du nom du processus via ActivityThread", exc);
        }

        // Méthode de dernier recours : parcours des processus via ActivityManager (plus coûteuse).
        int currentProcessId = android.os.Process.myPid();
        ActivityManager sysActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

        if (sysActivityManager != null) {
            List<activitymanager.runningappprocessinfo> activeProcesses = sysActivityManager.getRunningAppProcesses();
            if (activeProcesses != null && !activeProcesses.isEmpty()) {
                for (ActivityManager.RunningAppProcessInfo pInfo : activeProcesses) {
                    if (pInfo.pid == currentProcessId) {
                        return pInfo.processName;
                    }
                }
            }
        }
        return null;
    }
}
</activitymanager.runningappprocessinfo>

Cette classe peut être utilisée, par exemple, dans la méthode onCreate() de votre classe Application pour n'initialiser les composants spécifiques au processus principal que lorsque cela est nécessaire :


// Exemple d'utilisation dans la classe Application personnalisée
public class CustomApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (ApplicationProcessResolver.isMainAppProcess(this)) {
            // Ici, initialiser les singletons critiques, enregistrer les BroadcastReceivers
            // qui dépendent de l'état du processus principal (comme BluetoothManager$2),
            // ou configurer des SDK qui ne doivent s'exécuter qu'une seule fois.
            Log.d("CustomApplication", "Initialisation pour le processus principal de l'application.");
            // Exemple : TestServices.initService(this);
        } else {
            // Effectuer des initialisations légères ou spécifiques aux processus secondaires
            // (ex: configuration de services tiers pour des processus dédiés).
            Log.d("CustomApplication", "Initialisation pour un processus secondaire.");
        }
    }
}

En conditionnant les initialisations de cette manière, on garantit que TestServices.callingSession et d'autres dépendances du processus principal ne sont pas appelées depuis des processus secondaires où elles n'auraient pas été instanciées, évitant ainsi les NullPointerException.

Étiquettes: Android Multi-processus NullPointerException gestion des processus Services

Publié le 16 juin à 20h27