第九章 JNI

第九章 JNI

一.JNI与NDK简介

1、JNI(协议)

(1)定义

Java Native Interface,即 Java本地接口,相当于桥梁作用,一种协议;
即在 Java代码 里调用 C、C++等语言的代码 或 C、C++代码调用 Java 代码(互相调用)
Android系统架构中上层(框架层+应用层)JAVA通过JNI调用底层(Linux Kernel层)C;
JNI是 Java 调用 Native 语言的一种特性,是属于 Java 的,与 Android 无直接关系

(2)作用

实际使用中,Java需要与本地代码(Native code)进行交互,因为Java具备跨平台特点,所以Java与本地代码交互能力弱。可采用JNI特性增强Java与本地代码交互能力

2、NDK(工具)

(1)定义

Native Development Kit,是 Android的一个工具开发包;NDK是属于 Android 的,与Java并无直接关系

(2)作用

快速开发C、 C++的动态库,并自动将so和应用一起打包成 APK,即可通过 NDK在 Android中 使用 JNI与本地代码(如C、C++)交互
使Android开发的功能需在本地代码(C/C++)实现

(3)特点
  • 运行效率高:在开发要求高性能的需求功能中,采用C/C++更加有效率,如使用本地代码(C/C++)执行算法,能大大提高算法的执行效率
  • 代码安全性高:java是半解释型语言,容易被反会变后得到源代码,而本地代码(C/C++)不会,能提高系统安全性
  • 功能扩展性好:可方便地使用其他开发语言的开源库,除Java开源库还可以用(C/C++)开源库
  • 易于代码复用和移植
  • 用本地代码(C/C++)开发的代码不仅可以在安卓使用,还可以嵌入其他类型平台使用
  • 提供了把.so和.apk打包的工具(JNI只把.so文件放到文件系统特定位置)
  • NDK提供的库有限,仅用于处理算法效率和敏感问题
  • 提供了交叉编译器,用于生成特定的CPU平台动态库

3、JNI与NDK关系

JNI是实现的目的(java与本地语言交互的接口/协议),NDK是Android中实现JNI的工具(Android工具开发包)

二.具体使用

(1)NDK集成开发流程

1、配置Android NDK环境
2、关联Android Studio项目与NDK

a. 在Gradle的 local.properties中添加配置

ndk.dir=/Users/Carson_Ho/Library/Android/sdk/ndk-bundle

b.在Gradle的 gradle.properties中添加配置

#兼容老的Ndk
android.useDeprecatedNdk=true 

c.在Gradle的build.gradle添加ndk节点

        ndk {
            //.so文件 Linux下动态链接库(同windows下dll文件),二进制文件,多用于NDK开发.用户拿到动态库和头文件说明,就可以使用动态库中function
            moduleName "hello_jni"//对应本地代码文件,生成.so文件:lib+moduleName.so
			//abiFilters "x86","armeabi", "armeabi-v7a"//CPU类型
        }
3、创建JNI类声明native方法

package com.sdu.chy.chytest.ndkTest

/**
 * Java调用对应的C代码
 */

public class JNI {

    //加载JNI生成so库
    static {
        System.loadLibrary("hello_jni");
    }

    //定义Native方法,调用C代码对应方法
    public native String sayHello();
}
4、生成.h文件

(1)包目录下javac生成.class类文件

ndkTest danding$ javac JNI.java

(2)外部(java)目录下javah生成.h文件

java danding$ javah -jni com.sdu.chy.chytest.ndkTest.JNI

(3)将.h文件移到jni文件夹下

5、创建本地代码文件

需在Android项目中调用的本地代码hello.c

#include<stdio.h>
#include<stdlib.h>
#include<jni.h>

//类名:Java类型+本地类型 对应关系
//C函数命名格式:Java_全类名_方法名
//JNIEnv*:代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。
//jobject:代表native方法的实例(调用者),这里是JNI.ini
JNIEXPORT jstring JNICALL Java_com_sdu_chy_chytest_ndkTest_JNI_sayHello(JNIEnv* env,jobject jobj){
    char* text = "I am from C";
    return (*env)->NewStringUTF(env,text);
}

