目录
1.前言
最近工作上不是很忙,主要精力在于C/C++方面的学习和理解,一直对JNI开发很感兴趣,故此处进行总结和记录。JNI是Java和C/C++沟通的桥梁,到目前为止,其实现也有多种方式,本文旨在对这几种方式进行剖析,并通过简单的demo说明。
2.JNI的作用
相信大部分的Android Developer都知道JNI的作用,所以这里简单的叙述下:许多现有的函数库都是C/C++编写的,如ffmpeg相关so库,若重新用Java实现一套,人力、财力上是浪费的,同时Java的执行效率比C/C++要低,最终程序运行的效果也会差一些。聪明的我们肯定会想,为什么不把已有的so库直接利用起来呢,还能避免“重复制造车轮”的尴尬——这就是JNI的本职工作。
3.JNI的实现方式
参照网上的一些资料及自己的实用经验,JNI的实现分为以下几种:
- 传统的ndk-build命令行生成so;
- 结合AS配置生成so;
- 最新的CMake配置生成so;
对于以上的几种方式,在编写C/C++代码时,方法的注册也分为两种方式,分别为javah生成.h的静态方式和RegisterNatives方法的动态方式;
下面分别创建示例说明
3.1传统的ndk-build命令行生成so
3.1.1新建AS项目,并编写native方法
public class JniTest {
public native String getMyValue();
static {
System.loadLibrary("mylib");
}
}
3.1.2 生成native方法对应的头文件.h,在命令行下执行以下(注意修改为自己的包名)
javah -d D:\develop\study\jnidemo\app\src\main\jni -classpath D:\develop\study\jnidemo\app\src\main\java com.ranfeng.jnidemo.JniTest
可以看到在D:\develop\study\jnidemo\app\src\main\jni下生成的以包名类名组合而成的com_ranfeng_jnidemo_JniTest.h文件,如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_ranfeng_jnidemo_JniTest */
#ifndef _Included_com_ranfeng_jnidemo_JniTest
#define _Included_com_ranfeng_jnidemo_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_ranfeng_jnidemo_JniTest
* Method: getMyValue
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_ranfeng_jnidemo_JniTest_getMyValue
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
其中jni.h文件包含在jdk目录里面,如本人的PC路径:C:\Program Files\Java\jdk1.8.0_151\include,extern “C”所包括的函数表明按照C语言方式编译和链接。
3.1.3 根据生成的.h文件编写com_ranfeng_jnidemo_JniTest.c(或com_ranfeng_jnidemo_JniTest.cpp)文件,如下:
#include "com_ranfeng_jnidemo_JniTest.h"
JNIEXPORT jstring JNICALL Java_com_ranfeng_jnidemo_JniTest_getMyValue
(JNIEnv *env, jobject jobj)
{
return env->NewStringUTF("hello world!");
}
定义.h文件里面所声明的函数,返回一个字符串。
3.1.4编写Android.mk文件,该文件进行相关的编译配置:
#LOCAL_PATH指明当前编译的相对路径,my-dir是将当前的路径赋值与它
LOCAL_PATH := $(call my-dir)
#CLEAR_VARS指编译系统提供一个特殊的GUN MakeFile来清除所有的LOCAL_XXX变量,LOCAL_PATH不会被清除。使用这个变量是因为在编译系统时,所有的控制文件都会在一个GUN Make上下文进行执行,而在此上下文中所有的LOCAL_XXX都是全局的
include $(CLEAR_VARS)
#LOCAL_MODULE指明生成的so库名字。这个名字必须是唯一的同时不能含有空格,会自动的为文件添加适当的前缀或后缀,模块名为“mylib”它将会生成一个名为“libmylib.so”文件。
LOCAL_MODULE := mylib
#LOCAL_SRC_FILES指明需要编译的C/C++文件,相对于LOCAL_PATH所指路径
LOCAL_SRC_FILES := com_ranfeng_jnidemo_JniTest.cpp
#指明一个GUN Makefile脚本,并且收集从最近“include$(CLEAR_VARS)”下的所有LOCALL_XXX变量的信息,最后告诉编译系统如何正确的进行编译
include $(BUILD_SHARED_LIBRARY)
3.1.5编写Application.mk文件,可以配置输出的CPU平台
# all代表全平台 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64,all代表所有平台
APP_ABI := all
以上文件编写完成后,在命令行模式下(或安装cgywin工具)进入的jni目录,执行ndk-build,便可生成对应的so库
3.1.6 最后在MainActivity调用JniTest里的native方法
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.tv_title);
tv.setText(new JniTest().getMyValue());
}
}
同时在app/build.gradle里配置jniLibs路径(android{}层级下),意思是告诉AS在该路径下加载对应的平台的so库
sourceSets{
main {
jni.srcDirs = []
jniLibs.srcDirs "src/main/libs"
}
}
AS运行代码安装到模拟器,可以看到已获取到JNI下返回的字符串
3.2 结合AS配置生成so
这种方式不需要手动执行ndk-build命令,通过相关配置即可,相对3.1更简单些。
3.2.1 前面4步和3.1.1~3.1.4一样,都是先编写好对应的.h、.c/.cpp和Android.mk文件,最后在build.gradle进行ndk的配置如下:
apply plugin: 'com.android.application'
android {
defaultConfig {
...
ndk{
moduleName "mylib" //生成的so名字
ldLibs "log", "z", "m" //添加依赖库文件,如果有log打印等
abiFilters "armeabi-v7a","arm64-v8a", "x86", "x86_64" //输出指定cpu体系结构下的so库。
}
}
...
externalNativeBuild {
ndkBuild {
path file("src/main/jni/Android.mk")
}
}
sourceSets {
main {
jni.srcDirs('src/main/java/jni')
}
}
}
...
3.2.2 在AS的Build选项下,选择Rebuild Project项执行,最终会在 app/build/intermediates/ndkBuild下生成对应的平台的so库。
之后便可直接Run运行到设备上
3.3 最新的CMake配置生成so
3.3.1 新建AS项目勾选Include C++ suport
3.3.2 待项目创建完成后,AS会自动创建CMakeList.txt文件
# 配置so库信息
add_library(
# 输出的so库名称
my_lib
# STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用
# SHARED:动态库,会被动态链接,在运行时被加载
# MODULE:模块库,是不会被链接到其它目标中的插件,但是可能会在运行时使用dlopen-系列的函数动态链接
SHARED
# c/c++代码源文件
src/main/cpp/native-lib.cpp)
# 从系统查找依赖库
find_library(
# 引用log库
log-lib
# 配置库的链接(依赖关系)
target_link_libraries(
my_lib
${log-lib})
3.3.3 build.gradle配置如下,最后直接运行即可
apply plugin: 'com.android.application'
android {
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
...
4.JNI函数的注册方式
前面讲到的几种实现方式都是在JNI层提供固定的函数名称供Java层进行调用,这种函数注册称为静态注册。
在应用层加载so的时候,虚拟机首先会去自动执行JNI_OnLoad(),所以还可以通过JNI_OnLoad()函数中进行动态注册env->RegisterNatives(),在jni.h中定义了结构体JNINativeMethod,该结构体建立了Java层native方法到JNI函数的一一映射关系,这样从Java层到JNI层的调用实现便更加灵活,其实在AOSP源码中framework部分大多采用了这种动态方式,有兴趣的同学可以去了解一下,后面我也会以部分framework部分源码进行专题剖析。
# 摘抄自jni.h
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
4.1 新建包含native方法的Java类:JniTest
public class JniTest {
public native int add(int a,int b);
public native int minus(int a,int b);
static{
System.loadLibrary("my_lib");
}
}
4.2 新建my_onload.h头文件,完成对JNI层打印方法的宏定义
#include<jni.h>
#ifndef _ON_LOAD_HEADER_H__
#define _ON_LOAD_HEADER_H__
#ifdef __cplusplus
extern "C" {
#endif
#include<string.h>
#include<stdlib.h>
#include<android/log.h>
#include<stdio.h>
#define LOG_TAG "ranfeng"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif
4.3 新建my_onload.cpp源文件,里面的JNI_OnLoad函数实现了对Java方法到JNI函数的动态注册,代码比较简单,如下:
#include "my_onload.h"
extern int register_android_jni_durian_android(JNIEnv *env);
static const char *classpath = "com/ranfeng/jnidemo4/JniTest";
namespace android {
jint localAdd(JNIEnv *env, jobject thiz, jint a, jint b) {
LOGI("a : %d b : %d",a,b);
return (jint)(a+b);
}
jint localMinus(JNIEnv *env, jobject thiz, jint a, jint b) {
LOGI("a : %d b : %d",a,b);
return (jint)(a-b);
}
}
using namespace android;
/*typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值
第三个变量fnPtr是函数指针,指向C函数。
其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
"()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Func();
"(II)V" 表示 void Func(int, int);*/
static JNINativeMethod mMethods[] = {
{ "add", "(II)I",(void *) &localAdd },
{ "minus","(II)I",(void *) &localMinus},
};
static JavaVM *sEnv;
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return -1;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return -1;
}
return 0;
}
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = JNI_ERR;
sEnv = vm;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
LOGI("JNI_OnLoad...");
if (jniRegisterNativeMethods(env, classpath, mMethods,sizeof(mMethods) / sizeof(mMethods[0])) != JNI_OK) {
goto end;
}
return JNI_VERSION_1_4;
end: return result;
}
4.4 编写Android.mk文件,该文件进行相关的编译配置:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SHARED_LIBRARY := libnativehelper
LOCAL_MODULE :=my_lib
LOCAL_SRC_FILES := my_onload.cpp
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
4.5编写Application.mk文件,可以配置输出的CPU平台
# all代表全平台
APP_ABI := all
以上文件编写完成后,在命令行模式下(或安装cgywin工具)进入的jni目录,执行ndk-build,便可生成对应的so库
4.6 最后在MainActivity调用JniTest里的native方法
public class MainActivity extends AppCompatActivity {
private final static String TAG = "DurianMainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
JniTest jniTest = new JniTest();
int val1 = jniTest.add(2, 3);
int val2 = jniTest.minus(5,2);
Log.i(TAG, "val1 : " + val1 + " val2 : " + val2 );
}
}
直接运行即可
5.结束语
本文仅较初步的介绍的JNI的实现方式,后面将更深入的剖析jni.h的实现原理及一些更复杂的JNI实现。
本文参考:
https://blog.csdn.net/ezconn/article/details/82529101
https://blog.csdn.net/qq_31726827/article/details/50417327
https://blog.csdn.net/quwei3930921/article/details/78820991
https://blog.csdn.net/qq_31726827/article/details/50417327