Le Stack Android

Vous ne vous êtes jamais demandé comment votre portable fonctionnait ? Qu’est-ce qu’il se passe dans votre poche/main à cet instant ? Non ? Bon, dans cet article on va donner un coup de marteau sur nos portables et regarder à l’interieur pour découvrir l’architecture Android. L’idée c’est de comprendre le stack, ce que chaque couche logiciel fait, et regarder un peu de code tant qu’on y est afin de se préparer pour notre prochain article sur la couche HAL Android.

Pour commencer, voici une photo d’Android que vous n’avez peut-être jamais vu :

Source : https://source.android.com/devices/architecture

Dans Android il y a 2 mondes : le monde Java et le monde C. Cela permet aux développeurs d’utiliser un langage portable et de haut niveau comme Java tout en profitant d’un noyau performant écrit en C.

Application Framework

Les applications sont bien évidemment dans le monde Java (même si elles peuvent avoir des morceaux de code en C pour améliorer la performance). Chacune d’entre elles s’exécute dans sa propre machine virtuelle Dalvik (une version modifiée de la JVM), ce qui veut dire qu’elles ont leurs propres ressources, et qu’elles sont isolées.

Les applications ne sont pas toutes seules là-haut, il y a aussi les librairies qu’on peut utiliser en tant que dévéloppeurs Android pour communiquer avec le système ou d’autres applications, par exemple :

  • android.app pour utiliser le framework Android (Activity, Service, Fragment, etc)
  • android.content pour communiquer et partager des données avec d’autres applis
  • android.view et android.widget pour construire des interfaces graphiques
  • android.webkit pour permettre la navigation web
  • etc…

Binder IPC Proxies

Cette couche est ce qui permet à la couche du dessus de communiquer avec les services système et les applications du dispositif. Elle permet de transférer des appels entre les différents processus sans que les applications sachent qu’elle existe.

Le Binder IPC utilise le driver /dev/binder du noyau pour faire de l’IPC (Inter Process Communication), c’est pour ça qu’on pourrait penser que cette couche est trop haut dans le stack, vu qu’elle utilise le noyau presque directement. La raison de son placement est qu’elle est le lien entre les applications et les services, ce qui est très important parce que sans les appels service, on ne peut pas accéder au hardware, comme on le verra après.

Quand un processus veut communiquer avec un autre, il doit retrouver l’instance du service en utilisant le ServiceManager. Voici le déroulement de ce mécanisme :

  • Pendant le démarrage du système, le ServiceManager est registré auprès du driver binder
  • Au fur et à mesure que les services sont créés, ils sont registrés auprès du ServiceManager, en exposant une interface abstraite
  • Au moment d’un appel, le client obtient la référence au ServiceManager (il y a des fonctions définies pour faire ça)
  • Le client obtient une référence au service cible grâce au ServiceManager
  • Le client appelle une méthode du service, qui est un proxy local
  • Le proxy transmet l’appel au serveur régistré
  • Le serveur transmet l’appel à l’implémentation du service

Pour ceux qui connaissent le fonctionnement de RPC ou RMI, les 4 dernières étapes sont exactement ça.

System Services

Les services système sont des composants indépendants qui nous offrent des services prédéfinis, par exemple l’accès à la caméra, l’audio, la localisation, la fenêtre de notre application, etc. Ce qui est intéressant c’est que ces services habitent les 2 mondes (pensez à Eleven de Stranger Things), une partie est codée en Java et l’autre en C. Pour bien comprendre comment ça marche je vous laisse un morceau de code source Android.

public class LightsService extends SystemService {

    ...

    public LightsService(Context context) {
        super(context);

        mNativePointer = init_native();

        for (int i = 0; i < LightsManager.LIGHT_ID_COUNT; i++) {
            mLights[i] = new LightImpl(i);
        }
    }

    ...

    @Override
    protected void finalize() throws Throwable {
        finalize_native(mNativePointer);
        super.finalize();
    }

    private static native long init_native();
    private static native void finalize_native(long ptr);

    static native void setLight_native(long ptr, int light, int color, int mode,
            int onMS, int offMS, int brightnessMode);

    private long mNativePointer;
}

