这篇文章主要讲解了 JNI 的基础语法和交叉编译的基本使用,经过这篇文章的学习就彻底能够入门 Android 下 JNI 项目的开发了。java
从 JVM 角度,存在两种类型的代码:“Java”和“native”, native 通常指的是 c/c++,为了使 java 和 native 端可以进行交互,java 设计了 JNI(java native interface)。 JNI 容许java虚拟机(VM)内运行的java代码与C++、C++和汇编等其余编程语言编写的应用程序和库进行互操做。linux
虽然大部分状况下咱们的软件彻底能够由 java 来实现,可是某些场景下使用 native 代码更加适合,好比:android
native 层使用 JNI 主要能够作到:c++
使用 as 建立一个 native c++ 项目git
文件结构以下:程序员
能够看到生成了一个 cpp 文件夹,里面有 CMakeLists.txt, native-lib.cpp,CMakeLists后面再讲,这里先来看一下 native-lib.cpp 和 java 代码。github
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
...
public native String stringFromJNI();
}
复制代码
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
复制代码
能够看到在 MainActivity 中先定义了一个 native 方法,而后编译器在 cpp 文件中建立一个一个对应的方法Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI
。 它的命名规则就是 Java_packageName_methodName。shell
接下来咱们详细的解读一下 cpp 中的代码。编程
在 c++ 中使用 c 代码api
宏定义:#define JNIEXPORT __attribute__ ((visibility ("default")))
在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,定义为__attribute__ ((visibility ("default")))
GCC 有个visibility属性, 该属性是说, 启用这个属性:
宏定义,在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,它是个空的宏定义: #define JNICALL
,因此在 android 上删除它也能够。 快捷生成 .h 代码
struct _JNIEnv {
/** * 定义了不少的函数指针 **/
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
/// 经过类的名称(类的全名,这时候包名不是用.号,而是用/来区分的)来获取jclass
jclass FindClass(const char* name) { return functions->FindClass(this, name); }
...
}
复制代码
JNIEnv 的结构图以下:
JavaVM : JavaVM 是 Java虚拟机在 JNI 层的表明, JNI 全局只有一个
JNIEnv : JavaVM 在线程中的表明, 每一个线程都有一个, JNI 中可能有不少个 JNIEnv,同时 JNIEnv 具备线程相关性,也就是 B 线程没法使用 A 线程的 JNIEnv
JVM 的结构图以下:
这个 object 指向该 native 方法的 this 实例,好比咱们在 MainActivity 调用的下面的 native 函数中打印一下 thiz 的 className:
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);
extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
// 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
jclass thisclazz = env->GetObjectClass(thiz);
// 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass", "()Ljava/lang/Class;");
// 3. 执行 getClass 方法,得到 Class 对象
jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
// 4. 获取 Class 实例
jclass clazz = env->GetObjectClass(clazz_instance);
// 5. 根据 class 的 methodID
jmethodID mid_getName = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;");
// 6. 调用 getName 方法
jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName));
LOGE("class name:%s", env->GetStringUTFChars(name, 0));
return env->NewStringUTF(hello.c_str());
}
复制代码
打印结果以下:
Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
这里贴一张 oracle 文档中的图,虽然很丑但挺好:
JNIEvn 操做 java 对象时利用 java 中的反射,操做某个属性都须要 field 和 method 的 id,这些 id 都是指针类型:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method IDs */
复制代码
将一个 Java int[] 对象传入 C++ 中,如何操做这个数组呢?
JNIEXPORT void JNICALL Java_com_wangzhen_jnitutorial_MainActivity_setArray(JNIEnv *env, jobject thiz, jintArray array) {
// 1.获取数组长度
jint len = env->GetArrayLength(array);
LOGE("array.length:%d", len);
jboolean isCopy;
// 2.获取数组地址
// 第二个参数表明 javaArray -> c/c++ Array 转换的方式:
// 0: 把指向Java数组的指针直接传回到本地代码中
// 1: 新申请了内存,拷贝了数组
// 返回值: 数组的地址(首元素地址)
jint *firstElement = env->GetIntArrayElements(array, &isCopy);
LOGE("is copy array:%d", isCopy);
// 3.遍历数组(移动地址)
for (int i = 0; i < len; ++i) {
LOGE("array[%i] = %i", i, *(firstElement + i));
}
// 4.使用后释放数组
// 第一个参数是 jarray,第二个参数是 GetIntArrayElements 返回值
// 第三个参数表明 mode
env->ReleaseIntArrayElements(array,firstElement,0);
// 5. 建立一个 java 数组
jintArray newArray = env->NewIntArray(3);
}
复制代码
- mode = 0 刷新java数组 并 释放c/c++数组
- mode = JNI_COMMIT (1) 只刷新java数组
- mode = JNI_ABORT (2) 只释放c/c++数组
extern "C"
JNIEXPORT void JNICALL Java_com_wangzhen_jnitutorial_MainActivity_setString(JNIEnv *env, jobject thiz, jstring str) {
// 1.jstring -> char*
// java 中的字符创是 unicode 编码, c/C++ 是UTF编码,因此须要转换一下。第二个参数做用同上面
const char *c_str = env -> GetStringUTFChars(str,NULL);
// 2.异常处理
if(c_str == NULL){
return;
}
// 3.当作一个 char 数组打印
jint len = env->GetStringLength(str);
for (int i = 0; i < len; ++i) {
LOGE("c_str: %c",*(c_str+i));
}
// 4.释放
env->ReleaseStringUTFChars(str,c_str);
}
复制代码
调用完 GetStringUTFChars 以后不要忘记安全检查,由于 JVM 须要为新诞生的字符串分配内存空间,当内存空间不够分配的时候,会致使调用失败,失败后 GetStringUTFChars 会返回 NULL,并抛出一个OutOfMemoryError 异常。JNI 的异常和 Java 中的异常处理流程是不同的,Java 遇到异常若是没有捕获,程序会当即中止运行。而 JNI 遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的全部操做都是很是危险的,所以,咱们须要用 return 语句跳事后面的代码,并当即结束当前方法。
在 java 代码中,MainActivity 有两个成员变量:
public class MainActivity extends AppCompatActivity {
String testField = "test1";
static int staticField = 1;
}
复制代码
// 1. 获取类 class
jclass clazz = env->GetObjectClass(thiz);
// 2. 获取成员变量 id
jfieldID strFieldId = env->GetFieldID(clazz,"testField","Ljava/lang/String;");
// 3. 根据 id 获取值
jstring jstr = static_cast<jstring>(env->GetObjectField(thiz, strFieldId));
const char* cStr = env->GetStringUTFChars(jstr,NULL);
LOGE("获取 MainActivity 的 String field :%s",cStr);
// 4. 修改 String
jstring newValue = env->NewStringUTF("新的字符创");
env-> SetObjectField(thiz,strFieldId,newValue);
// 5. 释放资源
env->ReleaseStringUTFChars(jstr,cStr);
env->DeleteLocalRef(newValue);
env->DeleteLocalRef(clazz);
// 获取静态变量
jfieldID staticIntFieldId = env->GetStaticFieldID(clazz,"staticField","I");
jint staticJavaInt = env->GetStaticIntField(clazz,staticIntFieldId);
复制代码
GetFieldID 和 GetStaticFieldID 须要三个参数:
Type | Signature Java Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
V | void |
L fully-qualified-class; | fully-qualified-class |
[type | type[] |
(arg-types) ret-type | method type |
基本数据类型的比较好理解,不如要获取一个 int ,GetFieldID 须要传入签名就是 I;
若是是一个类,好比 String,签名就是 L+全类名; :Ljava.lang.String;
若是是一个 int array,就要写做 [I
若是要获取一个方法,那么方法的签名是:(参数签名)返回值签名,参数若是是多个,中间不须要加间隔符,好比: | java 方法|JNI 签名| |--|--| |void f (int n); |(I)V| |void f (String s,int n); |(Ljava/lang/String;I)V| |long f (int n, String s, int[] arr); |(ILjava/lang/String;[I)J|
操做 method 和 filed 很是类似,先获取 MethodID,而后对应的 CallXXXMethod 方法
Java层返回值 | 方法族 | 本地返回类型NativeType |
---|---|---|
void | CallVoidMethod() | (无) |
引用类型 | CallObjectMethod( ) | jobect |
boolean | CallBooleanMethod ( ) | jboolean |
byte | CallByteMethod( ) | jbyte |
char | CallCharMethod( ) | jchar |
short | CallShortMethod( ) | jshort |
int | CallIntMethod( ) | jint |
long | CallLongMethod() | jlong |
float | CallFloatMethod() | jfloat |
double | CallDoubleMethod() | jdouble |
在 java 中咱们要想获取 MainActivity 的 className 会这样写:
this.getClass().getName()
复制代码
能够看到须要先调用 getClass 方法获取 Class 对象,而后调用 Class 对象的 getName 方法,咱们来看一下如何在 native 方法中调用:
extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
// 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
jclass thisclazz = env->GetObjectClass(thiz);
// 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
jmethodID mid_getClass = env->GetMethodID(thisclazz, "getClass", "()Ljava/lang/Class;");
// 3. 执行 getClass 方法,得到 Class 对象
jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
// 4. 获取 Class 实例
jclass clazz = env->GetObjectClass(clazz_instance);
// 5. 根据 class 的 methodID
jmethodID mid_getName = env->GetMethodID(clazz, "getName", "()Ljava/lang/String;");
// 6. 调用 getName 方法
jstring name = static_cast<jstring>(env->CallObjectMethod(clazz_instance, mid_getName));
LOGE("class name:%s", env->GetStringUTFChars(name, 0));
// 7. 释放资源
env->DeleteLocalRef(thisclazz);
env->DeleteLocalRef(clazz);
env->DeleteLocalRef(clazz_instance);
env->DeleteLocalRef(name);
return env->NewStringUTF(hello.c_str());
}
复制代码
首先定义一个 java 类:
public class Person {
private int age;
private String name;
public Person(int age, String name){
this.age = age;
this.name = name;
}
public void print(){
Log.e("Person",name + age + "岁了");
}
}
复制代码
而后咱们再 JNI 中建立一个 Person 并调用它的 print 方法:
// 1. 获取 Class
jclass pClazz = env->FindClass("com/wangzhen/jnitutorial/Person");
// 2. 获取构造方法,方法名固定为<init>
jmethodID constructID = env->GetMethodID(pClazz,"<init>","(ILjava/lang/String;)V");
if(constructID == NULL){
return;
}
// 3. 建立一个 Person 对象
jstring name = env->NewStringUTF("alex");
jobject person = env->NewObject(pClazz,constructID,1,name);
jmethodID printId = env->GetMethodID(pClazz,"print","()V");
if(printId == NULL){
return;
}
env->CallVoidMethod(person,printId);
// 4. 释放资源
env->DeleteLocalRef(name);
env->DeleteLocalRef(pClazz);
env->DeleteLocalRef(person);
复制代码
JNI 分为三种引用:
上面的代码片断中最后都会有释放资源的代码,这是 c/c++ 编程的良好习惯,对于不一样 JNI 引用有不一样的释放方式。
JNI 函数返回的全部 Java 对象都是局部引用,好比上面调用的 NewObject/FindClass/NewStringUTF 等等都是局部引用。
有了自动释放以后为何还须要手动释放呢?主要考虑一下场景:
因此咱们应该养成手动释放本地引用的好习惯。
在调用 GetStringUTFChars 函数从 JVM 内部获取一个字符串以后,JVM 内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改。即然有内存分配,用完以后立刻释放是一个编程的好习惯。经过调用ReleaseStringUTFChars 函数通知 JVM 这块内存已经不使用了。
JNI 容许程序员从局部引用建立全局引用:
static jstring globalStr;
if(globalStr == NULL){
jstring str = env->NewStringUTF("C++");
// 从局部变量 str 建立一个全局变量
globalStr = static_cast<jstring>(env->NewGlobalRef(str));
//局部能够释放,由于有了一个全局引用使用str,局部str也不会使用了
env->DeleteLocalRef(str);
}
复制代码
全局引用在显式释放以前保持有效,能够经过 DeleteGlobalRef 来手动删除全局引用调用。
与全局引用相似,弱引用能够跨方法、线程使用。与全局引用不一样的是,弱引用不会阻止GC回收它所指向的VM内部的对象
因此在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,仍是指向一个已经被GC的对象
static jclass globalClazz = NULL;
//对于弱引用 若是引用的对象被回收返回 true,不然为false
//对于局部和全局引用则判断是否引用java的null对象
jboolean isEqual = env->IsSameObject(globalClazz, NULL);
if (globalClazz == NULL || isEqual) {
jclass clazz = env->GetObjectClass(instance);
globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
env->DeleteLocalRef(clazz);
}
复制代码
删除使用 DeleteWeakGlobalRef
局部变量只能在当前线程使用,而全局引用能够跨方法、跨线程使用,直到它被手动释放才会失效。
在 android 中有两种方式加载动态库:
好比下面代码会报错,在 java.library.path 下找不到 hello
static{
System.loadLibrary("Hello");
}
复制代码
可使用下面代码打印出 java.library.path ,而且吧 hello 拷贝到改路径下:
public static void main(String[] args){
System.out.println(System.getProperty("java.library.path"));
}
复制代码
调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,若是存在此函数则调用。 JNI_OnLoad 必须返回 JNI 的版本,好比 JNI_VERSION_1_六、JNI_VERSION_1_8。
JNI 匹配对应的 java 方法有两种方式:
静态注册的名字须要包名,太长了,可使用动态注册来缩短方法名。
好比咱们再 Java 中有两个 native 方法:
public class MainActivity extends AppCompatActivity {
public native void dynamicJavaFunc1();
public native int dynamicJavaFunc2(int i);
}
复制代码
在 native 代码中,咱们不使用静态注册,而使用动态注册
void dynamicNativeFunc1(){
LOGE("调用了 dynamicJavaFunc1");
}
// 若是方法带有参数,前面要加上 JNIEnv *env, jobject thisz
jint dynamicNativeFunc2(JNIEnv *env, jobject thisz,jint i){
LOGE("调用了 dynamicTest2,参数是:%d",i);
return 66;
}
// 须要动态注册的方法数组
static const JNINativeMethod methods[] = {
{"dynamicJavaFunc1","()V",(void*)dynamicNativeFunc1},
{"dynamicJavaFunc2","(I)I",(int*)dynamicNativeFunc2},
};
// 须要动态注册native方法的类名
static const char *mClassName = "com/wangzhen/jnitutorial/MainActivity";
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
// 1. 获取 JNIEnv,这个地方要注意第一个参数是个二级指针
int result = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);
// 2. 是否获取成功
if(result != JNI_OK){
LOGE("获取 env 失败");
return JNI_VERSION_1_6;
}
// 3. 注册方法
jclass classMainActivity = env->FindClass(mClassName);
// sizeof(methods)/ sizeof(JNINativeMethod)
result = env->RegisterNatives(classMainActivity,methods, 2);
if(result != JNI_OK){
LOGE("注册方法失败")
return JNI_VERSION_1_2;
}
return JNI_VERSION_1_6;
}
复制代码
这样咱们再 MainActivity 中调用 dynamicJavaFunc1 方法就会调用 native 中的 dynamicNativeFunc1 方法。
前面介绍过 JNIEnv* 是和线程相关的,那么若是在 c++ 中新建一个线程A,在线程A 中能够直接使用 JNIEnv* 吗? 答案是否认的,若是想在 native 线程中使用 JNIEnv* 须要使用 JVM 的 AttachCurrentThread 方法进行绑定:
JavaVM *_vm;
jint JNI_OnLoad(JavaVM* vm, void* reserved){
_vm = vm;
return JNI_VERSION_1_6;
}
void* threadTask(void* args){
JNIEnv *env;
jint result = _vm->AttachCurrentThread(&env,0);
if (result != JNI_OK){
return 0;
}
// ...
// 线程 task 执行完后不要忘记分离
_vm->DetachCurrentThread();
}
extern "C"
JNIEXPORT void JNICALL Java_com_wangzhen_jnitutorial_MainActivity_nativeThreadTest(JNIEnv *env, jobject thiz) {
pthread_t pid;
pthread_create(&pid,0,threadTask,0);
}
复制代码
在一个平台上编译出另外一个平台上能够执行的二级制文件的过程叫作交叉编译。好比在 MacOS 上编译出 android 上可用的库文件。 若是想要编译出能够在 android 平台上运行的库文件就须要使用 ndk。
linux 平台上的库文件分为两种:
Android 原生开发套件 (NDK):这套工具使您能在 Android 应用中使用 C 和 C++ 代码。 CMake:一款外部编译工具,可与 Gradle 搭配使用来编译原生库。若是您只计划使用 ndk-build,则不须要此组件。 LLDB:Android Studio 用于调试原生代码的调试程序。
原生开发套件 (NDK) 是一套工具,使您可以在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库。 咱们能够在 sdk/ndk-bundle 中查看 ndk 的目录结构,下面列举出三个重要的成员:
ndk 为何要提供多平台呢? 不一样的 Android 设备使用不一样的 CPU,而不一样的 CPU 支持不一样的指令集。更具体的内容参考官方文档
在 ndk 目录下的 toolchains 下有多个平台的编译工具,好比在 /arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin 下能够找到 arm-linux-androideabi-gcc 执行文件,利用 ndk 的这个 gcc 能够编译出在 android(arm 架构) 上运行的动态库:
arm-linux-androideabi-gcc -fPIC -shared test.c -o libtest.so
复制代码
参数含义 -fPIC: 产生与位置无关代码 -shared:编译动态库,若是去掉表明静态库 test.c:须要编译的 c 文件 -o:输出 libtest.so:库文件名
独立工具链 版本比较新的 ndk 下已经找不到 gcc 了,若是想用的话须要参考独立工具链。 好比执行
$NDK/build/tools/make_standalone_toolchain.py --arch arm --api 21 --install-dir/$yourDir
能够产生 arm 的独立工具链
$NDK 表明 ndk 的绝对路径, $yourDir 表明输出文件路径
当源文件不少的时候,手动编译既麻烦又容易出错,此时出现了 makefile 编译。
makefile 就是“自动化编译”:一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件须要先编译,哪些文件须要后编译,如何进行连接等等操做。 Android 使用 Android.mk 文件来配置 makefile,下面是一个最简单的 Android.mk:
# 源文件在的位置。宏函数 my-dir 返回当前目录(包含 Android.mk 文件自己的目录)的路径。
LOCAL_PATH := $(call my-dir)
# 引入其余makefile文件。CLEAR_VARS 变量指向特殊 GNU Makefile,可为您清除许多 LOCAL_XXX 变量
# 不会清理 LOCAL_PATH 变量
include $(CLEAR_VARS)
# 指定库名称,若是模块名称的开头已经是 lib,则构建系统不会附加额外的前缀 lib;而是按原样采用模块名称,并添加 .so 扩展名。
LOCAL_MODULE := hello
# 包含要构建到模块中的 C 和/或 C++ 源文件列表 以空格分开
LOCAL_SRC_FILES := hello.c
# 构建动态库
include $(BUILD_SHARED_LIBRARY)
复制代码
咱们配置好了 Android.mk 文件后如何告诉编译器这是咱们的配置文件呢? 这时候须要在 app/build.gradle 文件中进行相关的配置:
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
defaultConfig {
...
// 应该将源文件编译成几个 CPU so
externalNativeBuild{
ndkBuild{
abiFilters 'x86','armeabi-v7a'
}
}
// 须要打包进 apk 几种 so
ndk {
abiFilters 'x86','armeabi-v7a'
}
}
// 配置 native 构建脚本位置
externalNativeBuild{
ndkBuild{
path "src/main/jni/Android.mk"
}
}
// 指定 ndk 版本
ndkVersion "20.0.5594570"
...
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
...
}
复制代码
Google 推荐开发者使用 cmake 来代替 makefile 进行交叉编译了,makefile 在引入第三方预编译好的 so 的时候会在 android 6.0 版本先后有些差别,好比在 6.0 以前须要手动 System.loadLibrary 第三方 so,在以后则不须要。 关于 makefile 还有不少配置参数,这里不在讲解,更多参考官方文档。
在 6.0 如下,System.loadLibrary 不会自动加载 so 内部依赖的 so 在 6.0 如下,System.loadLibrary 会自动加载 so 内部依赖的 so 因此使用 mk 的话须要作版本兼容
CMake是一个跨平台的构建工具,能够用简单的语句来描述全部平台的安装(编译过程)。可以输出各类各样的makefile或者project文件。Cmake 并不直接建构出最终的软件,而是产生其余工具的脚本(如Makefile ),而后再依这个工具的构建方式使用。 Android Studio利用CMake生成的是ninja,ninja是一个小型的关注速度的构建系统。咱们不须要关心ninja的脚本,知道怎么配置cmake就能够了。
Make的脚本名默认是CMakeLists.txt,当咱们用 android studio new project 勾选 include c/c++ 的时候,会默认生成如下文件:
|- app |-- src |--- main |---- cpp |----- CMakeLists.txt |----- native-lib.cpp
先来看一下 CMakeLists.txt:
# 设置 cmake 最小支持版本
cmake_minimum_required(VERSION 3.4.1)
# 建立一个库
add_library( # 库名称,好比如今会生成 native-lib.so
native-lib
# 设置是动态库(SHARED)仍是静态库(STATIC)
SHARED
# 设置源文件的相对路径
native-lib.cpp )
# 搜索并指定预构建库并将路径存储为变量。
# NDK中已经有一部分预构建库(好比 log),而且ndk库已是被配置为cmake搜索路径的一部分
# 能够不写 直接在 target_link_libraries 写上log
find_library( # 设置路径变量的名称
log-lib
# 指定要CMake定位的NDK库的名称
log )
# 指定CMake应连接到目标库的库。你能够连接多个库,例如构建脚本、预构建的第三方库或系统库。
target_link_libraries( # Specifies the target library.
native-lib
${log-lib} )
复制代码
咱们再来看下 gradle 中的配置:
android {
compileSdkVersion 29
buildToolsVersion "29.0.1"
defaultConfig {
...
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 设置编译版本
externalNativeBuild {
cmake {
abiFilters "armeabi-v7a","x86"
}
}
}
...
// 设置配置文件路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
复制代码
这样在编译产物中就能够看到两个版本的 so:
好比咱们添加一个 extra.h:
#ifndef JNITUTORIAL_EXTRA_H
#define JNITUTORIAL_EXTRA_H
const char * getString(){
return "string from extra";
}
#endif //JNITUTORIAL_EXTRA_H
复制代码
而后在 native-lib.cpp 中使用:
#include <jni.h>
#include <string>
#include <android/log.h>
#include "extra.h"
// __VA_ARGS__ 表明... 可变参数
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);
extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// std::string hello = "Hello from new C++";
std::string hello = getString();
return env->NewStringUTF(hello.c_str());
}
复制代码
源文件已经写好了,这时候要修改一下 CMakeLists.txt:
add_library(
native-lib
SHARED
native-lib.cpp
// 添加 extra.h
extra.h )
#==================================
# 固然若是源文件很是多,而且可能在不一样的文件夹下,像上面明确的引入各个文件就会很是繁琐,此时能够批量引入
# 若是文件太多,能够批量加载,下面时将 cpp 文件夹下全部的源文件定义成了 SOURCE(后面的源文件使用相对路径)
file(GLOB SOURCE *.cpp *.h)
add_library(
native-lib
SHARED
# 引入 SOURCE 下的全部源文件
${SOURCE}
)
复制代码
那么如何添加第三方的动态库呢?
动态库必须放到 src/main/jniLibs/xxabi 目录下才能被打包到 apk 中,这里用的是虚拟机,因此用的是 x86 平台,因此咱们放置一个第三方库 libexternal.so 到 src/main/jniLibs/x86 下面。 libexternal.so 中只有一个 hello.c ,里面只有一个方法:
const char * getExternalString(){
return "string from external";
}
复制代码
这里将 CMakeLists.txt 从新放到了 app 目录下,和 src 同级,这样方便找到 jniLibs 下面的库。 因此别忘了修改 gradle
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.10.2"
}
}
复制代码
cmake_minimum_required(VERSION 3.4.1)
# 若是文件太多,能够批量加载,下面时将 cpp 文件夹下全部的源文件定义成了 SOURCE(后面的源文件使用相对路径)
file(GLOB SOURCE src/main/cpp/*.cpp src/main/cpp/*.h)
add_library(
native-lib
SHARED
# 引入 SOURCE 下的全部源文件
${SOURCE}
)
set_target_properties(native-lib PROPERTIES LINKER_LANGUAGE CXX)
#add_library( # Sets the name of the library.
# native-lib
#
# # Sets the library as a shared library.
# SHARED
#
# # Provides a relative path to your source file(s).
# native-lib.cpp
# extra.h )
find_library(
log-lib
log )
# ==================引入外部 so===================
message("ANDROID_ABI : ${ANDROID_ABI}")
message("CMAKE_SOURCE_DIR : ${CMAKE_SOURCE_DIR}")
message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}")
# external 表明第三方 so - libexternal.so
# SHARED 表明动态库,静态库是 STATIC;
# IMPORTED: 表示是以导入的形式添加进来(预编译库)
add_library(external SHARED IMPORTED)
#设置 external 的 导入路径(IMPORTED_LOCATION) 属性,不可使用相对路径
# CMAKE_SOURCE_DIR: 当前cmakelists.txt的路径 (cmake工具内置的)
# android cmake 内置的 ANDROID_ABI : 当前须要编译的cpu架构
set_target_properties(external PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86/libexternal.so)
#set_target_properties(external PROPERTIES LINKER_LANGUAGE CXX)
# ==================引入外部 so end===================
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib}
# 连接第三方 so
external
)
复制代码
#include <jni.h>
#include <string>
#include <android/log.h>
#include "extra.h"
// __VA_ARGS__ 表明... 可变参数
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);
extern "C"{
const char * getExternalString();
}
extern "C" JNIEXPORT jstring JNICALL Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// std::string hello = "Hello from new C++";
// std::string hello = getString();
// 这里调用了第三方库的方法
std::string hello = getExternalString();
return env->NewStringUTF(hello.c_str());
}
复制代码
除了上面的方式还能够给 CMake 增长一个查找 so 的 path,当咱们 target_link_libraries external 的时候就会在该路径下找到。
#=====================引入外部 so 的第二种方式===============================
# 直接给 cmake 在添加一个查找路径,在这个路径下能够找到 external
# CMAKE_C_FLAGS 表明使用 c 编译, CMAKE_CXX_FLAGS 表明 c++
# set 方法 定义一个变量 CMAKE_C_FLAGS = "${CMAKE_C_FLAGS} XXXX"
# -L: 库的查找路径 libexternal.so
#set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI} ")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/x86")
#=====================引入外部 so 的第二种方式 end===============================
复制代码