NDK开发——快速入门

一 相关配置

Android Studio创建新项目时选择最后一项Native C++,这时候生成的项目配置就是默认的CMake开发的配置,如下图所示:

但是如果原有项目不支持NDK开发,要添加native代码,需要以下配置流程:

  1. 创建native代码文件,在main目录下创建cpp目录,右键点击 cpp 目录,然后依次选择 New > C/C++ Source File,文件名取为demo,文件后缀为cpp,点击OK,这时候就会生成一个demo.cpp的原生代码文件。目前这个文件为空,后面我们会往里面添加C++代码。

  2. 创建 CMake 构建脚本。右键点击 cpp 目录,然后依次选择 New > File,输入CMakeLists.txt作为文件名,然后点击 OK。我们需要编辑这个txt脚本:

    • cmake_minimum_required:cmake_minimum_required(VERSION 3.22.1),表示cmake的最小版本号,按照习惯,脚本的第一行一般都是配置这个属性。

    • project("demo")。配置项目的名称,我们写成demo即可,这行配置可以没有。

    • add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [<source>...]),三个参数,分别表示库的 名字,库的类型,库的路径。我们配置如下:add_library(demo SHARED demo.cpp),其中第三个参数就是我们在第一步中创建的C++文件。这样配置的结果,cmake就会在/app/build/intermediates/cmake/debug/obj/目录下生成对应的libdemo.so

    • 这个命令以找到 NDK 库并将其路径存储为一个变量。可以使用此变量在构建脚本的其他部分引用 NDK 库。以下配置会找到 Android 专有的日志支持库,并将其路径存储在 log-lib 中:

      find_library( # Defines the name of the path variable that stores the
              # location of the NDK library.
              log-lib
              # Specifies the name of the NDK library that
              # CMake needs to locate.
              log )
      
    • 为了让原生库能够调用 log 库中的函数,需要使用 CMake 构建脚本中的 target_link_libraries() 命令来关联这些库:

      # Links your native library against one or more other native libraries.
      target_link_libraries( # Specifies the target library.
                       demo
                       # Links the log library to the target library.
                       ${log-lib} ) 
      

最终的CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 3.22.1)

# Declares and names the project.

project("demo")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        demo
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        demo.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib
        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        demo
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})
  1. 指定ndk路径。我们可以在项目根目录的local.properties文件中指定ndk的路径,比如这样配置:

      ndk.dir=/Users/zhouzhihui/Library/Android/sdk/ndk/25.2.9519653
    

    在新的Android Studio中这样配置会报一个警告:Please delete ndk.dir from local.properties and set android.ndkVersion to [25.2.9519653] in all native modules in the project,那么我们就用新的配置方法,先删除这一行配置,然后在app/build.gradle文件中添加ndkVersion的配置,如下代码:

        android {
            ndkVersion "25.2.9519653"
            compileSdk 33
            ......
    
  2. 将 Gradle 关联到原生库。将 externalNativeBuild块添加到模块级 build.gradle 文件中,并使用cmakendkBuild 块对其进行配置,这个配置和ndkVersion的配置是同一级别的:

    externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt')
            version '3.22.1'
        }
    }
    

    当然还可以指定其它参数,具体可以参考官网说明:关联Gradle

  3. 指定abi。如果不指定abi,gradle 默认只生成arm64-v8a构架的so文件(我的小米手机是这样,应该是生成与设备相对应的so,欢迎指正),我们可以如下指定abi:

      ndk {
            abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64"
      }
    

    这样配置,就能生成四个架构下的so文件。

经过上面四个步骤,我们ndk的原生代码开发配置基本完成了,然后点击Run app按钮,运行项目,最终apk中的lib目录生成了四个so,如图:

apk
因为我们的C++文件还没有写代码,因此so都比较小,只有2KB。

二 编写C++代码

由于native方法是在java层调用,因此对于一个so,我们就专门创建一个对应的java类。我们前面步骤新建了demo.cpp,那么我们就创建个对应的Demo.java类。我们在主package包目录下创建一个新目录demo,在该目录下创建一个java类:Demo.java,要使用native代码,首先将so加载到虚拟机中,如下代码:

    static {
        System.loadLibrary("demo");
    }

然后在Demo.java中创建我们的第一个native方法,而且是一个static方法:

public static native String getStr();

这时候会报错Cannot resolve corresponding JNI function Java_com_example_nativecpp_demo_Demo_getStr.,因为getStr方法还没在demo.cpp中实现,然后我们将鼠标移到飙红的代码处,同时按住键盘的alt+enter键,就会出现修复的建议,如图所示:

然后点击enter键,在demo.cpp中就会出现以下代码:

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_getStr(JNIEnv *env, jclass clazz) {
    // TODO: implement getStr()
}

这个自动生成的c++代码函数就是getStr的native实现,我们修改下,最终实现如下:

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_getStr(JNIEnv *env, jclass /* this */) {
    std::string hello = "stringFromJNI";
    return env->NewStringUTF(hello.c_str());
}

按照上面的步骤,我们继续添加两个非static的native方法:

    public native int add(int a, int b);
    public native String concat(String a, String b);

