JNI学习笔记_Java调用C —— 非Android中使用的方法

一、学习笔记

1.java源码中的JNI函数本机方法声明必须使用native修饰。

2.相对反编译 Java 的 class 字节码文件来说,反汇编.so动态库来分析程序的逻辑要复杂得多,为了应用的安全性,会将一些复杂的逻辑和
算法通过本地代码(C或C++)来实现,然后打包成.so动态库文件

3.使用了 JNI 接口的 JAVA 程序,不再像以前那样*的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相
应的动态库。

4.JNI 开发流程主要分为以下 6 步:
(1)编写声明了 native 方法的 Java 类。
(2)将 Java 源代码编译成 class 字节码文件。
(3)用 javah -jni 命令生成.h头文件(-jni 参数表示将 class 中用native 声明的函数生成 JNI 规则的函数)。
(4)用本地代码实现.h头文件中的函数。
(5)将本地代码编译成动态库。
(6)拷贝动态库至 java.library.path 本地库搜索目录下,并运行 Java 程序。

5.JVM 查找 native 方法的两种方式:
(1)按照 JNI 规范的命名规则
(2)调用 JNI 提供的 RegisterNatives 函数,将本地函数注册到 JVM 中。

6.Native方法签名中的 JNIEXPORT 和 JNICALL 在 Linux/Unix 系统中,这两个宏可以省略不加。

7.本地实现方法的第二个参数为:如果这个 native 方法是实例方法,则该参数是 jobject,如果是静态方法,则是 jclass。

8.JNI 把 Java 中的所有对象当作一个C指针传递到本地方法中,这个指针指向 JVM 中的内部数据结构,而内部的数据结构在内存中的存储方
式是不可见的。只能从 JNIEnv 指针指向的函数表中选择合适的 JNI 函数来操作 JVM 中的数据结构。

9.JNI 的异常和 Java 中的异常处理流程是不一样的,Java 遇到异常如果没有捕获,程序会立即停止运行。而 JNI 遇到未决的异常不会改变
程序的运行流程,也就是程序会继续往下走。

10.字符串操作
(1)GetStringChars/GetStringUTFChars和ReleaseStringChars/ReleaseStringUTFChars
用于获取和释放以 Unicode/UTF-8 格式编码的字符串。后者是用于释放。
(2)GetStringLength/GetStringUTFLength
由于 UTF-8 编码的字符串以'\0'结尾(也可以使用strlen获取长度),而 Unicode 字符串不是。上面函数获取字符串长度。
(3)GetStringCritical和ReleaseStringCritical
提高 JVM 返回源字符串直接指针的可能性,但是获取和释放之间是不能阻塞的也不能调用其它JNI函数。因为获取这个直接指针后会导致暂停
GC(垃圾回收) 线程,当 GC 被暂停后,如果其它线程触发 GC 继续运行的话,都会导致被阻塞。
(4)GetStringRegion和GetStringUTFRegion
分别表示获取 Unicode 和 UTF-8 编码字符串指定范围内的内容到一个预先分配好的缓冲区中,这个函数不会分配内存,因此也没有释放的函数。

11.JNI 中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是 JNI 的基本数据类型,可以直
接访问。而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问 Java 传递给 JNI 层的数组,必须选
择合适的 JNI 函数来访问和设置 Java 层的数组对象。

12.GC 会实时扫描所有创建的对象是否还有引用,如果没有引用则会立即清理掉。当我们创建一个像 int 数组对象的时候,当我们在本地代码
想去访问时,发现这个对象正被 GC 线程占用了,这时本地代码会一直处于阻塞状态,直到等待 GC 释放这个对象的锁之后才能继续访问。为了
避免这种现象的发生,JNI 提供了 Get/ReleasePrimitiveArrayCritical 这对函数,本地代码在访问数组对象时会暂停 GC 线程。同样这两个函
数之间不能阻塞或调用其它JNI函数