注:

  • 如果本地代码是C++(.cpp或者.cc),要使用extern “C” { }把本地方法括进去
  • JNIEXPORT jstring JNICALL中的JNIEXPORT 和 JNICALL不能省
  • 关于方法名Java_scut_carson_1ho_ndk_1demo_MainActivity_getFromJNI
    格式 = Java 包名 _ 类名_Java需要调用的方法名
    Java必须大写
    对于包名,包名里的.要改成
    ,_要改成_1。如我的包名是:scut.carson_ho.ndk_demo,则需要改成scut_carson_1ho_ndk_1demo
  • 最后,将创建好的test.cpp文件放入到工程文件目录中的src/main/jni文件夹。若无jni文件夹,则手动创建。
  • JNI类型与Java类型对应关系介绍
6、创建Android.mk文件 & Application.mk文件
6.1)创建Android.mk文件

作用:
指定源码编译的配置信息,如工作目录,编译模块的名称,参与编译的文件等
使用:
Android.mk(src/main/jni)

LOCAL_PATH := $(call my-dir)
// 设置工作目录,而my-dir则会返回Android.mk文件所在的目录
include $(CLEAR_VARS)
// 清除几乎所有以LOCAL——PATH开头的变量(不包括LOCAL_PATH)
LOCAL_MODULE    := hello_jni
// 设置模块的名称,即编译出来.so文件名
// 注,要和上述步骤中build.gradle中NDK节点设置的名字相同
LOCAL_SRC_FILES := hello.c \
// 指定参与模块编译的C/C++源文件名
include $(BUILD_SHARED_LIBRARY)
// 指定生成的静态库或者共享库在运行时依赖的共享库模块列表。
6.2)创建Application.mk文件

作用:配置编译平台相关内容
使用:
Application.mk(src/main/jni)

APP_MODULES := hello_jni
APP_ABI := all
// 最常用的APP_ABI字段:指定需要基于哪些CPU平台的.so文件
// 常见的平台有armeabi x86 mips,其中移动设备主要是armeabi平台
// 默认情况下,Android平台会生成所有平台的.so文件,即同APP_ABI := armeabi x86 mips
// 指定CPU平台类型后,就只会生成该平台的.so文件,即上述语句只会生成armeabi平台的.so文件
7、编译上述文件,生成.so库文件,并放入工程文件
7.1)修改build.gradle配置
        sourceSets {
            main {
                jni.srcDirs = []
                jniLibs.srcDirs = ['libs','src/main/libs']
                //生成.so文件位置'src/main/libs'
            }
        }
7.2)对JNI类执行ndk-build

在这里插入图片描述
编译成功后,在src/main/会多了两个文件夹libs & obj,其中libs下存放的是.so库文件
在这里插入图片描述

8、在Android Studio项目中通过JNI调用so文件

/Users/danding/Documents/chy_workspace/app/src/main/java/com/sdu/chy/chytest/ndkTest/JniTestActivity.java

public class JniTestActivity extends AppCompatActivity {

    private TextView JniTextView;
    private Button JniScheduleBtn;

    private JniClickListener jniClickListener = new JniClickListener();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_jni_test);
        initView();
    }

    public void initView(){
        JniTextView = (TextView)findViewById(R.id.jni_text_view);
        JniScheduleBtn = (Button) findViewById(R.id.jni_btn_schedule);

        JniScheduleBtn.setOnClickListener(jniClickListener);
    }

    public class JniClickListener implements View.OnClickListener{
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.jni_btn_schedule:
                    JniTextView.setText(new JNI().sayHello());//调用
                    break;
            }
        }
    }
}

点击按钮,调用C方法
在这里插入图片描述

(2)NDK开发

1、Java调用C函数
1.1)注册JNI函数
1.静态注册

先由Java得到本地方法的声明,然后再通过JNI实现该声明方法
静态注册就是根据函数名来遍历Java和JNI函数之间的关联,而且要求JNI层函数的名字必须遵循特定的格式。
步骤1:首先在Java代码中声明native函数

public class JniDemo1{
       static {
             System.loadLibrary("samplelib_jni");
        }

