jni学习

 

一个简单解释:

 http://www.cnblogs.com/mandroid/archive/2011/06/15/2081093.html

 

又一个:

http://developer.51cto.com/art/200509/2815.htm

 

jni手册:

http://java.sun.com/docs/books/jni/

 

 

 

jni是和java程序在一个进程中,当然, jni可以调用native code的api, 从而使native daemon和java在不同进程。

 

通常java和native进程还有其他的通讯方式, 比如:

1. tcp/ip或者其他ipc技术

2. jdbc

3. java idl

 

先生成java文件,并声明native函数,

然后 调用 javah -jni xxx生成jni的header file

 

JNIEnv:

 

 

The second argument differs depending on whether the native method is a
static or an instance method. The second argument to an instance native method is
a reference to the object on which the method is invoked, similar to the this
pointer in C++. The second argument to a static native method is a reference to
the class in which the method is defined. Our example, Java_Prompt_getLine,
implements an instance native method. Thus the jobject parameter is a reference
to the object itself.

 

 

There are two kinds of types in the Java programming language: primitive
types such as int, float, and char, and reference types such as classes, instances,
and arrays. In the Java programming language, strings are instances of the
java.lang.String class.

 

java premitive类型和jni类型map:

Java Language Type         Native Type                    Description
boolean                                jboolean                        unsigned  8 bits
byte                                        jbyte                               signed 8 bits
char                                       jchar                               unsigned 16 bits
short                                     jshort                              signed 16 bits
int                                          jint                                   signed 32 bits
long                                      jlong                                signed 64 bits
float                                      jfloat                                 32 bits
double                                 jdouble                            64 bits

 

 

The JNI passes objects to native methods as opaque references.

 

opaque references是c指针,指向jvm的内部数据结构, 没有c 的类型对应, 需要使用JNIEnv提供的函数来转换, 比如:

java的java.lang.String类型对应jni的jstring, 需要调用JNIEnv的GetStringUTFChars来得到真正的string。

 

所有的reference都有jobject类型, 其他类型是jobject的子类型:

 

 

The jvalue type is a union of the reference types and primitive types. It is defined
as follows:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;

 

 

Method and field IDs are regular C pointer types:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field ID */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method ID */

 

jstring转换的sample:

JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than
* 127 characters */
scanf("%s", buf);
return (*env)->NewStringUTF(env, buf);
}

 

 

The JNI uses C strings to represent class names, field and method names, and field
and method descriptors. These strings are in the UTF-8 format.

 

0-127表示ascii码, 128-255之间表示ASCII扩展(ibm字符集)或者ISO 8859-1Latin 1, 而UNICODE编码通过增加一个高字节对ISO Latin-1字符集进行扩展, UNICODE支持欧洲、非洲、中东、亚洲(包括统一标准的东亚象形汉字和韩国象形文字)。

 

如果UNICODE字符由2个字节表示,则编码成UTF-8很可能需要3个字节,而如果UNICODE字符由4个字节表示,则编码成UTF-8可能需要6个字节。用4个或6个字节去编码一个UNICODE字符可能太多了,但很少会遇到那样的UNICODE字符。

  

UTF-8部分格式对照表

 

下面是unicode对utf-8的转换码表:

UNICODEUTF-8
0000 0000 -
0000 007F
0XXX XXXX
0000 0080 -
0000 07FF
110X XXXX
10XX XXXX
0000 0800 -
0000 FFFF
1110 XXXX
10XX XXXX
10XX XXXX
0001 0000 -
001F FFFF
1111 0XXX
10XX XXXX
10XX XXXX
10XX XXXX
0020 0000 -
03FF FFFF
1111 10XX
10XX XXXX
10XX XXXX
10XX XXXX
10XX XXXX
0400 0000 -
7FFF FFFF
1111 110X
10XX XXXX
10XX XXXX
10XX XXXX
10XX XXXX
10XX XXXX

 

一个unicode的码值对应右边utf-8的一组编码, 最上面的是utf-8的首字节, 对应着uncode的高位。

每个字节由一个换码序列开始。第一个字节由唯一的换码序列,由n位连续的1加一位0组成, 首字节连续的1的个数表示字符编码所需的字节数

由上分析可以看到,UNICODE到UTF-8的转换就是先确定编码所需要的字节数,然后用UNICODE编码位从低位到高位依次填入上面表示为x的位上,不足的高位以0补充。

字节FF和FE在UTF-8编码中永远不会出现

 