对应的native实现如下:

extern "C" JNIEXPORT jint JNICALL
Java_com_example_nativecpp_demo_Demo_add(JNIEnv *env, jobject, jint a, jint b) {
    string sum = std::to_string(a + b);
    std::stringstream ss;
    ss << a << "+" << b << "==" << sum;
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", ss.str().c_str());
    return a + b;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_concat(JNIEnv *env, jobject thiz, jstring a, jstring b) {
    //1、直接使用GetStringUTFChars方法将传递过来的jstring转为char*
    char *c1 = (char *) (env->GetStringUTFChars(a, JNI_FALSE));
    char *c2 = (char *) (env->GetStringUTFChars(b, JNI_FALSE));
    //2、再使用本地函数strcat 拼接两个char*对象,然后NewStringUTF转为jstring返回去
    char *res = strcat(c1, c2);
    return env->NewStringUTF(res);
}

不难发现,demo.cpp的方法名字很长,而且遵循一定的约束

  • 定义:通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法
  • 规则:Java + 包名 + 类名 + 方法名, 其中使用下划线将每部分隔开,包名也使用下划线隔开,如果名称中本来就包含下划线,将使用下划线加数字替换。
    明显有以下缺点:
    1 必须遵循注册规则
    2 名字过长
    3 运行时去找效率不高

下面我们介绍一下更加简洁而且更加高效的native方法的注册方式:动态注册。

三 动态注册

我们在Demo.java中加上第四个native方法:

public native int[] sortArray(int[] arr);

为了动态注册该方法,首先,在demo.cpp中我们实现sortArray方法的功能,但不静态注册,如下:

void sort(int &a, int &b) {
    a = a + b;
    b = a - b;
    a = a - b;
}

jintArray sortArray(JNIEnv *env, jobject thiz, jintArray arr) {
    jsize len = env->GetArrayLength(arr);
//    jint *body = env->GetIntArrayElements(arr, 0);
    jint *out = env->GetIntArrayElements(arr, NULL);
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - i - 1; j++) {
            if (out[j] > out[j + 1]) {
                sort(out[j], out[j + 1]);
//                jsize temp = out[j + 1];
//                out[j + 1] = out[j];
//                out[j] = temp;
            }
        }
    }

    jintArray arrSorted = env->NewIntArray(len);
    env->SetIntArrayRegion(arrSorted, 0, len, out);
    env->ReleaseIntArrayElements(arr, out, 0);
    env->DeleteLocalRef(arr);
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", "new arr");
    for (int i = 0; i < len; i++) {
        __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%d", out[i]);
    }
    return arrSorted;
}

该方法就是接收一个java的整数数组,然后使用冒泡从小到大排序,返回一个新的排过序的新的数组。这个方法不满足前面讨论过的静态注册的两条约束条件,因此如果在java层调用该方法,就会报错:
java.lang.UnsatisfiedLinkError: No implementation found for int[] com.example.nativecpp.demo.Demo.sortArray(int[]) (tried Java_com_example_nativecpp_demo_Demo_sortArray and Java_com_example_nativecpp_demo_Demo_sortArray___3I),如图:

。我们需要注册该方法,那么下面就来动态注册该方法。

还记得我们在Demo.java中加载so的代码static { System.loadLibrary("demo"); }吗?这个加载会触发demo.cpp里的JNI_OnLoad方法,通常我们在 JNI_OnLoad 方法中完成动态注册,直接上代码吧:

jint RegisterNatives(JNIEnv *env) {
    jclass clazz = env->FindClass("com/example/nativecpp/demo/Demo");
    if (clazz == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,"con't find class: com/example/nativecpp/demo/Demo");
        return JNI_ERR;
    }
    JNINativeMethod method[] = {
            {"sortArray", "([I)[I", (void *) sortArray}
    };
    return env->RegisterNatives(clazz, method,sizeof(method) / sizeof(method[0]));
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "enter jni_onload");
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    jint result = RegisterNatives(env);
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "register natives result=%d", result);
    return JNI_VERSION_1_6;
}

动态注册的逻辑在RegisterNatives方法中,主要分三步:

  1. 通过反射找到方法所在的java类:jclass clazz = env->FindClass("com/example/nativecpp/demo/Demo");
  2. 定义要注册的方法:JNINativeMethod method[] = { {"sortArray", "([I)[I", (void *) sortArray} };,可以看到变量method是一个数组,数组的每个元素都对应一个要动态注册的方法,由于我们只有一个方法要动态注册,因此method数组只有一个元素。元素的第一个参数是java层定义的函数名,第三个参数是native层定义的函数名,两个函数名没有必然关系,可以不一样。要注意的是元素的第二个参数也就是函数签名(如果不知道函数签名也可以使用快捷键alt+enter自动修复),具体的函数签名可以参考:Android深入理解JNI(二)类型转换、方法签名和JNIEnv
  3. 进行注册:env->RegisterNatives(clazz, method,sizeof(method) / sizeof(method[0]));

四 最终的源代码

所有的代码如下:
Demo.java:

public class Demo {
    // Used to load the 'nativecpp' library on application startup.
    static {
        Log.i("zzh", "load 1");
        System.loadLibrary("demo");
        Log.i("zzh", "load 2");
    }

    /**
     * A native method that is implemented by the 'nativecpp' native library,
     * which is packaged with this application.
     */
    public static native String getStr();

    public native int add(int a, int b);

    public native String concat(String a, String b);

    // 动态注册
    public native int[] sortArray(int[] arr);
}

demo.cpp

#include <jni.h>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <sstream>
#include <android/log.h>

using namespace std;

#define LOG_TAG "zzh-cmake"

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_getStr(JNIEnv *env, jclass /* this */) {
    std::string hello = "stringFromJNI";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_nativecpp_demo_Demo_add(JNIEnv *env, jobject, jint a, jint b) {
    string sum = std::to_string(a + b);
    std::stringstream ss;
    ss << a << "+" << b << "==" << sum;
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", ss.str().c_str());
    return a + b;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativecpp_demo_Demo_concat(JNIEnv *env, jobject thiz, jstring a, jstring b) {
    //1、直接使用GetStringUTFChars方法将传递过来的jstring转为char*
    char *c1 = (char *) (env->GetStringUTFChars(a, JNI_FALSE));
    char *c2 = (char *) (env->GetStringUTFChars(b, JNI_FALSE));
    //2、再使用本地函数strcat 拼接两个char*对象,然后NewStringUTF转为jstring返回去
    char *res = strcat(c1, c2);

    return env->NewStringUTF(res);
}

void sort(int &a, int &b) {
    a = a + b;
    b = a - b;
    a = a - b;
}

jintArray sortArray(JNIEnv *env, jobject thiz, jintArray arr) {
    jsize len = env->GetArrayLength(arr);
//    jint *body = env->GetIntArrayElements(arr, 0);
    jint *out = env->GetIntArrayElements(arr, NULL);
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - i - 1; j++) {
            if (out[j] > out[j + 1]) {
                sort(out[j], out[j + 1]);
//                jsize temp = out[j + 1];
//                out[j + 1] = out[j];
//                out[j] = temp;
            }
        }
    }

    jintArray arrSorted = env->NewIntArray(len);
    env->SetIntArrayRegion(arrSorted, 0, len, out);
    env->ReleaseIntArrayElements(arr, out, 0);
    env->DeleteLocalRef(arr);
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s", "new arr");
    for (int i = 0; i < len; i++) {
        __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%d", out[i]);
    }
    return arrSorted;
}

jint RegisterNatives(JNIEnv *env) {
    jclass clazz = env->FindClass("com/example/nativecpp/demo/Demo");
    if (clazz == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,"con't find class: com/example/nativecpp/demo/Demo");
        return JNI_ERR;
    }
    JNINativeMethod method[] = {
            {"sortArray", "([I)[I", (void *) sortArray}
    };
    return env->RegisterNatives(clazz, method,sizeof(method) / sizeof(method[0]));
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "enter jni_onload");
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    jint result = RegisterNatives(env);
    __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "register natives result=%d", result);
    return JNI_VERSION_1_6;
}

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity:zzh";
    Demo mDemo;

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDemo = new Demo();
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(Demo.getStr());
        int[] arr = new int[]{10, 3, 2, 1, 9, 11, 8, 4};
        binding.sampleText.setOnClickListener(v -> {
            Log.i(TAG, "demo add 1+2=" + mDemo.add(1, 2));
            Log.i(TAG, "demo concat a concat b=" + mDemo.concat("a", "b"));
            int[] newArr = mDemo.sortArray(arr);
            Log.i(TAG, "demo after sort array=" + Arrays.toString(arr));
            Log.i(TAG, "demo after sort array2=" + Arrays.toString(newArr));
        });
    }
}

运行的日志:

2023-02-27 22:26:27.368 14524-14524 zzh                     com.example.nativecpp                I  load 1
2023-02-27 22:26:27.369 14524-14524 zzh-cmake               com.example.nativecpp                D  enter jni_onload
2023-02-27 22:26:27.369 14524-14524 zzh-cmake               com.example.nativecpp                D  register natives result=0
2023-02-27 22:26:27.369 14524-14524 zzh                     com.example.nativecpp                I  load 2
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  1+2==3
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh        com.example.nativecpp                I  demo add 1+2=3
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh        com.example.nativecpp                I  demo concat a concat b=ab
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  new arr
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  1
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  2
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  3
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  4
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  8
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  9
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  10
2023-02-27 22:26:31.715 14524-14524 zzh-cmake               com.example.nativecpp                D  11
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh        com.example.nativecpp                I  demo after sort array=[1, 2, 3, 4, 8, 9, 10, 11]
2023-02-27 22:26:31.715 14524-14524 MainActivity:zzh        com.example.nativecpp                I  demo after sort array2=[1, 2, 3, 4, 8, 9, 10, 11]

从日志中可以看出,native方法sortArray把java层传入的int[]也给修改了。






参考:
ndk使用入门

JNI 静态注册和动态注册

Android深入理解JNI(二)类型转换、方法签名和JNIEnv

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容