深入浅出Android NDK之JNI字符串转换

目录
上一篇 深入浅出Android NDK之往logcat输出日志

这一章我们学习一下JNI中相关字符串转换函数,主要有以下几个:

 	jstring NewString(const jchar* unicodeChars, jsize len);
    jsize GetStringLength(jstring string);
    const jchar* GetStringChars(jstring string, jboolean* isCopy);
    void ReleaseStringChars(jstring string, const jchar* chars);
    void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf);
    const jchar* GetStringCritical(jstring string, jboolean* isCopy);
    void ReleaseStringCritical(jstring string, const jchar* carray);

    jstring NewStringUTF(const char* bytes);
    jsize GetStringUTFLength(jstring string);
    const char* GetStringUTFChars(jstring string, jboolean* isCopy);
    void ReleaseStringUTFChars(jstring string, const char* utf);
    void GetStringUTFRegion(jstring str, jsize start, jsize len, char* buf);

接下来我们直接看例子:
首先新建类com.example.strtest.StrTest

package com.example.strtest;

import android.util.Log;

public class StrTest {
    static  {
        System.loadLibrary("strtest");
    }
    public static void test() {
        String str1 = testNewString();
        Log.i("MD_DEBUG", "testNewString return:" + str1);
        Log.i("MD_DEBUG", "testGetStringLength return:" + testGetStringLength(str1));
        Log.i("MD_DEBUG", "testGetReleaseStringChars return:" + testGetReleaseStringChars(str1));
        testGetReleaseStringCritical(str1);
        Log.i("MD_DEBUG", "谁说java字符串不可变,看我变,str1:" + str1);
        Log.i("MD_DEBUG", "testGetStringRegion return:" + testGetStringRegion(str1));

        String str2 = testNewStringUTF();
        Log.i("MD_DEBUG", "testNewStringUTF return:" + str2);
        Log.i("MD_DEBUG", "这后面是is:" + (int)str2.charAt(1));
        Log.i("MD_DEBUG", "testGetReleaseStringUTFChars return:" + testGetReleaseStringUTFChars(str2));
        testGetStringUTFRegion(str2);
    }
    public static final native String testNewString();
    public static final native int testGetStringLength(String str);
    public static final native String testGetReleaseStringChars(String str);
    public static final native void testGetReleaseStringCritical(String str);
    public static final native String testGetStringRegion(String str);

    public static final native String testNewStringUTF();
    public static final native String testGetReleaseStringUTFChars(String str);
    public static final native void testGetStringUTFRegion(String str);
}

新建C++源文件,strtest.cpp:

#include 
#include 
#include 
#include 

int ucs2_strlen(const jchar *string)
{
   int n = 0;
    while(*string++ != 0)
    {
        n++;
    }
    return n;
}

extern "C" JNIEXPORT jstring Java_com_example_strtest_StrTest_testNewString(JNIEnv *env, jclass clazz) {
    const wchar_t *cstr = L"这是在JNI中调用NewString创建的一个字符串";
    /*
     * linux平台下,一个unicode字符占4字节,而java的一个unicode占两字节。
     * 可以在Android.mk中加入LOCAL_CFLAGS := -fshort-wchar让编译器将unicode字符串编译为2字节的unicode。
     */
    __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "sizeof(wchar_t):%d", sizeof(wchar_t));
    jchar *jstr = (jchar*)cstr;
    /*
     * 加入了-fshort-wchar标志后,wchar.h中的所有字符串操作函数都不能使用了(wcslen,wcscpy,wcscmp等等),
     * 没办法我们只能自定义一个函数来ucs2_strlen来获得字符串长度。
     */
    int len = ucs2_strlen(jstr);
    return env->NewString((jchar*)jstr, len);
}

extern "C" JNIEXPORT jint Java_com_example_strtest_StrTest_testGetStringLength(JNIEnv *env, jclass clazz, jstring jstr) {
    return (jint)env->GetStringLength(jstr);//相当于java的String.length()函数;
}

