Android NDK开发 字符串(四)

几个概念首先要明确:

  • java内部是使用16bit的unicode编码(UTF-16)来表示字符串的,无论中文英文都是2字节;
  • jni内部是使用UTF-8编码来表示字符串的,UTF-8是变长编码的unicode,一般ascii字符是1字节,中文是3字节;
  • c/c++使用的是原始数据,ascii就是一个字节了,中文一般是GB2312编码,用两个字节来表示一个汉字。

明确了概念,操作就比较清楚了。下面根据字符流的方向来分别说明一下

1、java --> c/c++

这种情况中,java调用的时候使用的是UTF-16编码的字符串,jvm把这个字符串传给jni,c/c++得到的输入是jstring,这个时候,可以利用jni提供的两种函数,一个是GetStringUTFChars,这个函数将得到一个UTF-8编码的字符串;另一个是GetStringChars这个将得到UTF-16编码的字符串。无论那个函数,得到的字符串如果含有中文,都需要进一步转化成GB2312的编码。

       String 
(UTF-16)
|
[java] |
-------------------- JNI 调用
[cpp] |
v
jstring
(UTF-16)
|
+--------+---------+
|GetStringChars |GetStringUTFChars
| |
v v
wchar_t* char*
(UTF_16) (UTF-8)

 

 

2、c/c++ --> java

jni返回给java的字符串,c/c++首先应该负责把这个字符串变成UTF-8或者UTF-16格式,然后通过NewStringUTF或者NewString来把它封装成jstring,返回给java就可以了。

 

       String 
(UTF-16)
^
|
[java] |
-------------------- JNI 返回
[cpp] |
jstring
(UTF-16)
^
|
+--------+---------+
^ ^
| |
|NewString |NewStringUTF
wchar_t* char*
(UTF_16) (UTF-8)

 

如果字符串中不含中文字符,只是标准的ascii码,那么用GetStringUTFChars/NewStringUTF就可以搞定了,因为这种情况下,UTF-8编码和ascii编码是一致的,不需要转换。

但是如果字符串中有中文字符,那么在c/c++部分进行编码转换就是一个必须了。我们需要两个转换函数,一个是把UTF8/16的编码转成GB2312;一个是把GB2312转成UTF8/16。

这里要说明一下:linux和win32都支持wchar,这个事实上就是宽度为16bit的unicode编码UTF16,所以,如果我们的c/c++程序中完全使用wchar类型,那么理论上是不需要这种转换的。但是实际上,我们不可能完全用wchar来取代char的,所以就目前大多数应用而言,转换仍然是必须的。

具体的转换函数,linux和win32都有一定的支持,比如glibc的mbstowcs就可以用来把GB2312编码转成UTF16,但是这种支持一般是平台相关的(因为c/c++的标准中并没有包括这部分),不全面的(比如glibc就没有提供转成UTF8的编码),不独立的(linux下mbstowcs的行为要受到locale设置的影响)。所以我推荐使用iconv库来完成转换。

iconv库是一个免费的独立的编码转换库,支持很多平台,多种编码(事实上,它几乎可以处理我们所使用的所有字符编码),而且它的行为不受任何外部环境的影响。iconv在*nix平台上,基本上是缺省安装的。在win32平台上需要额外安装。

在JNI中,Java字符串被当作一个引用来处理。这些引用类型并不像原生C字符串一样可以直接使用,JNI提供了Java字符串与C字符串之间转换的必要函数,因为Java字符串对象是不可变的(如果对这里有异议,请复习Java SE),因此JNI不提供任何修改现有的Java字符串内容的函数。

JNI支持两种编码格式的字符串,分别是Unicode和UTF-8。

好了,废话说完了,下面按部就班来。

一、创建字符串

我们可以在原生的C或者C++代码中使用NewString函数来构建Unicode编码格式的字符串,也可以使用NewStringUTF函数来构建UTF-8格式的字符串,如下,用一个给定的C字符串构建一个Java字符串。

Java_com_tmf_ndk_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    //创建字符串,用给定的C字符串创建Java字符串
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

二、把Java字符串转换成C字符串

为了在原生代码中使用Java字符串,需要先将Java字符串转换成C字符串,使用GetStringChars函数可以将Unicode格式的Java字符串转换成C字符串,使用GetStringUTFChars函数可以将UTF-8格式的Java字符串转换成C字符串。这两个函数的第三个参数均为可选参数,该可选参数是isCopy,它让调用者确定返回的C字符串地址是指向副本还是指向堆中的固定对象。

