zoukankan      html  css  js  c++  java
  • JNI与NDK简析(一)


    1 JNI 简介

    在Android Framework中,需要提供一种媒介或 桥梁,将Java层(上层)与C/C++层(下层)有机的联系起来,使得他们互相协调完成某些任务。而充当这种媒介的就是Java本地接口(JNI,Java Native Interface)。

    JNI提供一些列的接口,允许Java类与C/C++等本地编辑语言(在JNI中,这些语言被称为 本地语言)编写的应用 程序、模块 、库进行交互操作。比如,在Java类中使用C语言库中的函数或在C语言中使用 Java类库,都需要借助JNI。

    Android NDK是一个开发工具集,提供一系列工具快速开发C/C++的动态库,并能自动将 .so/.dll 和 Java 应用一起打包到Apk;

    NDK提供工具可以方便JNI调用C/C++,而且提供了交叉编译器可以修改.mk文件生成特定CPU平台的动态库,并能将so和java应用一起打包到apk中;简单说就是JNI负责Java与C/C++进行互相操作,NDK提供工具方便在Android平台使用JNI;
     

    2 JNI 的使用场景

    JNI通常有下列使用场景:

    ▨  注重处理速度:

      与本地代码(C/C++等)相比,Java代码的执行速度回慢一些。如果对某段程序的执行速度有较高的要求,建议使用C/C++编写代码。而后在Java中通过JNI 调用基于C/C++编写的部分。在开发图像处理或信号处理这类对CPU处理速度有较高要求的程序时,使用C/C++等本地语言编写的相应模块,执行效率 更高,性能也好得多。

    ▨ 硬件控制:

      为了更好第控制硬件,硬件控制代码通常使用C语言编写。

     已有C/C++代码的复用:

      在编程过程中,常常会使用一些已经编写好的C/C++代码,及提高编写效率,又确保程序的安全性与健壮性,这类在第三方库里较为常见,现在许多第三方库都是有C/C++库编写的,比如Ffmpeg。

     代码保护:

      由于APK的Java层代码很容易反编译,而C/C++库反编译难度很大。

     平台之间移植应用。

    在实际Android应用开发中,开发者通常使用Android  SDK开发Java程序。而对于性能要求较高的,常常使用Android提供的 NDK(Native Development Kit) 开发基于C/C++的本地库。而后再通过 JNI将Java程序与C/C++程序集成在一起。NDK提供了一些列的工具,帮助开发者快速开发C/C++动态库。

    3 Java中调用C函数库

    3.1 在 Android Studio 新建项目

    第一步:我们需要下载两个至关重要的Tools,一个是CMake,一个是NDK。

     

    第二步:新建 Project

     

    创建完成后,如下图:

    C/C++文件一般存放于cpp目录下。接下来我们看下配置文件 build.gradle:

    CMakeLists.txt(代码 3.1-1):

     1 cmake_minimum_required(VERSION 3.4.1) # Android Studio最低要求版本
     2 
     3 add_library( # 当前库名称.
     4              native-lib
     5 
     6              # 将库设置为共享库。
     7              SHARED
     8 
     9              # 加载该库里的文件.
    10              native-lib.cpp)

      

    3.2 多目录,多层次目录时的配置问题

     

    这时,cpp.CMakeLists.txt 设置如下所示(代码 3.2-1):

    1 cmake_minimum_required(VERSION 3.4.1) #指定编译器版本
    2 
    3 #指定子文件夹
    4 add_subdirectory(first)
    5 add_subdirectory(second)

    cpp.first.CMakeLists.txt(代码 3.2-2) 

     1 set(LIBRARY first-lib) # 定义库名称 LIBRARY = first-lib
     2 
     3 file(GLOB_RECURSE cpp_first "./*.cpp") # first目录下的所有 .cpp 文件
     4 
     5 add_library( # 设置库的名称。
     6         ${LIBRARY}
     7 
     8         # 将库设置为共享库。
     9         SHARED
    10 
    11         # 提供源文件的相对路径。
    12         ${cpp_first}
    13         )
    14 
    15 find_library( #设置路径变量的名称。
    16         log-lib
    17 
    18         # 指定您希望CMake定位的NDK库的名称。
    19         log)
    20 
    21 target_link_libraries( #指定目标库。
    22         ${LIBRARY}
    23 
    24         # 将目标库链接到NDK中包含的日志库。
    25         ${log-lib})

      

    3.3 下面看看 java 代码是如何实现的

    以类 dinn.cappjni.HelloJNI.java 为例(代码 3.3-1):

     1 package dinn.cappjni;
     2 
     3 public class HelloJNI {
     4 
     5     static {
     6         System.loadLibrary("first-lib"); // 加载本地库“first-lib”,即 代码 3.2-2
     7     }
     8 
     9     public native void printHello(); // ① 使用【native】关键字申明本地方法,该方法与用C++编写的JNI本地函数相对应。
    10 
    11     public native String printString(String str);
    12 }

    那么C++代码如何写呢?我们可通过命令(javah  -jni xxx)生成(注意目录要定位到 java 这层,也可通过 -classpath 重写定位位置。):

     

    生成成功后,刷新工程目录,就可以看见生成的文件:

     

    然后,我们可以将文件移动到cpp目录下。下面是 dinn_cappjni_HelloJNI.h  的内容(代码 3.3-2):

     1 /* DO NOT EDIT THIS FILE - it is machine generated */
     2 #include <jni.h>
     3 /* Header for class dinn_cappjni_HelloJNI */
     4 
     5 #ifndef _Included_dinn_cappjni_HelloJNI
     6 #define _Included_dinn_cappjni_HelloJNI
     7 #ifdef __cplusplus
     8 extern "C" {  // extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。
     9 #endif
    10 /*
    11  * Class:     dinn_cappjni_HelloJNI
    12  * Method:    printHello
    13  * Signature: ()V
    14  */
    15 JNIEXPORT void JNICALL Java_dinn_cappjni_HelloJNI_printHello (JNIEnv *, jobject);
    16 
    17 /*
    18  * Class:     dinn_cappjni_HelloJNI
    19  * Method:    printString
    20  * Signature: (Ljava/lang/String;)Ljava/lang/String;
    21  */
    22 JNIEXPORT jstring JNICALL Java_dinn_cappjni_HelloJNI_printString  (JNIEnv *, jobject, jstring);
    23 
    24 #ifdef __cplusplus
    25 }
    26 #endif
    27 #endif

    接下来,我们实现 dinn_cappjni_HelloJNI.cpp 的内容(代码3.3-3):

     1 #include <string>
     2 #include "dinn_cappjni_HelloJNI.h"
     3 
     4 extern "C" {
     5 /*
     6  * Class:     dinn_appdemojni_HelloJNI
     7  * Method:    printhello
     8  * Signature: ()V
     9  */
    10 JNIEXPORT void JNICALL Java_dinn_cappjni_HelloJNI_printHello(JNIEnv *env, jobject obj) {
    11     printf("Hello JNI!");
    12     return;
    13 }
    14 
    15 /*
    16  * Class:     dinn_appdemojni_HelloJNI
    17  * Method:    printString
    18  * Signature: (Ljava/lang/String;)V
    19  */
    20 JNIEXPORT jstring JNICALL
    21 Java_dinn_cappjni_HelloJNI_printString(JNIEnv *env, jobject obj, jstring str) {
    22     // 将String字符串转换成 C字符串
    23     const char *chars = env->GetStringUTFChars(str, 0);
    24     printf("%s! 
    ", chars);
    25 
    26     std::string strOld = chars;
    27     std::string strNew = "您输入的是:" + strOld;
    28     return env->NewStringUTF(strNew.c_str());
    29 }
    30 }

    最后在 Java 中使用时其实很简单,直接调用类HelloJNI中的方法即可,如(代码 3.3-4)所示:

    (new HelloJNI()).printHello();

     至此,我们实现了 Java 调取本地 C/C++ 函数。那么 本地 C/C++ 库又是怎么调用 Java 的方法呢?

    4 在 C 中调用 Java 方法

     4.1 新建 Java文件 JniTest.java 

     1 public class JniTest {
     2 
     3     private String content;
     4 
     5     public JniTest(String content) {
     6         this.content = content;
     7     }
     8 
     9     // 此方法由本地函数调用
    10     public String getContent() {
    11         return content;
    12     }
    13 }

      

    4.2 在Java文件 HelloJNI.java 中新增获取对象JniTest的方法 createJniTestObject(), 注意要是静态方法:

    1 public static native JniTest createJniTestObject();

      

    4.3 生成该Java方法所对应的C++函数:

    1 JNIEXPORT jobject JNICALL
    2 Java_dinn_cappjni_HelloJNI_createJniTestObject(JNIEnv *, jclass);

     此时我们注意到,生成的C++函数中的第二个参数为 jclass 类型,不再是 jobject。原因是什么呢?想 弄清楚这个,我们需要了解第二个参数的含义。前面的 jobject 类型变量用来保存调用本地方法的对象的引用。而此时的 java 方法为静态(static)的,而静态方法可以不用创建对象,可通过类名直接获取到,因此这里函数的第二个参数为 jclass 类型。

    4.4 dinn_cappjni_HelloJNI.cpp 

     1 #include <string>
     2 #include <android/log.h> // 引用日志的包
     3 #include "dinn_cappjni_HelloJNI.h"
     4 
     5 extern "C" {
     6 #define TAG "日志【C】"
     7 #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
     8 
     9 // ...
    10 
    11 JNIEXPORT jobject JNICALL
    12 Java_dinn_cappjni_HelloJNI_createJniTestObject(JNIEnv *env, jclass clazz) {
    13     // 查找生成对象的类
    14     jclass targetClass = env->FindClass("dinn/cappjni/JniTest"); // 这里要包含类的包名
    15 
    16     // 查找构造方法
    17     jmethodID mid = env->GetMethodID(targetClass, "<init>", "(Ljava/lang/String;)V");
    18     if (mid == NULL) return NULL;
    19 
    20     // 生成 JniTest 对象(返回对象的引用)
    21     jobject newObject = env->NewObject(targetClass, mid, env->NewStringUTF("【生成 JniTest 对象】"));
    22 
    23     // 调用对象方法 getContent();
    24     mid = env->GetMethodID(targetClass, "getContent", "()Ljava/lang/String;");
    25     if (mid == NULL) return newObject;
    26     jstring str = (jstring) env->CallObjectMethod(newObject, mid);
    27 
    28     LOGI("【CPP】类JniTest中的变量content = %s
    ", env->GetStringUTFChars(str, 0)); // 打印日志
    29     return newObject;
    30 }
    31 
    32 }

     说明:

    第17行: env->GetMethodID(targetClass, "<init>", "(Ljava/lang/String;)V");

    ◆ 其中第二个参数<init>表示构造方法。如果是非构造方法,直接写方法名称,如第24行。

    ◆ 第三个参数表示Java变量/方法中的参数的签名。在调用某些JNI函数是,要求提供指定的成员变量或成员方法的签名。当然,开发者可以根据JNI规范中的Java签名生成规则,直接创建签名。但不建议这么做,java系统会为类的成员变量或成员方法生成签名。使用时,只需要使用javap 命令(Java反编译器),即可轻松获取指定的成员变量或成员方法的签名。

    形式:javap [选项] '类名(.class后缀的文件,我们可通过javac编译得到,或者在Android Studio中的buildintermediates...目录下找到)'

    选项:-s 输出java签名

         -p 输出所有类及成员

    以JniTest.class为例,如下图所示:

      

    红线处即是该成员方法的签名。

    最后在Java中我们可以获取到 dinn_cappjni_HelloJNI.cpp 返回的JniTest对象。

    1 JniTest jniTest = HelloJNI.createJniTestObject();
    2 if (jniTest != null)
    3     Log.i("日志(Java)", jniTest.getContent()));
    
    
  • 相关阅读:
    整数的溢出或回绕
    C语言每日一题
    C语言刷“矩阵”类题目(2维矩阵/2级指针)
    C语言刷2数/3数/4数之和
    C语言刷数组题记录
    二、IAR创建工程并进行烧写
    三、STM8的学习笔记-----GPIO操作
    51单片机--------如何使用keil软件建立一个工程
    一、搭建mosquitto
    二、解决端口占用被占用情况
  • 原文地址:https://www.cnblogs.com/steffen/p/9211481.html
Copyright © 2011-2022 走看看