extern "C" JNIEXPORT jstring Java_com_example_strtest_StrTest_testGetReleaseStringChars(JNIEnv *env, jclass clazz, jstring jstr) {
    //对我们来说这个参数并没有什么用,也可以不定义直接传入NULL
    jboolean isCopy;

    /*
     * 将Java的String对像转换为C的jchar*,返回的cstr有可能是java unicode字符串的一个拷贝,
     * 也有可能就是原始的字符串,不同的虚拟机会有不同的实现。
     * isCopy参数用于接收cstr是否是java字符串的一个拷贝
     * 注意:GetStringChars得到的cstr并不是以0结尾的。
     */
    const jchar *cstr = env->GetStringChars(jstr, &isCopy);
    __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "GetStringChars isCopy:%d", isCopy);

    //模拟处理字符串
    jsize len = env->GetStringLength(jstr);
    jchar *estr = new jchar[len + 3];
    for (int i = 0; i < len; i++) {
        estr[i] = cstr[i];
    }
    estr[len] = '-';
    estr[len + 1] = L'测';
    estr[len + 2] = L'试';
    jstring nstr = env->NewString(estr, len + 3);
    delete[]estr;

    //GetStringChars一定要和ReleaseStringChars配对使用,ReleaseStringChars用于释放内存cstr
    env->ReleaseStringChars(jstr, cstr);
    return nstr;
}

extern "C" JNIEXPORT void Java_com_example_strtest_StrTest_testGetReleaseStringCritical(JNIEnv *env, jclass clazz, jstring jstr) {
    jboolean isCopy;
    /*GetStringCritical和GetStringChars的意思是一样的,不过GetStringCritical极大提高了不拷贝返回java原始字符串的可能性。
     *不过限制也更多,在GetStringCritical和ReleaseStringCritical函数之间不能调用任何jni函数(可以简单认为是JNIEnv里的任何函数),
     * 也不能有任何阻塞当前线程的操作。
     */
    const jchar *cstr = env->GetStringCritical(jstr, &isCopy);
    __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "GetStringCritical isCopy:%d", isCopy);

    //谁说java字任串不可变,看我变
    ((jchar*)cstr)[0] = (jchar)L'变';
    env->ReleaseStringCritical(jstr, cstr);
}


extern "C" JNIEXPORT jstring Java_com_example_strtest_StrTest_testGetStringRegion(JNIEnv *env, jclass clazz, jstring jstr) {
    jchar ch[5];
    //GetStringRegion不需要release,因为ch数组是我们自己分配的,这个肯定是copy过来的。
    env->GetStringRegion(jstr, 0, 5, ch);
    return env->NewString(ch, 5);
}

extern "C" JNIEXPORT jstring Java_com_example_strtest_StrTest_testNewStringUTF(JNIEnv *env, jclass clazz) {
    /*
     * 定义一个utf8字符串,jni中使用的是修改过的utf8字符编码。
     * 什么叫修改过的utf8呢?以下面这个字符串为例,我们知道C语言的字符串是以0结尾的,
     * 并且一旦遇到0了以后就认为字符串结束了,所以在’串‘这个字的后面其实还有一个0。
     * 那么问题来了,如果我有一个这样的字符串,想在字符串的中间有一个0,那该怎么办呢?
     * 比如想在’这‘这个字后面有一个0?
     * 我们可以用\xc0\x80表示这个0。
     */
    char cstr[] = "这\xc0\x80是在JNI中调用NewStringUTF创建的一个字符串";

    /*
     * 在调用这个函数的时候一定要保证,cstr是真实有效的utf8编码的字符串,不会有乱码的情况出现,如果cstr有乱码的话,这个函数会崩溃。
     * 如果保证不了,比如这个字符串的来源有可能是从文件系统来的,文件系统里的文件名有可能是乱码的。
     * 这种情况下我们应当自己使用算法将utf8字符串转换为unicode字符串(这种算法网上很多),再调用NewString函数来创建字符串。
     * */
    jstring jstr =  env->NewStringUTF(cstr);
    return jstr;
}