Vous avez sûrement compris qu’il s’agit du service Java qui nous permet d’accéder aux lumières du dispositif (par exemple lorsqu’on reçoit une notification). Ce qui nous intéresse ici ce n’est pas de comprendre comment on allume les lumières, mais le mécanisme par lequel les services sont implémentés.

Ce morceau de code d’Android 7.1 utilise JNI pour exécuter du code C ! C’est justement ici que le pont est construit pour accéder aux drivers, on a un pointeur natif vers des dispositifs de lumières (qu’on n’utilise pas en Java), et une fonction native setLight_native pour changer l’état de ces dispositifs. Maintenant regardons ce qui se passe de l’autre côté.

Un développeur Java ouvre un fichier .cpp
struct Devices {
    light_device_t* lights[LIGHT_COUNT];
};

static jlong init_native(JNIEnv* /* env */, jobject /* clazz */)
{
    ...

    err = hw_get_module(LIGHTS_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
    if (err == 0) {
        devices->lights[LIGHT_INDEX_BACKLIGHT]
                = get_device(module, LIGHT_ID_BACKLIGHT);
        devices->lights[LIGHT_INDEX_KEYBOARD]
                = get_device(module, LIGHT_ID_KEYBOARD);
        ...
    } else {
        memset(devices, 0, sizeof(Devices));
    }

    return (jlong)devices;
}

static void setLight_native(JNIEnv* /* env */, jobject /* clazz */, jlong ptr,
        jint light, jint colorARGB, jint flashMode, jint onMS, jint offMS, jint brightnessMode)
{
    ...

    if (light < 0 || light >= LIGHT_COUNT || devices->lights[light] == NULL) {
        return ;
    }

    ...

    {
        ALOGD_IF_SLOW(50, "Excessive delay setting light");
        devices->lights[light]->set_light(devices->lights[light], &state);
    }
}

Comme on l’avait dit, la fonction init_native renvoie un pointeur vers la structure de dispositifs de lumières :

return (jlong)devices;

Et setLight_native change l’état du dispositif :

devices->lights[light]->set_light(devices->lights[light], &state);

Donc ça a du sens, mais d’où ça vient les dispositifs ? Et au final il va falloir changer les lumières quelque part, non ? Qui fait ça ? Le responsable est justement notre prochaine couche : le HAL.

Hardware Abstraction Layer (HAL)

Il s’agit d’une couche logiciel qui existe depuis longtemps dans les systèmes d’exploitation et qui permet par exemple de brancher notre clé USB à notre ordi et de l’utiliser presque automatiquement.

Donc c’est quoi exactement un HAL en termes techniques ? La réponse est très simple, c’est une interface ! Ou plus précisement, un ensemble d’interfaces, un contrat entre mon OS et ma clé USB, mon appareil photo, mon disque dur, etc…

Donc Android a une couche logiciel de bas niveau qui s’appelle HAL, qui est un ensemble d’interfaces définies par Google, et que les fabricants doivent respecter dans leurs drivers pour qu’Android marche avec leurs dispositifs.

Le sujet du HAL est énorme, et c’est justement le centre d’intêret de notre projet et notre série d’articles. C’est pour ça qu’il y aura un article complet dedié au HAL sur Android. On regardera plus en détail son fonctionnement, et les différences entre Nougat et Oreo, où Google a introduit Project Treble.

Linux Kernel

Le noyau Android est tout simplement le noyau Linux qu’on connaît et qu’on aime, avec quelques modifications afin de l’adapter aux dispositifs embarqués. C’est par exemple le cas des Wakelocks, qui ont été ajoutés pour améliorer la gestion de l’énergie.

Sur Android, les fabricants (et utilisateurs avancés) peuvent changer presque tout dans le système, y compris le noyau même. C’est pour cette raison que les fameuses ROMs personnalisées existent, et que les nouvelles versions d’Android prennent aussi longtemps pour arriver à nos portables, puisqu’il faut refaire ces modifications, rajouter des modifications particulières à la version, tester, etc.

Conclusion

Maintenant vous connaissez l’architecture du système Android, et j’espère que vous n’avez pas vraiment donné un coup de marteau sur vos portables. Dans les prochains articles on va aborder le sujet du HAL, dont on n’a pas beaucoup parlé ici, et on va expliquer ce que c’est que Project Treble et pourquoi il a été mis en place.

Sources

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.