8位字符的UTF-8编码会被email网关过滤,因为internet信息最初设计为7位ASCII码。因此产生了UTF-7编码。 UTF-8 在它的表示中使用值100xxxxx的几率超过50%, 而现存的实现如ISO 2022, 4873, 6429, 和8859系统,会把它错认为是C1 控制码。因此产生了UTF-7.5编码。

 

java的utf-8和标准的utf-8格式有点不同:

First, the null byte (byte)0 is encoded using the two-byte format rather than the
one-byte format. This means that JNI UTF-8 strings never have embedded nulls(因为有utf-8的识别码).

Second, only the one-byte, two-byte, and three-byte formats are used. The JNI
does not recognize the longer UTF-8 formats.

 

ReleaseStringUTFChars:用于释放utf-8z字符的buffer

NewStringUTF: 构建新的java.lang.String instance in the native method

GetStringChars and ReleaseStringChars obtain string characters represented
in the Unicode format.

UTF-8 strings are always terminated with the ‘\0’ character, whereas Unicode
strings are not.

GetStringLength:得到unicode string的长度

GetStringUTFLength or ANSI C function strlen: 得到utf-8的长度

 

GetStringChars和GetStringUTFChars有第三个参数jboolean *isCopy:

JNI_FALSE: 返回的string指向original java.lang.String instance.

JNI_TRUE:返回的string指向original java.lang.String 的copy的instance.

When the location pointed
to by isCopy is set to JNI_FALSE, native code must not modify the contents of the
returned string

It is in general not possible to predict whether the virtual machine will copy
the characters in a given java.lang.String instance.

 

Once a direct pointer to a java.lang.String instance is passed back to
the native code, the garbage collector can no longer relocate the
java.lang.String instance.

 

The ReleaseStringChars
call is necessary whether GetStringChars has set *isCopy to JNI_TRUE or
JNI_FALSE. ReleaseStringChars either frees the copy or unpins the instance,
depending upon whether GetStringChars has returned a copy or not.

 

Java 2 SDK release 1.2 introduces
a new pair of functions, Get/ReleaseStringCritical(disable GC)为了得到direct pointer, 但有一些限制条件:

When garbage collection(因为GC产生的这个copied string) is disabled, any other
threads that trigger garbage collection will be blocked as well.

Native code between a Get/ReleaseStringCritical pair must not issue blocking calls or
allocate new objects in the Java virtual machine. Otherwise, the virtual machine
may deadlock.

这个函数的嵌套调用是允许的。

 

The JNI does not support GetStringUTFCritical and ReleaseStringUTFCritical
functions.

 

 

GetStringRegion and GetStringUTFRegion: 将string输出到指定的buffer,例如:

char outbuf[128];

int len = (*env)->GetStringLength(env, prompt);
(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);
printf("%s", outbuf);

 

 

The JNI treats primitive arrays and object arrays differently。

 

 

int[][] arr2是object arrays。

 

访问primitive arrays的方法跟访问string的方法类似, 需要使用JNIEnv的函数, 例如一个访问int型array的例子:

jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;

 

 

Get/Release<Type>ArrayElements functions 返回直接的direct pointer to the elements of primitive arrays.

primitive array方法总结:

 

 

The JNI provides a separate pair of functions to access objects arrays.
GetObjectArrayElement returns the element at a given index, whereas
SetObjectArrayElement updates the element at a given index.

 

you cannot get all the object elements or copy
multiple object elements at once.

 

一个生成两维数组的例子(行列尺寸都是size):

JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env,
jclass cls,
int size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = (*env)->NewObjectArray(env, size, intArrCls,
NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}

 

 

 

the NewObjectArray function allocates an array whose element type is
denoted by the intArrCls class reference:

 jclass intArrCls = (*env)->FindClass(env, "[I");

 

The DeleteLocalRef call at the end of the loop ensures that the virtual
machine does not run out of the memory used to hold JNI references such as iarr.

 

一般情况下, jvm会释放reference, 有几种情况需要显示地释放:

1.  占用大量reference table

