android使用ndk

慕课网

基本知识

交叉编译

简单地说,就是在一个平台上生成另一个平台上可执行的代码
比如在windows生成Android的arm平台的可执行代码

jni是什么

Java native interface(JNI)标准成为Java平台的一部分,它允许Java代码和其他语言写的代码进行交互

jni的实现流程

jni的实现流程

链接库

  • 静态链接库
  • 动态链接库

    静态链接库

    静态链接库就是把所有用到的依赖都包含进去,比如写一个c语言helloworld,他会把stdio.h的所有都包含进去
  • 优点:这个库拿到各个地方都能用
  • 缺点:库体积非常大,只需要一个print但是吧stdio的所有内容都包含进去了

    动态链接库

    动态链接库是只包含自己写的那部分东西,别的都不包含,在别的地方用的时候动态的去找需要的依赖
  • 优点:文件体积小,只包含了printf
  • 缺点:可能出现找不到依赖的情况
    ndk的链接库大多数是动态链接库

NDK知识

android studio现在已经默认支持ndk,只需要创建项目的时候勾选include c++即可,运行的时候没有ndk和cmake就按照提示下载即可
我使用的环境是Android studio3.1

默认生成的文件

创建一个支持c++的项目以后会给一个cpp文件和一个默认的Mainactivity的Java文件

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <jni.h>
#include <string>
using namespace std;

extern "C" JNIEXPORT jstring

JNICALL
Java_cn_xwmdream_ndktest_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

看上面的那个方法名Java_cn_xwmdream_ndktest_MainActivity_stringFromJNI
意思应该是Java语言,包名是cn.xwmdream.ndktest类名是MainActivity,方法名是stringFromJNI
他有两个参数,还不懂什么意思,他创建了一个string类型的hello,赋值为’hello from c++’,然后调用NewStringUTF方法把c语言的字符串转化成Java能用的字符串,返回给Java程序
上面的 extern “C” JNIEXPORT jstring应该是指定这个方法的返回值是一个jstring类型

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {
//静态代码块,说明要用哪个文件,这里调用的是native-lib.cpp文件
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.sample_text);
//调用这个方法,输出hello from c++,写到那个textView上
tv.setText(stringFromJNI());
}
//声明一个native类型的方法,静态代码块说的那个cpp文件里面的方法对应
public native String stringFromJNI();
}

JNI交互处理

jni的一些基本知识

基本类型
基本类型和本地等效类型表:

Java类型 对应的jni类型 说明
boolean jboolean 无符号,8位
byte jbyte 无符号,8位
char jchar 无符号,16位
short jshort 有符号,16位
int jint 有符号,32位
long jlong 有符号,64位
float jfloat 32位
double jdouble 64位
void void N/A
String jstring 我自己加的,不知道

处理方式就是Java类型<->jni类型<->c语言类型
上面的例子就是c语言的char *(string.c_str())类型通过NewStringUTF转化成jni的jstring类型,然后传递到Java代码里通过String类型输出出来

Java和native层进行字符串交互处理

java向c语言传递一个字符串

还是上面的那个例子改改

native-lib.cpp

1
2
3
4
5
6
7
8
9
Java_cn_xwmdream_ndktest_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */jclas, jstring path) {
const char *c = env->GetStringUTFChars(path, NULL);
char d[50];
strcpy(d,c);
d[0]='7';
//对指针进行释放
env->ReleaseStringUTFChars(path,c);
return env->NewStringUTF(d);
}

参数由原来的两个增加了一个jstring类型,这个值就是Java层传进来的一个String类型的参数,然后通过GetStringUTFChars(jstring,jboolean)把jstring类型的参数转化成一个char类型的指针,通过strcpy复制给d,然后改第一位字符为’7’然后通过上面的把d给返回回去,其中用ReleaseStringUTFChars方法释放资源c

在调用 GetStringUTFChars 函数从 JVM 内部获取一个字符串之后,JVM 内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用ReleaseStringUTFChars 函数通知 JVM 这块内存已经不使用了,你可以清除了。注意:这两个函数是配对使用的,用了 GetXXX 就必须调用 ReleaseXXX,而且这两个函数的命名也有规律,除了前面的 Get 和 Release 之外,后面的都一样。
MainActivity.java

1
2
3
4
//声明方法中增加了一个String类型的参数,对应cpp中的jstring
public native String stringFomJNI(String a);
//调用函数
tv.setText(stringFromJNI("hello"));

结果就是在屏幕上显示了”7ello”

Java向jni传递一个数组

第一种方法是生成native层数组的拷贝

首先Java层先声明一个返回int[]的一个native的方法,然后调用它

1
2
3
4
5
6
7
//声明这个方法
public native int[] updatearrayJNI(int[] a);
//调用,并打印他的值
int[] a= updatearrayJNI(new int[]{1,2,3,4,5});
for(int i=0;i<a.length;i++) {
Log.d(TAG, "onCreate: "+a[i]);
}

