Android JNI解析(一)

目录

1.前言

2.JNI的作用

3.JNI的实现方式

3.1传统的ndk-build命令行生成so

3.2 结合AS配置生成so

3.3 最新的CMake配置生成so

4.JNI函数的注册方式

5.结束语

 


 

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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值