JNIEXPORT jstring JNICALL
Java_com_tmf_ndk_MainActivity_stringFromJNI1(JNIEnv *env, jobject instance, jstring fromjava_) {
    const char *fromjava = env->GetStringUTFChars(fromjava_, 0);
    char *hello = "I from C++";
//    env->ReleaseStringUTFChars(fromjava_, fromjava);
    int len=strlen(fromjava)+strlen(hello);
    char str[len];
    strcpy (str,fromjava);
    strcat (str,hello);
    return env->NewStringUTF(str);
}

这个方法的作用是把Java的传入的参数加上C自己的新建的字符串拼接,然后一起返回给Java

在Java层 调用stringFromJNI1("i from java ")

打印结果

i from java I from C++

 

如果返回的字符串有中文

//中文问题
JNIEXPORT jstring JNICALL
Java_com_tmf_ndk_MainActivity_stringFromJNI1(JNIEnv *env, jobject instance, jstring in)
    //输出
    //char *c_str = (*env)->GetStringUTFChars(env, in, JNI_FALSE);
    //printf("%s\n",c_str);

    //c -> jstring
    char *c_str = "我们是共产主义接班人";
    //jstring jstr = (*env)->NewStringUTF(env, c_str);
    //执行String(byte bytes[], String charsetName)构造方法需要的条件
    //1.jmethodID
    //2.byte数组
    //3.字符编码jstring

    jclass str_cls = (*env)->FindClass(env, "java/lang/String");
    jmethodID constructor_mid = (*env)->GetMethodID(env, str_cls, "<init>", "([BLjava/lang/String;)V");

    //jbyte -> char 
    //jbyteArray -> char[]
    jbyteArray bytes = (*env)->NewByteArray(env, strlen(c_str));
    //byte数组赋值
    //0->strlen(c_str),从头到尾
    //对等于,从c_str这个字符数组,复制到bytes这个字符数组
    (*env)->SetByteArrayRegion(env, bytes, 0, strlen(c_str), c_str);

    //字符编码jstring
    jstring charsetName = (*env)->NewStringUTF(env, "GB2312");

    //调用构造函数,返回编码之后的jstring
    return (*env)->NewObject(env,str_cls,constructor_mid,bytes,charsetName);
}

 

工具类将Java字符转换成CString

 

char* jstringTostring(JNIEnv* env, jstring jstr)
{
    char* rtn = NULL;
    jclass clsstring = env->FindClass("java/lang/String");
    jstring strencode = env->NewStringUTF("utf-8");
    jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
    jbyteArray barr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode);
    jsize alen = env->GetArrayLength(barr);
    jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE);
    if (alen > 0)
    {
        rtn = (char*)malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    env->ReleaseByteArrayElements(barr, ba, 0);
    return rtn;
}

 

char* to jstring

jstring chartoJstring(JNIEnv* env, const char* pat)
{
    jclass strClass = env->FindClass("Ljava/lang/String;");
    jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
    jbyteArray bytes = env->NewByteArray(strlen(pat));
    env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat);
    jstring encoding = env->NewStringUTF("utf-8");
    return (jstring)env->NewObject(strClass, ctorID, bytes, encoding);
}

 

GetStringUTFChars和ReleaseStringUTFChars

  调用完GetStringUTFChars之后不要忘记安全检查,因为JVM需要为新诞生的字符串分配内存空间,当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常。JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要用return语句跳过后面的代码,并立即结束当前方法

   调用GetStringUTFChars函数从JVM内部获取一个字符串之后,JVM内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用ReleaseStringUTFChars函数通知JVM这块内存已经不使用了,你可以清除了。注意:这两个函数是配对使用的,用了GetXXX就必须调用ReleaseXXX,而且这两个函数的命名也有规律,除了前面的Get和Release之外,后面的都一样

这里用完需要释放

JNIEXPORT jstring JNICALL
Java_com_tmf_ndk_MainActivity_stringFromJNI1(JNIEnv *env, jobject instance, jstring fromjava_) {
    const char *fromjava = env->GetStringUTFChars(fromjava_, 0);
    if (fromjava == NULL) { //不要忘记检测,否则分配内存失败会抛出异常
        return NULL; /* OutOfMemoryError already thrown */
    }
    char *hello = "I from C++";
    int len=strlen(fromjava)+strlen(hello);
    char str[len];
    strcpy (str,fromjava);
    strcat (str,hello);
    env->ReleaseStringUTFChars(fromjava_, fromjava);
    return env->NewStringUTF(str);
}

 四由GetStringUTFChars引起的问题

 Java内部使用的是16比特(两字节)表示一个小于65535的UNICODE码。无论是英文还是中文都是2个字节 。

 Jni内部是使用utf-8编码来表示字符串的。utf-8是变长的编码,ascii字符是一个字节,中文是三个字节。

