Android Studio(2.2+)构建native库可以使用原生构建工具包ndk-build,也可以使用外部构建工具CMake,搭配Gradle插件可以方便的构建原生库,进行Android JNI的开发。使用Android Studio创建的native工程默认使用的是CMake构建工具,本文从零开始介绍使用CMake搭建一个JNI工程。
为了阐述方便,我们以创建一个默认的Android工程为例,不使用创建向导里的Include C++ Support或者创建C++工程。现在我们在native代码(C++)中实现一个获取字符串并返回的操作,然后使用java jni来调用。
一、下载NDK和构建工具
打开Android Studio -> Perferences -> Appearance&Behavoir -> System Setting -> Android SDK,或者直接在左侧搜索Android SDK,选择SDK Tools,下载NDK、CMake、LLDB这三个工具包。
新建的native工程(Include C++ Support)在local.properties中都会配置ndk的默认的路径:
ndk.dir=/Users/derek/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/derek/Library/Android/sdk
如果没有,或者ndk在别的目录下,需要手动添加或修改路径。
二、在java类中声明native方法
在java中声明要使用的native方法,这些方法以native前缀,只需声明,无需实现。这些方法可以声明为static或非static方法,可以是任何访问权限。
package com.tsia.example.jnitest;
...
public class MainActivity extends Activity {
...
public native String stringFromJNI(String str);
}
三、添加C/C++代码
在c++代码中需添加和native方法对应的函数,注意如下几点:
- 文件名称可随意指定,可以只有源文件
- 头文件或源文件中要#include <jni.h>
- 方法声明要和java中的native方法对应:
- 方法名称。Java_包名_类名_方法名,包名也使用_分隔。
- 参数。
- 第一个参数为JNIEnv *
- 如果为static方法,第二个参数为jclass;如果非static方法,第二个参数为jobject
- 从第三个参数开始,和java的native方法的参数类型和顺序要一一对应
- 返回值。
- 返回值类型要对应java中的类型
- 需要JNIEXPORT、JNICALL前缀
方法格式可以概括为:JNIEXPORT <返回类型> JNICALL Java_<包名><类名><方法名>(JNIEnv *, jobject,<参数>); 包名中的“.”用下划线“_”代替。
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL Java_com_tsia_example_jnitest_MainActivity_stringFromJNI
(JNIEnv *env, jobject jobj, jstring str) {
const char* c_name = env->GetStringUTFChars(str, NULL);
std::string hello = "Hello, ";
std::string name(c_name);
return env->NewStringUTF((hello+name).c_str());
}
如果担心函数声明写错,可以使用命令行生成native方法对应的C++函数头文件,只需要到java文件所在的包名目录下执行javah命令,比如在com所在目录下执行:
tsias-MacBook-Pro:java tsia$ javah -jni com.tsia.example.jnitest.MainActivity
执行后会在该目录下生成一个头文件:com_tsia_example_jnitest_MainActivity.h,自动生成的格式为包名+类名.h,中间会使用_分隔。(这个文件名为命令自动生成的格式,可以随意修改)
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_tsia_example_jnitest_MainActivity */
#ifndef _Included_com_tsia_example_jnitest_MainActivity
#define _Included_com_tsia_example_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_tsia_example_jnitest_MainActivity
* Method: stringFromJNI
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_tsia_example_jnitest_MainActivity_stringFromJNI
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
可以在.h文件中看到所有native方法的声明,格式就是我们上文讲的一样。手动编写时也可参考自动生成头文件的格式,避免出错。
在调用native方法的时候会匹配函数名和参数,需要按照格式书写,不可随意修改。
四、编写CMakeLists.txt文件
接下来就是创建CMake的构建脚本,它是一个纯文本文件,必须命名为CMakeLists.txt。构建脚本用来告诉CMake将如何创建一个so库,例子中我们要将c++代码编译成一个名为native-lib的库,给jni调用。
cmake_minimum_required(VERSION 3.4.1)
add_library(
# 设置so文件名称.
native-lib
# 设置这个so文件为动态库(SHARED)。静态库使用STATIC
SHARED
# c/c++源文件的相对路径(相对于CMakeLists.txt)
src/main/java/jnitest.cpp)
当工程编译的时候,Gradle会自动将动态库native-lib库打包到APK中。脚本中指定的库名称为native-lib,但实际CMake生成的名称为libnative-lib.so。
CMake 使用以下规范来为库文件命名:
lib库名称.so
五、配置Gradle关联
在构建应用时,Gradle 会以依赖项的形式运行CMake,并将共享的库打包到的 APK中,因此我们需要提供一个指向 CMake脚本文件的路径。
手动配置
将 externalNativeBuild {}
块添加到模块级 build.gradle
文件中,并使用 cmake {}
对其进行配置
android {
...
defaultConfig {...}
buildTypes {...}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
这里的path为相对于build.gradle文件的路径,需正确配置。
使用Android Studio配置关联
从IDE左侧打开 Project 窗格并选择 Android视图,右键点击您想要关联到原生库的模块(例如 app 模块),并从菜单中选择 Link C++ Project with Gradle。
选择使用CMake构建,并制定CMakeLists的路径。
完成后可以看到模块级
build.gradle
文件中会增加externalNativeBuild {}
块配置,和我们手动配置的结果是一样的。因为gradle配置有修改,sync project下。
此时执行build -> Make Module 'app',可以看到所有架构的so都会打到APK的lib目录下。
五、在java文件中加载so库
在Java代码中加载so库时,请使用您在 CMake 构建脚本中指定的名称。如CMake生成的而文件是libnative-lib.so,只要指定加载native-lib即可。
package com.tsia.example.jnitest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String ret = stringFromJNI("world");
Log.i("jnitest", ret+"");
}
public native String stringFromJNI(String str);
}
然后在代码中调用native方法打印结果。运行后打印结果: Hello, world
上述就是一个简单的jni工程搭建步骤,如下是在构建和运行时的基本过程:
- Gradle调用外部构建脚本CMakeLists.txt
- CMake按照构建脚本中的命令将 C++源文件jnitest.cpp 编译到共享的对象库中,并命名为libnative-lib.so,Gradle随后在编译的时候会将其打包到APK中。
- 运行时,应用的
MainActivity
会使用System.loadLibrary()
加载原生库。这时候应用可以使用库的原生函数stringFromJNI(String str)
-
MainActivity.onCreate()
调用stringFromJNI("world")
,这将返回“Hello, world”并打印。
参考:https://developer.android.com/studio/projects/add-native-code