native层实现一个jintArray返回值的方法

1
2
3
4
5
6
7
8
9
10
11
12
extern "C" JNIEXPORT jintArray

JNICALL
Java_cn_xwmdream_ndktest_MainActivity_updatearrayJNI(JNIEnv *env, jobject /* this */jclas, jintArray array) {
jint nativeArray[5];
env->GetIntArrayRegion(array,0,5,nativeArray);
for (int i = 0; i < 5; ++i) {
nativeArray[i]+=6;
}
env->SetIntArrayRegion(array,0,5,nativeArray);
return array;
}

通过GetIntArrayRegion把从Java层传递进来的array的[0,5)赋值给nativeArray这个创建的数组,然后对nativeArray进行运算,最后通过SetIntArrayRegion把nativeArray的值放回array的[0,5)的位置,结果是Java收到的数组是7,8,9,10,11

第二种方法是直接调用数组指针操作

java层和上面一样
native层先获取一个int指针,获取他的长度,然后操作指针,最后释放指针,返回数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C" JNIEXPORT jintArray

JNICALL
Java_cn_xwmdream_ndktest_MainActivity_updatearrayJNI(JNIEnv *env, jobject /* this */jclas, jintArray array) {
//获取一个int型的指针
jint* data=env->GetIntArrayElements(array,NULL);

jsize len = env->GetArrayLength(array);
for(int i=0;i<len;i++){
data[i]+=3;
}
//释放data指针
env->ReleaseIntArrayElements(array,data,0);
return array;
}

data是一个int类型的指针,通过GetIntArrayElements获取传入array的指针,jsize就是一个jint,通过GetArrayLength获取到传入数组的长度,然后操作指针,最后通过ReleaseIntArrayElements释放data指针,返回数组
这样在Java层返回的结果是4,5,6,7,8

更多jni提供的方法在jni.h这个头文件里可以看到

自己创建一个cpp文件

  1. 在cpp文件夹下创建一个c或者cpp文件写上代码

    test.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <jni.h>
    #include <string>
    using namespace std;

    extern "C" JNIEXPORT jstring

    JNICALL
    Java_cn_xwmdream_ndktest_MainActivity_stringFromJNI(
    JNIEnv *env,
    jobject /* this */) {
    return env->NewStringUTF("hello c++");
    }

这个是默认生成的一个代码,函数名不用说了

  1. 在app/CMakeLists.txt文件中添加链接库
    1
    2
    3
    4
    5
    6
    7
    8
    add_library( # Sets the name of the library.
    testt

    # Sets the library as a shared library.
    SHARED

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

第二行是动态链接的名称,也就是Java中 System.loadLibrary(“testt”)的名称
最后那个是文件地址

android studio添加log

首先先在CMakeLists.txt文件中添加

1
2
3
4
5
6
target_link_libraries( # Specifies the target library.
native-lib

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

上面那个native-lib是add_library中指定的那个名字,下面是他引用的ndk链接库,这句话就是引入lib的链接库
然后在要调用的cpp文件中引入android/log.h

1
#include <android/log.h>

然后就能调用log了,有五个级别的log,和Java层一样,写法是

1
2
3
4
5
6
7
8
__android_log_print(ANDROID_LOG_ERROR,"JNITag","content");

//其他的等级
ANDROID_LOG_VERBOSE
ANDROID_LOG_DEBUG
ANDROID_LOG_INFO
ANDROID_LOG_WARN
ANDROID_LOG_ERROR

define优化

1
2
3
4
//define指定
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR,"JNITag",__VA_ARGS__))
//此时只用调用LOGE就行,tag是在define中指定
LOGE("你好");

jni调用Java静态资源

jni调用Java静态方法

对于JNI方法名,数据类型和方法签名的一些认识

首先写一个Java的静态方法

1
2
3
public static void h(String a){
Log.e(TAG, "h:"+a);
}

c++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extern "C"
JNIEXPORT void JNICALL
Java_cn_xwmdream_ndktest_MainActivity_testStaticMethod(JNIEnv *env, jobject instance) {
//先获取那个类名,就是包名.类名,只不过点都改成除号
jclass cls_hello = env->FindClass("cn/xwmdream/ndktest/MainActivity");
if(cls_hello==NULL){
LOGE("类获取失败");
} else{
//获取一个cls_hello指定那个类中一个名字为h,签名是(Ljava/lang/String;)V的静态方法,签名看前面的那个超链接
jmethodID mth_static_method=env->GetStaticMethodID(cls_hello,"h","(Ljava/lang/String;)V");
if(mth_static_method==NULL){
LOGE("静态方法获取错误");
}else{
//创建一个String类型的参数,用于传回Java层的h方法的string类型的参数
jstring ab=env->NewStringUTF("i m c");
//调用Java的静态方法,是cls_hello指定的类中的mth_static方法,参数是ab
env->CallStaticVoidMethod(cls_hello,mth_static_method,ab);
//释放资源ab
env->DeleteLocalRef(ab);
}
}
//资源回收
env->DeleteLocalRef(cls_hello);
}