为了兼容早期的jvm ,java使用16比特(两字节)表示一个小于65535的UNICODE码,用代理对的形式表示其他UNICODE码(关于代理对,http://zh.wikipedia.org/zh-cn/UTF-16

而将UNICODE编码时,若使用变种UTF8,java会把字节 00  变为 0xC0 80,编码代理对更复杂(http://zh.wikipedia.org/wiki/UTF-8)。

  通过调用NewStringUTF函数,会构建一个新的java.lang.String字符串对象。这个新创建的字符串会自动转换成Java支持的Unicode编码。如果JVM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回NULL。在这个例子中我们不必检查它的返回值,如果NewStringUTF创建java.lang.String失败,OutOfMemoryError这个异常会被在Sample.main方法中抛出。如果NewStringUTF创建java.lang.String成功,则返回一个JNI引用,这个引用指向新创建的java.lang.String对象。

jstring  到 c/c++字符串

应从 jchar 到 wchar_t  的转换,而不是jchar  到 char。也就是在转换时,要保持jstring的每个jchar的值不变。

JNIEnv *env = 获取相关句柄;
jstring jstr = java字符串;

//获取java字符串的长度
jsize jstr_len = env->GetStringLength(jstr);
//获取java字符串的jchar指针
const jchar * pjstr = env->GetStringChars(jstr);

//申请c字符串的内存空间
wchar_t *pcstr = new wchar_t[jstr_len];//通常 sizeof(wchar_t)不小于2,若为1,应考虑使用其他类型来容纳jchar
//或者
std::wstring wstr;
wstr.assign(jstr_len,0);

//复制
jstr_len --;
while( jstr_len > -1 )
{
    pcstr[jstr_len ] = pjstr[jstr_len];
    //或者
     wstr[jstr_len] = pjstr[jstr_len];
    jstr_len -- ;
}

上面代码使用了GetStringChars而不是GetStringUTFChars。之所以这样做,就是为了保持java字符串到c/c++字符串的无损转换。

不过,仍然要注意一个问题,就00字节的处理,建议使用c++的wstring类。

如果:

        1、jstring的每一个jchar都在[1,127]内

        2、或者,你需要做UTF8转换但是不关心GetStringUTFChars采用变种UTF8还是标准UTF8,也不关心可逆转换

可以使用GetStringUTFChars。

转载于:https://www.cnblogs.com/mingfeng002/p/6515031.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android NDK开发是指利用NDK(Native Development Kit)将C/C++开发的代码编译成so库,然后通过JNI(Java Native Interface)让Java程序调用。在Android开发中,默认使用的是Android SDK进行Java语言的开发,而对于一些需要使用C/C++的高性能计算、底层操作或跨平台需求的场景,可以使用NDK进行开发。 在Android Studio中进行NDK开发相对于Eclipse来说更加方便,特别是在Android Studio 3.0及以上版本中,配置更加简化,并引入了CMake等工具,使得开发更加便捷。首先要进行NDK开发,需要配置环境,包括导入NDK、LLDB和CMake等工具。可以通过打开Android Studio的SDK Manager,选择SDK Tools,在其中选中相应的工具进行导入。 在项目的build.gradle文件中,可以配置一些NDK相关的参数,例如编译版本、ABI过滤器等。其中,可以通过externalNativeBuild配置CMake的相关设置,包括CMakeLists.txt文件的路径和版本号。此外,在sourceSets.main中还可以设置jniLibs.srcDirs,指定so库的位置。 在进行NDK开发时,可以在jni文件夹中编写C/C++代码,并通过JNI调用相关的函数。通过JNI接口,可以实现Java与C/C++之间的相互调用,从而实现跨语言的开发。 综上所述,Android NDK开发是指利用NDK将C/C++开发的代码编译成so库,并通过JNI实现与Java的相互调用。在Android Studio中进行NDK开发相对方便,可以通过配置环境和相应的参数来进行开发。<span class="em">1</span><span class="em">2</span><span class="em">3</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值