JNI的字符串字符集转换

澳门新浦京8455com 5

以下方式 总归 还是在使用使用java的string:java/lang/String

现在,你知道了如何通过JNI来访问JVM中的基本类型数据和字符串、数组这样的引用类型数据,下一步就是学习怎么样和JVM中任意对象的字段和方法进行交互。比如从本地代码中调用JAVA中的方法,也就是通常说的来自本地方法中的callbacks(回调)。
我们从进行字段访问和方法回调时需要的JNI函数开始讲解。本章的稍后部分我们会讨论怎么样通过一些cache(缓存)技术来优化这些操作。在最后,我们还会讨论从本地代码中访问字段和回调方法时的效率问题。

在Android Studio 新建一个包含C++的工程,新建完成后,可以在app
module目录下看到一个叫做CMakeLists.txt的文件,我们打开该文件,可以看到以下的内容,我对其中的cmake代码做了注释:

治根的办法 还是完整地在native code 中实现。推荐可以使用开源代码icu
或者iconv

4.1 访问字段

# cmake的最低版本
cmake_minimum_required(VERSION 3.4.1)

# 添加库
add_library( #  库名称
             native-lib

             # 库的类型 静态(STATIC)/动态(SHARED)
             SHARED

             # 包含的源文件
             src/main/cpp/native-lib.cpp )

# 查找库
find_library( # 重新命名
              log-lib

              # 查找NDK中的库
              log )

# 链接库
target_link_libraries( # 库名称,跟前面add_library中的名称保持一致
                       native-lib

                       # 需要链接的库名称, 可以链接多个库,用空格或换行符隔开
                       ${log-lib} )

android本身也是用了开源的库:iconv,只默认是未对外导出。

JAVA支持两种field(字段),每一个对象的实例都有一个对象字段的复制;所有的对象共享一个类的静态字段。本地方法使用JNI提供的函数可以获取和修改这两种字段。先看一个从本地代码中访问对象字段的例子:
class InstanceFieldAccess {
private String s;

在app/src/main/cpp/目录下看到一个native-lib.cpp的文件。打开文件,我们可以看到诸如以下的代码:

先放段代码作为开头,本段主要调用java中到构造函数,本段代码实现,字符串字符集转换。

 private native void accessField();
 public static void main(String args[]) {
     InstanceFieldAccess c = new InstanceFieldAccess();
     c.s = "abc";
     c.accessField();
     System.out.println("In Java:");
     System.out.println("  c.s = "" + c.s + """);
 }
 static {
     System.loadLibrary("InstanceFieldAccess");
 }
#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_cgfay_ndklearningproject_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
jstringMyNewString(JNIEnv*env,constchar*chars,jintlen){jclassstringClass;jmethodIDcid;jbyteArrayelemArr;jstringresult;jstringjencoding;stringClass=env-FindClass(java/lang/String);if(stringClass==NULL){returnNULL;/*exceptionthrown*/}/*GetthemethodIDfortheString(byte[]data,StringcharsetName)constructor*/cid=env-GetMethodID(stringClass,init,([BLjava/lang/String;)V);if(cid==NULL){returnNULL;/*exceptionthrown*/}jencoding=env-NewStringUTF(GBK);/*Createabyte[]thatholdsthestringcharacters*/elemArr=env-NewByteArray(len);if(elemArr==NULL){returnNULL;/*exceptionthrown*/}env-SetByteArrayRegion(elemArr,0,len,(jbyte*)chars);/*Constructajava.lang.Stringobject*/result=(jstring)(env-NewObject(stringClass,cid,elemArr,jencoding));/*Freelocalreferences*/env-DeleteLocalRef(elemArr);env-DeleteLocalRef(stringClass);returnresult;}

}
InstanceFieldAccess这个类定义了一个对象字段s。main方法创建了一个对象并设置s的值,然后调用本地方法InstanceFieldAccess.accessField在本地代码中打印s的值,并把它修改为一个新值。本地方法返回后,JAVA中把这个值再打印一次,可以看出来,字段s的值已经被改变了。下面是本地方法的实现:
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv env, jobject obj)
{
jfieldID fid; /
store the field ID */
jstring jstr;
const char *str;

然后我们回到Java层的MainActivity.java文件,可以看到诸如以下的代码:

以上代码时是参照《The JavaTM NativeInterfaceProgrammer’s Guide and
Specification》改的。

 /* Get a reference to obj's class */
 jclass cls = (*env)->GetObjectClass(env, obj);

 printf("In C:n");

 /* Look for the instance field s in cls */
 fid = (*env)->GetFieldID(env, cls, "s",
                          "Ljava/lang/String;");
 if (fid == NULL) {
     return; /* failed to find the field */
 }




 /* Read the instance field s */
 jstr = (*env)->GetObjectField(env, obj, fid);
 str = (*env)->GetStringUTFChars(env, jstr, NULL);
 if (str == NULL) {
     return; /* out of memory */
 }
 printf("  c.s = "%s"n", str);
 (*env)->ReleaseStringUTFChars(env, jstr, str);

 /* Create a new string and overwrite the instance field */
 jstr = (*env)->NewStringUTF(env, "123");
 if (jstr == NULL) {
     return; /* out of memory */
 }
 (*env)->SetObjectField(env, obj, fid, jstr);
public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

这里将字符集转换的全部代码贴上,记下以备忘,此外如果对大家有帮助,那就更好了。

}
运行程序,得到输出为:
In C:
c.s = “abc”
In Java:
c.s = “123”

那么,根据这个模板,我们来分析一下,Android JNI的使用方法吧。
首先,Java层中使用了下面的代码加载了一个库,库的名称是native-lib:

