JNI学习(一)

本文介绍了JNI(JavaNativeInterface)的作用,如何通过NDK开发C/C++动态库,以及ABI的概念。还涵盖了静态和动态注册的区别,以及JNI在Android中的应用和常见问题。
摘要由CSDN通过智能技术生成

什么是JNI?

JNI(java Native Interface)java本地接口,是为方便java调用C或者C++等本地的代码所封装的一层接口。由于java的跨平台性导致本地交互能力不好,一些和操作系统相关的特性Java无法完成,于是Java提供了JNI专门用于和本地代码交互

什么是NDK?

NDK(Native Development Kit)本地开发工具链,是Android提供的一个工具合集,帮助开发者快速开发C(或C++)的动态库,并能自动将.so和java应用一起打包成apk。NDK集成了交叉编译器(交叉编译器需要UNIX或LINUX系统环境),并提供了相应的mk文件或CMake文件隔离cpu、平台、ABI等差异,开发人员只需要简单修改mk文件或CMake文件(指出"哪些文件需要编译"、"编译特性要求"等),就可以创建出.so。

什么是ABI?

ABI(Application binary interface)应用程序二进制接口。不同CPU与指令集的每种组合都有定义的ABI(应用程序而进程接口),一段程序只有遵循这个接口规范才能在该CPU上运行,所以同样的程序代码为了兼容多个不同的cpu,需要为不同的ABI构建不同的库文件。当然对于CPU来说,不同的构建并不意味着一定互不兼容。

armeabi设备只兼容armeabi;
armeabi-v7a设备兼容armeabi-v7a、armeabi;
arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
X86设备兼容X86、armeabi;
X86_64设备兼容X86_64、X86、armeabi;
mips64设备兼容mips64、mips;
mips只兼容mips;

Android的应用层的类都是以java写的,这些java类编译为Dex型式的字节码之后,必须依靠Dalvik虚拟机来运行,在Android中Dalvik虚拟机中扮演很重要的角色。而Android中间件是由C/C++写的,这些C/C++写的组件并不是在Dalvik虚拟机上运行的,一旦使用JNI,JAVA程序就丧失了JAVA平台的两个优点:
1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。
2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数及各类中。这样就降低了Java和C之间的耦合性。

库的加载

在Android中提供了System.loadLibrary()或者System.load()来加载库。那这两个方法有啥区别呢?
System.load的参数必须为库文件的绝对路径,可以是任意路径,例如:System.load(“usr/lib.TestJNI.so”)
System.loadLibrary()方法的参数为库文件名,不包含库文件的扩展名。例如:System.loadLibrary(“TestJNI”);

需要注意的是,如果.so动态库不存在时,会抛出couldn’t find "libxxx.so"异常

    load library error=dalvik.system.PathClassLoader[DexPathList[[
    zip file "/data/app/com.frank.ffmpeg/base.apk"],
    nativeLibraryDirectories=[/data/app/com.frank.ffmpeg/lib/arm64,
    data/app/com.frank.ffmpeg/base.apk!/lib/arm64-v8a,/system/lib64, /vendor/lib64, /product/lib64]]]
    couldn't find "libhello.so"

如果期待加载的是64bit的库,却加载到32bit的,会报错如下:

java.lang.UnsatisfiedLinkError: dlopen failed: “xxx.so” is 32-bit instead of 64-bit

动态注册与静态注册

java层调用带native关键字的JNI方法,需要注册java层与native层的对应关系,有静态注册和动态注册两种方式。静态注册一般是应用层使用,绑定包名+类名+方法名,在调用JNI方法时,通过类加载器查找对应的函数。动态注册一般是当Java通过System.load加载完JNI动态库后,紧接着会调用JNI_OnLoad,当JNI_OnLoad回调时,把JNINativeMethod注册到函数表。

静态注册

Android 提供了两种开发jni的方式 1、用.mk文件的方式进行JNI开发
2、用CMake的方式进行JNI开发

mk文件方式静态注册的步骤:

1、创建JNIDemo工程,编写java类,例如:TextDemo.java
2、在命令行下输入javac TextDemo.java 生成TextDemo.class文件
3、在TextDemo.class目录下通过 javah -jni 包名.TextDemo(全类名)生成包名_TextDemo.h文件
4、在app目录下新建jni文件夹,在jin文件夹下创建test.c文件
5、编写test.c文件,并拷贝包名_TextDemo.h下的函数,并实现这些函数,且在其中导入jni.h,包名_TextDemo.h头文件。
6、在jni文件夹下创建Android.mk文件,并在Android.mk文件中指定要生成的so库名称以及要使用的test.c源文件
7、在app目录下执行ndk-build命令生成对应的so文件‘
8 、在AndroidStudio里面配置ndk的路径以及so所在的目录,在local.properties文件里面添加ndk的路径:ndk.dir=E:\sdk\ndk-bundle
在build.gradle文件的buildType标签里面添加:

   	//放在libs目录中
       sourceSets.main{
            {
               jniLibs.srcDirs = ['libs']
           }
       }

9、运行so库。
.mk方式加载so可参考博客

Cmake方式静态注册的步骤