        private native void nativeMethod();
}

步骤2:通过javah来生成native函数的.h文件

javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.gebilaolitou.jnidemo.JniDemo1

然后就会得到一个JNI的.h文件,里面包含这几个native函数的声明
观察一下文件名以及函数名。其实JNI方法名的规范就出来了:
返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数
步骤3:编写代码在.h文件中实现方法

2.动态注册

先通过JNI重载JNI_OnLoad()实现本地方法,然后直接在Java中调用本地方法。
通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而无需遵循特定的方法命名格式。

1.2)从JNI调用C函数

步骤一:加载so库

public class JniDemo1{
       static {
             System.loadLibrary("samplelib_jni");
        }
}

步骤二:在JNI中的实现

jint JNI_OnLoad(JavaVM* vm, void* reserved)

步骤三:在这个函数里面去动态的注册native方法

#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2";

static void sayHello(JNIEnv *env, jobject, jlong handle) {
    LOGI("JNI", "native: say hello ###");
}

static JNINativeMethod gJni_Methods_table[] = {
    {"sayHello", "(J)V", (void*)sayHello},
};

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGI("JNI","Registering %s natives\n", className);
    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        LOGE("JNI","Native registration unable to find class '%s'\n", className);
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
        LOGE("JNI","RegisterNatives failed for '%s'\n", className);
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    LOGI("JNI", "enter jni_onload");

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

注:
1)JNINativeMethod
定义
JNI允许我们提供一个函数映射表,注册给Java虚拟机,这样JVM就可以用函数映射表来调用相应的函数。这样就可以不必通过函数名来查找需要调用的函数了。Java与JNI通过JNINativeMethod的结构来建立联系,它被定义在jni.h中
结构

typedef struct { 
    const char* name; //Java中函数名
    const char* signature; //Java中参数和返回值
    void* fnPtr; //指向C函数的函数指针
} JNINativeMethod; 

绑定
在jniRegisterNativeMethods内,通过调用RegisterNatives函数将注册函数的Java类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定。
2)JNI中的签名
原因:
即将参数类型和返回值类型的组合。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。(防止Java函数重载找不到对应实现方法)
规范
(参数1类型标示;参数2类型标示;参数3类型标示…)返回值类型标示

2、C回调Java方法
(1)获取Class对象

为了能够在C/C++中调用Java中的类,jni.h的头文件专门定义了jclass类型表示Java中Class类。JNIEnv中有3个函数可以获取jclass。

1.jclass jcl_string=env->FindClass("java/lang/String");//通过类的名称
2.jclass GetObjectClass(jobject obj);//通过对象实例来获取jclass,相当于Java中的getClass()函数
3.jclass getSuperClass(jclass obj);//通过jclass可以获取其父类的jclass对象
(2)获取属性方法

所以为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性\方法的时候,首先就要现在本地代码中取得代表该Java类的属性的jfieldID\jmethodID,然后才能在本地代码中进行Java属性的操作.
方法:一般是使用JNIEnv来进行操作
GetFieldID/GetMethodID:获取某个属性/某个方法
GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法
具体实现

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
(3)构造一个对象,并通过对象调用响应方法
jobject NewObject(jclass clazz, jmethodID methodID, ...)

三.实际场景

当出现一些用java语言无法处理的任务时,开发人员就可以利用JNI技术来完成。一般来说下面几种情况需要用到JNI技术:
一、 开发时,需要调用java语言不支持的依赖于操作系统平台的特性的一些功能。例如:需要调用当前的Unix系统的某个功能,而java不支持这个功能,就需要用到JNI技术来实现。
二、 开发时,为了整合一些以前的非java语言开发的某些系统。例如,需要用到开发早期实现的一些C或C++语言开发的一些功能或系统,将这些功能整合到当前的系统或新的版本中。
三、 开发时,为了节省程序的运行时间,必须采用一些低级或中级语言。例如为了创建一个省时的应用,不得不采用汇编语言,然后采用java语言通过JNI技术调用这个低级语言的应用。
例如:
美图秀秀处理图片:用java获取图片文件,再用C通过颜色矩阵(RGBA)对图片进行处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李一恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值