基本知识
交叉编译
简单地说,就是在一个平台上生成另一个平台上可执行的代码
比如在windows生成Android的arm平台的可执行代码
jni是什么
Java native interface(JNI)标准成为Java平台的一部分,它允许Java代码和其他语言写的代码进行交互
jni的实现流程
链接库
- 优点:这个库拿到各个地方都能用
- 缺点:库体积非常大,只需要一个print但是吧stdio的所有内容都包含进去了
动态链接库
动态链接库是只包含自己写的那部分东西,别的都不包含,在别的地方用的时候动态的去找需要的依赖 - 优点:文件体积小,只包含了printf
- 缺点:可能出现找不到依赖的情况
ndk的链接库大多数是动态链接库
NDK知识
android studio现在已经默认支持ndk,只需要创建项目的时候勾选include c++即可,运行的时候没有ndk和cmake就按照提示下载即可
我使用的环境是Android studio3.1
默认生成的文件
创建一个支持c++的项目以后会给一个cpp文件和一个默认的Mainactivity的Java文件
native-lib.cpp
1 |
|
看上面的那个方法名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 | public class MainActivity extends AppCompatActivity { |
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 | Java_cn_xwmdream_ndktest_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */jclas, jstring path) { |
参数由原来的两个增加了一个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 | //声明方法中增加了一个String类型的参数,对应cpp中的jstring |
结果就是在屏幕上显示了”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
12extern "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
15extern "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文件
- 在cpp文件夹下创建一个c或者cpp文件写上代码
test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
using namespace std;
extern "C" JNIEXPORT jstring
JNICALL
Java_cn_xwmdream_ndktest_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
return env->NewStringUTF("hello c++");
}
这个是默认生成的一个代码,函数名不用说了
- 在app/CMakeLists.txt文件中添加链接库
1
2
3
4
5
6
7
8add_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
6target_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.h1
然后就能调用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指定
//此时只用调用LOGE就行,tag是在define中指定
LOGE("你好");
jni调用Java静态资源
jni调用Java静态方法
首先写一个Java的静态方法1
2
3public static void h(String a){
Log.e(TAG, "h:"+a);
}
c++代码
1 | extern "C" |
结果是
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
3public void sayHello(String a){
Log.e(TAG, "sayHello: "+a);
}
c++
1 | //先获取那个类名,就是包名.类名,只不过点都改成除号,获取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 | jfieldID field = env->GetFieldID(cls_hello,"num","Ljava/lang/String;"); |
以上就能修改实例的属性
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;
}