接上文,看了上篇文章,大家肯定对NDK开发已经有了已经直观的感受了。并且已经可以写出了Demo。但是具体Jni程序的内部是什么样子的?JniEnv是什么?常用的数据类型怎么转换?内存怎么占用的?以及怎么编译成so,编译成什么样的so。本文将带着这些问题进行介绍,有句话说的好,“纸上得来终觉浅,绝知此事要躬行”,代码还是要写一些才能记得住的。
JNIEnv介绍
JNIEnv是一个与线程相关的代表JNI环境的结构体,它是Java世界与C++/C世界连接的桥梁。JNIEnv中封装了一系列的参数和函数,方便我们编写java和C++互相调用的代码。下面先看看JNIEnv这个结构体的源码
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
.............
..........省略部分代码
#define CALL_NONVIRT_TYPE(_jtype, _jname) \
CALL_NONVIRT_TYPE_METHOD(_jtype, _jname) \
CALL_NONVIRT_TYPE_METHODV(_jtype, _jname) \
CALL_NONVIRT_TYPE_METHODA(_jtype, _jname)
CALL_NONVIRT_TYPE(jobject, Object)
CALL_NONVIRT_TYPE(jboolean, Boolean)
CALL_NONVIRT_TYPE(jbyte, Byte)
CALL_NONVIRT_TYPE(jchar, Char)
CALL_NONVIRT_TYPE(jshort, Short)
CALL_NONVIRT_TYPE(jint, Int)
CALL_NONVIRT_TYPE(jlong, Long)
CALL_NONVIRT_TYPE(jfloat, Float)
CALL_NONVIRT_TYPE(jdouble, Double)
void CallNonvirtualVoidMethod(jobject obj, jclass clazz,
jmethodID methodID, ...)
{
va_list args;
va_start(args, methodID);
functions->CallNonvirtualVoidMethodV(this, obj, clazz, methodID, args);
va_end(args);
}
void CallNonvirtualVoidMethodV(jobject obj, jclass clazz,
jmethodID methodID, va_list args)
{ functions->CallNonvirtualVoidMethodV(this, obj, clazz, methodID, args); }
void CallNonvirtualVoidMethodA(jobject obj, jclass clazz,
jmethodID methodID, const jvalue* args)
{ functions->CallNonvirtualVoidMethodA(this, obj, clazz, methodID, args); }
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
jlong GetDirectBufferCapacity(jobject buf)
{ return functions->GetDirectBufferCapacity(this, buf); }
/* added in JNI 1.6 */
jobjectRefType GetObjectRefType(jobject obj)
{ return functions->GetObjectRefType(this, obj); }
#endif /*__cplusplus*/
};
上文省略了大部分代码,如果想查看详细的源码,请到Android NDK中查看。由代码可知,JNIEnv可以做很多事情。调用java函数,转换数据类型,释放内存…。JNIEnv是一个线程相关的变量,也就是说不同线程里面的JNIEnv,这里大家一定要注意。在写多线程相关的应用的时候,避免由于多线程的调用由于不同的JNIEnv造成的bug。那么,怎么样才能获取到其他线程的JNIEnv呢?还记得我们在上一个章节中讲到的动态注册吗?动态注册里面有个函数。
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM* vm,void* reserved){
JNIEnv* env = NULL;
if(vm->GetEnv(reinterpret_cast<void**>(&env),JNI_VERSION_1_6)!=JNI_OK)
{
return -1;
}
assert(env!=NULL);
if(!registerNatives(env)){
return -1;
}
return JNI_VERSION_1_6;
}
Jni_OnLoad里面的第一个参数就是JavaVM,对它就是一个java的虚拟机。并且他是一个全局变量,也就是说在一个应用程序的进程中只有一个JavaVM,通过调用
vm->AttachCurrentThread()
我们就可以获得JNIEnv,当然在线程结束之后我们也需要调用
vm->DetachCurrentThread()
来释放资源。
Jni常用数据类型操作
Jni操作数据类型分为基本数据类型和引用数据类型。不管是操作何种数据类型,都是通过JNIEnv来操作的,下面我将给出操作数据类型的一个对照表,并且给出一个例子来讲解Jni对数据类型的操作。
C/C++ | java |
---|---|
void | void |
jboolean | boolean |
jint | int |
jlong | long |
jdouble | double |
jfloat | float |
jbyte | byte |
jchar | char |
jshort | short |
jbooleanArray | boolean[] |
jintArray | int[] |
jlongArray | long[] |
jdoubleArray | double[] |
jfloatArray | float[] |
jbyteArray | byte[] |
jcharArray | char[] |
jshortArray | short[] |
jobject | class |
把上一篇文章中的表格稍微修改一下,就变成了Jni常用操作的数据类型。不过我在代码中添加了Bitmap和String这两种类型的例子,因为这两种虽然属于Object但是非常的典型。
#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/bitmap.h>
#define RGB565_R(p) ((((p) & 0xF800) >> 11) << 3)
#define RGB565_G(p) ((((p) & 0x7E0 ) >> 5) << 2)
#define RGB565_B(p) ( ((p) & 0x1F ) << 3)
#define MAKE_RGB565(r,g,b) ((((r) >> 3) << 11) | (((g) >> 2) << 5) | ((b) >> 3))
#define RGBA_A(p) (((p) & 0xFF000000) >> 24)
#define RGBA_R(p) (((p) & 0x00FF0000) >> 16)
#define RGBA_G(p) (((p) & 0x0000FF00) >> 8)
#define RGBA_B(p) ((p) & 0x000000FF)
#define MAKE_RGBA(r,g,b,a) (((a) << 24) | ((r) << 16) | ((g) << 8) | (b))
extern "C" JNIEXPORT jstring JNICALL
Java_com_nanguiyu_jnitest_JniTest_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_nanguiyu_jnitest_JniTest_intFromJNI(JNIEnv *env, jobject instance) {
jint s = 100;
return s;
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_nanguiyu_jnitest_JniTest_intArrayFromJNI(JNIEnv *env, jobject instance) {
jintArray s = env->NewIntArray(10);
jint *arr = env->GetIntArrayElements(s,NULL);
int i = 0;
for(;i<10;i++)
{
arr[i] = i;
}
env->ReleaseIntArrayElements(s,arr,0);
return s;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_nanguiyu_jnitest_JniTest_convertGreyBitmap(JNIEnv *env, jobject instance, jobject bitmap) {
if(bitmap == nullptr){
__android_log_print(ANDROID_LOG_DEBUG,"Test","%s","bitmap is null\n");
return;
}
AndroidBitmapInfo info;
memset(&info,0, sizeof(info));
AndroidBitmap_getInfo(env,bitmap,&info);
if(info.width<=0||info.height<=0||
(info.format!=ANDROID_BITMAP_FORMAT_RGB_565&&
info.format!=ANDROID_BITMAP_FORMAT_RGBA_8888)){
__android_log_print(ANDROID_LOG_DEBUG,"Test","%s","invalid bitmap \n");
return;
}
// Lock the bitmap to get the buffer
void * pixels = NULL;
int res = AndroidBitmap_lockPixels(env, bitmap, &pixels);
if (pixels == NULL) {
return;
}
int x = 0, y = 0;
// From top to bottom
for (y = 0; y < info.height; ++y) {
// From left to right
for (x = 0; x < info.width; ++x) {
int a = 0, r = 0, g = 0, b = 0;
void *pixel = NULL;
// Get each pixel by format
if (info.format == ANDROID_BITMAP_FORMAT_RGB_565) {
pixel = ((uint16_t *)pixels) + y * info.width + x;
uint16_t v = *(uint16_t *)pixel;
r = RGB565_R(v);
g = RGB565_G(v);
b = RGB565_B(v);
} else {// RGBA
pixel = ((uint32_t *)pixels) + y * info.width + x;
uint32_t v = *(uint32_t *)pixel;
a = RGBA_A(v);
r = RGBA_R(v);
g = RGBA_G(v);
b = RGBA_B(v);
}
// Grayscale
int gray = (r * 38 + g * 75 + b * 15) >> 7;
// Write the pixel back
if (info.format == ANDROID_BITMAP_FORMAT_RGB_565) {
*((uint16_t *)pixel) = MAKE_RGB565(gray, gray, gray);
} else {// RGBA
*((uint32_t *)pixel) = MAKE_RGBA(gray, gray, gray, a);
}
}
}
AndroidBitmap_unlockPixels(env, bitmap);
}
运行结果如下图:
项目挂在到了**[github][2]**上面,欢迎大家star,fork。 ## Jni的内存占用分析 大家都知道每个进程占用一定的内存空间,以我的小米8手机为例,进程占用的最大内存为512M。Android内存一部分占用是虚拟机内存,另一部分占用的则是Native内存。但是进程占用的总内存是不变的。也就是说虚拟机内存和Native内存加在一起是不能超过总内存的最大值的,超过了就会报内存溢出。这一节分为两个部分来讲一个是分析一个进程中Jni内存在分布在哪儿,另一个部分主要来讲讲Jni部分的内存泄漏问题。 ### Jni部分内存占用 上面讲了,Jni这部分内存是保存在Native堆中的。这一部分的内存是不受gc控制的。完全由C++来控制。C/C++使用malloc()/new分配内存,需要手动使用free()/delete回收内存。然而,JNI又有少许不同。Jni的引用类型,比如jstring,jObject...。它们属于jni的。所以他们的引用保存在Native桢栈中,而他们的数据是保存在Java的Heap中的。举个列子: ```c jstring str = env->NewStringUTF("Hello,World"); ``` 上面的例子中,str这个引用的类型是jstring,所以引用保存在Native内存中。不过具体的数据“Hello,World”保存在Java的Heap中。
图图
Jni内存泄漏
Jni部分的内存管理包含两个方面,一个是java的引用类型在Jni里面的表示,比如刚才的jstring。这部分内存是存在Native Stack中的。还有一部分内存,是普通的C++分配的内存。涉及到Jni部分的内存如要上释放的话,那我们就必须要提到三个概念
- Local Reference
- Global Reference
- Weak Global Reference
先说Local Reference,Local Reference 只在native方法运行是存在,当native 方法运行结束Local Reference自动删除。可能大家会觉得,这样怎么会导致内存问题呢?每当线程从Java环境切换到Native代码环境时,JVM 会分配一块内存用于创建一个Local Reference Table,这个Table用来存放本次Native Method 执行中创建的所有Local Reference。每当在 Native代码中引用到一个Java对象时,JVM 就会在这个Table中创建一个Local Reference。之前版本的NDK在超过512的时候会发生 local reference table overflow (max=512),但是新的版本中已经没有这个问题了。不过官方还是建议调用env->DeleteLocalRef(env, jstr);来释放局部引用,特别是在你使用大量的局部引用的时候。官方文档中有句话“实际上,这意味着如果您要创建大量局部引用(也许是在运行对象数组时),应该使用 DeleteLocalRef 手动释放它们,而不是让 JNI 为您代劳。”。
再说Global Reference,全局引用,特点是创建之后可以在不同的线程中使用,但是必须手动调用DeleteGlobalRef来释放全局引用,不然内存就不会释放,会导致内存泄漏的。怎么样创建全局引用呢?看一下下面的代码:
globalStr = env->NewGlobalRef(env->NewStringUTF("ddd"));
//delete ref
env->DeleteGlobalRef(globalStr);
再看看Weak Global Reference,他和全局引用很像,唯一的全部是他指向的数据可能会被GC清理掉。弱全局引用用 NewGlobalWeakRef 创建,用 DeleteGlobalWeakRef 释放。我们在jni中经常需要缓存jclass,使用弱全局引用是个不错的选择
JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self){
static jclass myCls2 = NULL;
if (myCls2 == NULL) {
jclass myCls2Local =
env->FindClass(env, "mypkg/MyCls2");
if (myCls2Local == NULL) {
return; /* can't find class */
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL) {
return; /* out of memory */
}
}
... /* use myCls2 */
}
NDK编译so。
我们知道,C++在编译的时候,可以根据CPU的架构编译成不同的so文件,对应不同的ABI。不同的 Android 手机使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口,即 ABI。ABI可以非常精确地定义应用的机器代码在运行时如何与系统交互。您必须为应用要使用的每个 CPU 架构指定 ABI。
Executable : C:\SDK\cmake\3.10.2.4988404\bin\cmake.exe
arguments :
-HC:\source\appdev\JniTest\app\src\main\cpp
-BC:\source\appdev\JniTest\app\.externalNativeBuild\cmake\debug\arm64-v8a
-DANDROID_ABI=arm64-v8a
-DANDROID_PLATFORM=android-26
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=C:\source\appdev\JniTest\app\build\intermediates\cmake\debug\obj\arm64-v8a
-DCMAKE_BUILD_TYPE=Debug
-DANDROID_NDK=C:\SDK\ndk-bundle
-DCMAKE_CXX_FLAGS=
-DCMAKE_SYSTEM_NAME=Android
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a
-DCMAKE_SYSTEM_VERSION=26
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DCMAKE_ANDROID_NDK=C:\SDK\ndk-bundle
-DCMAKE_TOOLCHAIN_FILE=C:\SDK\ndk-bundle\build\cmake\android.toolchain.cmake
-G Ninja
-DCMAKE_MAKE_PROGRAM=C:\SDK\cmake\3.10.2.4988404\bin\ninja.exe
jvmArgs :
上面的是我举的一个例子,可以看到DANDROID_ABI=arm64-v8a,已经so的位置。DCMAKE_LIBRARY_OUTPUT_DIRECTORY=C:\source\appdev\JniTest\app\build\intermediates\cmake\debug\obj\arm64-v8a。当Android Studio项目编译完成之后就会生成系列的。
NDK系列
什么是NDK开发(一)