1、JNI 使用场景
(1)、提高代码安全性。因为.so文件反编译困难。(加密一些算法等)
(2)、方便使用已经存在的c/c++库。可以加载 .dll 和 .so 格式的动态库。
(3)、提高某些特定情况下的执行效率。(c/c++的效率比java高。不过并不能明显提升android程序的性能)
2、JNI语法
#ifdef __cplusplus
extern "C"
#endif
// 如果是c++代码,也统一用c的语法去编译。
#ifdef __cplusplus
extern "}"
#endif
extern "C" JNIEXPORT void JNICALL
Java_com_xxx_nPlay(JNIEnv *env, jobject instance, jstring url_) {
...
}
// JNIEXPORT 标记为该方法可以被外部调用
3、JNIEnv
JNIEnv是 JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv;
每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv;
但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。
//JNIEnv 作用
(1)、调用Java函数
JNIEnv代表Java运行环境,可以使用JNIEnv调用Java中的代码;
(2)、操作Java对象
Java对象传入JNI层就是Jobject对象, 需要使用JNIEnv来操作这个Java对象;
4、JNI的数据类型
java基本类型 JNI类型 bit -
boolean jboolean 8
byte jbyte 8
char jchar 16
short jshort 16
void void 无
int jint 32
long jlong 64
float jfloat 32
double jdouble 64
java引用类型 JNI类型
Object jobject
Class jclass
String jstring
Object[ ] jobjectArray
boolean[ ] jbooleanArray
byte[ ] jbyteArray
char[ ] jcharArray
short[ ] jshortArray
int[ ] jintArray
long[ ] jlongArray
float[ ] jfloatArray
double[ ] jdoubleArray
Throwable jthrowable
5、JNI签名
java中,对于某一个对象来说, 可以用过方法名和参数 就可以确定唯一的一个方法。
在JNI中,通过函数名和 签名信息 可以确定唯一的方法。
(1)、基本数据签名的规则
java类型 JNI类型
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
(2)、签名详解
a、类的签名(注意末尾的分号)
L+包名+类名+;
java.lang.String -> Ljava/lang/String;
b、// 对象签名
跟它所属的类的签名一样。
c、 一维数组签名
格式: [类型
double[ ] -> [ D
d、多维数组签名
格式: n个 [ + 类型签名
int[ ] [ ] -> [[I
e、方法签名 (跟方法名无关)
格式: (参数类型签名) + 返回值类型签名
boolean fun( int a , double b , int[ ] c) -> (ID[I)Z
void fun(int i) -> (I)V
6、Java 和 C 函数的映射表
{ java中函数名字 , 参数和返回值 , 对应的c函数}
{
"setUsbLinkVideoData",
"(Ljava/nio/ByteBuffer;I)V",
(void*)com_media_ffmpeg_FFMpegPlayer_setUsbLinkVideoData
}
7、常用函数
(1)、FindClass
(2)、GetMethodID
(3)、CallIntMethod 调用返回值为Int的java方法
(4)、CallVoidMethod
(5)、GetObjectClass
(6)、GetStaticMethodID
(7)、CallStaticIntMethod
(8)、NewObject
(9)、CallVoidMethod
(10)、CallStaticObjectMethod
(11)、NewStringUTF
(12)、GetStringUTFChars 得到一个UTF-8编码的字符串
(13)、GetStringChars 得到UTF-16编码的字符串
(14)、ReleaseStringUTFChars
(15)、JNI_OnLoad so库加载时调用。如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认使用最老的JNI 1.1版本。
(16)、NewGlobalRef 新建全局变量
(17)、DeleteGlobalRef
(18)、AttachCurrentThread 当前线程获取JNI环境
(19)、DetachCurrentThread
(20)、SetByteArrayRegion 将本地的数组数据拷贝到了 Java 端的数组中
(21)、CallObjectMethod
(21)、env->ThrowNew 抛出异常信息
jclass exceptionClazz = env->FindClass("java/lang/RuntimeException");
env->ThrowNew(exceptionClazz, "自错误信息");
(22)、reinterpret_cast 类型强制转换
MyParcel* parcel = reinterpret_cast <MyParcel*>(nativePtr);
(23)、GetStaticFieldID
(24)、SetStaticFieldID
(25)、DeleteLocalRef
8、JNI_OnLoad(...)作用
System.loadLibrary()函数时,会立即先呼叫JNI_OnLoad()
JavaVM* java_vm = NULL;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv *env;
if (vm->GetEnv( (void**) &env, JNI_VERSION_1_4) != JNI_OK)
{
return JNI_ERR ;
}
java_vm = vm;
return JNI_VERSION_1_4;
}
用途有两个:
(1)、告诉VM此C组件使用那一个JNI版本。
如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。
由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,
例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。
(2)、由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),
所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization)
9、打印日志
#include <android/log.h>
#define LogTag "WK_LOGER"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LogTag ,__VA_ARGS__);
10、缓存策略
直接用全局变量也可以(但是可能会被其他类的程序修改),所以建议用静态static较安全。
Java_com_xxx_staticLocalCache(JNIEnv *env, jclass jclz, jstring name) {
static jfieldID j_fid = NULL; //局部缓存
if (j_fid == NULL) {
j_fid = (*env)->GetStaticFieldID(env, jclz, "name", "Ljava/lang/String;")
}
}
static jfieldID j_fid = NULL;// 全局缓存
Java_com_xxx_initStaticCache (JNIEnv *env, jclass jclz){
j_fid = (*env)->GetStaticFieldID(env, jclz, "name", "Ljava/lang/String;");
}
// 这也叫缓存?乱七八糟。
11、java调用native
注意文件夹路径要和native中的签名一样
static {
System.loadLibrary("music-player");
}
private native void nPlay(String url);
12、native 调用 java
(1)、C++主线程调用Java方法
a、根据jobject获取jclass
jclass jlz = env->GetObjectClass(instance);//jobject instance
b、获取 jmethodID
jmethodID jmid_error= jniEnv->GetMethodID(jlz, "onError", "(ILjava/lang/String;)V");
//注意:方法签名的规则 : (参数类型签名) + 返回值类型签名
//要注意参数的签名,否则会报错 找不到对应的方法
c、调用方法
jniEnv->CallVoidMethod(jobj, jmid_error, code, jmsg);// jobject,jmethodID , 参数...
(2)、C++子线程调用Java方法
注意:JniEnv是线程相关的,所以子线程中不能使用创建线程的JniEnv;
JVM是进程相关的,可以通过JVM来获取当前线程的JniEnv,然后才能调用Java的方法。
JNIEnv *jniEnv;
if (javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK)
{
return;
}
// 如果获取成功,AttachCurrentThread 就会返回一个属于当前线程的JNIEnv指针。
jniEnv->CallVoidMethod(...);
//执行相关操作...
javaVM->DetachCurrentThread();
13、POSIX线程
Android NDK的线程是通过POSIX标准实现的,所以也叫POSIX线程。
POSIX 使用
//头文件
#include <pthread.h>
// 创建线程锁对象
pthread_mutex_t mutex;
// 用来申明一个线程对象
pthread_t thread
//创建线程条件对象
pthread_cond_t cond;
pthread_attr_t:线程属性
Thread-local storage(或者以Pthreads术语,称作线程特有数据):
常用方法 说明
pthread_create(...) 创建线程.
pthread_create(&playThreadT, NULL, threadPlay, this);
最后一个参数是运行函数的参数。
pthread_detach() 即主线程与子线程分离,子线程结束后,子线程资源自动回收
pthread_exit() 终止当前线程
pthread_join() 阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init() 初始化线程的属性
pthread_attr_setdetachstate() 决定这个线程在终止时是否可以被结合
pthread_attr_getdetachstate() 获取脱离状态的属性
pthread_attr_destroy() 删除线程的属性
pthread_kill() 向线程发送一个信号
pthread_mutex_init(...) 初始化互斥锁
pthread_mutex_destroy(...) 删除互斥锁
pthread_mutex_lock(...) 占有互斥锁(阻塞操作)
pthread_mutex_trylock() 试图占有互斥锁
pthread_mutex_unlock() 释放互斥锁
pthread_cond_init(...) 初始化条件对象
pthread_cond_destroy(...) 销毁条件对象
pthread_cond_signal(...) 发出条件信号,唤醒第一个调用pthread_cond_wait()而进入睡眠的线程
pthread_cond_wait() 用于线程阻塞等待,直到 pthread_cond_signal 发出条件信号后才执行后续操作
pthread_key_create() 分配用于标识进程中线程特定数据的键
pthread_setspecific() 为指定线程特定数据键设置线程特定绑定
pthread_getspecific() 获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中
pthread_key_delete() 销毁现有线程特定数据键
pthread_attr_getschedparam() 获取线程优先级
pthread_attr_setschedparam() 设置线程优先级
pthread_equal() 对两个线程的线程标识号进行比较
pthread_self() 查询线程自身线程标识号
pthread_cancel() 在安卓NDK中已经被删除。因为比较危险。
14、java与native共享内存
大概原理,在native开辟一块内存。将这块内存的首地址传给java层,以后的相关操作就通过这个地址获得这块内存,用native代码直接操作内存。
暂时用得少,以后再补充。
android中的应用有 Parcel,因为直接操作内存的,所以比操作IO的Serializable 效率高。