13.在 JNI 中,只有 jobject 以及子类属于引用变量,会占用引用表的空间,jint,jfloat,jboolean 等都是基本类型变量,不会占用引用
表空间,即不需要释放。引用表最大空间为 512 个,如果超出这个范围,JVM 就会挂掉。

14.本地代码调用 Java 层某个对象的方法或属性,这就是来自 C/C++层本地函数的 callback(回调)。

15.引用类型统一调用CallStaticObjectMethod 函数.(不存在CallStaticStringMethod)。

16.GetMethodID/GetStaticMethodID 和 CallIntMethod/CallStaticIntMethod 和 SetIntField/SetStaticIntField,对应static和非static成员方法的调用函数不同。

17.JNI方法签名的格式为:(形参参数类型列表)返回值。形参参数列表中,引用类型以 L 开头,后面紧跟类的全路径名(需将.全部替换成/),
以分号结尾。

18.GetMethodID()返回的jmethodID应该不是引用,不需要对它调用DeleteLocalRef()。

19.调用 GetMethodID 获取方法 ID 和调用 FindClass 获取 Class 实例后,要做异常判断

20.在 Java 中任何一个类的.class字节码文件被加载到内存中之后,该class子节码文件统一使用 Class 类来表示该类的一个引用(相当于
Java 中所有类的基类是 Object一样)。然后就可以从该类的 Class 引用中动态的获取类中的任意方法和属性。

21.在本地代码中可以调用 JNI 函数可以访问 Java 对象中的非 public(eg private) 属性和方法。

22.由于 ID 对于特定类是相同的,因此只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。

23.class 和 member id 在一定范围内是稳定的,但在动态加载的 class loader 下,保存全局的 class 要么可能失效,要么可能造成无法卸载classloader。

24.(*env)->FindClass()返回的jclass类型的变量是local referenced的,不能被缓存,因为一次JNI调用返回后它引用的已经回收了,下次在
调用JNI函数时出问题。

25.方法ID和域ID应该是可以缓存的(它不是引用,而且是稳定存在的)。

26.如果在用使用时缓存的 ID,要注意只要本地代码依赖于这个 ID 的值,那么这个类就不会被 unload。另外一方面,如果缓存发生在静态初
始化时,当类被 unload 或 reload 时,ID 会被重新计算。因此,尽量在类静态初始化时就缓存字段 ID、方法 ID。

27.有两种域ID的缓存方法:
静态缓存:在静态代码块中调用JNI native函数缓存。
使用时缓存:就是在正常调用native函数时进行缓存。

28.三种引用介绍
(1)局部引用:
通过 NewLocalRef() 和各种 JNI 接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。LocalRef 会阻止 GC 回收所引用
的对象。JNI函数返回后局部引用所引用的对象会被 JVM 自动释放,或在JNI函数中调用 DeleteLocalRef 释放。(*env)->DeleteLocalRef(env,local_ref)

局部引用只有在创建它的本地方法返回前有效,本地方法返回到 Java 层之后,如果 Java 层没有对return的局部引用使用的话,局部引用就会被
JVM 自动释放。在JNI函数中将局部引用存储在静态变量中缓存起来,供下次调用时使用是错误的。

JNI 会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。经测试,Android 上的
JNI 局部引用表最大数量是 512 个。不用时注意及时调用DeleteLocalRef().

JNI 提供了一系列函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。

局部引用不能跨线程使用,只在创建它的线程有效。不要试图在一个线程中创建局部引用并存储到全局引用中,然后在另外一个线程中使用。

(2)全局引用:
调用 NewGlobalRef() 基于局部引用创建,会阻止 GC 回收所引用的对象。可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 DeleteGlobalRef() 手
动释放。(*env)->DeleteGlobalRef(env, g_cls_string)

只能通过 NewGlobalRef 函数创建全局引用。

(3)弱全局引用:
调用 NewWeakGlobalRef() 基于局部引用或全局引用创建,不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在 JVM 认为应
该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef() 手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)

