Android平台JNI调用

 最近接触了Android平台JNI调用,发现网络资料对此没有从原理到具体实现有一份更为详细的介绍。所以,参照了一些材料并根据项目开发加入自己的一些体会:

JNI 的由来
JNI是Java Native Interface的缩写,中文为JAVA本地调用。它允许Java代码和其他语言写的代码进行交互。JNI 是本地编程接口。它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行互操作。
使用JNI的原因
1.平台相关性
标准JAVA库不支持一些平台特性,你可以用别的语言,编写代码使得你的软件支持这些平台特性,例如对IPC(Internet Process Connection )机制不支持(消息队列、共享内存、信号量等)。
2.为提高效率,可能需要用低级语言编写一些算法以提高程序的性能。例如Java数据库访问和socket通讯的效率低。
3.Android 的Java层级是外壳框架,大部分的android本身的系统控件都在Native层(C/C++),与Native层相关问题的解决。
4.欲在Android平台的java层利用原先已经用C/C++写的库文件。
5.需要保密的应用逻辑使用C开发。毕竟,Java包都是可以反编译的 。
 
JNI在Android的层级关系图:
Android平台利用java调用C/C++
一、编写带有native声明的方法的java类
1.定义带有native关键词的java的类方法(method),且不能在java中实现;
  package sam.test.jnitest;
public class JniTest extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
   
    public native void test();
    public native String  stringcat(String str1,String str2);
    public native int  reduce(int x,int y);
}
 
2.在java代码中load用C/C++实现的库文件,例如在java类中加入:
                     static {
                             System.loadLibrary("jni-test");
                       }
     load一个名为jni-test的C/C++库,没有后缀,以便在不同操作系 统间兼容(夸平台性),因为各个平台的库的后缀不同,例如libjni-test.so或libjni-test.dll;
3.在java中调用此native方法(与调用其他类方法相同)。
二、编译成class文件
首先确保已下载安装JDK,并配置JDK的系统环境变量
1.一种方法是通过javac编译,可以通过命令行中的 javac JniTest.java进行编译:
E:\workspace\JniTest\src>javac –classpath E:\Working\Android \SDK\android-sdk-windows\platforms\android-6/android.jar sam /test/jnitest/jnitest.java
最终在jnitest.java的目录生成jnitest.class文件
另外,可以利用-classpath 选项加入类文件所依赖的jar包,此例中JniTest继承Activity 类,javac编译过程需加入android.jar包。
2.另一种利用Eclipse编译。可以直接在Eclipse项目下的bin目录中找到编译后的JniTest.class文件.
三、使用javah命令生成扩展名为h的头文件
1.处理javac生成的class文件:
     E:\workspace\JniTest\src>javah sam.test.jnitest.JniTest
2. 处理Eclipse编译成的class文件
     E:\workspace\JniTest\src>javah -classpath ../bin     sam.test.jnitest.JniTest
3.注意类要包含包名,上例中类名是JniTest,包名是sam.test.jnitest。
4.路径文件夹下要包含所有包中的类,否则会报找不到类的错误。上例中对于javac编译的情况,工作目录是workspace\JniTest\src,该路径已包含类sam.test.jnitest(在Java 中包的层次结构类似于文件夹的层次结构, 类sam.test.jnitest对应的目录为sam/test/jnitest)
5.classpath参数指定到包名前一级文件夹。
   对于Eclipse编译的情况,指定bin为包名的前一级目录
最终,生成sam_test_jnitest_JniTest.h文件定义了从java语言映射到C/C++语言的native函数:
JNIEXPORT void JNICALL Java_sam_test_jnitest_JniTest_play
  (JNIEnv *, jobject);
JNIEXPORT jstring JNICALL Java_sam_test_jnitest_JniTest_stringcat
  (JNIEnv *, jobject, jstring, jstring);
JNIEXPORT jint JNICALL Java_sam_test_jnitest_JniTest_reduce
  (JNIEnv *, jobject, jint, jint);