说穿了,其实就是在调用java.lang.String的方法。

4.1.1 访问一个对象字段的流程

static {
        System.loadLibrary("native-lib");
    }

一、CharToJString。

为了访问一个对象的实例字段,本地方法需要做两步:
首先,通过在类引用上调用GetFieldID获取field
ID(字段ID)、字段名字和字段描述符:
Fid=(env)->GetFieldID(env,cls,”s”,”Ljava/lang/String;”);
上例中的代码通过在对象引用obj上调用GetObjectClass获取到类引用。一旦获取到字段ID,你就可以把对象和字段ID作为参数来访问字段:
Jstr=(
env)->GetObjectField(env,obj,fid);
因为字符串和数组是特殊的对象,所以我们使用GetObjectField来访问字符串类型的实例字段。除了Get/SetObjectField,JNI还支持其它如GetIntField、SetFloatField等用来访问基本类型字段的函数。

这个库就是CMakeLists.txt中的add_library中定义的名称,接着定义了一个native方法:

二、JStringToChar。

4.1.2 字段描述符

public native String stringFromJNI();

Mark:

在上一节我们使用过一个特殊的C字符串“Ljava/lang/String”来代表一个JVM中的字段类型。这个字符串被称为JNI
field descriptor(字段描述符)。
字符串的内容由字段被声明的类型决定。例如,使用“I”来表示一个int类型的字段,“F”来表示一个float类型的字段,“D”来表示一个double类型的字段,“Z”来表示一个boolean类型的字段等等。
像java.lang.String这样的引用类型的描述符都是以L开头,后面跟着一个JNI类描述符,以分号结尾。一个JAVA类的全名中的包名分隔符“.”被转化成“/”。因此,对于一个字段类型的字段来说,它的描述符是“Ljava/lang/String”。
数组的描述符中包含“]”字符,后面会跟着数组类型的描述符,如“[I”是int[]类型的字段的描述符。12.3.3详细介绍了各种类型的字段描述以及他们代表的JAVA类型。
你可以使用javap工具来生成字段描述符。

然后,我们看到native-lib.cpp文件中定义了同样的方法:

以下代码在转中英文混合的字符串时会出错,原因是参数中的char*。更合适的接口可能应该如下:

4.1.3 访问静态字段

JNIEXPORT jstring

JNICALL
Java_com_cgfay_ndklearningproject_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */)
jstringCharToJString(JNIEnv*env,constvoid*pszSrc,intnLen,constchar*pszEncoding);intJStringToChar(JNIEnv*env,jstringjstr,constchar*pszEncoding,void*pszOutBuf,intnBufLen);

访问静态字段和访问实例字段相似,看下面这个InstanceFieldAccess例子的变形:
class StaticFielcdAccess {
private static int si;

这个方法包含了包名,Java层的包名中的 “.” 了
“_”。至此,我们了解到了JNI调用的最基本方法。就是通过定义一串长长的包名以及对应方法,通过这样的方式绑定native层。好了,废话不多说,直接进入正题。首先,为了方便使用log输出,我们定义如下方法帮助我们打印输出,用来替代printf:

java/native调用会比java/java调用慢上2~3倍,而虽然native/java调用,理论上和java/native效率一样,可是,实际上,由于后者的使用比前者多得多,所以java/native可能得到了足够多优化,这样,native/java调用可能比java/native调用慢上10倍之多。

