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, );
  24. printf("%s! \n", 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)所示:

  1. (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\n", env->GetStringUTFChars(str, )); // 打印日志
  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中的build\intermediates\...目录下找到)'

选项:-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()));
  1.  

JNI与NDK简析(一)的更多相关文章

  1. 《Android开发艺术探索》读书笔记 (13) 第13章 综合技术、第14章 JNI和NDK编程、第15章 Android性能优化

    第13章 综合技术 13.1 使用CrashHandler来获取应用的Crash信息 (1)应用发生Crash在所难免,但是如何采集crash信息以供后续开发处理这类问题呢?利用Thread类的set ...

  2. Android JNI(一)——NDK与JNI基础

    本系列文章如下: Android JNI(一)——NDK与JNI基础 Android JNI学习(二)——实战JNI之“hello world” Android JNI学习(三)——Java与Nati ...

  3. Android -- 多媒体播放之MediaPlayer使用内部实现简析

    Android -- MediaPlayer内部实现简析 在之前的博客中,已经介绍了使用MediaPlayer时要注意的内容.如今,这里就通过一个MediaPlayer代码实例,来进一步分析Media ...

  4. CGLib 简析

    背景 JDK 动态代理存在的一些问题: 调用效率低 JDK 通过反射实现动态代理调用,这意味着低下的调用效率: 每次调用 Method.invoke() 都会检查方法的可见性.校验参数是否匹配,过程涉 ...

  5. JNI和NDK编程

    Java JNI的本意是Java Native Interface(Java本地接口),它是为了方便Java调用C.C++等本地代码所封装的一层接口.通过Java JNI,用户可以调用C.C++所编写 ...

  6. 简析.NET Core 以及与 .NET Framework的关系

    简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...

  7. 简析 .NET Core 构成体系

    简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...

  8. RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  9. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

随机推荐

  1. 记一次Task抛异常,调用线程处理而引发的一些随想

    记一次Task抛异常,调用线程处理而引发的一些随想 多线程调用,任务线程抛出异常如何在另一个线程(调用线程)中捕获并进行处理的问题. 1.任务线程在任务线程执行语句上抛出异常. 例如: private ...

  2. bs4使用

    目录 安装使用 遍历文档树 查找文档树 安装使用 # 安装 pip3 install beautifulsoup4 from bs4 import BeautifulSoup soup=Beautif ...

  3. 【Java技术系列】爱情36技之Bug大战

    1. 鲁迅先生说:程序员,天不怕地不怕,就怕小虫儿爬呀爬,爬呀爬. 随着时间的推移,鲁迅先生又说:真正勇猛的程序员,敢于让虫子面对惨淡的虫生. 虫子在程序员心中是啥东西?虫子的学名为 Bug,是多少入 ...

  4. MTK Android Android数据保存到系统数据库

    如果有留意Android中系统设置Settings里面的源码,你会发现代码中频繁用到了Settings.System操作,该类通过键值对的形式,将一些特定的值以全局的模式保存到Setting的数据库中 ...

  5. Tcl编成第二天,set与unset

    代码如下: #!/usr/bin/tclsh set value "one" puts $value unset value puts $value set表示创建一个变量第一个参 ...

  6. JVM日常排查问题。基本操作和命令

    1.jstat jstat -gcutil pid 5s    //pid进程号 每隔5s监控一次内存回收情况 E 代表 Eden 区使用率:O(Old)代表老年代使用率    :P(Permanen ...

  7. 使用Network Emulator Toolkit工具模拟网络丢包测试(下)

    用户会在各种网络环境下使用我们的App,PC应用,我们决不能祈求用户的网络环境都是稳定的,因此我们需要模拟出弱网络的情况,用来测试我们的APP在弱网络环境下的表现如何.Network Emulator ...

  8. AJ学IOS 之微博项目实战(12)发送微博自定义工具条代理实现点击事件

    AJ分享,必须精品 一:效果 二:封装好的工具条 NYComposeToolbar.h 带代理方法 #import <UIKit/UIKit.h> typedef enum { NYCom ...

  9. JMF 下载安装与测试 测试成功

    本来就是想在自己写的java里面加入实习的摄像头监控,然后个各种百度了一下,就用JMF来弄了,不过这个东西貌似比较旧,网上的资料虽然说有,但是也不是太多,并且遇到的一下问题也不能解决,总之经过了一天的 ...

  10. android学习笔记——计时器实现

    根据android疯狂讲义来写写代码,在博客里面将这些写过的代码汇总一下.实现的功能很简单:就是一个简单的计时器,点击启动按钮会开始计时,当计时到20秒时会自动停止计时. 界面如下: 界面代码: &l ...