Connor学Android - JNI和NDK编程
Learn && Live
虚度年华浮萍于世,勤学善思至死不渝
前言
Hey,欢迎阅读Connor学Android系列,这个系列记录了我的Android原理知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:http://t.csdn.cn/YshKi,话不多说我们马上开始!
1.JNI
Java JNI 是指 Java Native Interface,即 Java 本地接口,它是为了方便 Java 调用 C、C++ 等本地代码所封装的一层接口
1.1 JNI 的开发流程
在 Java 中声明 native 方法
在 Java 中声明 native 方法,其内部可以完成如下操作
(1)调用 System.loadLibrary 方法加载 JNI 的动态库
(2)声明 native 的 get、set 方法
package com.connor;
import java.lang.System;
public class JniTest {
static {
System.loadLibrary("jni-test");
}
public static void main(String[] args) {
JniTest jniTest = new JniTest();
System.out.println(jniTest.get());
jniTest.set("hello world");
}
public native String get();
public native void set(String str);
}
编译 Java 源文件得到 .class 文件,然后通过 javah 命令导出 JNI 的头文件
(1)javah 命令会自动生成一个 com_connor_JniTest.h 头文件,其内声明了代码风格、get 和 set 方法的 C 语言版本
(2)函数名的格式遵循如下规则:Java_包名_类名_方法名,以 set 方法为例,对应的命名是 JNIEXPORT void JNICALL Java_com_connor_JniTest_set(JNIEnv*, jobject, jstring)
-
com_connor 是包名
-
JniTest 是类名
-
jstring 代表 String 类型的参数
-
JNIEnv*:表示一个指向 JNI 环境的指针,可以通过它来访问 JNI 提供的接口方法
-
jobject:表示 Java 对象中的 this
-
JNIEXPORT:JNI 定义的宏,作用是保证在本动态库中声明的方法 , 能够在其他项目中、外部代码中可以被调用,根据不同平台替换成不同的声明
- Windows 平台:#define JNIEXPORT __declspec(dllexport)
- Linux 平台:#define JNIEXPORT _attribute_ ((visibility (“default”)))
- 当-fvisibility=hidden时,动态库中的函数默认是被隐藏的即 hidden。
- 当-fvisibility=default时,动态库中的函数默认是可见的。
-
JNICALL:JNI 定义的宏,在 Windows 中调用函数时 , 该函数的参数是以栈的形式保存的,而在 Linux 平台没有对其进行定义
-
extern C:用于声明内部的函数是采用 C 语言的命名风格来编译,如果 JNI 使用 C++ 来实现时,会导致 JNI 在链接时无法根据函数名查找到具体的函数,无法完成 JNI 调用
#include<jni.h>
#ifndef _Included_com_connor_JniTest
#define _Included_com_connor_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/ *
* Class: com_connor_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_connor_JniTest_get(JniEnv*, jobject);
/ *
* Class: com_connor_JniTest
* Method: set
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_connor_JniTest_set(JNIEnv*, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
实现 JNI 方法
JNI 方法是指 Java 中声明的 native 方法,可以选择 C++ 或 C 来实现
(1)首先,在工程的主目录下创建一个子目录,名称自定
(2)然后将之前通过 javah 生成的头文件复制到这个目录下
(3)接着创建 test.cpp 和 test.c 两个文件完成内部的实现
编译 so 库并在 Java 中调用
使用 gcc 编译实现 JNI 方法的 cpp、c 文件
gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
(1)/usr/lib/jvm/java-7-openjdk-amd64 为本地 jdk 的安装路径,其他环境编译时将其指向本机的 jdk 路径即可
(2)libjni-test.so 是生成的 so 库的名字,可以根据这个名称在第一步中加载到 Java 中,其中 lib、.so 可以省略
java 指令执行 Java 程序
java -Djava.library.path=jni com.connor.JniTest
其中 -Djava.library.path=jni 用于指明 so 库的路径
1.2 JNI 的数据类型
基本类型
JNI 类型 | Java 类型 | 描述 |
---|---|---|
jboolean | boolean | 无符号 8 位整型 |
jbyte | byte | 有符号 8 位整型 |
jchar | char | 无符号 16 位整型 |
jshort | short | 有符号 16 位整型 |
jint | int | 32 位整型 |
jlong | long | 64 位整型 |
jfloat | float | 32 位浮点型 |
jdouble | double | 64 位浮点型 |
void | void | 无类型 |
引用类型
JNI 类型 | Java 类型 | 描述 |
---|---|---|
jobject | Object | Object 类型 |
jclass | Class | Class 类型 |
jstring | String | 字符串 |
jobjectArray | Object[] | 对象数组 |
jbooleanArray | boolean[] | boolean 数组 |
jbyteArray | byte[] | byte 数组 |
jcharArray | char[] | char 数组 |
jshortArray | short[] | short 数组 |
jintArray | int[] | int 数组 |
jlongArray | long[] | long 数组 |
jfloatArray | float[] | float 数组 |
jdoubleArray | double[] | double 数组 |
jthrowable | Throwable | Throwable |
1.3 JNI 的类型签名
类的签名
Java 类型 | 签名 | Java 类型 | 签名 |
---|---|---|---|
boolean | Z | long | J |
byte | B | float | F |
char | C | double | D |
short | S | void | V |
int | I |
对象的签名
它的签名就是对象所属的类的签名,String → Ljava/lang/String
数组的签名
签名为 [ + 类型签名,如 int[] → [I、int[][] → [[I
方法的签名
签名为 (参数类型签名) + 返回值类型签名
boolean fun1(int a, double b, int[] c) → (ILjava/lang/String;[I)Z
1.4 JNI 调用 Java 方法的流程
(1)首先定义一个静态方法
public static void methodCalledByJni(String msgFromJni) {
Log.d(TAG, "methodCalledByJni, mst:" + msgFromJni);
}
(2)然后在 JNI 中调用上面定义的静态方法
- 首先根据全类名找到声明 native 方法的类
- 根据方法名、方法签名找到对应的方法
- 接着通过 JNIEnv 对象的 CallStaticVoidMethod 方法来调用 Java 对应的方法
void callJavaMethod(JNIEnv* env, jobject thiz) {
jclass clazz = env -> FindClass("com/connor/JniTestApp/MainActivity");
...
jmethodID id = env -> GetStaticMethodID(clazz, "methodCalledByJni", "(Ljava/lang/String;)V");
...
jstring msg = env -> NextStringUTF("msg send by callJavaMethod in test.cpp");
env -> CallStaticVoidMethod(clazz, id, msg);
}
(3)最后在 Java_com_connor_JniTestApp_MainActivity_get 方法中调用 callJavaMethod 方法
(4)JNI 调用 Java 的过程和 Java 中方法的定义有很大关联,针对不同类型的 Java 方法,JNIEnv 提供了不同的接口去调用
2.NDK
(1)NDK 是 Android 提供的一个工具集合,通过 NDK 可以在 Android 中更方便地通过 JNI 来访问本地代码
(2)NDK 提供了交叉编译器,使用时只需要简单地修改 mk 文件就可以生成特定的 CPU 平台的动态库
(3)在 Linux 环境中,JNI 和 NDK 开发所用到的动态库的格式是以 .so 为后缀的文件,简称 so 库
(4)使用 NDK 有如下好处:
- 提高代码的安全性。由于 so 库反编译比较困难,因此 NDK 提高了 Android 程序的安全性
- 可以很方便地使用目前已有的 C/C++ 开源库
- 便于平台间的移植。通过 C/C++ 实现的动态库可以很方便地在其他平台上使用
- 提高程序在某些特定情形下的执行效率,但是并不能明显提升 Android 程序的性能
NDK 的开发流程
下载并配置 NDK
创建 Android 项目,并声明所需要的 native 方法
与 Java JNI 类似,比如可以在 Activity 中完成加载、声明
实现 Android 项目中所声明的 native 方法
在外部创建 jni 目录,创建三个文件:test.cpp、Android.mk、Application.mk
(1)Android.mk 文件中会设置如下几个参数
- LOCAL_MODULE:表示模块的名称
- LOCAL_SRC_FILES:表示需要参与编译的源文件
(2)Application.mk 文件中常用的配置项是 APP_ABI,表示 CPU 的架构平台的类型,如 armeabi、x86、mips、all
(3)默认情况下 NDK 会编译产生各个 CPU 平台的 so 库(all),通过 APP_ABI 指定平台即可只编译该平台下的 so 库了
// Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jni-test
LOCAL_SRC_FILES := test.cpp
include $(BUILD_SHARED_LIBRARY)
// Application.mk
APP_ABI := armeabi
切换到 jni 目录的父目录,然后通过 ndk-build 命令编译产生 so 库
(1)此时会创建一个和 jni 平级的 libs 目录,libs 内存放的就是 so 库的目录
(2)需要注意,ndk-build 默认指定 jni 目录为本地源码的目录,如果源码不在这个目录下,则无法成功编译
(3)之后会在 app/src/main 中创建一个 jniLibs 目录,将生成的 so 库复制到这个目录中,然后就可以通过 AndroidStudio 编译运行了
- 这个文件的位置、命名可以通过 App 的 build.gradle 文件指定:sourceSets.main { jniLibs.srcDir ‘src/main/jni_libs’ }
android {
...
sourceSets.main {
jniLibs.srcDir 'src/main/jni_libs'
}
}
(4)除了通过命令编译,还可以自动编译产生 so 库
- 首先在 App 的 build.gradle 的 defaultConfig 区域内添加 NDK 选项,其中 moduleName 指定打包后的 so 库的文件名
android {
...
defaultConfig {
...
ndk {
moduleName "jni-test"
}
}
}
- 接着需要将 JNI 的代码放在 app/src/main/jni 目录下,也可以在 build.gradle 的 sourceSets.main 区域内指定 JNI 的代码路径
android {
...
sourceSets.main {
jni.srcDirs 'src/main/jni_src'
}
}
- 同样可以指定产生某个平台的 so 库,修改 build.gradle 配置,然后在 Build Variants 面板中选择 armDebug 选项编辑即可
android {
...
productFlavors {
arm {
ndk {
abiFilter "armeabi"
}
}
x86 {
ndk {
abiFilter "x86"
}
}
}
}