 private native void accessField();
 public static void main(String args[]) {
     StaticFieldAccess c = new StaticFieldAccess();
     StaticFieldAccess.si = 100;
     c.accessField();
     System.out.println("In Java:");
     System.out.println("  StaticFieldAccess.si = " + si);
 }
 static {
     System.loadLibrary("StaticFieldAccess");
 }
#include "android/log.h"
#define JNI_TAG "JNI_LEARN"
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, JNI_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, JNI_TAG, __VA_ARGS__)
P/PPREclass=cppname=code/**youcanusethesefunctionfreely,butpleasekeepthisdeclarationwithnomodified.**createdbyjerry.lin,2011-08-04*/#includestdio.h#includejni.h#includestring.h/**convertthecharstringtoJString*parameter:*[in]pszSrc:thesourcestringtobeconverted.*[in]pszEncoding:theencodingofpszSrc,ifitisnull,meansutf8**returnvalue:*returntheconveredstringifissuccessful,otherwisereturnNULL.*/jstringCharToJString(JNIEnv*env,constchar*pszSrc,constchar*pszEncoding){jstringjstr=NULL;jclassstringClass=NULL;jbyteArraybyteElemArr=NULL;do{jmethodIDcid=NULL;jstringjstrEncoding=NULL;constchar*pszEnco=NULL;intnStrLen=0;/*checktheincomeparameters*/if(NULL==pszSrc||NULL==env){break;}/*gettheStringclassofjavaanditsconstructortocreateanewobject*//*gettheStringclassofjava*/stringClass=env-FindClass(java/lang/String);if(NULL==stringClass){break;}/*GetthemethodIDforthejava.lang.Stringconstructor*/cid=env-GetMethodID(stringClass,init,([BLjava/lang/String;)V);if(NULL==cid){break;}if(NULL==pszEncoding){pszEnco=utf-8;}else{pszEnco=pszEncoding;}jstrEncoding=env-NewStringUTF(pszEnco);if(NULL==jstrEncoding){break;}/*putcharstringintoarrayofjava*/nStrLen=(int)strlen(pszSrc);byteElemArr=env-NewByteArray(nStrLen);if(NULL==byteElemArr){break;}env-SetByteArrayRegion(byteElemArr,0,nStrLen,(jbyte*)pszSrc);/*createannew0objectofjava.lang.Stringwiththeconstructorstring(byte[],string)*/jstr=(jstring)(env-NewObject(stringClass,cid,byteElemArr,jstrEncoding));}while(0);/*Freelocalreferences*/if(NULL!=byteElemArr){env-DeleteLocalRef(byteElemArr);}if(NULL!=stringClass){env-DeleteLocalRef(stringClass);}returnjstr;}/**converttheJStringtocharstring.**parameter:*[in]jstr:thesourcestringtobeconverted.*[in]pszEncoding:theencodingtowhichtheJStringtobeconverted.*[out]pszOutBuf:thebuffertoremainthechangedstring.*[in]nBufLen:thelengthofthebuffer.**returnvalue:*thelengthofthestringinbytes,thathasbeencopiedintopszOutBuf.*/intJStringToChar(JNIEnv*env,jstringjstr,constchar*pszEncoding,char*pszOutBuf,intnBufLen){intnRet=-1;jclassstringClass=NULL;jbyteArraybyteElemArr=NULL;do{jmethodIDcid=NULL;jstringjstrEncoding=NULL;constchar*pszEnco=NULL;intnStrLen=0;/*checktheincomeparameters*/if(NULL==jstr||NULL==env){break;}/*gettheStringclassofjava*/stringClass=env-FindClass(java/lang/String);if(NULL==stringClass){break;}/*GetthemethodIDforthejava.lang.StringgetBytes(String)*/cid=env-GetMethodID(stringClass,getBytes,(java/lang/String;)[B);if(NULL==cid){break;}if(NULL==pszEncoding){pszEnco=utf-8;}else{pszEnco=pszEncoding;}jstrEncoding=env-NewStringUTF(pszEnco);if(NULL==jstrEncoding){break;}/*getcharstringintoarraywithdesignatedencode*/byteElemArr=(jbyteArray)env-CallObjectMethod(jstr,cid,jstrEncoding);if(NULL==byteElemArr){break;}nStrLen=(int)(env-GetArrayLength(byteElemArr));/*toreturnthelengthofthecharstring,ifnBufLenis0,orpszOutBufisNULL*/if(0==nBufLen||NULL==pszOutBuf){nRet=nStrLen;break;}/*ifthenBufLennStrLen,returnfailed*/if(nBufLennStrLen){nRet=-2;break;}/*getthecontentofthebyteArray,andcopyintotheoutbuffer*/jbyte*pszBytes=env-GetByteArrayElements(byteElemArr,JNI_FALSE);if(NULL!=pszBytes){memcpy(pszOutBuf,pszBytes,nStrLen);pszOutBuf[nStrLen]=0;nRet=nStrLen;}}while(0);/*Freelocalreferences*/if(NULL!=byteElemArr){env-DeleteLocalRef(byteElemArr);}if(NULL!=stringClass){env-DeleteLocalRef(stringClass);}returnnRet;}/PREBRBRP/PPBR/PPRE/PREPRE/PRE

}
StaticFieldAccess这个类包含一个静态字段si,main方法创建了一个对象,初始化静态字段,然后调用本地方法StaticFieldAccess.accessField在本地代码中打印静态字段中的值,然后设置新的值,为了演示这个值确实被改变了,在本地方法返回后,JAVA中再次这个静态字段的值。
下面是本地方法StaticFieldAccess.accessField的实现:
JNIEXPORT void JNICALL
Java_StaticFieldAccess_accessField(JNIEnv env, jobject obj)
{
jfieldID fid; /
store the field ID */
jint si;

数据类型介绍

JNI的原始数据类型 —— Primitive Data Types

Java Type JNI Type C/C++ Type Size
Boolean Jboolean unsigned char unsigned 8 bits
Byte jbyte char signed 8 bits
Char jchar unsigned short unsgined 16 bits
Short jshort short signed 16 bits
Int jint int signed 32 bits
Long jlong long long signed 64 bits
Float jfloat float 32 bits
Double jdouble double 64 bits

JNI 引用类型:

Java Type Native Type
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.String jstring
Other objects jobject
java.lang.Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
Other Arrays jArray
 /* Get a reference to obj's class */
 jclass cls = (*env)->GetObjectClass(env, obj);

 printf("In C:n");

 /* Look for the static field si in cls */
 fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
 if (fid == NULL) {
     return; /* field not found */
 }
 /* Access the static field si */
 si = (*env)->GetStaticIntField(env, cls, fid);
 printf("  StaticFieldAccess.si = %dn", si);
 (*env)->SetStaticIntField(env, cls, fid, 200);
String 类型

}
运行程序可得到输出结果:
In C:
StaticFieldAccess.si = 100
In Java:
StaticFieldAccess.si = 200
访问静态字段和对象实例字段的不同点:
1、
访问静态字段使用GetStaticFieldID,而访问对象的实例字段使用GetFieldID,但是,这两个方法都有相同的返回值类型:jfieldID。

新建一个Java String
std::string str = "hello world";
jstring javaString = env->NewStringUTF(str.c_str())

4.2 调用方法

将Java String转成 C String
    const char *str;
    jboolean isCopy;
    str = env->GetStringUTFChars(javaString, &isCopy);
    if (!str) {
        printf("java string: %s", str);
        if (isCopy == JNI_TRUE) {
            ALOGI("C string is a copy of the Java string");
        } else {
            ALOGI("C string points to actual string");
        }
    }
    env->ReleaseStringUTFChars(javaString, str);

编译运行我们可以打印一下Log:

澳门新浦京8455com 1

打印的log