函数的参数如下:
JNIEnv *:JNI环境的指针,虚拟机中当前线程的一个句柄,包含了映射信息及其它操作信息 (A pointer to the JNI environment. This pointer is a handle to the current thread in the Java virtual machine, and contains mapping and other hosuekeeping information ) 。
VM是多线程执行环境,每个线程在调用JNI函数是传入进来的线程ID都不同。
Jobject:调用该本地代码的函数引用。(A reference to the method that called this native code ),可以由此参数并结合JNIEnv*得到调用该函数对应的java类。
JNIEnv * Jobject之后参数 :与java代码调用native函数时具体传入的参数对应。
从生成的头文件可以看出JNI的一些语法规则,例如注册的native函数总是以Java_开头,后面跟包名_类名,最后是函数名。
详细的规则内容可参考官方文档:
四、使用C/C++实现本地方法
1.根据步骤3生成的.h文件中对C/C++的函数声明,补充具体方法
以hello.c为例:
JNIEXPORT jstring JNICALL Java_sam_test_jnitest_JniTest_stringcat
  (JNIEnv *env, jobject thiz, jstring jstrSrc, jstring jstrDes)
  {
    char buffer[512];
    android_log_print(ANDROID_LOG_INFO, "JniTest", "stringcat Begin......");
    const char* pSrc; = (*env)->GetStringUTFChars(env,jstrSrc, NULL);
    if(pSrc == NULL)
        return NULL;
    const char* pDes = (*env)->GetStringUTFChars(env,jstrDes, NULL);
    if(pDes == NULL)
        return NULL;
strcpy(buffer,pSrc);
    strcat(buffer,pDes);
   
    (*env)->ReleaseStringUTFChars(env,jstrSrc, pSrc);
    (*env)->ReleaseStringUTFChars(env,jstrDes, pDes);
   
    return (*env)->NewStringUTF(env, buffer);
}
JNIEXPORT jint JNICALL Java_sam_test_jnitest_JniTest_reduce
  (JNIEnv *env, jobject, jint i, jint j)
  {
  return i-j;
  }
如果是用C++写的文件,我们可以用更简洁的方式实现此部分:
 extern “C” JNIEXPORT jstring JNICALL Java_sam_test_jnitest_JniTest_stringcat
  (JNIEnv *env, jobject thiz, jstring jstrSrc, jstring jstrDes)
  {
    …
    const char* pSrc; = env->GetStringUTFChars(jstrSrc, NULL);
    ...
return env->NewStringUTF(buffer);
   }
