前言

Android studio工程中经常会用到native 方法,方法之一是在libs文件夹中引入so文件,这样需要用到native方法的类直接调用如下方法加载库即可:



System.loadLibrary("test-lib");



如果不想引用so文件,想要直接在AS中实现native方法也是可以的。此时需要在项目中添加cpp文件,且需要做相关的配置,下面详细说明。

概述

环境准备

要想在AS中编译cpp文件,则需要下载NDK和安装cmake工具,否则无法使用。

NDK和cmake的安装很简单,AS中直接就可以处理,操作如下:



Tools->SDK Manager->Android SDK->SDK Tools



按如上点击,找到SDK Tools,就可以看到一系列的工具供安装,如下图是已经安装了NDK和Cmake的。




androidstudio cmake android studio cmake so库工程默认路径_androidstudio cmake


创建cpp目录以及cpp文件

在如下目录创建cpp文件夹:


mkdir app/src/main/cpp/


接着在该目录下创建cpp文件:


androidstudio cmake android studio cmake so库工程默认路径_方法名_02


在此命名cpp文件名为test.cpp,并添加如下内容:


#include <jni.h>
#include <string.h>

JNIEXPORT jstring JNICALL Java_com_xxx_yyy_MainActivity_startTestJni(
        JNIEnv *env,
        jobject thiz) {
            char *hello = "hello test...";
            return env->NewStringUTF(hello);
        }


如上com_xxx_yyy为包名,而MainActivity则是表示在MainActivity类中使用native方法。

每个类对应一个native文件,有一一对应的关系,这里需要注意。否则运行的时候会报找不到对应的class。

创建CMakeLists.txt 文件

CMakeLists.txt是编译cpp文件的规则,在该文件中会指定cpp文件编译出来的so库名称。创建方法如下:


androidstudio cmake android studio cmake so库工程默认路径_#include_03


在此命名为CMakeLists.txt。在该文件中添加如下内容:


cmake_minimum_required(VERSION 3.4.1)

add_library( # Sets the name of the library.
         test-lib

         # Sets the library as a shared library.
         SHARED

         # Provides a relative path to your source file(s).
         src/main/cpp/test.cpp )

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 )

target_link_libraries( # Specifies the target library.
                   test-lib

                   # Links the target library to the log library
                   # included in the NDK.
                   ${log-lib} )


在add_library中指定so库名称,属性(是否设置为共享库)以及源文件。

build.gradle中配置

打开build.gradle向android/defaultConfig节区追加以下内容:


externalNativeBuild {
    cmake {
        cppFlags ""
    }
}


向android节区追加以下内容:


externalNativeBuild {
    cmake {
        path "CMakeLists.txt"
    }
}


最终配置如下图:


androidstudio cmake android studio cmake so库工程默认路径_android_04


MainActivity中使用该native方法

先声明对应的native方法,如这里的test.cpp中添加的native方法如下:


Java_com_xxx_yyy_MainActivity_startTestJni


那么我们在MainActivity中声明如下:


public native String startTestJni();


当然,在调用该方法前,还需要加载对应的so库,名称为test-lib,如下:


static {
    System.loadLibrary("test-lib");
}


这样就可以使用该native方法了。

如何简化 native方法名

在上面的例子中,native方法名长得可怕,且方法名受包名限制,如下:


JNIEXPORT jstring JNICALL Java_com_xxx_yyy_MainActivity_startTestJni


这样一坨方法名,简直无法接受。

方法名如此受限是因为JNI方法采用了静态注册的方法。聪明如你,这时肯定想到,要想使用简洁不受限的方法名,则需要使用动态注册的方法了。

使用动态注册的方法时,需要在cpp文件中实现JNI_OnLoad方法:


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

    if (!registerNatives(env)) {
        return -1;
    }

    result = JNI_VERSION_1_6;
    return result;
}


当我们调用System.loadLibrary方法加载so库时,系统就会去查找对应的JNI_OnLoad方法,如果没找到,则默认为静态注册方法,此时直接返回so加载成功的标志。

如果系统找到了JNI_OnLoad方法,但没做相应的方法注册,则此时系统会抛出异常。

动态注册实现

动态注册方法实现源码如下:


//方法名
static JNINativeMethod gMethods[] = {
    { "startTestJni", "()Ljava/lang/String;",(void*) native_startTestJni },
};


static int registerNativeMethods(JNIEnv* env, const char* className,
                                 JNINativeMethod* gMethods, int numMethods) {
    jclass clazz;

    clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}


static int registerNatives(JNIEnv* env) {
    //这里需要指定使用该native方法的类名,由包名和类名组成
    const char* kClassName = "com/xxx/yyy/MainActivity";
    return registerNativeMethods(env, kClassName, gMethods,
                                 sizeof(gMethods) / sizeof(gMethods[0]));
}


registerNatives方法最终是在JNI_OnLoad方法中调用。

这里重点说下gMethods的规则:


static JNINativeMethod gMethods[] = {
    { "startTestJni", "()Ljava/lang/String;",(void*) native_startTestJni },
};


其中startTestJni为Java类中定义的Native方法名。

()Ljava/lang/String; 为方法的签名, ()表示该方法无参数, Ljava/lang/String;表示返回值为Java中的String类型。其他类型的使用以及带参如何写,可以网上查找相关知识,有很多。

而(void*) native_startTestJni为Native实现的方法名。这里强制转换成了函数指针。

所以如果采用动态注册的方法,最终的test.cpp文件如下:


#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <jni.h>

#include <android/log.h>
static const char *TAG = "test";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO,  TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)


jstring  native_startTestJni(JNIEnv * env, jobject thiz)
{
    char *hello = "hello test ...";
    return env->NewStringUTF(hello);
}

static JNINativeMethod gMethods[] = {
        { "startTestJni", "()Ljava/lang/String;",(void*) native_startTestJni },
};


static int registerNativeMethods(JNIEnv* env, const char* className,
                                 JNINativeMethod* gMethods, int numMethods) {
    jclass clazz;

    clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static int registerNatives(JNIEnv* env) {
    const char* kClassName = "com/cvte/irsensor/MainActivity";
    return registerNativeMethods(env, kClassName, gMethods,
                                 sizeof(gMethods) / sizeof(gMethods[0]));
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

    if (!registerNatives(env)) {
        return -1;
    }

    result = JNI_VERSION_1_6;
    LOGD("this is JNI_OnLoad for test-lib..");
    return result;
}