JAVA中有几种不同类型的方法,实例方法必须在一个类的某个对象实例上面调用。而静态方法可以在任何一个对象实例上调用。对于构建方法的调用我们推迟到下一节。
JNI支持一系列完整的函数让你可以在本地代码中回调JAVA方法,下面例子演示了如何从本地代码中调用一个JAVA中的实例方法:
class InstanceMethodCall {
private native void nativeMethod();
private void callback() {
System.out.println(“In Java”);
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
澳门新浦京8455com,c.nativeMethod();
}
static {
System.loadLibrary(“InstanceMethodCall”);
}
}
下面的是本地方法的实现:
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
jclass cls = (
env)->GetObjectClass(env, obj);
jmethodID mid =
(env)->GetMethodID(env, cls, “callback”, “()V”);
if (mid == NULL) {
return; /
method not found /
}
printf(“In Cn”);
(
env)->CallVoidMethod(env, obj, mid);
}
运行程序,得到如下输出:
In C
In Java

Array 类型

Array类型操作如下:

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_arrayFromJNI(JNIEnv *env, jobject instance) {

    // 新建一个 int 数组
    jintArray javaArray = env->NewIntArray(10);
    if (javaArray != 0) {
        // 赋值操作
        jint *h = new jint[10];
        for (int i = 0; i < 10; ++i) {
            h[i] = i;
        }
        env->SetIntArrayRegion(javaArray, 0, 10, h);

        // 从jArray中取得数据
        jint nativeArray[10];
        env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);
        for (int i = 0; i < sizeof(nativeArray) / sizeof(jint); ++i) {
            ALOGI("nativeArray: %d", nativeArray[i]);
        }

        // 使用GetIntArrayElements方法获取的是一个指针,是需要释放的
        jint *nativeDirectArray;
        jboolean isCopy;
        nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);
        for (int i = 0; i < 10; ++i) {
            ALOGI("nativeDirectArray: %d", nativeDirectArray[i]);
        }
        if (isCopy == JNI_TRUE) {
            ALOGI("native direct array is a copy of the java array");
        } else {
            ALOGI("native direct array points to actual array");
        }
        // 释放类型
        // 0: 复制内容并释放本地数组
        // JNI_CIMMIT: 复制内容但不释放本地数据,用于定期更新Java数据
        // JNI_ABORT: 释放本地数组,但不复制内容
        env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);
    }

    return javaArray;
}

4.2.1 调用实例方法

NIO操作

NIO(Native
I/O)在缓冲管理区、大规模网络和文件I/O以及字符集支持方面的性能有所改进。NIO缓冲区的数据传送性能较好,更适合在原生代码和Java应用程序之间传送大量数据。

本地方法Java_InstanceMethodCall_nativeMethod的实现演示了在本地代码中调用JAVA方法的两步:
1、
本地方法首先调用JNI函数GetMethodID。这个函数在指定的类中寻找相应的方法。这个寻找过程是基于方法描述符的。如果方法不存在,GetMethodID返回NULL。这时,立即从本地方法中返回,并引发一个NoSuchMethodError错误。
2、 本地方法通过调用CallVoidMethod来调用返回值为void的实例方法。
除了CallVoidMethod这个函数以外,JNI也支持对返回值为其它类型的方法的调用。如果你调用的方法返回值类型为int,你的本地方法会使用CallIntMethod。类似地,你可以调用CallObjectMethod来调用返回值为java.lang.String、数组等对象类型的方法。
你也可以使用Call<Type>Method系列的函数来调用接口方法。你必须从接口类型中获取方法ID,下面的代码演示了如何在java.lang.Thread实例上面调用Runnable.run方法:
jobject thd = …; /* a java.lang.Thread instance /
jmethodID mid;
jclass runnableIntf =
(
env)->FindClass(env, “java/lang/Runnable”);
if (runnableIntf == NULL) {
… /* error handling /
}
mid = (
env)->GetMethodID(env, runnableIntf, “run”, “()V”);
if (mid == NULL) {
… /* error handling /
}
(
env)->CallVoidMethod(env, thd, mid);
… /* check for possible exceptions */
在3.3.5中,我们使用FindClass来获取一个类的引用,在这里,我们可以学到如何获取一个接口的引用。

创建字节缓冲区
unsigned char *buffer  = (unsigned char *) malloc(1024);
...
jobject directBuffer = env->NewDirectByteBuffer(buffer, 1024);

4.2.2 生成方法描述符

备注:

原生方法中的内存分配超出虚拟机管理范围,且不能用虚拟机的垃圾回收器回收原生方法中的内存。原生函数应该手动释放未使用的内存以避免内存泄漏。

