前言
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的。
创建cpp目录以及cpp文件
在如下目录创建cpp文件夹:
mkdir app/src/main/cpp/
接着在该目录下创建cpp文件:
在此命名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库名称。创建方法如下:
在此命名为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"
}
}
最终配置如下图:
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;
}