2. 添加入口函数JNI_OnLoad()
当Android的VM(Virtual Machine)执行到System.loadLibrary()函数时,首先会去执行C组件中的JNI_OnLoad()函数。它的用途有二:
•告诉VM此C组件使用那一个JNI版本。如果你的*.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer,就必须由JNI_OnLoad()函数来告诉VM。
•由于VM执行到System.loadLibrary()函数时,就会立即先调用JNI_OnLoad(),所以C组件的开发者可以在JNI_OnLoad()中进行C组件内的初期值设定(Initialization)
1).建立jni与JniTest类的映射表
static JNINativeMethod gJniTestMethods[] = {
    /* name, signature, funcPtr */
    {"test","()V",
    (void*)Java_sam_test_jnitest_JniTest_test},
    {" stringcat","(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
    (void*)Java_sam_test_jnitest_JniTest_stringcat},
    {"reduce","(II)I",
    (void*)Java_sam_test_jnitest_JniTest_reduce},
};
Andoird 中使用了一种不同传统Java JNI的方式来定义其native的函数。其中很重要的区别是Andorid使用了一种Java 和 C 函数的映射表数组,并在其中描述了函数的参数和返回值。这个数组的类型是JNINativeMethod,定义如下:
typedef struct {
const char* name;            /*Java中函数的名字*/         
const char* signature;      /*描述了函数的参数和返回值*/
void* fnPtr;               /*函数指针,指向C函数*/
} JNINativeMethod;
第一个参数对应java中函数名字,其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
“()” 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Func();
"(II)V" 表示 void Func(int, int);
具体的每一个字符的对应关系如下
字符    Java类型      C类型
V       void         void
Z      jboolean     boolean
I       jint         int
J        jlong        long
D      jdouble       double
F      jfloat            float
B      jbyte            byte
C      jchar           char
S      jshort          short
数组则以"["开始,用两个字符表示
[I     jintArray       int[]
[F     jfloatArray     float[]
[B     jbyteArray     byte[]
[C    jcharArray      char[]
[S    jshortArray      short[]
[D    jdoubleArray    double[]
[J     jlongArray      long[]
[Z    jbooleanArray    boolean[]
上面的都是基本类型。如果Java函数的参数是class,则以“L”开头,以“;”结尾,
中间是用“/” 隔开的包及类名。而其对应的C函数名的参数则为jobject. 一个例外
是String类,其对应的类为jstring
Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject
如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。
例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
2).为类中不同的native函数注册
•自定义registerNativeMethods函数:
static int registerNativeMethods(JNIEnv* env, const char* className,
    JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL)
        return  0;
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)
        return 0;
    return 1;
}
这种做法一般用于NDK(Native Develepment kit,参见5.1)编译的code,在NDK中registerNativeMethods接口目前还没有开放出来,所以需要自定义实现,该部分code与Android 源码中自带的registerNativeMethods的code一致,具体可参考..\dalvik\libnativehelper\JNIHelp.c中jniRegisterNativeMethods的实现。
•如果源码是在Android 源码中编译,则可以包含AndroidRuntime.h后直接调用AndroidRuntime::registerNativeMethods函数进行注册。
•RegisterNatives函数的作用:
应用层级的Java程序调用本地函数时,通过虚拟机寻找库文件中的本地函数。如果某函数需要频繁调用,每一次调用都寻找一遍,会花很多不必要的时间,那么,开发者可以自行将本地函数向虚拟机进行注册,以达到更有效率的寻找函数。
3).调用JNI_OnLoad,返回JNI 版本号虚拟机
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    __android_log_print(ANDROID_LOG_INFO, "JniTest", "JNI_OnLoad......");
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)
 return JNI_FALSE;
    if (!registerNativeMethods(env, "sam/test/jnitest/JniTest",
            gJniTestMethods, sizeof(gJniTestMethods) / sizeof(gJniTestMethods[0])))
        return JNI_FALSE;
return JNI_VERSION_1_4;
}
4). 根据需要添加JNI_Unload 。
JNI_Unload 与JNI_Load 对应,当虚拟机释放该c组件时,会调用
JNI_Unload 做一些善后清理工作。
五、编译C/C++源文件成库文件
1. 利用NDK进行编译
1).  NDK介绍
•NDK全称是Native Development Kit ,它提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so库文件和java应用一起打包成apk。
•NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
•NDK提供了一份稳定、功能有限的API头文件声明 ,这些API支持的功能非常有限,包含有:C标准库(libc)、标准数学库(libm)、压缩库(libz)、Log库(liblog)。
2). 环境配置
Windows:
a).下载android-ndk-r4-windows.zip。
b).安装cygwin 1.7以上版本,一个模拟的linux环境,安装需要的组件。成功之后,配置环境变量:
在windows安装目录中修改 home\<你的用户名>\.bash_profile 文件,添加环境变量
NDK=/cygdrive/<你的盘符>/<android ndk 目录> 例如:NDK=/cygdrive/d/android/android-ndk-r4-windows
export NDK
其中"NDK"这个名字随便起,因为后面要用经常使用,建议不要太长。
重启cygwin,输入cd $NDK可以进入对应目录,就成功了
c). 确保系统已经安装JDK 5以上版本。
Linux:
在Windows环境也可安装VMWare,模拟带图形界面的Linux开发环境,它的环境配置与下相同:
a). 下载NDK开发包(例如android-ndk-1.6_r1-linux-x86.zip)。
b).  终端中运行:
gedit ~/.bashrc
在打开的配置文件中为当前用户添加环境变量:
NDK=<android ndk 目录> 例如:
NDK=/home/Android_ndk_1.6/android-ndk-1.6_r1
export NDK
c).  确保系统已经安装JDK 5以上版本。
 3).编辑编译脚本