2. You want to write a utility function that is called from unknown contexts.(unkown contexts是什么意思?指这个jni函数不知道被谁调用还是指jni函数里面调用了如后方法?cid = (*env)->GetMethodID(env, stringClass,
"<init>", "([C)V");  其实这里指的一种情况是在jni中生成java对象

3. 函数不返回(释放是在jni函数返回以后或者后面不用(case 4 例外))

4. Your native method accesses a large object, thereby creating a local reference
to the object. The native method then performs additional computation before
returning to the caller. The local reference to the large object will prevent the
object from being garbage collected until the native method returns, even if
the object is no longer used in the remainder of the native method.

 

显示释放也是出发GC。

 

两种fields:

 instance fields: 为对象

static fields: 所有类共享

 

访问instance field的步骤:

1. jclass cls = (*env)->GetObjectClass(env, obj);  //先得到instance的class对象

2. fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;"); //得到成员的field id

3. jstr = (*env)->GetObjectField(env, obj, fid); //得到成员, 这里是string的例子

4. (*env)->SetObjectField(env, obj, fid, jstr); //更改成员值

 

"Ljava/lang/String;"是JNI field descriptors, 对于reference type的, 需在前面加个L, "."号变成了"/"

“[”代表array类型, "[I"表示int[]类型

“double[][][]”用"[[[D"表示

 

Field Descriptor Java Language Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double

 

 

Field Descriptor Java Language Type
"Ljava/lang/String;" String
"[I" int[]
"[Ljava/lang/Object;" Object[]

 

 

访问static field的步骤:

1. jclass cls = (*env)->GetObjectClass(env, obj);  //先得到class ref

2.fid = (*env)->GetStaticFieldID(env, cls, "si", "I"); //得到成员的field id

3.si = (*env)->GetStaticIntField(env, cls, fid); //得到成员, 这里是int的例子

4. (*env)->SetStaticIntField(env, cls, fid, 200); //更改成员值

 

 

 

两种methods:

 instance method: 为对象

static method: 所有类共享

 

访问instance field的步骤:

1. jclass cls = (*env)->GetObjectClass(env, obj); //得到instance obj

2. jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V"); //得到method

3. (*env)->CallVoidMethod(env, obj, mid); //调用

 

也可用接口来访问, 如下:

 jclass runnableIntf =
(*env)->FindClass(env, "java/lang/Runnable");

 

A method descriptor combines the argument types and the
return type of a method.

 

"(I)V": 参数int型。 返回类型是void

"()D": 没有参数, 返回double型

Method Descriptor Java Language Type
"()Ljava/lang/String;" String f();
"(ILjava/lang/Class;)J" long f(int i, Class c);
"([B)V" String(byte[] bytes);

 

 

c风格的“int f(void)” 变成"()I"; // 不能传递void *了, java没有指针, init f(void, void)也应该没有,(java没有void类型)

 

native private String getLine(String);
has the following descriptor:
"(Ljava/lang/String;)Ljava/lang/String;"

 

array如下:

public static void main(String[] args);
is as follows:
"([Ljava/lang/String;)V"

 

 

访问instance field的步骤:

1. jclass cls = (*env)->GetObjectClass(env, obj); //得到instance obj

2. jmethodID mid = (*env)->GetStaticMethodID(env, cls, "callback", "()V");//得到method

3. (*env)->CallStaticVoidMethod(env, cls, mid); //调用

 

用CallNonvirtual<Type>Method方法访问父类实现的函数

 

在jni中构造java对象

1. stringClass = (*env)->FindClass(env, "java/lang/String");

2. cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V"); // 得到构造函数id, 注意"<init>", 为构造函数用的

3. result = (*env)->NewObject(env, stringClass, cid, elemArr); //生成新对象, 构造函数将运行

 

 

 

result = (*env)->AllocObject(env, stringClass); //生成一个未初始化的object

(*env)->CallNonvirtualVoidMethod(env, result, stringClass, cid, elemArr); //调用构造函数

用static变量保存cached feild或者method(method可用id, 而用 (*env)->FindClass(env, "java/lang/String");的方法得到的是local reference,local reference将被释放,  用id的方式比如static jmethodID cid = NULL;)(调用jni时)。

static jfieldID fid_s = NULL;

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

One thread may overwrite
the static variable fid_s computed by another thread. Luckily, although this
race condition leads to duplicated work in multiple threads, it is otherwise harmless.
The field IDs computed by multiple threads for the same field in the same
class will necessarily be the same.

这种方法是线程安全的!!!!

 

可以用初始化函数的方式, 通过全局的变量或者方法cache feild和method, 比如: 

jmethodID MID_InstanceMethodCall_callback;
JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls)
{
MID_InstanceMethodCall_callback =
(*env)->GetMethodID(env, cls, "callback", "()V");
}

 

一些宏:

JNIEXPORT and JNICALL are macros used to specify the calling and linkage convention
of both JNI functions and native method implementations. The

 

JNIEXPORT jint JNICALL
Java_pkg_Cls_f(JNIEnv *env, jobject this);

 

jboolean:

#define JNI_FALSE 0
#define JNI_TRUE 1

 

#define JNI_OK 0
#define JNI_ERR (-1)

 

#define JNI_COMMIT 1
#define JNI_ABORT 2

 

#define JNI_VERSION_1_1 0x00010001 /* JNI version 1.1 */
#define JNI_VERSION_1_2 0x00010002 /* JNI version 1.2 */

 

#define JNI_EDETACHED (-2) /* thread detached from the VM */
#define JNI_EVERSION (-3) /* JNI version error */

 

 

一个使用global reference的例子:

static jclass stringClass = NULL;
...
if (stringClass == NULL) {
jclass localRefCls =
(*env)->FindClass(env, "java/lang/String");
if (localRefCls == NULL) {
return NULL; /* exception thrown */
}
/* Create a global reference */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* The local reference is no longer useful */
(*env)->DeleteLocalRef(env, localRefCls);
/* Is the global reference created successfully? */
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}

 

 

a weak global reference allows to still be unloaded, 使用They are created using
NewGlobalWeakRef and freed using DeleteGlobalWeakRef.

对于系统提供的class, 用global或者weak global没有不同。

 

使用下面的方法判断是否同样的obj:

(*env)->IsSameObject(env, obj1, obj2)

 

 

The JNI specification dictates that the virtual machine automatically ensures
that each native method can create at least 16 local references.

用下面的方法确认有无空间可放local ref:

if ((*env)->EnsureLocalCapacity(env, len)) < 0) {
... /* out of memory */
}

 