29.在管理局部引用的生命周期中,Push/PopLocalFrame 是非常方便且安全的。我们可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用
PopLocalFrame,这样的话,在函数内任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。需要注意的是,
如果在函数的入口处调用了PushLocalFrame,记住要在函数所有出口(有 return 语句出现的地方)都要调用 PopLocalFrame。

30.JNI函数签名
JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *, jobject); //这两个宏可以忽略
JNIEnv:JNIEnv接口指针指向包含指向函数表的指针的位置。函数表中的每个条目都指向一个JNI函数。本机方法始终通过其中一个JNI函数访问Java虚拟机中的数据结构。
jobject:是对HelloWorld对象本身的引用(有点像C ++中的“this”指针),若是java中的静态native方法,参数二是jclass.

JNIEXPORT和JNICALL宏(在jni.h头文件中定义)确保从本机库导出此函数和C编译器生成具有此函数的正确调用约定的代码。
在/usr/lib/jvm/java-1.7.0-openjdk-amd64/include/jni_md.h中(jni.h中包含它):
#define JNIEXPORT __attribute__((visibility("default")))
#define JNICALL //空宏

31.编译本地方法
$ gcc -fPIC -shared HelloWorld.c -o libHelloWorld.so -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include/
5.运行
$ export LD_LIBRARY_PATH=./ 或 setenv LD_LIBRARY_PATH . 或 不设置环境变量运行$ java -Djava.library.path=. HelloWorld
$ java HelloWorld

32.JNI以不同方式处理原始类型(java中的8种基本类型)和引用类型; 实例方法和静态方法。

33.C和C++使用JNI的区别
从jni.h中关于__cplusplus的条件编译来看,native中的类型和JNIenv中提供的函数都是不同的!
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv; //C++使用这个JNIEnv函数集合,它里面的functions域内嵌了JNINativeInterface_!
#else
typedef const struct JNINativeInterface_ *JNIEnv; //C使用这个JNIEnv函数集合
#endif

二、试验Demo

1.互传基本类型测试

/* hello_world.c */
#include <jni.h>
#include <stdio.h>
#include "com_study_jnilearn_HelloWorld.h" JNIEXPORT jint JNICALL Java_com_study_jnilearn_HelloWorld_test
(JNIEnv *env, jclass cls, jshort s, jint i, jlong l, jfloat f, jdouble d, jchar c, jboolean z, jbyte b)
{
jint ret = ; printf("C: s=%hd, i=%d, l=%ld, f=%f, d=%lf, c=%c, z=%d, b=%d\n", s, i, l, f, d, c, z, b); return ret;
}
/* HelloWorld.java */
package com.study.jnilearn; public class HelloWorld { public static native int test(short s, int i, long l, float f, double d, char c, boolean z, byte b); public static void main(String[] args) {
short s = 1;
long l = 20;
byte b = 127;
int ret;
ret = test(s, 1, l, 1.0f, 10.5, 'A', true, b);
System.out.println("java: ret=" + ret);
} static {
System.loadLibrary("hello_world");
}
}
编译:
$ javac HelloWorld.java -d ./
$ javah -jni com.study.jnilearn.HelloWorld
$ gcc -shared -fPIC hello_world.c -I /usr/lib/jvm/java-1.7.-openjdk-amd64/include/ -o libhello_world.so
运行:
$ java com.study.jnilearn.HelloWorld
C: s=, i=, l=, f=1.000000, d=10.500000, c=A, z=, b=
java: ret=

2.互传String类型测试