在android项目目录下建立jni目录,将C/C++源文件复制到该目录,生成并编辑Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := jni-test
LOCAL_SRC_FILES := hell.c
LOCAL_LDLIBS :=-llog
include $(BUILD_SHARED_LIBRARY)
指定模块名称(jni-test), 需编译的文件(hell.c), 编译方式(动态链接库),以及根据需要添加程序依赖的库文件(例如-llog使用android 打印输出信息)。
4).编译
(windows环境需打开cygwin)cd至项目目录,运行$NDK/ndk-build, NDK编译并生成库文件(libjni-hello.so), 可到项目目录libs/armeabi验证库文件的存在。
5).运行
直接在模拟器或者设备运行编译成功后的.apk程序。
利用NDK编译成的.apk文件已经打包了.so文件,如果不想自动打包,可将android工程目录libs/armeabi下的.so库删除并编译生成一个未打包库文件的apk,然后手动启动adb, 将之前的.so库文件push到模拟器或设备的system/libs目录下。
但是,模拟器或者设备的system/lib目录默认是read-only的,必须首先改变它的属性为可写,才能利用adb将库文件push到system/lib目录。
首先进入adb shell,运行mount查看文件系统:
...
/dev/block/mtdblock0 /system yaffs2 ro 0 0
/dev/block/mtdblock1 /data yaffs2 rw,nosuid,nodev 0 0
...
可以看到,/system是挂靠在/dev/block/mtdblock0(不同设备可能不同),参照红线部分,运行:# mount -o remount -rw /dev/block/mtdblock0 system
即可改变系统目录属性。
2. 在Android 源码环境下编译
1).  将需要编译的C/C++源文件添加至android源码目录,例如添加hello.c到Android_SDK/frameworks/base/JniTest
2).  修改android.mk文件
LOCAL_MODULE    := libjni-test
LOCAL_SRC_FILES := hello.c
#LOCAL_PRELINK_MODULE := false
LOCAL_C_INCLUDES += $(JNI_H_INCLUDE)
LOCAL_SHARED_LIBRARIES := libcutils
#LOCAL_LDLIBS :=-llog
include $(BUILD_SHARED_LIBRARY)
•指定动态库是否需要提前添加映射信息。Android系统为动态库提供了一种映射模式,在该模式下能以更快的方式加载库文件。
具体需修改build/core/prelink-linux-arm.map中的信息,指定动态库的地址,例如
libjni-test.so      0x9A000000
如果不需要进行地址映射,需要在android.mk中做如下设定:
LOCAL_PRELINK_MODULE := false
•添加编译需要的JNI头文件路径及其它所依赖的动态库
LOCAL_C_INCLUDES += $(JNI_H_INCLUDE)
LOCAL_SHARED_LIBRARIES := libcutils(打印log信息)
3). 修改C/C++源码
可以包含AndroidRuntime.h后直接调用AndroidRuntime::registerNativeMethods进行函数注册以及使用android内部logcat进行信息输出等。
4). 编译android整个源码,如果编译成功,则可以在out/target/product/generic/system/lib
下找到编译成功的libjni-test.so文件
5). 运行
将编译成功的system.img替换模拟器或者设备使用的system.img
例如模拟器替换路径:android-sdk-windows\platforms\android-7\images)
 
利用java调用C/C++流程总结:
图2
 
