阅读文章大概需要5分钟
1.什么是NDK
1)Android NDK 是一套允许您使用原生代码语言(例如C和C++) 实现部分应用的工具集。在开发某些类型应用时,这有助于您重复使用以这些语言编写的代码库。
2)Android NDK 就是一套工具集合,允许你使用C/C++语言来实现应用程序的部分功能。
3)NDK 是Native Develop Kit的含义,从含义很容易理解,本地开发。大家都知道,Android 开发语言是Java,不过我们也知道,Android是基于Linux的,其核心库很多都是C/C++的,比如Webkit等。那么NDK的作用,就是Google为了提供给开发者一个在Java中调用C/C++代码的一个工作。NDK本身其实就是一个交叉工作链,包含了Android上的一些库文件,然后,NDK为了方便使用,提供了一些脚本,使得更容易的编译C/C++代码。总之,在Android的SDK之外,有一个工具就是NDK,用于进行C/C++的开发。一般情况,是用NDK工具把C/C++编译为.co文件,然后在Java中调用。
4.NDK不适用于大多数初学的Android工程师,对于许多类型的Android应用没有什么价值。因为它不可避免地会增加开发过程的复杂性,所以一般很少使用。
2.为什么使用NDK
1、在平台之间移植其应用
2、重复使用现在库,或者提供其自己的库重复使用
3、在某些情况下提性能,特别是像游戏这种计算密集型应用
4、使用第三方库,现在许多第三方库都是由C/C++库编写的,比如Ffmpeg这样库。
5、不依赖于Dalvik Java虚拟机的设计
6、代码的保护。由于APK的Java层代码很容易被反编译,而C/C++库反编译难度大。
3.NDK到so
Linux下能执行的函数库——so文件,目前Android系统支持以下七种不用的CPU架构,每一种对应着各自的应用程序二进制接口ABI:(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。
ARMv5——armeabi
ARMv7 ——armeabi-v7a
ARMv8——arm64- v8a
x86——x86
MIPS ——mips
MIPS64——mips64
x86_64——x86_64
1.什么是JNI
1)Java调用C/C++在Java语言里面本来就有的,并非Android自创的,即JNI。JNI就是Java调用C++的规范。当然,一般的Java程序使用的JNI标准可能和android不一样,Android的JNI更简单。
2)JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码JNI是一种在Java虚拟机机制下的执行代码的标准机制。代码被编写成汇编程序或者C/C++程序,并组装为动态库。也就允许非静态绑定用法。这提供了一个在Java平台上调用C/C++的一种途径,反之亦然。
PS:
开发JNI程序会受到系统环境限制,因为用C/C++ 语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起。而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和CPU指令集,而且各个平台对标准C/C++的规范和标准库函数实现方式也有所区别。这就造成了各个平台使用JNI接口的Java程序,不再像以前那样自由的跨平台。如果要实现跨平台, 就必须将本地代码在不同的操作系统平台下编译出相应的动态库
2.为什么需要JNI
因为在实际需求中,需要Java代码与C/C++代码进行交互,通过JNI可以实现Java代码与C/C++代码的交互
3.JNI的优势
与其它类似接口Microsoft的原始本地接口等相比,JNI的主要竞争优势在于:它在设计之初就确保了二进制的兼容性,JNI编写的应用程序兼容性以及其再某些具体平台上的Java虚拟机兼容性(当谈及JNI时,这里并不特比针对Davik虚拟机,JNI适用于所有JVM虚拟机)。这就是为什么C/C++编译后的代码无论在任何平台上都能执行。不过,一些早期版本并不支持二进制兼容。二进制兼容性是一种程序兼容性类型,允许一个程序在不改变其可执行文件的条件下在不同的编译环境中工作。
1.添加Android Log
这时候,我们创建一个Android.mk文件,然后进行如下的编辑:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jnidemo3
LOCAL_SRC_FILES := jnitools.c
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY)
然后在jnitools.c添加#include
这时候,开始注册代码,我们开始编写注册代码
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
//打印日志,说明已经进来了
__android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload");
JNIEnv* env = NULL;
jint result = -1;
// 判断是否正确
if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){
return result;
}
//注册方法,注意签名
const JNINativeMethod method[]={
{"add","(II)I",(void*)addNumber}
};
//找到对应的JNITools类
jclass jClassName=(*env)->FindClass(env,"com/gebilaolitou/jni/JNITools");
//开始注册
jint ret = (*env)->RegisterNatives(env,jClassName,method, 4);
//如果注册失败,打印日志
if (ret != JNI_OK) {
__android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error");
return -1;
}
return JNI_VERSION_1_6;
}
2.块拷贝
考虑一下这种场景:Native 层需要从/往 Java 数组拷贝一块内容,根据上面的内容很容易写出以下代码:
jbyte* data = env->GetByteArrayElements(javaArray, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(javaArray, data, JNI_ABORT);
}
先获取指向 Java 数组堆内存(或者副本)的指针,将头 len 个字节拷贝到 buffer 后调用 Release 释放。由于没有改变数组内容,因此使用 JNI_ABORT 避免回写开销。
但其实有更简单的做法,就是调用块拷贝函数:
env->GetByteArrayRegion(javaArray, 0, len, buffer);
相比前一种方式,块拷贝有以下优点:
只需要一次 JNI 调用,减少开销;
无需创建副本或引用 Java 数组的内存(不影响 GC)
降低编程出错的风险——不会因漏调用 Release 函数而引起泄漏。
对于字符串也有类似的拷贝函数,下面是原型:
// Region copy for Array.
// Throw ArrayIndexOutOfBoundsException if one of the indexes in the region is not valid
void GetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
void SetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, const NativeType *buf);
// Region copy for String.
// Throws StringIndexOutOfBoundsException on index overflow
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
3.扩展检查
JNI 几乎没有错误检查,出错通常都会导致崩溃。Android 额外提供了一种名为 CheckJNI 的模式,该模式下会将 JavaVM 和 JNIEnv 的函数表指针重定向到带检查能力的函数表,该表里函数会先执行扩展检查再调用实际的 JNI 函数。
扩展检查项包括:
数组:尝试分配一个负数长度的数组;
错误的指针:将错误的jarray / jclass / jobject / jstring传递给JNI调用,或者将NULL指针传递给具有不可空参数的JNI调用;
类名称:将错误样式的类名传递给JNI调用;
临界调用:在临界区中进行 JNI 调用;
Direct ByteBuffers:将错误的参数传递给NewDirectByteBuffer;
异常:在有待处理异常时进行 JNI 调用;
JNIEnv指针:跨线程使用 JNIEnv;
jfieldIDs:使用 NULL jfieldID 或使用 jfieldID 设置值时类型不正确,或使用 jfieldID 设置未持有该 jfieldID 的类成员等;
jmethodIDs:同 jfieldIDs;
引用:在错误的引用类型上调用 DeleteGlobalRef/DeleteLocalRef;
Release modes:调用 Release 时传入错误的 mode 参数(例如传入除 0,JNI_ABORT,JNI_COMMIT 之外的值);
类型安全:Native 方法返回一个与声明不兼容的类型;
UTF-8:将一个非法的 Modified UTF-8 字符串传给 JNI 调用。
以下方式可以打开扩展检查能力:
如果使用模拟器,则默认开启了全局 CheckJNI;
如果编译的是Debug版本的App,也默认开启了;
root过的手机可以用以下命令开启:
adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
未 root 的可以用:adb shell setprop debug.checkjni 1
通过以下 Logcat 内容可以确认是否开启成功:
D AndroidRuntime: CheckJNI is ON
Android 平台从一开就已经支持了C/C++了。我们知道Android的SDK主要是基于Java的,所以导致了在用Android SDK进行开发的工程师们都必须使用Java语言。不过,Google从一开始就说明Android也支持JNI编程方式,也就是第三方应用完成可以通过JNI调用自己的C动态度。于是NDK就应运而生了。