1、创建JNIDemo工程,编写java类,例如:TextDemo.java
2、build project 整个工程,在build\intermediates\javac\debug\classes\包名 文件下会生成TextDemo.class文件
3、在build\intermediates\javac\debug\classes 目录下打开命令行,输入 javah -jni 包名.TextDemo(全类名)生成包名_TextDemo.h文件
4、在app目录下新建jni文件夹,在jin文件夹下创建test.c文件
5、编写test.c文件,并拷贝包名_TextDemo.h下的函数,并实现这些函数,且在其中导入jni.h,包名_TextDemo.h头文件。
6、在jni文件夹下创建CMakeLists.txt文件,并在CMakeLists.txt文件中指定要生成的so库名称以及要使用的test.c源文件
7、在build.gradle文件中添加cmkeLists.text文件的路径,如下:

externalNativeBuild {
        cmake {
            path file('jni/CMakeLists.txt')
            version '3.22.1'
        }
    }

8、运行so库。
因为CMake是跨平台的构建工具,对JNI开发来说有比较明显的优点就是:1、编写C代码有提示,2、自动搜索正在构建的软件可能需要的程序、库和头文件的能力,3、能够为自动生成的文件创建复杂的自定义命令,其他的高级特性可自行搜索

通过上面两种方式我们可以先熟悉一下开发JNI的流程。接下来我们看一下静态注册生成的.h文件和.c文件

com_example_jnidemo_TextDemo.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnidemo_TextDemo */

#ifndef _Included_com_example_jnidemo_TextDemo
#define _Included_com_example_jnidemo_TextDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_jnidemo_TextDemo
 * Method:    getText
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring
 JNICALL Java_com_example_jnidemo_TextDemo_getText
  (JNIEnv *, jobject);

/*
 * Class:     com_example_jnidemo_TextDemo
 * Method:    setText
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_example_jnidemo_TextDemo_setText
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
在这里插入代码片
test.c
#include <jni.h>
#include "com_example_jnidemo_TextDemo.h"
#include <string.h>
#include <android/log.h>

JNIEXPORT jstring JNICALL Java_com_example_jnidemo_TextDemo_getText
  (JNIEnv *env, jobject this){
//参数一是日志级别,参数二是日志TAG,参数三就是日志内容
    __android_log_print(ANDROID_LOG_ERROR, "test", "invoke get from C\n");
    return (*env)->NewStringUTF(env, "Hello world from c  !");
  }

  JNIEXPORT void JNICALL Java_com_example_jnidemo_TextDemo_setText
    (JNIEnv *env, jobject this, jstring string){
__android_log_print(ANDROID_LOG_ERROR, "test", "invoke set from C\n");
    char* str = (char*)(*env)->GetStringUTFChars(env,string,NULL);
    __android_log_print(ANDROID_LOG_ERROR, "test", "%s\n", str);
    (*env)->ReleaseStringUTFChars(env, string, str);
    }

可以看到静态注册是根据函数名来建立java方法和JNI函数间的一一对应关系。
静态注册的弊端:
1、后期类名、文件名改动,头文件所有函数将失效,需要手动改,超级麻烦易出错
2、代码编写不方便,由于JNI层函数的名字必须遵循特定的格式,且名字特别长
3、会导致程序员的工作量很大,因为必须为所有声明了native函数的java类编写JNI头文件
4、程序运行效率低,因为初次调用native函数时需要根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。

遇到问题:平时我们也可以把native方法写在MainActivity里面,此时直接执行javah -jni 包名.MainActivity会出现找不到该类中的androidx.appcompat.app.AppCompatActivity类,解决办法如下:
\JNIDemo\app\src\main\java> javah -jni -bootclasspath E:\sdk\platforms\android-33\android.jar com.example.jnidemo.MainActivity

可参考:解决javah生成.h头文件找不到找不到android.support.v7.app.AppCompatActivity的问题
android .class文件,javah编译class文件找不到android.app.Activity的类文件

  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android JNI学习路线可以按照以下步骤进行: 1. 了解JNI的基本概念和作用:JNI(Java Native Interface)是Java提供的一种机制,用于实现Java与其他编程语言(如C、C++)之间的交互。它允许在Java代码中调用本地代码(Native Code),并且可以在本地代码中调用Java代码。 2. 学习JNI的基本语法和规则:JNI使用一组特定的函数和数据类型来实现Java与本地代码之间的交互。你需要学习如何声明本地方法、如何在Java代码中调用本地方法、如何在本地代码中调用Java方法等。 3. 学习JNI的数据类型映射:JNI提供了一套数据类型映射规则,用于将Java数据类型映射到本地代码中的数据类型。你需要学习如何处理基本数据类型、对象类型、数组类型等。 4. 学习JNI的异常处理:在JNI中,Java代码和本地代码之间的异常处理是非常重要的。你需要学习如何在本地代码中抛出异常、如何在Java代码中捕获异常等。 5. 学习JNI的线程处理:JNI允许在本地代码中创建和操作线程。你需要学习如何创建和销毁线程、如何在线程之间进行通信等。 6. 学习JNI的性能优化:JNI涉及到Java代码和本地代码之间的频繁切换,因此性能优化是非常重要的。你需要学习如何减少JNI调用的次数、如何避免不必要的数据拷贝等。 7. 学习JNI的调试和测试:在开发JNI程序时,调试和测试是非常重要的。你需要学习如何使用调试器调试本地代码、如何进行单元测试等。 8. 学习JNI的进阶主题:一旦掌握了基本的JNI知识,你可以进一步学习JNI的进阶主题,如JNI与Java虚拟机的交互、JNI与动态链接库的交互、JNI与多线程的交互等。 总结起来,Android JNI学习路线包括了基本概念、基本语法、数据类型映射、异常处理、线程处理、性能优化、调试和测试以及进阶主题等内容。通过系统地学习这些知识,你将能够更好地理解和应用JNI技术。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值