利用C/C++调用java
在android库文件中,实现C/C++调用java,有以下步骤:
一、获取指定对象的类定义(jclass)
有两种途径来获取对象的类定义:
1.  在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码  :
//把点号换成斜杠
jclass cls = (*env)->FindClass(env, "sam/test/jnitest/JniTest");
2. 通过传入的参数对象直接得到其所对应的类定义 
//其中obj是要引用的对象, 类型是jobject
jclass cls = (*env)-> GetObjectClass(env, obj);
二、读取要调用方法的定义(jmethodID)
我们先来看看Android jni.h中获取方法定义的函数:
jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。
env就是JNI环境;第二个参数class是对象的类定义;第三个参数是方法名称;第四个参数,是方法的定义,方法定义的规则可以参照前面章节【建立jni与JniTest类的映射表】中的介绍。
•/* 假设我们已经有一个 sam.test.jnitest. JniTest的实例obj */  
//获取实例的类定义  
jclass Javacls = (*env)-> GetObjectClass (env, obj);
  jmethodID mid=(*env)->GetMethodID (env, Javacls, "GetJavaMessage", "()Ljava/lang/String; ");
if(mid ==0)return;
三、调用对象方法和属性
1.调用对象方法。
为了调用对象的某个方法,可以使用函数CallxxxxMethod或者CallStaticxxxxMethod(访问类的静态方法),根据不同的返回类型而定。例如:
CallIntMethod,CallCharMethod,CallStaticVoidMethod
在该例中调用一个返回string类型的方法,调用如下:
jstring msg = (*env)-> CallObjectMethod(env, obj, mid);
/* 如果该方法是静态方法,只需要将 最后一句代码改为以下写法: jstring msg = (*env)-> CallStaticObjectMethod (env, Javacls , mid); */
2.读取和设置属性值

访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已,有几个方法用来读取和设置类的属性,它们是:
     GetField、SetField、GetStaticField、SetStaticField。比如读取JniTest类的strMsg属性就可以用GetObjectField,相关代码如下 :
  jclass Javacls = (*env)->GetObjectClass (env, obj);
        
jfieldID field = (*env)->GetFieldID(env,Javacls,"strMsg", "Ljava/lang/String;");
jstring msgField = (*env)->GetObjectField(env, obj, field);
也可以改变类的属性:
(* env)->SetObjectField( env, obj, field,(* env)->NewStringUTF( env, "Changed to C string"));
四、处理异常
C/C++中调用Java时,注意捕获并处理Java方法抛出的异常信息。
异常应在每个方法调用后检查:
  msg = (jstring)env->CallObjectMethod(obj, mid);
       if (env->ExceptionOccurred())
       {
           env->ExceptionDescribe();         
            env->ExceptionClear();
           return;
        }
 
结束
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android JNI是一种可用于在Java代码中调用C/C++代码的技术,驱动ko则是指内核模块文件,通常用于实现与硬件设备的交互。在Android应用中,我们可以使用JNI调用驱动ko,实现与底层硬件的交互。 要在Android应用中调用驱动ko,首先需要在应用中使用JNI技术编写C/C++代码,实现与驱动ko的交互逻辑。在C/C++代码中,我们可以通过使用Linux系统提供的API来访问并且控制驱动ko。通过JNI,我们可以将C/C++代码嵌入到Java代码中,并在Android应用中调用这些代码。 具体来说,调用驱动ko的过程如下: 1. 在JNI层编写与驱动ko交互的C/C++代码。这些代码可以使用Linux系统提供的ioctl函数等API来与驱动ko进行通信,并发送相应的指令和数据。 2. 在Java层编写对应的JNI接口和方法。这些方法将与JNI层的C/C++代码进行绑定,以便在Java代码中调用这些方法来间接调用驱动ko。 3. 在Java代码中加载JNI库,并调用JNI接口方法。加载JNI库可以使用System.loadLibrary方法,然后在Java代码中就可以调用JNI接口方法,进而间接调用驱动ko。 通过这种方式,我们可以在Android应用中通过JNI技术调用驱动ko,实现与底层硬件的交互。注意,使用JNI调用驱动ko需要确保设备具备相应的权限,否则可能会出现权限不足的错误。同时,也需要确保驱动ko文件存在于系统中,并且可以被应用访问到。 总之,使用Android JNI可以实现在应用中调用驱动ko文件的功能,通过编写C/C++代码并与Java代码进行绑定,实现与底层硬件交互的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值