/* StringTest.java */
package com.study.jnilearn; public class StringTest { public native static String sayHello(String text); public static void main(String[] args) {
System.out.println("Java send: Hello, I am Java");
String text = sayHello("Hello, I am Java");
System.out.println("Java get: " + text);
} static {
System.loadLibrary("string_test");
}
}
/* string_test.c */
#include <stdio.h>
#include <jni.h> JNIEXPORT jstring JNICALL Java_com_study_jnilearn_StringTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str)
{
const char *str_get = NULL;
const char *str_send = "Hello I am C";
jboolean isCopy; //返回JNI_TRUE表示原字符串的拷贝,返回JNI_FALSE表示返回原字符串的指针 str_get = (*env)->GetStringUTFChars(env, j_str, &isCopy);
if(str_get == NULL) {
return NULL;
}
printf("C get: %s, isCopy=%d\n", str_get, isCopy);
(*env)->ReleaseStringUTFChars(env, j_str, str_get); printf("C send: %s\n", str_send);
return (*env)->NewStringUTF(env, str_send);
}
编译:
$ javac StringTest.java -d ./
$ javah -jni com.study.jnilearn.StringTest
$ gcc -shared -fPIC string_test.c -I /usr/lib/jvm/java-1.7.-openjdk-amd64/include/ -o libstring_test.so
运行:
$ java com.study.jnilearn.StringTest
Java send: Hello, I am Java
C get: Hello, I am Java, isCopy=
C send: Hello I am C
Java get: Hello I am C

3.基本类型数组类型双向传参

