什么是NDK开发(二)

上文,看了上篇文章,大家肯定对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
voidvoid
jbooleanboolean
jintint
jlonglong
jdoubledouble
jfloatfloat
jbytebyte
jcharchar
jshortshort
jbooleanArrayboolean[]
jintArrayint[]
jlongArraylong[]
jdoubleArraydouble[]
jfloatArrayfloat[]
jbyteArraybyte[]
jcharArraychar[]
jshortArrayshort[]
jobjectclass

把上一篇文章中的表格稍微修改一下,就变成了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开发(一)

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值