Cet article fait partie d'une série consacrée au développement Android avec JNI et NDK. Nous aborderons ici les bases de la création d'un projet "Hello World" utilisant ces technologies, en explorant d'abord la méthode traditionnelle, puis en présentant une approche plus moderne avec CMake.
Environnement de développement
Assurez-vous de disposer des éléments suivants :
- Système d'exploitation : Votre système d'exploitation habituel (Windows, macOS, Linux).
- Android Studio : Une version récente est recommandée.
- SDK Android : Installez les plateformes SDK nécessaires via le SDK Manager.
- NDK (Native Development Kit) : Installez le NDK via le SDK Manager.
- Émulateur ou appareil Android : Pour tester votre application.
Méthode traditionnelle (ndk-build)
Cette approche implique plusieurs étapes manuelles pour compiler et lier le code natif.
1. Création du projet Android
Créez un nouveau projet Android dans Android Studio. Par exemple, nommez le package com.example.jnidemo.
2. Création de la classe Java pour les appels natifs
Créez une classe Java (par exemple, NativeLib.java) qui déclarera les méthodes natives.
package com.example.jnidemo;
public class NativeLib {
// Charge la bibliothèque native au démarrage
static {
System.loadLibrary("native-lib");
}
// Déclare la méthode native qui sera implémentée en C/C++
public static native String getGreeting();
}
3. Modification de l'interface utilisateur
Dans le fichier XML de votre mise en page (par exemple, activity_main.xml), ajoutez un TextView avec un ID pour afficher le résultat.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/greetingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chargement..."
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Dans votre MainActivity, appelez la méthode native et affichez le résultat.
package com.example.jnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.greetingTextView);
String greeting = NativeLib.getGreeting();
textView.setText(greeting);
}
}
4. Génération du fichier d'en-tête JNI (.h)
Compilez votre projet (Build > Make Project). Ensuite, ouvrez le terminal dans Android Studio et naviguez jusqu'au répertoire app/build/intermediates/classes/debug. Exécutez la commande suivante pour générer le fichier d'en-tête :
javah -jni com.example.jnidemo.NativeLib
Cela créera un fichier com_example_jnidemo_NativeLib.h dans le même répertoire. Ce fichier contient la signature de la fonction native à implémenter.
5. Création du fichier source natif (.c) et du fichier Android.mk
Créez un répertoire jni dans src/main. Déplacez le fichier .h généré dans ce répertoire. Créez également un fichier .c (par exemple, native-lib.c) dans le répertoire jni.
native-lib.c :
#include "com_example_jnidemo_NativeLib.h"
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_NativeLib_getGreeting
(JNIEnv *env, jclass clazz) {
return (*env)->NewStringUTF(env, "Bonjour depuis le code natif via JNI !");
}
Créez également un fichier Android.mk dans le répertoire jni.
Android.mk :
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := native-lib
LOCAL_SRC_FILES := native-lib.c
include $(BUILD_SHARED_LIBRARY)
6. Confiugration de build.gradle
Modifiez le fichier build.gradle (module : app) pour intégrer le processus de build natif.
android {
// ... autres configurations ...
defaultConfig {
// ... autres configurations ...
externalNativeBuild {
ndkBuild {
// Spécifie le chemin vers Android.mk
path 'src/main/jni/Android.mk'
}
}
ndk {
// Spécifie les architectures cibles
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
buildTypes {
release {
// ...
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
sourceSets {
main {
jni.srcDirs = [] // Désactive la recherche automatique de JNI
jniLibs.srcDirs = ['src/main/jniLibs'] // Répertoire où les .so seront placés
}
}
}
Assurez-vous que votre fichier local.properties contient le chemin vers votre NDK.
7. Construction et exécution
Compilez et exécutez le projet. Android Studio utilisera ndk-build pour compiler le code C/C++ et générer la bibliothèque partagée (.so) qui sera incluse dans l'APK.
Méthode moderne avec CMake
Android Studio facilite l'intégration de CMake pour gérer la compilation native.
1. Création du projet avec support C++
Lors de la création d'un nouveau projet dans Android Studio, cochez la case "Include C++ support" dans l'assistant de configuration. Vous pouvez choisir d'utiliser CMake comme système de build natif.
2. Structure du projet générée
Android Studio crée automatiquement :
- Un répertoire
cppcontenant votre code source natif (par exemple,native-lib.cpp). - Un fichier
CMakeLists.txtdans le répertoire racine du module, qui spécifie comment compiler votre code natif. - Le code C++ par défaut inclut souvent une fonction simple comme
stringFromJNI.
3. Le fichier CMakeLists.txt
Le fichier CMakeLists.txt est essentiel. Voici un exemple de base :
# Version minimale de CMake requise
cmake_minimum_required(VERSION 3.4.1)
# Ajoute la bibliothèque native (partagée)
add_library( # Nom de la bibliothèque
native-lib
# Type de bibliothèque (SHARED pour dynamique)
SHARED
# Chemin vers le fichier source C++
src/main/cpp/native-lib.cpp )
# Trouve la bibliothèque de logging du NDK
find_library( log-lib log )
# Lie la bibliothèque native à la bibliothèque de logging
target_link_libraries( native-lib ${log-lib} )
4. Modification du code natif
Vous pouvez modifier le fichier native-lib.cpp pour implémenter votre logique.
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_NativeLib_getGreeting(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Salut depuis CMake et C++ !";
return env->NewStringUTF(hello.c_str());
}
Notez que la signature de la fonction suit la convention de nommage JNI : Java_package_name_ClassName_methodName.
5. Configuration de build.gradle pour CMake
Dans le build.gradle (module : app), configurez la section externalNativeBuild :
android {
// ...
defaultConfig {
// ...
externalNativeBuild {
cmake {
// Options de compilation C++
cppFlags ""
}
}
}
buildTypes {
// ...
}
externalNativeBuild {
cmake {
// Chemin vers le fichier CMakeLists.txt
path "CMakeLists.txt"
version "3.4.1" // Optionnel: spécifier la version de CMake
}
}
}
6. Construction et exécution
En exécutant le projet, Gradle invoquera CMake pour compiler le code natif, puis intégrera la bibliothèque résultante dans l'APK.
Le fichier .so généré
Que vous utilisiez ndk-build ou CMake, le processus aboutit à la création d'une bibliothèque partagée (.so) pour chaque architecutre CPU cible. Ces bibliothèques sont automatiquement incluses dans votre APK. Vous pouvez les inspecter en utilisant l'outil "Analyze APK" dans Android Studio.
L'utilisation de CMake est généralement préférée pour les nouveaux projets en raison de sa simplicité et de son intégration plus poussée avec Android Studio.