一、介绍
目前Android系统支持以下七种不用的CPU架构,每一种对应着各自的应用程序二进制接口ABI:(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。
关系如下:
架构 | 简称 |
---|---|
ARMv7以下 | armeabi |
ARMv7 | armeabi-v7a |
ARMV8 | arm64-v8a |
x86 | x86 |
x86_64 | x86_64 |
MIPS | mips |
MIPS64 | mips64 |
JNI是什么
全称为Java Native Interface,即Java本地接口。JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互。由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码JNI是一种在Java虚拟机机制下的执行代码的标准机制。代码被编写成汇编程序或者C/C++程序,并组装为动态库。也就允许非静态绑定用法。
函数命名的规则是什么
例如在.c文件中:JNIExport jstring JNICALL Java_com_yang_demo_MainActivity_stringFromJNI( JNIEnv* env,jobject thiz )
- jstring为返回类型
- Java_com_yang_demo是包名
- MainActivity是类名
- stringFromJNI是方法名
- JNIExport和JNICALL是不固定保留的关键字
二、基础记录
1 类型
1.1 基本类型
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 |
1.2 数组类型
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 |
1.3 引用类型
- jobject
- jclass
- jstring
- jarray
- jobjectArray
- jbooleanArray
- jbyteArray
- jcharArray
- jshortArray
- jintArray
- jlongArray
- jfloatArray
- jdoubleArray
- jthrowable
1.4 属性和方法
- jfieldID
- jmethodID
1.5 jvalue(特殊类型)
jvalue共用体可以保存多种类型的数据typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
1.6 类型签名
用于对java虚拟机内容表示
类型签名 | java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L全类名; | 类 |
[type | type[] |
(参数类型签名,…)返回值类型签名 | 方法类型 |
例如:int func(float a, String b, byte[] c)
对应的签名为:(FLjava/lang/String;[B)I
1.7 字符串处理
1.7.1 创建jstring
创建Unicode格式的jstring串jstring NewString(const jchar *unicodeChars, jsize len);
创建UTF-8格式的jstring串jstring NewStringUTF(const char *bytes);
1.7.2 获取字符串长度
获取Unicode格式的jstring串长度jsize GetStringLength(jstring string);
获取UTF-8格式的jstring串长度jsize GetStringUTFLength(jstring string);
1.7.3 jstring转化为C串及释放jstring串
简单字符串处理
GetStringXXXChars & ReleaseStringXXXChars
将jstring转为C字符串:const jchar* GetStringChars(jstring string, jboolean *isCopy);
const char* GetStringUTFChars(jstring string, jboolean *isCopy );
如果生成串的一个副本,isCopy参数将被置为JNI_TRUE,否则置为NULL或者JNI_FALSE
通知虚拟机平台相关代码无需再访问 chars,释放指针。void ReleaseStringChars(jstring string, const jchar *chars);
void ReleaseStringUTFChars(jstring string, const char *utf);
复杂字符串处理
GetStringRegion & GetStringUTFRegion
GetStringRegion函数将串str的一个子串传送到一个字符缓存器。该子串在位置start处开始,在len-1处结束(这样传送的字符数就是len)void GetStringRegion(jstring str, jsize start, jsize len, jchar *buf);
void GetStringUTFRegion(jstring str, jsize start, jsize len, char *buf);
复杂字符串处理2
GetStringCritical & ReleaseStringCritical
该函数返回一个指向特定串中字符的指针。如果需要复制该字符,并且函数返回时将isCopy置为JNI_TRUE,否则置为NULL或JNI_FALSE。const jchar* GetStringCritical(jstring string, jboolean *isCopy);
void ReleaseStringCritical(jstring string,const jchar *carray);
在调用该函数后,直至调用ReleaseStringCritical之前,所使用的所有函数都无法使当前线程被阻塞。
2.静态方法(jclass)与实例方法(jobject)
在添加native方法时有静态方法与实例方法两种方式,但是在生成的C/C++函数会略有不同:Java_com_example_myrtmp_YuvLib_init(JNIEnv *env, jobject jobj);
Java_com_example_myrtmp_YuvLib_init(JNIEnv *env, jclass clazz);
区别就在于获取的应用类型:实例引用(jobject)和类引用(jclass)。
实例应用对应于Object,类引用对应于Class。前者为实例操作,后者为类操作。可以通过下面函数看出来:// 参数都是jclass
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...);
jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
// 参数都是jobject
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
object (*GetObjectField)(JNIEnv*, jobject, jfieldID);
三、类与异常问题
1.类
1.1 加载类
|
- name:类的全名。
- loader:类加载器。
- buf:包含类数据的数据指针。包含.class文件数据的buffer。
- bufLen:buf长度。
- return:java类对象。发生错误时返回NULL。
如果没有指定这个Java类的,则会抛出ClassFormatError
如果是一个类或者接口是它自己的一个父类/父接口,则会抛出ClassCircularityErrora,类循环错误
如果内存不足,则会抛出OutOfMemoryError
如果想尝试在Java包中定义一个类,则会抛出SecurityException
1.2 查找一个类
|
- env:JNI接口指针
- name:一个完全限定的类名,即包含“包名”+“/”+类名。如果类名以[开头,将返回一个数组类]
- return:返回对应完全限定类对象,当找不到类时,返回NULL
如果没有指定这个Java类的,则会抛出ClassFormatError
如果是一个类/接口是它自己的一个父类/父接口,则会抛出ClassCircularityError
如果没有找到该类/接口的定义,则抛出NoClassDefFoundError
如果内存不足,则会抛出OutOfMemoryError
1.3 获取父类
|
- env:JNI接口指针
- clazz:Java的Class类
- retur:如果clazz有父类则返回其父类,如果没有其父类则返回NULL
1.4 类型转换
|
判断clazz1的对象是否可以安全地转化为clazz2的对象
- env:JNI接口指针
- clazz1:Java的Class类,即需要被转化的类
- clazz2:Java的Class类,即需要转化为目标的类
- return:jni_true or jni_false
如果满足以下任一条件,则返回JNI_TRUE:
如果clazz1和clazz2是同一个Java类。
如果clazz1是clazz2的子类
如果clazz1是clazz2接口的实现类
2.异常操作
2.1 抛出异常
|
传入一个jthrowable对象,并且在JNI并将其抛出。
- env:JNI接口指针
- jthrowable:一个Java的java.lang.Throwable对象
- return:成功为0,失败为负数
2.2 构造一个新的异常并抛出
|
传入一个message,并用其构造一个异常并且抛出。
- env:JNI接口指针
- jthrowable:一个Java的java.lang.Throwable对象
- message:用于构造一个java.lang.Throwable对象的消息,该字符串用modified UTF-8编码
- return:成功为0,失败为负数
2.3 检查是否发生异常,并抛出异常
|
检测是否发生了异常,如果发生了,则返回该异常的引用(再调用ExceptionClear()函数前,或者Java处理异常前),如果没有发生异常,则返回NULL。
- env:JNI接口指针
- return:jthrowable的异常引用或者NULL
2.4 打印异常的堆栈信息
|
打印这个异常的堆栈信息
- env:JNI接口指针
2.5 清除异常的堆栈信息
|
清除正在抛出的异常,如果当前没有异常被抛出,这个函数不起作用
- env:JNI接口指针
2.6 致命异常
|
致命异常,用于输出一个异常信息,并终止当前VM实例,即退出程序。
- env:JNI接口指针
- msg:异常的错误信息,该字符串用modified UTF-8编码
2.7 仅仅检查是否发生异常
|
检查是否已经发生了异常,如果已经发生了异常,则返回JNI_TRUE,否则返回JNI_FALSE
- env:JNI接口指针
四、全局引用、局部引用、弱全局引用
全局引用
给对象obj创建一个全局引用,可以跨线程处理。
创建
|
obj可以是任意引用。由于全局引用不再受到JVM统一管理,因此全局引用必须通过DeleteGlobalRef()手动释放。否则引用不会被GC回收。
- obj:任意类型的引用。
- return:全局引用。如果内存不足返回NULL。
删除
|
删除全局引用。
- globalRef:全局引用。
局部引用
局部引用有JVM负责的引用类型,占用JVM资源。在native方法返回后会被自动回收。因此只会在创建他们的线程中有效。
创建
|
- ref:全局或者局部引用
- return:局部引用
删除
|
删除局部引用
- localRef:局部引用。
生命周期管理函数组
在虚拟机管理局部引用时,JNI为了确保可以创建成功,提供一组生命周期管理函数来帮助管理。
- 确认局部引用容量
确认空间是否充足
jint EnsureLocalCapacity(JNIEnv *env, jint capacity);
capacity:给定局部引用的数量。
return:JNI_OK表示当前线程栈可以创建capacity个局部引用。返回其他值表示不可用,并抛出一个OutOfMemoryError异常 - 局部栈帧的入栈
创建局部引用的入栈处理
jint PushLocalFram(JNIEnv *env ,jint capacity);
capacity:给定局部引用的数量。
return:JNI_OK表示当前线程栈可以创建capacity个局部引用。返回其他值表示不可用,并抛出一个OutOfMemoryError异常
注意:当前的局部帧中,前面的局部帧创建的局部引用仍然是有效的 - 局部栈帧的出栈
销毁局部引用的出栈处理
jobject PopLocalFrame(JNIEnv *env, jobject result);
result:给定保存栈帧的引用,如果不需要前一个栈帧则可以传入NULL。
return:前一个帧的引用。
弱全局引用
弱全局引用是一种特殊的全局引用。不同的是,一个弱全局引用允许Java对象被垃圾回收器回收。一个被回收了的弱引用指向NULL。
创建
|
创建一个弱全局引用
- obj:任意对象。
- return:返回弱全局引用,如果obj为NULL则返回NULL。
删除
|
删除一个弱全局引用
- obj:弱全局引用。
五、对象操作
1 直接创建一个Java对象
|
不借助任何构造函数的情况下分配一个新的Java对象,返回对象的一个引用。
- env:JNI接口指针
- clazz:Java类对象
- return:返回一个Java对象,如果该对象无法被创建,则返回NULL。
注意:
如果该类是接口或者是抽象类,则抛出InstantiationException
如果是内存溢出,则抛出OutOfMemoryError
2 根据某个构造函数来创建Java对象
|
提供了三种创建对象的方式,构造一个新的Java对象。
- clazz:类
- methodID:构造器方法ID
- …:可变参数列表
- args:这里需要传入参数数组
- args:指向变参列表的指针
3 获取某个对象的类信息
|
返回obj对象对应的类信息
- env:JNI接口指针
- obj:Java对象,不能为NULL
- return:java对象对应的类
4 获取某个对象的引用类型
|
返回obj对象对应的引用类型,可以得到当前对象的引用类型是全局引用、局部引用还是弱全局引用。
- return:当前对象的引用类型。这些类型在jni.h文件中的定义如下所示。
typedef enum jobjectRefType {
JNIInvalidRefType = 0,//无效引用
JNILocalRefType = 1,//局部引用
JNIGlobalRefType = 2,//全局引用
JNIWeakGlobalRefType = 3//弱全局引用
} jobjectRefType;
5 判断某个对象是否是某个“类”的子类
|
测试obj是否是clazz的一个实例
- env:JNI接口指针
- obj:一个Java对象
- clazz:一个Java的类
- return:是则返回JNI_TRUE;否则则返回JNI_FALSE
6 判断两个引用是否指向同一个引用
|
判断两个引用是否指向同一个对象
- env:JNI接口指针
- ref1:Java对象
- ref2:Java对象
- return:同一个类对象,返回JNI_TRUE;否则,返回JNI_FALSE
7 返回属性id
|
获取某个类的非静态属性id。通过方法属性名以及属性的签名,来确定对应的是哪个属性。
- env:JNI接口指针
- clazz:一个Java类对象
- name:以”0”结尾的,而且字符类型是”utf-8”的属性名称
- sig:以”0”结尾的,而且字符类型是”utf-8”的属性签名
- return:属性对应ID,如果操作失败,则返回NULL
注意:
如果找不到指定的属性,则抛出NoSuchFieldError
如果类初始化失败,则抛出ExceptionInitializerError
如果内存不足了,则抛出OutOfMemoryError
8 返回属性id系列
|
返回某个类的非静态属性的值,这是一组函数的简称。
- env:JNI接口指针
- obj:Java对象,不能为空
- fieldID:有效的fieldID
- return:对应属性的值
9 设置属性id系列
|
设置某个类的的非静态属性的值。其中具体哪个属性通过GetFieldID()来确定哪个属性。这是一组函数的简称。
- env:JNI接口指针
- obj:Java对象,不能为空
- fieldID:有效的属性ID
- value:属性的新值
10 获取某个类的某个方法id
|
返回某个类或者接口的方法ID,该方法可以是被定义在clazz的父类中,然后被clazz继承。
- env:JNI接口指针
- clazz:Java类对象
- name:以0结尾的,并且是”utf-8”的字符串的方法名称
- sig:以0结尾的,并且是”utf-8”的字符串的方法签名
- return:方法ID,没有找到指定的方法,则返回NULL
注意:
如果找不到指定的方法,则抛出NoSuchMethodError
如果累初始化失败,则抛出ExceptionInInitializerError
如果内存不够,则抛出OutOfMemoryError
11 调用Java实例的某个非静态方法“系列”
|
这一些列都是在native中调用Java对象的某个非静态方法,它们的不同点在于传参不同。其中的type表示函数返回值类型。切不能是他父类的方法。
- env:JNI接口指针
- obj:对应的Java对象
- methodID:某个方法的方法id
12 调用某个类的非抽象方法
|
调用父类中的实例方法。与Call type Method函数不同的是前者是基于类的对象,而后者是基于类。
- env:JNI接口指针
- obj:Java对象
- clazz:Java类
- methodID:方法ID
- return:调用Java方法的结果
13 获取静态属性
|
获取某个类的某个静态属性ID,根据属性名以及标签来确定是哪个属性。
- env:JNI接口指针
- clazz:Java类
- name:静态属性的属性名,是一个编码格式”utf-8”并且以0结尾的字符串。
- sig:属性的签名,是一个编码格式”utf-8”并且以0结尾的字符串。
- return:返回静态属性ID,如果指定的静态属性无法找则返回NULL
注意:
如果指定的静态属性无法找到则抛出NoSuchFieldError
如果类在初始化失败,则抛出ExceptionInInitializerError
如果内存不够,则抛出OutOfMemoryError
14 获取静态属性系列
|
这个系列返回一个对象的静态属性的值。
- env:JNI接口指针
- clazz:Java类
- field:静态属性ID
- return:返回静态属性
15 设置静态属性系列
|
这个系列是设置类的静态属性的值。
- env:JNI接口指针
- clazz:Java类
- field:静态属性ID
- value:设置的值
16 获取静态函数ID
|
返回类的静态方法ID,通过它的方法名以及签名来确定哪个方法。
- env:JNI接口指针
- clazz:Java类
- name:静态方法的方法名,以”utf-8”编码的,并且以0结尾的字符串
- sig:方法签名,以”utf-8”编码的,并且以0结尾的字符串
- return:返回方法ID,如果操作失败,则返回NULL
注意:
如果没有找到对应的静态方法,则抛出NoSuchMethodError
如果类初始化失败,则抛出ExceptionInInitializerError
如果系统内存不足,则抛出OutOfMemoryError
17 调用静态函数系列
|
根据指定的方法ID,就可以操作Java对象的静态方法了。
- env:JNI接口指针
- clazz:Java类
- methodID:静态方法ID
- return:返回静态的Java方法的调用方法
六、字符串操作
七、数组操作
八、系统级别的操作
九、NIO操作
打开文件
|
接收参数为文件路径和操作类型
操作类型
- r(rt):打开一个文本文件,文件必须存在,只允许读
- r+(rt+):打开一个文本文件,文件必须存在,允许读写
- rb:打开一个二进制文件,文件必须存在,只允许读
- rb+:打开一个二进制文件,文件必须存在,允许读写
- w(wt):新建一个文本文件,已存在的文件将内容清空,只允许写
- w+(wt+):新建一个文本文件,已存在的文件将内容清空,允许读写
- wb:新建一个二进制文件,已存在的文件将内容清空,只允许写
- wb+:新建一个二进制文件,已存在的文件将内容清空,允许读写
- a(at):打开或新建一个文本文件,只允许在文件末尾追写
- a+(at+):打开或新建一个文本文件,可以读,但只允许在文件末尾追写
- ab:打开或新建一个二进制文件,只允许在文件末尾追写
- ab+:打开或新建一个二进制文件,可以读,但只允许在文件末尾追写
上述说明:
1.r(read):只读
2.w(write):只写
3.a(append):追加
4.t(text):文本文件,可省略不写
5.b(binary):二进制文件
6.+:读和写
7.用r打开文件时,文件必须存在
8.用w打开的文件,只能向文件写入。若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件。
9.若要向一个已存在的文件追加新的信息,用a方式打开文件。如果指定文件不存在则尝试创建该文件。
10.打开文件出错时,返回一个NULL。
写入文件
|
- buffer:是一个指针,对fwrite来说,是要获取数据的地址;
- size:要写入内容的单字节数;
- count:要进行写入size字节的数据项的个数;
- stream:目标文件指针;
返回实际写入的数据项个数count。
注意:此时只是将数据推送至缓冲区,并未同步到文件中,所以要将内存与文件同步还需要用fflush(FILE *fp)函数同步。
十、反射支持
十一、线程
Android是基于Linux内核,Linux是遵循POSIX线程标准,而POSIX线程库有一系列Pthread API来操作linux线程。#include <pthread.h>
线程概念
操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
相关函数
- pthread_t :用于声明一个线程对象如:pthread_t thread;
- pthread_create :用于创建一个实际的线程如:pthread_create(&pthread,NULL,threadCallBack,NULL);其总共接收4个参数,第一个参数为pthread_t对象,第二个参数为线程的一些属性我们一般传NULL就行,第三个参数为线程执行的函数( void* threadCallBack(void *data) ),第四个参数是传递给线程的参数是void*类型的既可以传任意类型。
- pthread_exit :用于退出线程如:pthread_exit(&thread),参数也可以传NULL。注:线程回调函数最后必须调用此方法。
- pthread_join :等待一个线程的结束,线程间同步的操作。
- pthread_mutex_t :用于创建线程锁对象如:pthread_mutex_t mutex;
- pthread_mutex_init :用于初始化pthread_mutex_t锁对象如:pthread_mutex_init(&mutex, NULL);
- pthread_mutex_destroy :用于销毁pthread_mutex_t锁对象如:pthread_mutex_destroy(&mutex);
- pthread_cond_t :用于创建线程条件对象如:pthread_cond_t cond;
- pthread_cond_init :用于初始化pthread_cond_t条件对象如:pthread_cond_init(&cond, NULL);
- pthread_cond_destroy :用于销毁pthread_cond_t条件对象如:pthread_cond_destroy(&cond);
- pthread_mutex_lock :用于上锁mutex,本线程上锁后的其他变量是不能被别的线程操作的如:pthread_mutex_lock(&mutex);
- pthread_mutex_unlock :用于解锁mutex,解锁后的其他变量可以被其他线程操作如:pthread_mutex_unlock(&mutex);
- pthread_cond_signal :用于发出条件信号如:pthread_cond_signal(&mutex, &cond);
- pthread_cond_wait :用于线程阻塞等待,直到pthread_cond_signal发出条件信号后才执行退出线程阻塞执行后面的操作。
创建线程
|
- thread:指向 pthread_t 类型变量的指针,用它代表返回线程的句柄
- attr:指向 pthread_attr_t 结构的指针形式存在的新线程属性,可以通过该结构来指定新线程的一些属性,比如栈大小、调度优先级等,具体看 pthread_attr_t 结构的内容。如果没有特殊要求,可使用默认值,把该变量取值为 NULL 。
- 第三个参数是指向启动函数的函数指针,它的函数签名格式如下:void * start_routine(void * args),启动程序将线程参数看成 void 指针,返回 void 指针类型结果。
- 线程启动程序的参数,也就是函数的参数,如果不需要传递参数,它可以为 NULL 。
- 函数如果执行成功了则返回 0 ,如果返回其他错误代码。
注意:当前线程中只能完成一些简单的C层操作,如果需要对Java层做一些操作就不可以来,因为这边没有JVM环境。
此时需要通过AttachCurrentThread、DetachCurrentThread函数来将当前线程绑定到JVM上面,并且获取到当前线程中的JNIEnv对象。通过他就可以在Java层做一些操作。// 获取JVM虚拟环境
env->GetJavaVM(&g_VM);
// 获取当前线程中的JNIEnv对象
g_VM->GetEnv((void **) &env,JNI_VERSION_1_6);
// 将当前线程绑定到JVM中
g_VM->AttachCurrentThread(&env, NULL)
...
// 从JVM中分离当前线程
g_VM->DetachCurrentThread();
等待子线程
|
- thread:等待退出线程的线程号。
- retval:退出线程的返回值。
pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。
十二、JavaVM对象与JNIEvn对象
JavaVM结构体
在Java环境中,每个进程可以有多个VM实例,每个实例会有一个JavaVM结构体实例和他对应。但是在Android环境中,每个进程只能有一个VM示例。所以只有一个JavaVM结构体对应实例。
通常在VM加载*.so程序库时,会先调用JNI_OnLoad()函数,在该函数中会将JavaVM指针对象保存到C层JNI全局变量中。他在所有线程共享。
JNIEnv结构体
当Java线程调用到C层的JNI函数的时候,一定会进入VM,VM会产生相对应的JNIEnv对象。这个对象和线程是一一对应的关系。
在调用JNIEnv中的函数时,每个线程调用的JNIEnv中的本地函数都是相互独立的,因为VM会为每个线程产生一个JNIEnv对象实体。
在调用函数的时候如果不方便传递JNIEnv对象,可以先获取JavaVM对象,再使用GetEnv()函数获取JNIEnv对象。
如果在C层中单独创建线程,就必须用VM创建对应的JNIEnv对象,并回传该对象的指针。