/*IntArray.java*/
class IntArray { private native int[] sortArray(int[] arr); public static void main(String[] args) {
IntArray obj = new IntArray();
int arr1[] = new int[10]; for (int i = 0; i < 10; i++) {
arr1[i] = i;
} int arr2[] = obj.sortArray(arr1); for (int i = 0; i < 10; i++) {
System.out.print(arr2[i] + " ");
}
System.out.println("");
} static {
System.loadLibrary("IntArray");
}
}
/* IntArray.c */
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "IntArray.h" void arr_short(jint *arr, jint num) {
jint i, j, tmp; for (i = ; i < num; i++) {
for (j = i; j < num; j++) {
if (arr[i] < arr[j]) {
tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
}
} JNIEXPORT jintArray JNICALL Java_IntArray_sortArray
(JNIEnv *env, jobject obj, jintArray jarr)
{
jint *array, length;
jint buff[] = {};
array = (*env)->GetIntArrayElements(env, jarr, NULL);
if (array == NULL) {
return ; /* exception occurred */
}
length = (*env)->GetArrayLength(env, jarr);
memcpy(buff, array, length * sizeof(jint));
(*env)->ReleaseIntArrayElements(env, jarr, array, ); /*不能直接传jarr然后返回jarr,因为其是一个local reference, native调用后就被回收了*/
arr_short(buff, length); jintArray new_arr = (*env)->NewIntArray(env, length);
(*env)->SetIntArrayRegion(env, new_arr, , length, buff); return new_arr;
}
编译:
$ javac IntArray.java
$ javah -jni IntArray
$ gcc -shared -fPIC IntArray.c -I /usr/lib/jvm/java-1.7.-openjdk-amd64/include/ -o libIntArray.so
运行:
$ java IntArray

4.JNI函数中回调类的静态方法和实例方法

/* AccessMethod.java */
package com.study.jnilearn; class ClassMethod { private static int javaStaticCallback(String str, int i) {
int ret = 1;
System.out.format("Java: javaStaticCallback: str=%s " + " i=%d\n", str, i);
return ret;
} private int javaInstanceCallback(String str, int i) {
int ret = 2;
System.out.format("Java: javaInstanceCallback: str=%s " + "i=%d\n", str, i);
return ret;
}
} public class AccessMethod { public static native int javaStaticMethod();
public static native int javaInstaceMethod(); public static void main(String[] args) {
int ret1, ret2;
ret1 = javaStaticMethod();
ret2 = javaInstaceMethod();
System.out.println("ret1=" + ret1 + " ret2=" + ret2);
} static {
System.loadLibrary("AccessMethod");
}
}
/* AccessMethod.c */
#include <stdio.h>
#include "com_study_jnilearn_AccessMethod.h" JNIEXPORT jint JNICALL Java_com_study_jnilearn_AccessMethod_javaStaticMethod(JNIEnv *env, jclass cls)
{
jclass clazz = NULL;
jstring str_arg = NULL;
jmethodID method_id;
jint ret = ; // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
clazz =(*env)->FindClass(env, "com/study/jnilearn/ClassMethod");
if (clazz == NULL) {
printf("Couldn't find class: com/study/jnilearn/ClassMethod");
return;
} // 2、从clazz类中查找javaStaticCallback方法
method_id = (*env)->GetStaticMethodID(env, clazz, "javaStaticCallback", "(Ljava/lang/String;I)I");
if (method_id == NULL) {
printf("Couldn't find method: javaStaticCallback");
return;
} // 3、调用clazz类的callStaticMethod静态方法
str_arg = (*env)->NewStringUTF(env, "C: str pass to java static method");
ret = (*env)->CallStaticIntMethod(env, clazz, method_id, str_arg, ); // 4.删除局部引用
(*env)->DeleteLocalRef(env, clazz);
(*env)->DeleteLocalRef(env, str_arg); // id is not reference,needn't to delete return ret;
} JNIEXPORT jint JNICALL Java_com_study_jnilearn_AccessMethod_javaInstaceMethod(JNIEnv *env, jclass cls)
{
jclass clazz = NULL;
jobject jobj = NULL;
jmethodID mid_construct = NULL;
jmethodID mid_instance = NULL;
jstring str_arg = NULL;
jint ret = ; // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");
if (clazz == NULL) {
printf("Couldn't find class: com/study/jnilearn/ClassMethod");
return;
} // 2、获取类的默认构造方法ID
mid_construct = (*env)->GetMethodID(env,clazz, "<init>", "()V");
if (mid_construct == NULL) {
printf("Couldn't find construct method");
return;
} // 3、查找实例方法的ID
mid_instance = (*env)->GetMethodID(env, clazz, "javaInstanceCallback", "(Ljava/lang/String;I)I");
if (mid_instance == NULL) {
return;
} // 4、创建该类的实例
jobj = (*env)->NewObject(env, clazz, mid_construct);
if (jobj == NULL) {
printf("Couldn't create new object");
return;
} // 5、调用对象的实例方法
str_arg = (*env)->NewStringUTF(env, "C: str pass to java instance method");
ret = (*env)->CallIntMethod(env, jobj, mid_instance, str_arg, ); // 删除局部引用
(*env)->DeleteLocalRef(env, clazz);
(*env)->DeleteLocalRef(env, jobj);
(*env)->DeleteLocalRef(env, str_arg); return ret;
}
编译:
$ javac AccessMethod.java -d ./
$ javah -jni com.study.jnilearn.AccessMethod
$ gcc -shared -fPIC AccessMethod.c -I /usr/lib/jvm/java-1.7.-openjdk-amd64/include/ -o libAccessMethod.so
运行:
$ java com.study.jnilearn.AccessMethod
Java: javaStaticCallback: str=C: str pass to java static method i=
Java: javaInstanceCallback: str=C: str pass to java instance method i=
ret1= ret2=

5.JNI函数中使用静态和非静态成员属性

/* AccessField.java */
package com.study.jnilearn; class ClassField { private static int num;
private String str; public int getNum() {
return num;
} public void setNum(int num) {
this.num = num;
} public String getStr() {
return str;
} public void setStr(String str) {
this.str = str;
}
} public class AccessField { private native static void accessInstanceField(ClassField obj); private native static void accessStaticField(); public static void main(String[] args) {
ClassField obj = new ClassField();
obj.setNum(10);
obj.setStr("Hello"); // 本地代码访问和修改ClassField为中的静态属性num
accessStaticField();
accessInstanceField(obj); System.out.println("Java: ClassField.num = " + obj.getNum());
System.out.println("Java: ClassField.str = " + obj.getStr());
} static {
System.loadLibrary("AccessField");
}
}
#include "com_study_jnilearn_AccessField.h"  

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessInstanceField
(JNIEnv *env, jclass cls, jobject obj)
{
jclass clazz;
jfieldID fid;
jstring j_str;
jstring j_newStr;
const char *c_str = NULL; // 1.获取AccessField类的Class引用
clazz = (*env)->GetObjectClass(env, obj);
if (clazz == NULL) {
return;
} // 2. 获取AccessField类实例变量str的属性ID
fid = (*env)->GetFieldID(env, clazz, "str", "Ljava/lang/String;");
if (clazz == NULL) {
return;
} // 3. 获取实例变量str的值
j_str = (jstring)(*env)->GetObjectField(env, obj, fid); // 4. 将unicode编码的java字符串转换成C风格字符串
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
return;
}
printf("C: ClassField.str = %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str); // 5. 修改实例变量str的值
j_newStr = (*env)->NewStringUTF(env, "World");
if (j_newStr == NULL) {
return;
} (*env)->SetObjectField(env, obj, fid, j_newStr); // 6.删除局部引用
(*env)->DeleteLocalRef(env, clazz);
(*env)->DeleteLocalRef(env, j_str);
(*env)->DeleteLocalRef(env, j_newStr);
} JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessStaticField
(JNIEnv *env, jclass cls)
{
jclass clazz;
jfieldID fid;
jint num; // 1.获取ClassField类的Class引用
clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassField");
if (clazz == NULL) {
return;
} // 2.获取ClassField类静态变量num的属性ID
fid = (*env)->GetStaticFieldID(env, clazz, "num", "I");
if (fid == NULL) {
return;
} // 3.获取静态变量num的值
num = (*env)->GetStaticIntField(env, clazz, fid);
printf("C: ClassField.num = %d\n", num); // 4.修改静态变量num的值
(*env)->SetStaticIntField(env, clazz, fid, ); // 删除属部引用
(*env)->DeleteLocalRef(env, clazz);
}
编译:
$ javac AccessField.java -d ./
$ javah -jni com.study.jnilearn.AccessField
$ gcc -shared -fPIC AccessField.c -I /usr/lib/jvm/java-1.7.-openjdk-amd64/include/ -o libAccessField.so
运行:
$ java com.study.jnilearn.AccessField
C: ClassField.num =
C: ClassField.str = Hello
Java: ClassField.num =
Java: ClassField.str = World

6.优化:类静态初始化缓存

/* AccessCache.java */
public class AccessCache { public static native void initIDs(); public native void nativeMethod();
public void callback() {
System.out.println("Java: AccessCache.callback invoked!");
} public static void main(String[] args) {
AccessCache accessCache = new AccessCache();
accessCache.nativeMethod();
} static {
System.loadLibrary("AccessCache");
initIDs();
}
}
#include <stdio.h>
#include "AccessCache.h" jmethodID MID_AccessCache_callback; JNIEXPORT void JNICALL Java_AccessCache_initIDs(JNIEnv *env, jclass cls)
{
printf("initIDs called!\n");
MID_AccessCache_callback = (*env)->GetMethodID(env, cls, "callback", "()V");
} JNIEXPORT void JNICALL Java_AccessCache_nativeMethod(JNIEnv *env, jobject obj)
{
printf("C: call java's callback()\n");
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
}
编译:
javac AccessCache.java
javah -jni AccessCache
$ gcc -shared -fPIC AccessCache.c -I /usr/lib/jvm/java-1.7.-openjdk-amd64/include/ -o libAccessCache.so
运行:
$ java AccessCache
initIDs called!
C: call java's callback()
Java: AccessCache.callback invoked!

jni.pdf翻译总结版:http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/recommend.html

上一篇:黄聪:MySql Host is blocked because of many connection errors; unblock with 'mysqladmin flush-hosts' 解决方法(转)


下一篇:Aladdin and the Flying Carpet LightOJ - 1341 (素数打表 + 算术基本定理)