结果是

cn.xwmdream.ndktest E/JNITag: h:i m c
c++代码中先使用findclass通过Java类的包名获取到类,然后通过getstaticmethodid的方法获取那个类中名字为h,签名为(Ljava/lang/String;)V的方法,签名的使用看上面引用的超链接,都获取到了以后,因为Java的h方法需要传递一个string类型的参数,所以要先创建一个jstring的对象,然后用env的callstaticvoidmethod方法调用Java层的那个静态没有返回值的方法,参数是刚创建的jstring对象。

jni修改Java静态属性

定义一个Java静态属性

1
public static String name="aaa";

c++和上面调用静态方法类似,都是先获取类,然后通过签名获取静态实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//先获取那个类名,就是包名.类名,只不过点都改成除号
jclass cls_hello = env->FindClass("cn/xwmdream/ndktest/MainActivity");
if(cls_hello==NULL){
LOGE("类获取失败");
} else{
//找到cls_hello中一个属性名为name的,类型签名是Ljava/lang/String;的一个静态成员属性
jfieldID field_name = env->GetStaticFieldID(cls_hello,"name","Ljava/lang/String;");
if(field_name==NULL)
{
LOGE("静态属性获取失败");
}else{
jstring n = env->NewStringUTF("bbb");
//设置cls_hello类的field_name的静态属性值为n(bbb)
env->SetStaticObjectField(cls_hello,field_name,n);
}
}
//资源回收
env->DeleteLocalRef(cls_hello);

以上代码就能通过c++把Java的那个静态属性name的值从aaa变成bbb

jni调用Java实例资源

jni调用Java实例方法

先创建一个hello类,然后写一个sayhello方法

1
2
3
public void sayHello(String a){
Log.e(TAG, "sayHello: "+a);
}

c++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//先获取那个类名,就是包名.类名,只不过点都改成除号,获取hello类
jclass cls_hello = env->FindClass("cn/xwmdream/ndktest/Hello");
if (cls_hello == NULL) {
LOGE("类获取失败");
return;
}
//获取sayhello方法
jmethodID sayhello = env->GetMethodID(cls_hello, "sayHello", "(Ljava/lang/String;)V");
if (sayhello == NULL) {
LOGE("实例方法获取失败");
return;
}

//获取Hello的构造方法,第二个参数是构造方法的标识,第三个参数是构造方法的签名
jmethodID method_gouzao = env->GetMethodID(cls_hello, "<init>", "()V");
if(method_gouzao==NULL){
LOGE("构造方法获取失败");
return;
}

//通过类和构造方法创建出一个对象,第三个参数应该是构造方法传入的参数
jobject hello_obj = env->NewObject(cls_hello, method_gouzao, NULL);
if (hello_obj == NULL) {
LOGE("new对象失败");
return;
}

//因为Java的sayhello中有一个参数,创建一个jstring当作传入的参数
jstring message = env->NewStringUTF("hello abc");

//调用hello_obj的sayhello方法,参数是上面的jstring
env->CallVoidMethod(hello_obj, sayhello, message);
//回收资源
env->DeleteLocalRef(message);
env->DeleteLocalRef(hello_obj);
env->DeleteLocalRef(cls_hello);

11-14 12:54:48.721 21647-21647/? E/JNITag: sayHello: hello abc

首先先获取类,然后获取sayhello这个实例方法,然后获取Hello的构造方法,通过构造方法构造一个对象,然后调用sayhello方法

jni修改实例属性

Hello类增加一个属性

1
public String num="100";

1
2
3
4
5
6
7
8
9
10
11
12
jfieldID field = env->GetFieldID(cls_hello,"num","Ljava/lang/String;");
if(field==NULL){
LOGE("成员属性获取失败");
}
//同上获取创建对象......

//创建一个新的值
jstring newvalue = env->NewStringUTF("50");

//把hello_obj的field属性的值改成newvalue
env->SetObjectField(hello_obj, field, newvalue);
//.....调用成员方法

以上就能修改实例的属性

jni异常处理

  • 如果jni调用Java程序出现异常的话ndk还是可以继续执行下面的程序
    可以在调用可能出现异常Java方法的语句下面写一下方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ///env->CallVoidMethod(hello_obj, sayhello, message);调用了可能出现异常的Java方法
    if(env->ExceptionCheck()){
    env->ExceptionDescribe();
    env->ExceptionClear();
    LOGE("出现了异常");
    //抛出一个异常
    jclass cls_exception = env->FindClass("java/lang/Exception");
    env->ThrowNew(cls_exception,"call java method ndk error");
    return;
    }