可以用Push/PopLocalFrame管理local ref帧:

#define N_REFS ... /* the maximum number of local references
used in each iteration */
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) < 0) {
... /* out of memory */
}
jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
(*env)->PopLocalFrame(env, NULL);
}

 

创建local ref:

NewLocalRef,

The NewLocalRef function is useful when you write utility functions that are
expected to return a local reference.

 

怎么用(应该是启动虚拟机的时候用):

Java 2 SDK release 1.2 supports a command-line option -verbose:jni.
When this option is enabled, the virtual machine implementation reports excessive
local reference creation beyond the reserved capacity.

 

DeleteGlobalRef: 释放global ref

DeleteWeakGlobalRef: 释放weak global ref

global和weak global的不同: GC不会释放global

 

 

ExceptionOccurred: 检查异常发生

ExceptionDescribe:  打印异常

ExceptionClear: 清除异常

 ThrowNew: 抛出异常

ExceptionCheck: 检查是否出现异常

 

使用下列函数在c语言中创建jvm,并运行java程序:

1. JNI_CreateJavaVM

2. mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");

3. (*env)->CallStaticVoidMethod(env, cls, mid, args);

4. (*jvm)->DestroyJavaVM(jvm);

这种方式甚至可在native code中使用多线程来调用虚拟机对应的java代码。

需要这些库:  cc -I<jni.h dir> -L<libjava.so dir> -lthread -ljava invoke.c

 

 JNIEnv不能在对线程中作为参数传递, 应该用jvm attach((*jvm)->AttachCurrentThread)的方法获取

local reference不能在线程中作为参数传递, 可转换成global

 

MonitorEnter/MonitorExit就像互斥锁, 用于线程阻塞访问,MonitorEnter and MonitorExit work on jclass, jstring, and jarray, 主要用于java侧
types, which are special kinds of jobject references.

if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
... /* error handling */
}
... /* synchronized block */
if ((*env)->MonitorExit(env, obj) != JNI_OK) {

}

 

还有一些其他同步方法:这些主要在(*env)->CallVoidMethod等的时候作为参数:

 static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;

 

native thread model: 操作系统或者其他native线程库提供(lock/mutex等)

user thread model:jvm提供(MonitorEnter等)

 

一般建议在jni中用user thread model,因为可移植性。

 

把native chars转换成jstring的方法:

jstring JNU_NewStringNative(JNIEnv *env, const char *str)
{
jstring result;
jbyteArray bytes = 0;
int len;
if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
return NULL; /* out of memory error */
}
len = strlen(str);
bytes = (*env)->NewByteArray(env, len);
if (bytes != NULL) {
(*env)->SetByteArrayRegion(env, bytes, 0, len,
(jbyte *)str);
result = (*env)->NewObject(env, Class_java_lang_String,
MID_String_init, bytes);
(*env)->DeleteLocalRef(env, bytes);
return result;
} /* else fall through */
return NULL;
}

 

