JNI,是Java Native Interface的缩写,中文为Java本地调用。通俗地说,JNI是一种技术,通过这种技术可以做到以下两点: · Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。 · Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。
交叉编译
- 在一个平台下,编译出另一个平台能够执行的二进制代码* 平台:windows , mac os, linux* 处理器:x86(主要厂商英特尔和英伟达,pc一般用这个),arm(嵌入式设备,手机用这个),mips(开源的一个处理器架构,很多厂商对它进行修改)
- 原理:
- 源代码->编译->链接->可执行程序
- 模拟其他平台特性
- 交叉编译的工具链
- 多个工具集合,一个工具使用完后接着调用下一个工具
- 常见工具
- NDK:模拟其他平台特性来编译代码的工具
- CDT:C/C++ Development Tools(高亮显示C语言关键字)
- cygwin:模拟器,可以在windows下运行linux指令
NDK介绍
- NDK目录结构
- build/tools:linux的批处理文件
- platforms:编译C代码需要使用的头文件和类库
- prebuild:预编译使用的二进制可执行文件
- python-pachages:
- sources:NDK的源码
- toolchains:工具链
- ndk-build.cmd:编译打包C代码的一个指令,肯定会调用toolchains开发人员不用管
使用JNI
- 在项目根目录下创建 jni文件夹
- 在jni文件夹中创建一个 C 文件
- 除了两个标准头文件在包含
<jni.h>
- 在Java代码中创建一个本地方法helloFromC
public native String helloFromC(); - 在JNI中定义函数实现这个方法,函数必须这么写
Java_com_hk_hellojni_MainActivity_helloFromC(JNIEnv * env,jobject obj)
- 其中 Java是必须的关键字,后面跟着包名类名方法名,中间用_隔开
- 参数必须是JNIEnv* env和jobject obj。
- env是一个二级指针,指向存放Java虚拟机的内存地址的内存块
- obj表示那个对象调用该方法
- 可以使用javah指令自动生成:javah 包名.类名
- JDK1.7 在src目录下执行
- JDK1.6 在bin/classes目录下执行
- 结果在src下会生成一个c++文件,文件中有这么个方法:
jstring JNICALL Java_com_hk_hellojni_MainActivity_helloFromC(JNIEnv *, jobject);`
直接复制黏贴加参数名就可以在我们的C或C++文件中用了,保证无误。
- 返回一个字符串,用C定义一个字符串
char* cstr = "hello from C"; - 将C字符串转化成Java字符串
jstring jstr = (*env)->NewStringUTF(env,cstr);
- NewStringUTF是env所指向的指针所指向的结构体中的函数指针变量
- 在jni文件夹中创建Android.mk文件,文件内容如下
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
#编译生成的类库叫什么名字
LOCAL_MODULE:= hello
#要编译的C文件
LOCAL_SRC_FILES:= Hello.c
include $(BUILD_SHARED_LIBRARY) - 在jni文件夹下执行ndk-build.cmd指令(要提前配置到环境变量中)
- Java代码中加载so类库,调用本地方法
System.loadLibrary("hello");//一般在静态代码块中执行
注意:ndk-build.cmd指令执行默认生成arm架构的类库,如果需要支持其他cpu架构需要在jni文件夹下创建Application.mk文件里面加入需要支持的架构,如,加入x86的支持
APP_ABI :=armeabi armeabi-v7a x86
如果需要支持cpu全部架构,把等号右边换成all
JNI常见错误
- findLibrary returned null (类库加载失败)
- CPU平台不匹配
- 加载类库时,写错类库名字
- 本地方法找不到
- 忘记加载类库
- C代码中的和Java本地方法对应的方法名写错
Eclipse配置NDK开发环境
- 指定NDK位置:Windows->Preferences->Android->NDK->Browser到NDK所放位置下的build文件夹
- 关联jni.h:选中项目右键->Properties->C/C++ General->Paths and Symbols->Add->选择NDK目录下的platforms选择对应的SDK版本号,选择cpu架构选择usr选择include(例如:D:\android\android-ndk-r11c\platforms\android-21\arch-arm\usr\include)->点击完成> 配置完成后可以直接运行Android Application,在编译的时候会自动生成类库
C语言使用Logcat(调试)
-
Android.mk
文件增加LOCAL_LDLIBS += -llog
- C代码中增加
#include <android/log.h> #define LOG_TAG "hvcker"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, VA_ARGS)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, VA_ARGS) - 在代码中就可以用了
LOGI("info\n");
LOGD("debug\n");
在C中使用反射调用Java方法
/**
* 得到Java字节码对象内存地址
*/
//jclass (*FindClass)(JNIEnv*, const char*);
jclass clazz = (*env)->FindClass(env,"com/example/jniccj/MainActivity");
/**
* 得到Java方法,最后一个参数是方法签名,可用javap -s 获取
*/
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID methodID = (*env)->GetMethodID(env,clazz,"show","(Ljava/lang/String;)V");
/**
* 调用方法,CallXxxMethod,Xxx为方法返回值,最后一个参数是可变参数,传入方法参数
*/
//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
(*env)->CallVoidMethod(env,obj,methodID,(*env)->NewStringUTF(env,"hello Java"));
- javap -s 要在projectName/bin/classes目录下执行
- 执行代码:javap -s packageName.classnName
调用C++代码
#include <jni.h> //包含同一个文件夹下的声明函数的头文件
#include "com_example_jnicpp_MainActivity.h"
JNIEXPORT jstring JNICALL
Java_com_example_jnicpp_MainActivity_helloCpp( JNIEnv * env, jobject obj) {
char * cstr = "hello from cpp"; //C++的env只是一个一级指针
return env->NewStringUTF(cstr);
}
- 包含头文件(用javah 生成的那个在src目录下的文件)
- C的env和C++的env的区别
- 在C中JNIEnv是一个结构体指针
typedef const struct JNINativeInterface* JNIEnv;
,后面再加一个就表示是一个二级指针,所以用的时候要先取出env所指向的地址,该地址是指向JNINativeInterface结构体的指针,所以要调用JNINativeInterface里面的方法要(env)->xxx - 在C++中JNIEnv是一个里面含有
JNINativeInterface
的结构体
typedef _JNIEnv JNIEnv;
struct _JNIEnv {
/* do not rename this;
it does not seem to be entirely opaque
/
const struct JNINativeInterface functions;
...
}
所以C++的 JNIEnv* 是一个一级指针,所以调用的时候直接env->xxx就可以了。
- 在C中JNIEnv是一个结构体指针
还有一点要注意的是:C++是面向对象的,所以它在执行类似newStringUTF方法时不需要传入本身env,直接env->newStringUTF("hello from c");底层也是调用
JNINativeInterface
中的函数jstring NewStringUTF(const char* bytes) { return functions->NewStringUTF(this, bytes); }
分支C进程
int pid = fork();
//如果pid = 0分支成功
if(pid == 0){
while(1){
LOGD("fork C");
sleep(1);
}
}
- 分支出来的C进程在不同型号的手机会有不同的表现形式,例如在魅族手机,依赖于Android进程,程序管理里面强制停止,C进程随之停止,但在有些型号手机中,C进程怎么杀也杀不死,只能用adb shell kill [pid]来删除
- 可以分支出两个守护进程,这样怎么杀也杀不掉(可以给线程做保活)
常用代码(C部分)
中文乱码
jstring ctojstring(JNIEnv env, char tmpstr) {
jclass Class_string;
jmethodID mid_String, mid_getBytes;
jbyteArray bytes;
jbyte* log_utf8;
jstring codetype, jstr;
Class_string = (env)->FindClass(env, "java/lang/String"); //获取class
//先将gbk字符串转为java里的string格式
mid_String = (env)->GetMethodID(env, Class_string, "<init>", "([BLjava/lang/String;)V");
bytes = (env)->NewByteArray(env, strlen(tmpstr));
(env)->SetByteArrayRegion(env, bytes, 0, strlen(tmpstr), (jbyte) tmpstr);
codetype = (env)->NewStringUTF(env, "gbk");
jstr = (jstring) (env)->NewObject(env, Class_string, mid_String, bytes,codetype);
(env)->DeleteLocalRef(env, bytes);
//再将string变utf-8字符串。
mid_getBytes = (env)->GetMethodID(env, Class_string, "getBytes", "(Ljava/lang/String;)[B"); codetype = (env)->NewStringUTF(env, "utf-8");
bytes = (jbyteArray) (env)->CallObjectMethod(env, jstr, mid_getBytes, codetype);
log_utf8 = (env)->GetByteArrayElements(env, bytes, JNI_FALSE);
return (*env)->NewStringUTF(env, log_utf8);
}Java字符串转C
char* _JString2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (env)->FindClass(env, "java/lang/String");
jstring strencode = (env)->NewStringUTF(env,"GB2312");
jmethodID mid = (env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
jsize alen = (env)->GetArrayLength(env, barr);
jbyte ba = (env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
rtn = (char)malloc(alen+1); //"\0" memcpy(rtn, ba, alen);
rtn[alen]=0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}C语言操作Java数组
//拿到整形数据的长度和整形数组的指针
//jsize (GetArrayLength)(JNIEnv, jarray);
int length = (env)->GetArrayLength(env,jintarr);
//jint (GetIntArrayElements)(JNIEnv, jintArray, jboolean);
int * arrp = (env)->GetIntArrayElements(env,jintarr,0);
int i;
for(i = 0;i<length;i++){
*(arrp+i)+=20;
}