JNI中描述字段使用字段描述符,描述方法同样有方法描述符。一个方法描述符包含参数类型和返回值类型。参数类型出现在前面,并由一对圆括号将它们括起来,参数类型按它们在方法声明中出现的顺序被列出来,并且多个参数类型之间没有分隔符。如果一个方法没有参数,被表示为一对空圆括号。方法的返回值类型紧跟参数类型的右括号后面。
例如,“(I)V”表示这个方法的一个参数类型为int,并且有一个void类回值。“()D”表示这个方法没有参数,返回值类型为double。
方法描述符中可能会包含类描述符(12.3.2),如方法native private String
getLine(String);的描述符为:“(Ljava/lang/String;)Ljava/lang/String;”
数组类型的描述符以“[”开头,后面跟着数组元素类型的描述符。如,public
static void main(String[]
args);的描述符是:”([Ljava/lang/String;)V”
12.3.4详细描述了怎么样生成一个JNI方法描述符。同样,你可以使用javap工具来打印出JNI方法描述符。
4.2.3 调用静态方法
前一个例子演示了一个本地方法怎样调用实例方法。类似地,本地方法中同样可以调用静态方法:
1、
通过GetStaticMethodID获取方法ID。对应于调用实例方法时的GetMethodID。
2、
传入类、方法ID、参数,并调用提供静态方法调用功能的JNI系列函数中的一个,如:CallStaticVoidMethod,CallStaticBooleanMethod等。
调用静态方法和调用实例方法的JNI函数有一个很大的不同,前者第二个参数是类引用,后者是对象实例的引用。
在JAVA访问一个静态方法可以通过类,也可以通过对象实例。而JNI的规定是,在本地代码中回调JAVA中的静态方法时,必须指定一个类引用才行。下面的例子演示了这个用法:
class StaticMethodCall {
private native void nativeMethod();
private static void callback() {
System.out.println(“In Java”);
}
public static void main(String args[]) {
StaticMethodCall c = new StaticMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary(“StaticMethodCall”);
}
}
下面是本地方法的实现:
JNIEXPORT void JNICALL
Java_StaticMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
jclass cls = (
env)->GetObjectClass(env, obj);
jmethodID mid =
(env)->GetStaticMethodID(env, cls, “callback”, “()V”);
if (mid == NULL) {
return; /
method not found /
}
printf(“In Cn”);
(
env)->CallStaticVoidMethod(env, cls, mid);
}
当调用CallStaticVoidMethod时,确保你传入的是类引用cls而不是对象引用obj。运行程序,输出为:
In C
In Java
4.2.4 调用父类的实例方法
如果一个方法被定义在父类中,在子类中被覆盖,你也可以调用这个实例方法。JNI提供了一系列完成这些功能的函数:CallNonvirtual<Type>Method。为了调用一个定义在父类中的实例方法,你必须遵守下面的步骤:
1、 使用GetMethodID从一个指向父类的引用当中获取方法ID。
2、
传入对象、父类、方法ID和参数,并调用CallNonvirtualVoidMethod、CallNonvirtualBooleanMethod等一系列函数中的一个。
这种调用父类实例方法的情况其实很少遇到,通常在JAVA中可以很简单地做到:super.f();
CallNonvirtualVoidMethod也可以被用来调用父类的构造函数。这个在下节就会讲到。
4.3 调用构造函数
JNI中,构造函数可以和实例方法一样被调用,调用方式也相似。传入“<init>”作为方法名,“V”作为返回类型。你可以通过向JNI函数NewObject传入方法来调用构造函数。下面的代码实现了与JNI函数NewString相同的功能:把存储在C缓冲区内的Unicode编码的字符序列,创建成一个java.lang.String对象:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jmethodID cid;
jcharArray elemArr;
jstring result;

直接缓冲区获取

直接获取原生字节数组的内存地址:

unsigned char* buffer = (unsigned char *) env->GetDirectBufferAddress(directBuffer);
 stringClass = (*env)->FindClass(env, "java/lang/String");
 if (stringClass == NULL) {
     return NULL; /* exception thrown */
 }
访问域

Java有两类域: 实例域 和
静态域。所有实例共享一个静态域,实例有自己的实例域副本。访问域的操作如下:
JniHelper中增加两个变量和一个native方法:

    // 静态域
    private static String staticField = "Static Field";
    // 实例域
    private String instanceField = "Instance Field";
    // 静态域与实例域
    public native void instanceFieldAndStaticField();

extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_instanceFieldAndStaticField(JNIEnv *env,
                                                                        jobject instance) {
    // 获取class对象
    jclass clazz =  env->GetObjectClass(instance);
    // 获取实例域的Id
    jfieldID instanceFieldId = env->GetFieldID(clazz, "instanceField", "Ljava/lang/String;");
    // 根据实例域Id获取实际的java 的字符串,也就是获取实例域
    jstring instanceField = (jstring) env->GetObjectField(instance, instanceFieldId);
    const char *instanceStr;
    jboolean isCopy;
    instanceStr = env->GetStringUTFChars(instanceField, &isCopy);
    ALOGI("instance field: %s", instanceStr);
    env->ReleaseStringUTFChars(instanceField, instanceStr);

    // 获取静态域的Id
    jfieldID staticFieldId = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
    // 根据静态域Id获取实际的Java的字符串,也就是获取静态域
    jstring staticField = (jstring) env->GetStaticObjectField(clazz, staticFieldId);
    const char *staticStr;
    staticStr = env->GetStringUTFChars(staticField, 0);
    ALOGI("instance field: %s", staticStr);
    env->ReleaseStringUTFChars(staticField, staticStr);

}

其中,”Ljava/lang/String;” 这个后续会讲到。现在你只要知道这是获取String
类型就行。
打印输出的Log如下:

澳门新浦京8455com 2

访问域输出

/* Get the method ID for the String(char[]) constructor /
cid = (
env)->GetMethodID(env, stringClass,
“<init>”, “([C)V”);
if (cid == NULL) {
return NULL; /* exception thrown */
}

备注:

获取单个域值需要调用两到三个JNI函数,存在性能问题,一般情况下都是直接从Java层传输native方法调用,而不是让Native方法去访问java中的域值。

 /* Create a char[] that holds the string characters */
 elemArr = (*env)->NewCharArray(env, len);
 if (elemArr == NULL) {
     return NULL; /* exception thrown */
 }
 (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);

 /* Construct a java.lang.String object */
 result = (*env)->NewObject(env, stringClass, cid, elemArr);

 /* Free local references */
 (*env)->DeleteLocalRef(env, elemArr);
 (*env)->DeleteLocalRef(env, stringClass);
 return result;
