native方法为static
在java中声明的JNI的native方法静态方法和非静态,对于底层的C/C++代码来说是有区别的:
1,JNI函数的参数也由三部分组成:首先是JNIEnv*,是一个指向JNI运行环境的指针;2,第二个参数随本地方法是静态还是非静态而有所不同一一非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其Java类的引用;
3,其余的参数对应通常Java方法的参数,参数类型需要根据一定规则进行映射。native方法static和非static,在c/c++中回调java方法的区别
1,static:JNI回调静态方法: 不需要对象实例,只需要拿到class即可
void jniCallUnStaticMethod()
{
getJNIEnv(&env);
clazz = env->FindClass("com.test.Test");
jmethodID method = env->GetStaticMethodID(clazz, "staticTestMethod", "(I)V");
env->CallStaticVoidMethod(clazz, method, channel);
}
2,非static:JNI回调非静态方法: 需要class及其一个实例,可通过如下方式注册一个实例。
void jniCallUnStaticMethod()
{
getJNIEnv(&env);
clazz = env->FindClass("com.test.Test");
jmethodID method = env->GetMethodID(clazz, "unStaticTestMethod", "(I)V");
env->CallVoidMethod(initedObj, method, channel);
}
传递数据
- 传递int
public native int add(int x , int y);
JNIEXPORT jint JNICALL Java_com_huachao_jnipassdata_JNI_add
(JNIEnv *env, jobject obj, jint x, jint y){
return x+y;
}
- 传递字符串
①, Java字符串转为c字符串
#include <jni.h>
//因为下面用到的NULL在stdlib库中,所以导入该库
#include <stdlib.h>
/**
* 把一个jstring转换成一个c语言的char* 类型.
*/
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;
}
②, 与①函数结合,将字符串的每一个字符+1,实现简单加密
//对java字符串进行处理
public native String sayHelloInC(String c);
JNIEXPORT jstring JNICALL Java_com_huachao_jnipassdata_JNI_sayHelloInC
(JNIEnv *env, jobject obj, jstring jstr){
//调用①函数将java字符串转为c语言字符串
char* cstr = _JString2CStr(env , jstr);
//调用strlen获取cstr字符串的长度
int length = strlen(cstr);
int i;
for (i= 0; i< length; i++) {
*(cstr+i)+=1;
}
return (*env)->NewStringUTF(env , cstr);
}
- 传递java的int数组
public native int[] arrElementIncarse(int[] arr);
JNIEXPORT jintArray JNICALL Java_com_huachao_jnipassdata_JNI_arrElementIncarse
(JNIEnv *env, jobject obj, jintArray jArray){
//获取java数组的长度
jsize length = (*env)->GetArrayLength(env,jArray);
LOGI("size=%d",length);
/*
* 将java数组转为c语言指针
* 参数:(JNIEnv*, jintArray, jboolean*)
* 第三个参数 : &iscopy
*/
jboolean iscopy = 1;
jint* arrayPointer = (*env)->GetIntArrayElements(env,jArray,NULL);
int i;
for (i = 0; i<length; i++) {
*(arrayPointer+i) += 10;
//pintf("--%d" , *(arrayPointer+i));
LOGI("arr[%d]=%d",i,*(arrayPointer+i));
}
/*
*亲眼看见别人写代码时,不需要下面这个调用,直接返回jArray就是已经被修改过的数组
*但是我亲自尝试直接返回jArray,还是原来的数组,值并没有改变,需要调用下面的函数给jArray数组赋值
*/
//给需要返回的数组赋值
(*env)->SetIntArrayRegion(env , jArray , 0,length , arrayPointer);
return jArray;
}
注意:上面(*env)->SetIntArrayRegion(env , jArray , 0,length , arrayPointer);
JNI中使用LogCat输出
1,在Android.mk中添加(一定要添加在include $(CLEAR_VARS)
之后)
#增加log函数对应的log库liblog.so
LOCAL_LDLIBS += -llog
实质就是加载F:\as\plugin\android-ndk-r9d\platforms\android-14\arch-arm\usr\lib
下的liblog.so库,例如:如果想加载libEGL.so
,那么就在Android.mk中添加LOCAL_LDLIBS += -lEGL
2,在.c
文件中添加`
include <android/log.h>`
3,在.c
文件中添加宏
#define TAG "MY
JNILog
" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型
4,调用上面的宏打印日志(与printf用法相似)
LOGI("size=%d",length);
或
LOGI("arr[%d]=%d",i,*(arrayPointer+i));
注意:JNI开发中,如果在eclipse的.c
文件中报错,可能并不是你代码的错误,而是插件的问题,解决:将项目clean一下,或者将项目close然后open。
C回调JAVA(利用放射)
JNI.java
package com.hhc.ccalljava;
import android.util.Log;
public class JNI {
static{
System.loadLibrary("callback");
}
public native void callbackvoidmethod();
public native void callbackintparammethod();
public native void callbackstringparammethod();
//c调用带两个int参数的方法
public int add(int x , int y) {
return x+y;
}
//c调用java空的方法
public void helloFromJava() {
System.out.println("hello from java");
}
//c调java带字符串的方法
public void printString(String s) {
Log.i("hhc", s);
}
}
- 获取方法签名
1,cd 到:项目根目录\bin\classes目录下,本例中如下:
F:\as\Eclipse\personal\JNI\04-C回调Java\bin\classes>
2,使用javap命令:javap -s com.hhc.ccalljava.JNI
Compiled from "JNI.java"
public class com.hhc.ccalljava.JNI {
public com.hhc.ccalljava.JNI();
descriptor: ()V
public native void callbackvoidmethod();
descriptor: ()V
public int add(int, int);
descriptor: (II)I
public void helloFromJava();
descriptor: ()V
public void printString(java.lang.String);
descriptor: (Ljava/lang/String;)V
}
callback.c
#include <jni.h>
#include <android/log.h>
#define TAG "myJNILog" // 这个是自定义的LOG的标识
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
JNIEXPORT void JNICALL Java_com_hhc_ccalljava_JNI_callbackvoidmethod
(JNIEnv *env, jobject obj){
//1,获取字节码对象
//jclass (*FindClass)(JNIEnv*, const char*);
jclass clazz = (*env)->FindClass(env , "com/hhc/ccalljava/JNI");
//2,获取Method方法
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
/*
* 参数3:方法名称
* 参数4:方法签名(因为java中有方法重载)
* 使用javap查看
*/
jmethodID methodID = (*env)->GetMethodID(env , clazz , "helloFromJava" , "()V");
//3,通过字节码创建java对象,如果native方法和要回调的java方法在同一个类里面,可以直接用JNI传过来的java对象调用创建的方法
//此处的obj就是这个java对象,所以不需要创建
//4,通过对象调用方法
//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
//obj 其实就是我们要调用的方法的那个对象的实例
(*env)->CallVoidMethod(env , obj , methodID);
}
JNIEXPORT void JNICALL Java_com_hhc_ccalljava_JNI_callbackintparammethod
(JNIEnv *env, jobject obj){
LOGI("running");
//1,获取字节码对象
jclass claz = (*env)->FindClass(env , "com/hhc/ccalljava/JNI");
//2,获取Method方法
jmethodID methodID = (*env)->GetMethodID(env , claz ,"add", "(II)I");
//jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
int result = (*env)->CallIntMethod(env , obj , methodID ,9,6);
LOGI("result=%d" , result);
}
JNIEXPORT void JNICALL Java_com_hhc_ccalljava_JNI_callbackstringparammethod
(JNIEnv *env, jobject obj){
jclass clazz = (*env)->FindClass(env , "com/hhc/ccalljava/JNI");
jmethodID methodID = (*env)->GetMethodID(env ,clazz,"printString","(Ljava/lang/String;)V");
//void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
jstring str = (*env)->NewStringUTF(env,"weiwei,caonima");
(*env)->CallVoidMethod(env ,obj,methodID,str);
}
-
MainActivity
中调用
public class MainActivity extends Activity {
private JNI jni;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jni = new JNI();
}
public void cCallJava(View v) {
jni.callbackvoidmethod();
}
public void cCallJavaInt(View v) {
jni.callbackintparammethod();
}
public void cCallJavaString(View v) {
jni.callbackstringparammethod();
}
}
注意:CallVoidMethod
、CallIntMethod
这里的void和int都是指返回值类型
- C回调显示Toast
JNI.java
public class JNI {
static{
System.loadLibrary("callback");
}
public native void callbackShowToast();
}
callback.c
/**
* AllocObject(env,clazz);这相当于新建一个Activity,但Activity是不能新建的,如果新建的话,Context上下文是空,
* 但是显示Toast,要传Context,这个时候会报NullPointerException
* 解决方案一:native方法和显示Toast的方法都定义在Activity中,这样传过来的jobject就是Activity实例了,就不需要新建
* 解决方案二:在JNI的构造方法中传入上下文,showToast方法放入JNI.java中
*/
JNIEXPORT void JNICALL Java_com_hhc_ccalljava_JNI_callbackShowToast
(JNIEnv *env, jobject obj){
//1,获取字节码对象
jclass clazz = (*env)->FindClass(env , "com/hhc/ccalljava/MainActivity");
//2,获取方法methodID
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID methodID = (*env)->GetMethodID(env,clazz,"showToast","(Ljava/lang/String;)V");
//此时的obj是JNI.java这个类的实例,而被调的函数在MainActivity中,所以不能直接用obj调用
//3,通过字节码创建java对象
//jobject (*AllocObject)(JNIEnv*, jclass);
jobject instance = (*env)->AllocObject(env,clazz);
//4,通过对象调用方法
jstring *jstr = (*env)->NewStringUTF(env,"this is C");
(*env)->CallVoidMethod(env,instance,methodID,jstr);
}
MainActivity.java
public class MainActivity extends Activity {
private JNI jni;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jni = new JNI();
}
public void cCallJavaShowToast(View v) {
jni.callbackShowToast();
}
public void showToast(String s) {
Toast.makeText(getApplicationContext(), "from c="+s, Toast.LENGTH_LONG).show();
}
}
注意:上面方式会抛NullPointerException
- 解决和优化
1,解决方案一:native方法和显示Toast的方法都定义在Activity中
2,解决方案二:JNI.java
的构造函数传入Context上下文,showToast方法放入JNI.java中。
JNI.java
public class JNI {
static{
System.loadLibrary("callback");
}
private Context mContext;
public JNI(Context mContext) {
super();
this.mContext = mContext;
}
public native void callbackShowToast();
public void showToast(String s) {
Toast.makeText(mContext, "from c="+s, Toast.LENGTH_LONG).show();
}
}
callback.c
JNIEXPORT void JNICALL Java_com_hhc_ccalljava_JNI_callbackShowToast
(JNIEnv *env, jobject obj){
//1,获取字节码对象
jclass clazz = (*env)->FindClass(env , "com/hhc/ccalljava/JNI");
//2,获取方法methodID
//jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID methodID = (*env)->GetMethodID(env,clazz,"showToast","(Ljava/lang/String;)V");
//此时的obj是JNI.java这个类的实例,而被调的函数在MainActivity中,所以不能直接用obj调用
//3,通过对象调用方法
jstring *jstr = (*env)->NewStringUTF(env,"this is C");
(*env)->CallVoidMethod(env,obj,methodID,jstr);
}
MainActivity.java调用
public class MainActivity extends Activity {
private JNI jni;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jni = new JNI(this);
}
public void cCallJavaShowToast(View v) {
jni.callbackShowToast();
}
}
jstring转char*
- 方式一:env提供的函数
//字符串逆置
char *strrev(char *s){
char *p = s;
char *q = s;
char temp;
while(*q != '\0')
q++;
q--;//移动'\0'之前的字符位置
while(p < q){
temp = *p;
*p = *q;
*q = temp;
p++;
q--;
}
return s;
}
JNIEXPORT jstring JNICALL Java_com_hhc_ccalljava_JNI_jstringtocchar
(JNIEnv *env, jobject obj, jstring jstr){
char* cstr = NULL;
jsize len = 0;
jstring new_str;
//字符串长度
len = (*env)->GetStringLength(env,jstr);
//申请内存,大小等于len+1
int char_len = sizeof(char);
cstr = (char *)malloc((len+1)*char_len);//需要结束符
//把java字符串转为c字符串
//void (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);
(*env)->GetStringUTFRegion(env,jstr,0,len,cstr);
//字符串逆置
// LOGI("before---%s" , cstr);
cstr = strrev(cstr,len);
// LOGI("hou---%s" , cstr);
//转为jstr
new_str = (*env)->NewStringUTF(env,cstr);
//释放申请的内存空间
free(cstr);
return new_str;
}
注意:
1,实际测试,在genymotion android 6.0的模拟器返回的字符串尾部会多一个tochar
,而用下面的方式二不会,应该是编码的问题,因为下面的方式可以指定编码方式
2,上面的问题经过反复尝试将(*env)->GetStringUTFRegion(env, jstr, 0, len, cstr);
替换成char* cs = (*env)->GetStringUTFChars(env,jstr,0);
问题解决,但是会有警告。
- 方式二:自定义函数实现
//详解jstring转char*的函数
char* _JString2CStr(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
//获取String的字符串
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
//"GB2312"的jstring类型
jstring strencode = (*env)->NewStringUTF(env,"GB2312");
//就是调用java的String类的:byte[] bs = .getBytes("GB23122");方法
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
//byte数组的长度
jsize alen = (*env)->GetArrayLength(env, barr);
//转为jbyte*,就是c语言的char*, 指向数组的首地址
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if(alen > 0) {
//c的字符串实质就是字符数组,C的字符数组必须要有结束符,所以需要加1
//申请堆内存
rtn = (char*)malloc(alen+1); //"\0"
//内存拷贝,将ba拷贝到rtn,长度为alen
memcpy(rtn, ba, alen);
//结束符为0
rtn[alen]=0;
}
//释放不再使用的变量内存
(*env)->ReleaseByteArrayElements(env, barr, ba,0);
return rtn;
}
JNI开发中编码问题
1,JNI返回字符串乱码或报错
2,在.c
代码中打印中文字符串乱码
解决方式:将.c
文件的编码方式改为UTF-8编码
声明:java中所有引用类型(非基本数据类型),在jni底层都是对应的void*
typedef void* jobject;
C++开发JNI
- C的预处理命令
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
- #开头的就是C/C++的预处理命令
- 在编译之前,先会走预编译阶段,预编译阶段的作用就是把include进来的头文件copy到源文件中,define这些宏定义,用真实的值替换一下
- c++开发JNI时,env不再是结构体JNINativeInterface的二级指针
- _JNIEnv、JNIEnv:_JNIEnv是C++的结构体,C++结构体跟C的区别:C++结构体可以定义函数
- env 是JNIEnv的一级指针,也就是结构体_JNIEnv的一级指针,env->来调用
- _JNIEnv的函数,实际上调用的就是结构体JNINativeInterface的同名函数指针,在调用时第一个参数env已经传过去了。
- C++的函数要先声明在使用,可以把javah生成的头文件include进来作为函数的声明
native方法
static{
System.loadLibrary("jnicpp");
}
public native String hellofromcpp();
jnicpp.cpp
#include <jni.h>
#include "com_huachao_jnicpp_MainActivity.h"
JNIEXPORT jstring JNICALL Java_com_huachao_jnicpp_MainActivity_hellofromcpp
(JNIEnv *env, jobject obj){
return env->NewStringUTF("1234haha");
}
Fork子进程
MainActivity.java
中调用
public class MainActivity extends Activity {
static{
System.loadLibrary("cfork");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public native void cfork();
public void cforksub(View v){
cfork();
}
}
cfork.c
#include <jni.h>
#include <android/log.h>
#define TAG "MYJNILog" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
JNIEXPORT void JNICALL Java_com_huachao_forksub_MainActivity_cfork
(JNIEnv *env, jobject obj){
//fork成功的分叉出一个子进程,会返回当前进程的id,但是只能在主进程中fork成功
//在子进程中运行fork,会返回0,但是不能再分叉出新进程
//所以这个方法会被调用两次,第二次是在子进程中调用
//fork的返回值三种可能 >0 ==0 <0
int pid = fork();
if(pid<0){
LOGD("PID<0 ,pid=%d" , pid);
}else if(pid==0){
LOGD("PID==0");
//进入子进程
while(1){
LOGI("sub process is running");
sleep(2);
}
}else{
LOGD("PID>0 ,pid=%d" , pid);
}
}
注意:在手机上,无论你做什么操作,退出应用、杀死进程、退出后台进程、等等...(只要不关机),会发现fork的c进程一直在打印。
利用此特性,我们可以实现杀不死的进程。
AM命令
- am命令 :在adb shell里可以通过am命令进行一些操作 如启动activity Service 启动浏览器等等
- am命令的源码在Am.java中, 在adb shell里执行am命令实际上就是启动一个线程执Am.java的main方法,am命令后面带的参数都会当作运行时的参数传递到main函数中
- am命令可以用start子命令,并且带指定的参数
- 常见参数: -a: action -d data -t 表示传入的类型 -n 指定的组件名字
- 举例: 在adb shell中通过am命令打开网页
- am start --user 0 -a android.intent.action.VIEW -d http://www.baidu.com
- 通过am命令打开activity
- am start --user 0 -n com.itheima.cppJNI/com.itheima.cppJNI.MainActivity
(如果是Android API 16以上,上面的--user可以省略) - adb shell 中做一些操作 ,例如打开activity
execlp
- execlp c语言中执行系统命令的函数
- execlp() 会从PATH环境变量所指的目录中查找符合参数file的文件找到后就执行该文件, 第二个参数开始就是执行这个文件的 args[0],args[1] 最后一个参数用(char*)NULL结束
- android开发中 execlp函数对应android的path路径为
- system/bin/目录
- 调用格式:
execlp("am", "am", "start", "--user","0","-a", "android.intent.action.VIEW", "-d", "http://www.baidu.com", (char *) NULL);
execlp("am", "am", "start", "--user","0", "-n" , "com.itheima.cforktest/com.itheima.cforktest.MainActivity",(char *) NULL);
监听APP被卸载后打开浏览器跳到指定网址实例
JNIEXPORT void JNICALL Java_com_huachao_forksub_MainActivity_cfork
(JNIEnv *env, jobject obj){
int pid = fork();
int ppid = 0;//父进程的id
FILE *file = NULL;
if(pid<0){
LOGD("PID<0 ,pid=%d" , pid);
}else if(pid==0){
LOGD("PID==0");
//进入子进程
while(1){
//拿到父进程的编号
ppid = getppid();
//如果父进程为1,说明父进程被杀死了
if(ppid == 1){
/**
* c语言打开流
* 参数一:路径,应用的安装文件存放路径,来检测应用是否被卸载
*/
file = fopen("/data/data/com.huachao.forksub" , "r");
if(file == NULL){
/*
* 打开网页
* 实际开发中,将网址换成我们自己的网址
*/
execlp("am", "am", "start", "--user","0","-a", "android.intent.action.VIEW", "-d", "http://www.baidu.com", (char *) NULL);
}else{
//启动Activity
execlp("am", "am", "start", "--user","0", "-n" , "com.huachao.forksub/com.huachao.forksub.MainActivity",(char *) NULL);
}
//break,父进程死了,子进程执行完操作也要退出
break;
}
LOGI("sub process is running");
sleep(2);
}
}else{
LOGD("PID>0 ,pid=%d" , pid);
}
}