【进阶解密】JNI初学篇(一)-静态注册
一、JNI简介
Android系统按语言来划分的话由两个世界组成,分别是。为什么要这样划分呢?Android系统全部由Java开发不行吗?除了性能的原因外,更多的原因是,Java诞生前,有多程序代码都是由Native语言写的,因此重复利用这些Native语言编写的库十分必要,况且Native拥有更好的性能。这样就产生一个问题,如何将Java世界的代码和Native代码连接起来呢?这就需要一个桥梁,也就是JNI。
JNI
是Java Native Interface
的缩写,译为java本地接口,是java与其他语言通信的桥梁。当java语言无法处理的时候,就可以使用JNI
技术来完成,主要有以下几种情况需要使用JNI技术。● 需要调用Java语言不支持的依赖于操作系统平台特性的一些功能。例如:需要调用当前的UNIX系统的某个功能,而Java不支持这个功能,就需要用到JNI技术来实现
● 为了整合以前的非Java语言开发的系统。例如早期使用C/C++实现的功能或系统,通过JNI可以直接整合到项目,而无需再使用Java语言重复造轮子
● 为了提升运行效率,节约程序运行时间。例如:游戏,音视频编解码、图形绘制等,使用C/C++开发会运行更快
二、手动实现JNI demo(静态注册)
Native方法注册分为静态注册和动态注册,先对两者进行简要说明:
静态注册
原理:根据函数名来建立 java 方法与 JNI 函数的一一对应关系
实现流程:
编写 java 代码;
利用 javah 指令生成对应的 .h 文件;
对 .h 中的声明进行实现;
缺点
编写不方便,JNI 方法名字必须遵循规则且名字很长;
编写过程步骤多,不方便;
程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,
然后建立对应关系,这个过程比较耗时;
动态注册
原理
利用 RegisterNatives 方法来注册 java 方法与 JNI 函数的一一对应关系;
实现流程
1、java中定义Native方法
2、编写C/C++文件,实现jni接口方法,以及JNI_OnLoad方法
3、编译so库
4、java加载so库,调用native方法
优点:
灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高
缺点
对新手来说稍微有点难理解, 同时会由于搞错签名, 方法, 导致注册失败
下面分别对这两种注册方法进行讲解。
本篇我们先讲一下如何静态注册Native,并实现我们的JNI demo
1、首先我们得创建一个Native方法吧,所以新建类JNITest.java,并创建Native方法getName
public class JNITest {
static {
System.loadLibrary("mgjnitest");
}
public static native String getName();
}
2、第二步,我们需要手动使用Javac 命令生产.h头文件:jni_com_cj_constom_jnitest_JNITest.h
C:\workspace\MyPracticeApp\jnitest\src\main\java\jni\com\cj\constom\jnitest>javac JNITest.java
C:\workspace\MyPracticeApp\jnitest\src\main\java\jni\com\cj\constom\jnitest>cd ../../../../../
C:\workspace\MyPracticeApp\jnitest\src\main\java>javah -jni jni.com.cj.constom.jnitest.JNITest
C:\workspace\MyPracticeApp\jnitest\src\main\java>
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_com_cj_constom_jnitest_JNITest */
#ifndef _Included_jni_com_cj_constom_jnitest_JNITest
#define _Included_jni_com_cj_constom_jnitest_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jni_com_cj_constom_jnitest_JNITest
* Method: getName
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_jni_com_cj_constom_jnitest_JNITest_getName
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
3、根据.h头文件,创建对应的.c文件jni_com_cj_constom_jnitest_JNITest.c,用于Native层的具体实现
#include <jni.h>
#include <string.h>
jstring Java_jni_com_cj_constom_jnitest_JNITest_getName(JNIEnv *env,jclass type){
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
备注:jstring Java_jni_com_cj_constom_jnitest_JNITest_getName(JNIEnv *env,jclass type)方法中,和java层的方法定义有两点差异:
1、返回值不同于java中的String,而是jString,这个是jni所定义的数据类型别名,可以参考下方“java、C/C++,jni数据类型参照表”和“引用类型JNI参照表”
2、另外方法名称由java层的getName变为了Java_jni_com_cj_constom_jnitest_JNITest_getName,JAVA开头指的是从JAVA层来调用JNI方法的,jni_com_cj_constom_jnitest_JNITest_getName则指的是 包名+类名+方法名
引用类型jni参照表.png
4、将.h文件和.c文件存放到同一个目录,这里我们建立一个jni目录
image.png5、在上一步创建的jni目录中,创建NDK编译规则Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := mgjnitest
LOCAL_SRC_FILES := jni_com_cj_constom_jnitest_JNITest.c
include $(BUILD_SHARED_LIBRARY)
Android.mk文件参数规则说明如下:
# 此变量表示源文件在开发树中的位置
# 在这里,构建系统提供的宏函数 my-dir 将返回当前目录(包含 Android.mk 文件本身的目录)的路径
LOCAL_PATH := $(call my-dir)
# 清除除了LOCAL_PATH之外的所有LOCAL_XXX变量
# 这个清理动作是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能避免相互影响
include $(CLEAR_VARS)
# 表示Android.mk中的每一个模块,名字必须唯一且不包含空格
# 构建系统在生成最终共享库文件时,会将正确的前缀和后缀自动添加到您分配给 LOCAL_MODULE 的名称
LOCAL_MODULE := hello-jni
# 此可选变量可让您覆盖构建系统默认用于其生成的文件的名称
# 例如,如果 LOCAL_MODULE 的名称为 foo,您可以强制系统将它生成的文件命名为 libnewfoo
LOCAL_MODULE_FILENAME := libhello-jni
# 枚举源文件,以空格分隔多个文件
# LOCAL_SRC_FILES 变量必须包含要构建到模块中的 C 和/或 C++ 源文件列表
LOCAL_SRC_FILES =: src/hello-jni.cpp \
src/hello-jnicallback.cpp
# 此变量用于存储当前模块依赖的静态库模块列表
# 如果当前模块是共享库或可执行文件,此变量将强制这些库链接到生成的二进制文件
# 如果当前模块是静态库,此变量只是指示,依赖当前模块的模块也会依赖列出的库
LOCAL_STATIC_LIBRARIES := world-jni
# 此变量包含在构建共享库或可执行文件时要使用的其他链接器标志列表
# 它可让您使用 -l 前缀传递特定系统库的名称
# 例如,以下示例 -lz 指示链接器生成在加载时链接到 /system/lib/libz.so 的模块
LOCAL_LDLIBS := -llog -lz -lm -landroid
# 设置头文件的include目录
LOCAL_C_INCLUDES := $(LOCAL_PATH)//include
# 指向 GNU Makefile 脚本,用于收集您自最近 include 后在 LOCAL_XXX 变量中定义的所有信息
# 此脚本确定要构建的内容及其操作方法
# BUILD_STATIC_LIBRARY: 编译为静态库,静态库变量导致构建系统生成扩展名为 .a 的库
# BUILD_SHARED_LIBRARY: 编译为动态库,共享库变量导致构建系统生成具有 .so 扩展名的库文件
# BUILD_EXECUTABLE: 编译为Native C可执行程序
# BUILD_PREBUILT: 该模块已经预先编译,指向预建共享库的单一路径,例如 foo/libfoo.so
include $(BUILD_SHARED_LIBRARY)
6、在项目根目录创建Application.mk
#APP_ABI := armeabi armeabi-v7a arm64-v8a x86
APP_ABI := all
APP_OPTIM := release
## 引用静态库
APP_STL := stlport_static
#NDK_TOOLCHAIN_VERSION=4.8
#APP_PLATFORM := android-14
Application.mk的参数规则如下:
# 此变量用于存储应用项目根目录的绝对路径
# 构建系统使用此信息将生成的 JNI 共享库的简缩版放入 APK 生成工具已知的特定位置
# 如果将 Application.mk 文件放在 $NDK/apps/<myapp>/ 下,则必须定义此变量
# 如果将其放在 $PROJECT/jni/ 下,则此变量可选
# APP_PROJECT_PATH
# 将此可选变量定义为 release 或 debug,在构建应用的模块时可使用它来更改优化级别
# 发行模式是默认模式,可生成高度优化的二进制文件。调试模式会生成未优化的二进制文件,更容易调试
# 请注意,您可以调试发行或调试二进制文件。但发行二进制文件在调试时提供的信息较少
# 例如,构建系统会选择某些合适的变量,您无需检查它们
# 此外,代码重新排序可能增大单步调试代码的难度;堆叠追踪可能不可靠
# 在应用清单的 <application> 标记中声明 android:debuggable 将导致此变量默认使用 debug而非 release
# 将 APP_OPTIM 设置为 release 可替换此默认值
# APP_OPTIM
# 指定机器指令集
# APP_ABI := all
APP_ABI := armeabi-v7a arm64-v8a
# 目标 Android 平台的名称
APP_PLATFORM := android-26
7、使用NDK工具对.c文件进行编译,生成对应的so库
C:\workspace\MyPracticeApp\jnitest>cd jni
C:\workspace\MyPracticeApp\jnitest\jni>ndk-build
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16.
[arm64-v8a] Compile : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[arm64-v8a] SharedLibrary : libmgjnitest.so
[arm64-v8a] Install : libmgjnitest.so => libs/arm64-v8a/libmgjnitest.so
[armeabi-v7a] Compile thumb : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[armeabi-v7a] SharedLibrary : libmgjnitest.so
[armeabi-v7a] Install : libmgjnitest.so => libs/armeabi-v7a/libmgjnitest.so
[x86] Compile : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[x86] SharedLibrary : libmgjnitest.so
[x86] Install : libmgjnitest.so => libs/x86/libmgjnitest.so
[x86_64] Compile : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[x86_64] SharedLibrary : libmgjnitest.so
[x86_64] Install : libmgjnitest.so => libs/x86_64/libmgjnitest.so
image.png
8、配置jnitest module中的build.gradle文件,对
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {//打包apk时所集成的so库类型
abiFilters "armeabi-v7a"
}
}
externalNativeBuild{//配置ndk编译路径,自动化ndk编译,从而无需第七步的手动ndk-build命令编译
ndkBuild{
path "jni/Android.mk"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {//指定jnilib目录路径
main {
jni.srcDirs = []
jniLibs.srcDirs = ['libs']
}
}
}
9、开始从Java层调用Native层的方法,回到我们最初创建的JNITest.java文件,我们在static初始化中加载Native so,然后我们就可以在Activity中调用getName方法了
public class JNITest {
static {
System.loadLibrary("mgjnitest");
}
public static native String getName();
}
MainActivity.java中的按钮点击事件,通过JNITest.getName()获取到我们.c文件中返回的"Hello from JNI !"字符串
public void getJNIValue(View view) {
String test = JNITest.getName();
Toast.makeText(getApplicationContext(), "value=" + test, Toast.LENGTH_SHORT).show();
}
运行效果图如下,第一个JNI demo成功!
demo github地址为:
https://github.com/MRCHENJUN/JNItest/tree/master/MyPracticeApp