调用方法

java有两类方法 ——
实例方法和静态方法。下面介绍如何使用Native方法反过来调用java层的方法。
在JniHelper中添加三个方法:

    /**
     * native调用的静态方法
     * @return
     */
    private static String staticMethod() {
        return "Static Method";
    }

    /**
     * native调用的实例方法
     * @return
     */
    private String instanceMethod() {
        return "Instance Method";
    }

    // 静态方法与实例方法
    public native void instanceMethodAndStaticMethod();

访问方法的方式如下:

extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_instanceMethodAndStaticMethod(JNIEnv *env,
                                                                          jobject instance) {

    // 获取class对象
    jclass clazz = env->GetObjectClass(instance);
    // 获取实例方法Id
    jmethodID instanceMethodId = env->GetMethodID(clazz, "instanceMethod", "()Ljava/lang/String;");
    jstring instanceMethodResult = (jstring) env->CallObjectMethod(instance, instanceMethodId);
    const char *instanceStr;
    instanceStr = env->GetStringUTFChars(instanceMethodResult, 0);
    ALOGI("instance method: %s", instanceStr);
    env->ReleaseStringUTFChars(instanceMethodResult, instanceStr);

    // 获取静态方法Id
    jmethodID staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()Ljava/lang/String;");
    jstring staticMethodResult = (jstring) env->CallStaticObjectMethod(clazz, staticMethodId);
    const char *staticStr;
    staticStr = env->GetStringUTFChars(staticMethodResult, 0);
    ALOGI("static method call: %s", staticStr);
    env->ReleaseStringUTFChars(staticMethodResult, staticStr);
}

其中,”()Ljava/lang/String;”
表示的是方法的签名,包括参数和返回值。后续将会介绍到。编译运行,则可以看到打印输出一下Log:

澳门新浦京8455com 3

访问方法输出

}
上面这个本地方法有些复杂,需要详细解释一下。首先,FindClass返回一个java.lang.String类的引用,接着,GetMethodID返回构造函数String(char[]
chars)的方法ID。我们调用NewCharArray分配一个字符数组来保存字符串元素。JNI函数NewObject调用方法ID所标识的构造函数。NewObject函数需要的参数有:类的引用、构造方法的ID、构造方法需要的参数。
DeleteLocalRef允许VM释放被局部引用elemArr和stringClass引用的资源。5.2.1中详细描述了调用DeleteLocalRef的时机和原因。
这个例子引出了一个问题,既然我们可以利用JNI函数来实现相同的功能,为什么JNI还需要NewString这样的内置函数?原因是,内置函数的效率远高于在本地代码里面调用构造函数的API。而字符串又是最常用到的对象类型,因此需要在JNI中给予特殊的支持。
你也可以做到通过CallNonvirtualVoidMethod函数来调用构造函数。这种情况下,本地代码必须首先通过调用AllocObject函数创建一个未初始化的对象。上面例子中的result
= (env)->NewObject(env, stringClass, cid,
elemArr);可以被如下代码替换:
result = (
env)->AllocObject(env, stringClass);
if (result) {
(env)->CallNonvirtualVoidMethod(env, result, stringClass,
cid, elemArr);
/
we need to check for possible exceptions /
if ((
env)->ExceptionCheck(env)) {
(*env)->DeleteLocalRef(env, result);
result = NULL;
}
}
AllocObject创建了一个未初始化的对象,使用时一定要非常小心,确保一个对象上面,构造函数最多被调用一次。本地代码不应该在一个对象上面调用多次构造函数。有时,你可能会发现创建一个未初始化的对象然后一段时间以后再调用构造函数的方式是很有用的。尽管如此,大部分情况下,你应该使用NewObject,尽量避免使用容易出错的AllocObject/CallNonvirtualVoidMethod方法。

备注:

为了性能考虑,一般会缓存常用的方法Id。比如native层执行操作完成之后,通过这样的方式回调输出。

4.4 缓存字段ID和方法ID

域和方法描述符

前面提到的获取于域ID 和 方法Id
均分别需要域描述符和方法描述符。描述符可以通过下面的表格对应的签名映射获得:

Java 类型 签名
Boolean Z
Byte B
Char C
Short S
Int I
Long J
Float F
Double D
类全称 L类全称
type[] [type
void V
方法类型 (参数类型)返回值类型

比如前面的 “()Ljava/lang/String;”
表示的是参数为void,返回值为String的方法。java/lang/String;
表示的是类的全称,也就是java.lang.String。
我们可以通过使用javap 来反汇编程序,得到各种类里面的签名

获取字段ID和方法ID时,需要用字段、方法的名字和描述符进行一个检索。检索过程相对比较费时,因此本节讨论用缓存技术来减少这个过程带来的消耗。缓存字段ID和方法ID的方法主要有两种。两种区别主要在于缓存发生的时刻,是在字段ID和方法ID被使用的时候,还是定义字段和方法的类静态初始化的时候。

异常处理

native中的异常行为与Java中的有所不同。我们可以在native层发生异常后回传给java层做处理。

4.4.1 使用时缓存

捕获与抛出异常

在JniHelper中定义两个方法:

    /**
     * native层捕获异常与抛出异常
     * @throws NullPointerException
     */
    private void throwingMethod() throws NullPointerException {
        throw new NullPointerException("Null Pointer!");
    }
    // 捕获异常
    public native void acessMethods();

native层的实现如下:

/**
 * 捕获异常与抛出异常
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_acessMethods(JNIEnv *env, jobject instance) {

    jthrowable ex;
    jclass clazz = env->GetObjectClass(instance);
    // 获取跑出异常的方法
    jmethodID throwingMethodId = env->GetMethodID(clazz, "throwingMethod", "()V");
    // 调用方法
    env->CallVoidMethod(instance, throwingMethodId);
    // 查询虚拟机中是否有挂起的异常(显式做异常处理)
    ex = env->ExceptionOccurred();
    if (ex != 0) {
        // 描述
        env->ExceptionDescribe();
        // 使用完成需要清除异常
        env->ExceptionClear();

        // 抛出异常
        jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
        if (newExcCls) {
            env->ThrowNew(clazz, "Exception message");
        }
    }

}

调用acessMethods 方法后,你就会看到如下所示的异常:

澳门新浦京8455com 4

异常处理

字段ID和方法ID可以在字段的值被访问或者方法被回调的时候缓存起来。下面的代码中把字段ID存储在静态变量当中,这样当本地方法被重复调用时,不必重新搜索字段ID:
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv env, jobject obj)
{
static jfieldID fid_s = NULL; /
cached field ID for s */

局部和全局引用
 jclass cls = (*env)->GetObjectClass(env, obj);
 jstring jstr;
 const char *str;

 if (fid_s == NULL) {
     fid_s = (*env)->GetFieldID(env, cls, "s", 
                                "Ljava/lang/String;");
     if (fid_s == NULL) {
         return; /* exception already thrown */
     }
 }

 printf("In C:n");

 jstr = (*env)->GetObjectField(env, obj, fid_s);
 str = (*env)->GetStringUTFChars(env, jstr, NULL);
 if (str == NULL) {
     return; /* out of memory */
 }
 printf("  c.s = "%s"n", str);
 (*env)->ReleaseStringUTFChars(env, jstr, str);

 jstr = (*env)->NewStringUTF(env, "123");
 if (jstr == NULL) {
     return; /* out of memory */
 }
 (*env)->SetObjectField(env, obj, fid_s, jstr);
局部引用

大多数JNI函数返回局部引用。可以通过FindClass函数返回一个局部引用。局部引用不能再后续的调用中被缓存以及重用。当一个原生方法返回时,它被自动释放,也可以调用DeleteLocalRef(clazz);方法释放原生代码

}
由于多个线程可能同时访问这个本地方法,上面方法中的代码很可能会导致混乱,其实没事,多个线程计算的ID其实是相同的。
同样的思想,我们也可以缓存java.lang.String的构造方法的ID:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jcharArray elemArr;
static jmethodID cid = NULL;
jstring result;

全局引用

全局引用在原生方法的后续调用过程中依然有效,除非被显式地释放掉。全局方法的使用如下:
1、创建全局引用

jclass localClazz;
jclass globalClazz;
...
localClazz = env->FindClass("java/lang/String");
globalClazz = env->NewGlobalRef(localClazz);
...
env->DeleteLocalRef(localClazz);

2、删除全局引用

env->DeleteGlobalRef(globalClazz);
 stringClass = (*env)->FindClass(env, "java/lang/String");
 if (stringClass == NULL) {
     return NULL; /* exception thrown */
 }

 /* Note that cid is a static variable */
 if (cid == NULL) {
     /* Get the method ID for the String constructor */
     cid = (*env)->GetMethodID(env, stringClass,
                               "<init>", "([C)V");
     if (cid == NULL) {
         return NULL; /* exception thrown */
     }
 }

 /* Create a char[] that holds the string characters */
 elemArr = (*env)->NewCharArray(env, len);
 if (elemArr == NULL) {
     return NULL; /* exception thrown */
 }
 (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);

 /* Construct a java.lang.String object */
 result = (*env)->NewObject(env, stringClass, cid, elemArr);

 /* Free local references */
 (*env)->DeleteLocalRef(env, elemArr);
 (*env)->DeleteLocalRef(env, stringClass);
 return result;
弱全局引用

1、创建弱全局引用

jclass weakGlobalClazz = env->NewWeakGlobalRef(localClazz);

2、弱全局引用的有效性检验

if (env->IsSameObject(weakGlobalClazz, NULL) == JNI_FALSE) {
// 对象仍然处于活动状态且可以使用
} else {
// 对象回收,不能使用
}

3、删除弱全局引用

env->DeleteWeakGlobalRef(weakGlobalClazz);

}
当MyNewString方法第一次被调用时,我们计算java.lang.String的构造方法的ID,并存储在静态变量cid中。
4.4.2 类的静态初始化过程中缓存字段和方法ID
我们在使用时缓存字段和方法的ID的话,每次本地方法被调用时都要检查ID是否已经被缓存。许多情况下,在字段ID和方法ID被使用前就初始化是很方便的。VM在调用一个类的方法和字段之前,都会执行类的静态初始化过程,所以在静态初始化该类的过程中计算并缓存字段ID和方法ID是个不错的选择。
例如,为了缓存InstanceMethodCall.callback的方法ID,我们引入了一个新的本地方法initIDs,这个方法在InstanceMethodCall的静态初始化过程中被调用。代码如下:
class InstanceMethodCall {
private static native void initIDs();
private native void nativeMethod();
private void callback() {
System.out.println(“In Java”);
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary(“InstanceMethodCall”);
initIDs();
}
}
与4.2节中的代码相比,上面这段代码多了两行,initIDs方法简单地计算并缓存方法ID:
jmethodID MID_InstanceMethodCall_callback;

线程

虚拟机支持运行时原生代码,但有约束:
只在原生方法执行期间以及正在执行原生方法的线程环境下局部引用是有效的,局部引用不能在多线程间共享
被传递给每个原生方法的JNIEnv接口指针在于方法调用相关的线程中也是有效的,它不能被其他线程缓存或使用

JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv env, jclass cls)
{
MID_InstanceMethodCall_callback =
(
env)->GetMethodID(env, cls, “callback”, “()V”);
}
VM进行静态初始化时在调用任何方法前调用initIDs,这样方法ID就被缓存了全局变量中,本地方法的实现就不必再进行ID计算:
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv env, jobject obj)
{
printf(“In Cn”);
(
env)->CallVoidMethod(env, obj,
MID_InstanceMethodCall_callback);
}
4.4.3 两种缓存ID的方式之间的对比
如果JNI程序员不能控制方法和字段所在的类的源码的话,在使用时缓存是个合理的方案。例如在MyNewString当中,我们不能在String类中插入一个initIDs方法。
比起静态初始时缓存来说,使用时缓存有一些缺点:
1、 使用时缓存的话,每次使用时都要检查一下。
2、
方法ID和字段ID在类被unload时就会失效,如果你在使用时缓存ID,你必须确保只要本地代码依赖于这个ID的值,那么这个类不被会unload(下一章演示了如何通过使用JNI函数创建一个类引用来防止类被unload)。另一方面,如果缓存发生在静态初始化时,当类被unload和reload时,ID会被重新计算。
因此,尽可能在静态初始化时缓存字段ID和方法ID。
4.5 JNI操作JAVA中的字段和方法时的效率
学完了如何缓存ID来提高效率后,你可能会对使用JNI访问java字段和方法的效率不太明白,native/java比起java/native和java/java来的话,效率如何呢?
当然,这取决于VM的实现。我们不能给出在大范围的VM上通用的数据,但我们可以通过分析本地方法回调java方法和JNI操作字段以及方法的过程来给出一个大致的概念。
我们从比较java/native和java/java的效率开始。java/native调用比java/java要慢,主要有以下几个原因:
1、
java/native比起JVM内部的java/java来说有一个调用转换过程,在把控制权和入口切换给本地方法之前,VM必须做一些额外的操作来创建参数和栈帧。
2、
对VM来说,对方法调用进行内联比较容易,而内联java/native方法要难得多。
据我们的估计,VM进行java/native调用时的消耗是java/java的2~3倍。当然VM可以进行一些调整,使用java/native的消耗接近或者等于java/java的消耗。
技术上来讲,native/java调用和java/native是相似的。但实际上native/java调用很少见,VM通常不会优化native/java这种回调方式。多数VM中,native/java调用的消耗可以达到java/java调用的10倍。
使用JNI访问字段的花费取决于通过JNIEnv进行调用的消耗。以废弃一个对象引用来说,本地代码必须依赖于特定的JNI函数才能做到,而这个依赖是必须的,它把本地代码和VM中对象的内部形式很好地隔离开。

同步

JNI允许原生代码利用Java对象同步,虚拟机保证存储监视器的线程能够安全执行,而其他线程等待监视器对象变成可用状态。
Java 同步代码块

synchronized(obj) {
// 同步线程安全代码块
}

等价原生代码块:

if (env->MonitorEnter(obj) == JNI_OK) {
// 错误处理
}

// 同步线程安全代码块


if (env->MonitorExit(obj) == JNI_OK) {
// 错误处理
}
原生线程

JNI通过JavaVM接口指针提供了AttachCurrentThread
函数便于让原生代码将原生线程附着到虚拟机上。JavaVM接口指针应该尽早被缓存,否则不能被获取
将当前线程与虚拟机附着和分离:

JavaVM *cachedJvm;

JNIEnv *env;

// 将当前线程附着到虚拟机
cachedJvm->AttachCurrentThread(&env, NULL);

/* 可以用JNIEnv 接口实现线程与Java应用程序的通信 */
...

// 将当前线程与虚拟机分离
cachedJvm->DetachCurrentThread();

线程使用例子如下:
在JniHelper中添加以下方法:

    /**
     * native 线程回调
     */
    private String threadCallBack() {
        return "thread call back";
    }

    // 开始线程
    public native void startThread();

native层实现如下:

/**
 * 线程操作例子
 */

JavaVM* javaVM;
jobject gInstance;

static void *thread_sample(void* argv) {
    JNIEnv *env;
    // 从全局的JavaVM中获取环境变量
    javaVM->AttachCurrentThread(&env, NULL);
    ALOGI("log in another thread!!!!");
    // 获取Java层对应的类
    jclass clazz = (jclass) env->GetObjectClass(gInstance);
    // 获取方法
    jmethodID id = env->GetMethodID(clazz, "threadCallBack", "()Ljava/lang/String;");
    jstring jStr = (jstring) env->CallObjectMethod(gInstance, id);
    const char *instanceStr;
    instanceStr = env->GetStringUTFChars(jStr, 0);
    ALOGI("instance method: %s", instanceStr);
    env->ReleaseStringUTFChars(jStr, instanceStr);
    sleep(1);
    // 删除全局变量
    env->DeleteGlobalRef((jobject)gInstance);
    javaVM->DetachCurrentThread();

    return NULL;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_cgfay_ndklearningproject_JniHelper_startThread(JNIEnv *env, jobject instance) {

    // 获取JavaVM
    env->GetJavaVM(&javaVM);
    // 创建全局对象
    gInstance = env->NewGlobalRef(instance);

    // 创建新线程
    pthread_t thread;

    pthread_create(&thread, NULL, thread_sample, NULL);
}

最后编译运行,可以看到以下的Log:

澳门新浦京8455com 5

线程使用log

工程的Github 地址如下:
NDKLearningProject

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图