目录
上一篇 深入浅出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有如如下输出:
关于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数组操作