把jstring转换成native chars的方法:

char *JNU_GetStringNativeChars(JNIEnv *env, jstring jstr)
{
jbyteArray bytes = 0;
jthrowable exc;
char *result = 0;
if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
return 0; /* out of memory error */
}
bytes = (*env)->CallObjectMethod(env, jstr,
MID_String_getBytes);
exc = (*env)->ExceptionOccurred(env);
if (!exc) {
jint len = (*env)->GetArrayLength(env, bytes);
result = (char *)malloc(len + 1);
if (result == 0) {
JNU_ThrowByName(env, "java/lang/OutOfMemoryError",
0);
(*env)->DeleteLocalRef(env, bytes);
return 0;
}
(*env)->GetByteArrayRegion(env, bytes, 0, len,
(jbyte *)result);
result[len] = 0; /* NULL-terminate */
} else {
(*env)->DeleteLocalRef(env, exc);
}
(*env)->DeleteLocalRef(env, bytes);
return result;
}

 

 

可用RegisterNatives方法注册jni native方法:

JNINativeMethod nm;
nm.name = "g";
/* method descriptor assigned to signature field */
nm.signature = "()V";
nm.fnPtr = g_impl;
(*env)->RegisterNatives(env, cls, &nm, 1);

 

这样做的好处:

• It is sometimes more convenient and more efficient to register a large number
of native method implementations eagerly, as opposed to letting the virtual
machine link these entries lazily.
• You may call RegisterNatives multiple times on a method, allowing the
native method implementation to be updated at runtime.
• RegisterNatives is particularly useful when a native application embeds a
virtual machine implementation and needs to link with a native method implementation
defined in the native application. The virtual machine would not be
able to find this native method implementation automatically because it only
searches in native libraries, not the application itself.

 

C/C++的不同:

in C:

jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
In C++, however, you need to insert an explicit conversion:
jstring jstr = (jstring)env->GetObjectArrayElement(arr, i);

 

 

 构造jni可用one-to-one map或者share stub的方式:

one-to-one means一个java调用对应一个native方法

share stub means: 通过dispatch实现

 

 

 不能传递NULL or (jobject)0xFFFFFFFF给jni作为参数, 否则引起yndefine或者crash

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Android JNI学习路线可以按照以下步骤进行: 1. 了解JNI的基本概念和作用:JNIJava Native Interface)是Java提供的一种机制,用于实现Java与其他编程语言(如C、C++)之间的交互。它允许在Java代码中调用本地代码(Native Code),并且可以在本地代码中调用Java代码。 2. 学习JNI的基本语法和规则:JNI使用一组特定的函数和数据类型来实现Java与本地代码之间的交互。你需要学习如何声明本地方法、如何在Java代码中调用本地方法、如何在本地代码中调用Java方法等。 3. 学习JNI的数据类型映射:JNI提供了一套数据类型映射规则,用于将Java数据类型映射到本地代码中的数据类型。你需要学习如何处理基本数据类型、对象类型、数组类型等。 4. 学习JNI的异常处理:在JNI中,Java代码和本地代码之间的异常处理是非常重要的。你需要学习如何在本地代码中抛出异常、如何在Java代码中捕获异常等。 5. 学习JNI的线程处理:JNI允许在本地代码中创建和操作线程。你需要学习如何创建和销毁线程、如何在线程之间进行通信等。 6. 学习JNI的性能优化:JNI涉及到Java代码和本地代码之间的频繁切换,因此性能优化是非常重要的。你需要学习如何减少JNI调用的次数、如何避免不必要的数据拷贝等。 7. 学习JNI的调试和测试:在开发JNI程序时,调试和测试是非常重要的。你需要学习如何使用调试器调试本地代码、如何进行单元测试等。 8. 学习JNI的进阶主题:一旦掌握了基本的JNI知识,你可以进一步学习JNI的进阶主题,如JNIJava虚拟机的交互、JNI与动态链接库的交互、JNI与多线程的交互等。 总结起来,Android JNI学习路线包括了基本概念、基本语法、数据类型映射、异常处理、线程处理、性能优化、调试和测试以及进阶主题等内容。通过系统地学习这些知识,你将能够更好地理解和应用JNI技术。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值