extern "C" JNIEXPORT jstring Java_com_example_strtest_StrTest_testGetReleaseStringUTFChars(JNIEnv *env, jclass clazz, jstring jstr) {
    //对我们来说这个参数并没有什么用,也可以不定义直接传入NULL,isCopy总是true
    jboolean isCopy;

    /*
     * 将String转换为一个以0结尾的修改过的utf8编码的C语言字符串。
     */
    const char *cstr = env->GetStringUTFChars(jstr, &isCopy);
    __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "GetStringUTFChars isCopy:%d", isCopy);


    //模拟处理字符串
    jsize len = env->GetStringUTFLength(jstr);//和strlen(cstr)的效果相同
    char *estr = new char[len + 32];
    sprintf(estr, "%s-测试", cstr);

    jstring nstr = env->NewStringUTF(estr);
    delete[]estr;

    //GetStringUTFChars一定要和ReleaseStringUTFChars配对使用,ReleaseStringUTFChars用于释放内存cstr
    env->ReleaseStringUTFChars(jstr, cstr);
    return nstr;
}

extern "C" JNIEXPORT void Java_com_example_strtest_StrTest_testGetStringUTFRegion(JNIEnv *env, jclass clazz, jstring jstr) {
    char ch[5 * 3]="";
    //GetStringUTFRegion不需要release,因为ch数组是我们自己分配的,这个肯定是copy过来的。
    env->GetStringUTFRegion(jstr, 0, 5, ch);
    __android_log_print(ANDROID_LOG_DEBUG, "MD_DEBUG", "GetStringUTFRegion(0,5) is:%s", ch);
}

Android.mk如下:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := strtest
LOCAL_SRC_FILES := strtest.cpp
LOCAL_CFLAGS := -fvisibility=hidden
LOCAL_CFLAGS += -fshort-wchar #将wchar_t编译为2字节的unicode字符,linux平台默认情况下一个unicode占4个字节
LOCAL_LDLIBS += -llog
LOCAL_DISABLE_FATAL_LINKER_WARNINGS := true#加入-fshort-wchar编译标志后,链接的时候会报一个错误,我们忽略它。

include $(BUILD_SHARED_LIBRARY)

在MainActivity中调用StrTest.test();编译运行后,logcat有如如下输出:
深入浅出Android NDK之JNI字符串转换_第1张图片
关于GetStringChars/GetStringRegion/GetStringCritical三个函数的探讨:
这三个函数的作用都一样,都是将Java的String对像,转换为C的jchar*。
JVM在返回这个jchar时有两个选择。
第一种选择,将java String所对应的原始jchar
拷贝一份给你,这时候他会将isCopy赋值为true。isCopy是true意味着这是一份拷贝,所以放心用吧,即使修改一下也没关系,反正也是拷贝,对原始字符串不会有啥影响。

第二种选择,将Java String所对应的原始jchar*直接返回给你,这时候他会将isCopy赋值为false。isCopy是false意味着这是原始字符串,所以兄弟你可千万小心点,你用就用了,千万别改,要不然就违背JAVA字符串不可变的特性了。
至于JVM会使用哪个选择呢?这要看平台了,windows/linux/mac/Android上可能各不相同。

GetStringChar和GetStringCritical的区别在于,GetStringCritical返回原始字符串的可能性更高,我觉得吧,一般情况下GetStringChar这个还不好说,但是GetStringCritical肯定会返回给你原始字符串,要不然他也不会新弄一个函数出来,并且在使用时还有那么多限制。

根据我的测试,在Android平台上,GetStringChar返回给我们的总是一份拷贝。
GetStringCritical返回的总是原始字符串。

使用GetStringCritical可以直接返回原始字符串,因为不需要拷贝,所以效率会高很多。
当然天下没有免费的午餐,效率是高了,限制也多。
在GetStringCritical/ReleaseStringCritical函数之间不能再调用其他任何JNI函数,也不能有任何阻塞当前线程的操作。
这里的不能调用JNI函数我们可以简单的理解为不调用里JNIEnv的任何函数。
总之听起来麻烦多多,所以一般情况下还是调用GetStringChar的好,虽然会拷贝,但是省心。只有当你的字符串特别长,长到拷贝特别耗时,会影响到性能时,再考虑GetStringCritical吧。

那GetStringRegion呢?GetStringRegion肯定是拷贝呀,这个就不用说了,看函数声明就知道。只不过GetStringRegion的时候这个内存是你自己分配的,JVM只拷贝过来就行了,而GetStringChar是JVM分配一个内存,然后拷贝到这个内存,再把这个内存返回给你,所以才需要ReleaseStringChar。

下一篇 深入浅出Android NDK之JNI数组操作

你可能感兴趣的:(深入浅出Android,NDK开发,android,ndk,jni)