JNI 方法大全及使用示例

神游风云     发表于  2021-08-03 17:12       154

一、说明
这里介绍的函数大多是 NDK 开发中常用的函数,但并不是全部,内容稍多,基本可以满足我们的开发需求了,建议通过目录索引来找需要了解的。
这里的函数都是 JNIEnv 操作的相关函数,JNI_OnLoad 等 JavaVM 的方法不在这里介绍。
JNI 有 C、C++ 两种代码风格,即:
C风格:(*env)->NewStringUTF(env, "Hellow World!");
C++风格:env->NewStringUTF("Hellow World!");
这里我们使用 C++ 风格作为示例。

内容一览
二、获取版本

1. jint GetVersion()

三、类操作

1. jclass FindClass(const char* name)

2. jclass GetSuperclass(jclass clazz)

3. jboolean IsAssignableFrom(jclass clazz1, jclass clazz2)

四、对象操作

1. jobject AllocObject(jclass clazz)

2. jobject NewObject(jclass clazz, jmethodID methodID, …)

3. jobject NewObjectA(jclass clazz, jmethodID methodID, jvalue* args)

4. jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)

5. jclass GetObjectClass(jobject obj)

6. jobjectRefType GetObjectRefType(jobject obj)

7. jboolean IsInstanceOf(jobject obj, jclass clazz)

8. jboolean IsSameObject(jobject ref1, jobject ref2)

五、域操作

1. jfieldID GetFieldID(jclass clazz, const char name, const char sig)

2. NativeType GetField(jobject obj, jfieldID fieldID)

3. void SetField(jobject obj, jfieldID fieldID, NativeType value)

4. jfieldID GetStaticFieldID(jclass clazz, const char name, const char sig)

5. NativeType GetStaticField(jobject obj, jfieldID fieldID);

6. void SetStaticField(jobject obj, jfieldID fieldID, NativeType value)

六、方法操作

1. jmethodID GetMethodID(jclass clazz, const char name, const char sig)

2. NativeType CallMethod(jobject obj, jmethodID methodID, …)

3. NativeType CallMethodA(jobject obj, jmethodID methodID, jvalue* args)

4. NativeType CallMethodV(jobject obj, jmethodID methodID, va_list args)

5. jmethodID GetStaticMethodID(jclass clazz, const char name, const char sig)

6. NativeType CallStaticMethod(jclass clazz, jmethodID methodID, …)

7. NativeType CallStaticMethodA(jclass clazz, jmethodID methodID, jvalue* args)

8. NativeType CallStaticMethodV(jclass clazz, jmethodID methodID, va_list args)

七、全局引用和局部引用

1. jobject NewGlobalRef(jobject obj)

2. void DeleteGlobalRef(jobject globalRef)

3. jobject NewLocalRef(jobject ref)

4. void DeleteLocalRef(jobject localRef)

5. jweak NewWeakGlobalRef(jobject obj)

6. void DeleteWeakGlobalRef(jweak obj)

八、字符串操作

1. jstring NewString(const jchar* unicodeChars, jsize len)

2. jstring NewStringUTF(const char* bytes)

3. jsize GetStringLength(jstring string)

4. jsize GetStringUTFLength(jstring string)

5. const jchar GetStringChars(jstring string, jboolean isCopy)

6. const char GetStringUTFChars(jstring string, jboolean isCopy)

7. void ReleaseStringChars(jstring string, const jchar* chars)

8. void ReleaseStringUTFChars(jstring string, const char* utf)

九、数组操作

1. jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement)

2.ArrayType NewArray(jsize length)

3. jsize GetArrayLength(jarray array)

4. jobject GetObjectArrayElement(jobjectArray array, jsize index)

5. void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)

6. NativeType GetArrayElements(ArrayType array, jboolean isCopy)

7. void ReleaseArrayElements(ArrayType array, NativeType* elems, jint mode)

8. void GetArrayRegion(ArrayType array, jsize start, jsize len, NativeType* buf)

9. void SetArrayRegion(ArrayType array, jsize start, jsize len, const NativeType* buf)

十、异常操作

1. jint Throw(jthrowable obj)

2. jint ThrowNew(jclass clazz, const char* message)

3. jthrowable ExceptionOccurred()

4. jboolean ExceptionCheck()

5. void ExceptionDescribe()

6. void ExceptionClear()

7. void FatalError(const char* msg)



二、获取版本
1. jint GetVersion()
说明:获取当前 JNI 的版本号
返回值:

#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006


三、类操作
1. jclass FindClass(const char* name)
说明:根据类的全路径找到相应的 jclass 对象
参数:

name:类的全路径,例如 "Ljava/lang/String;"
示例:

jclass mStringClass = env->FindClass("Ljava/lang/String;");


2. jclass GetSuperclass(jclass clazz)
说明:返回一个类的父类,如果 clazz 是 Object 类,没有父类,那么将返回 NULL
参数:

jclazz:当前类对象
示例:

jclass clazz = env->GetSuperclass(mStringClass); // clazz is Ljava/lang/Object;


3. jboolean IsAssignableFrom(jclass clazz1, jclass clazz2)
说明:判断类1是否可以安全的强制转换类2
参数:

clazz1:类1
clazz2:类2


四、对象操作
1. jobject AllocObject(jclass clazz)
说明:不调用构造方法创建实例
参数:

clazz:指定对象的类


2. jobject NewObject(jclass clazz, jmethodID methodID, …)
3. jobject NewObjectA(jclass clazz, jmethodID methodID, jvalue* args)
4. jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)
说明:使用指定的构造方法创建类的实例,唯一不同的是输入参数的传入形式不同
参数:

clazz:指定对象的类
methodID:指定的构造方法
args:输入参数列表
示例:

jclass rect_clazz = env->FindClass("android/graphics/Rect");
jmethodID rect_constructor = env->GetMethodID(rect_clazz, "<init>", "()V");
jobject rect = env->NewObject(rect_clazz, rect_constructor);


5. jclass GetObjectClass(jobject obj)
说明:根据对象获取所属类
参数:

obj:某个 Java 对象实例,不能为 NULL


6. jobjectRefType GetObjectRefType(jobject obj)
说明:获取到对象的引用类型,JNI 1.6 新增的方法

参数:

obj:某个 Java 对象实例
返回:

JNIInvalidRefType = 0 // 该 obj 是个无效的引用
JNILocalRefType = 1 // 该 obj 是个局部引用
JNIGlobalRefType = 2 // 该 obj 是个全局引用
JNIWeakGlobalRefType = 3 // 该 obj 是个全局的弱引用


7. jboolean IsInstanceOf(jobject obj, jclass clazz)
说明:判断某个对象是否是指定类的实例
参数:

obj:某个 Java 对象实例
clazz:指定的类对象


8. jboolean IsSameObject(jobject ref1, jobject ref2)
说明:判断两个对象的引用是否指向的是相同的 Java 对象
参数:

ref1:某个 Java 对象的引用1
ref2:某个 Java 对象的引用2


五、域操作
1. jfieldID GetFieldID(jclass clazz, const char name, const char sig)
说明:获取类中某个非静态成员变量的ID(域ID)
参数:

clazz:指定对象的类
name:这个域(Field)在 Java 类中定义的名字
sig:这个域(Field)的类型描述符
示例:

jclass clazz = env->FindClass("android/graphics/Rect");
jfieldID left_field = env->GetFieldID(clazz, "left", "I");
jfieldID top_field = env->GetFieldID(clazz, "top", "I");
jfieldID right_field = env->GetFieldID(clazz, "right", "I");
jfieldID bottom_field = env->GetFieldID(clazz, "bottom", "I");


2. NativeType Get<type>Field(jobject obj, jfieldID fieldID)
说明:获取实例域的变量值,这里 type 表示的是一系列方法,如下:

Get<type>Field Routine Name

Native Type

GetObjectField()

jobject

GetBooleanField()

jboolean

GetByteField()

jbyte

GetCharField()

jchar

GetShortField()

jshort

GetIntField()

jint

GetLongField()

jlong

GetFloatField()

jfloat

GetDoubleField()

jdouble

参数:

obj:某个 Java 对象实例
fieldID:这个变量的域ID
示例:

jobject rect; // 初始化过程省略
jclass clazz = env->FindClass("android/graphics/Rect");
jfieldID left_field = env->GetFieldID(clazz, "left", "I");
jint left = env->GetIntField(rect, left_field);


3. void Set<type>Field(jobject obj, jfieldID fieldID, NativeType value)
说明:修改实例域的变量值,这里 type 对应上面的 Get 方法,不再累述
参数:

obj:需要修改的 Java 对象实例
fieldID:这个变量的域ID
value:需要设置的值
示例:

jobject rect; // 初始化过程省略
jclass clazz = env->FindClass("android/graphics/Rect");
jfieldID left_field = env->GetFieldID(clazz, "left", "I");
env->SetIntField(rect, left_field, 1);


4. jfieldID GetStaticFieldID(jclass clazz, const char name, const char sig)
说明:同 GetFieldID,只不过这里操作的是静态的域(Filed)



5. NativeType GetStaticField(jobject obj, jfieldID fieldID)
说明:同 GetField,只不过这里操作的是静态的域(Filed)



6. void SetStaticField(jobject obj, jfieldID fieldID, NativeType value)
说明:同 SetField,只不过这里操作的是静态的域(Filed)



六、方法操作
1. jmethodID GetMethodID(jclass clazz, const char name, const char sig)
说明:获取类中某个非静态方法的ID

参数:

clazz:指定对象的类
name:这个方法在 Java 类中定义的名称,构造方法为 ““
sig:这个方法的类型描述符,例如 “()V”,其中括号内是方法的参数,括号后是返回值类型
示例:

Java 的类定义如下:

package com.afei.jnidemo;

class Test {
public Test(){}
public int show(String msg, int number) {
System.out.println("msg: " + msg);
System.out.println("number: " + number);
return 0;
}
}
JNI 调用如下:

jclass clazz = env->FindClass("com/afei/jnidemo/Test");
jmethodID constructor_method = env->GetMethodID(clazz, "<init>", "()V");
jmethodID show_method = env->GetMethodID(clazz, "show", "(Ljava/lang/String;I)I");
签名时其中括号内是方法的参数,括号后是返回值类型。例如 show 方法,第一个参数是 String 类,对应 Ljava/lang/String;(注意后面有一个分号),第二个参数是 int 基本类型,对应的类型描述符是 I,返回值也是 int,同样是 I,所以最终该方法的签名为 “(Ljava/lang/String;I)I”。



2. NativeType CallMethod(jobject obj, jmethodID methodID, …)
3. NativeType CallMethodA(jobject obj, jmethodID methodID, jvalue* args)
4. NativeType CallMethodV(jobject obj, jmethodID methodID, va_list args)
说明:调用对象的某个方法,唯一不同的是输入参数的传入形式不同,这里 type 表示的是一系列方法,如下:

Call<type>Method Routine Name

Native Type

CallVoidMethod()

CallVoidMethodA()

CallVoidMethodV()

void

CallObjectMethod()

CallObjectMethodA()

CallObjectMethodV()

jobject

CallBooleanMethod()

CallBooleanMethodA()

CallBooleanMethodV()

jboolean

CallByteMethod()

CallByteMethodA()

CallByteMethodV()

jbyte

CallCharMethod()

CallCharMethodA()

CallCharMethodV()

jchar

CallShortMethod()

CallShortMethodA()

CallShortMethodV()

jshort

CallIntMethod()

CallIntMethodA()

CallIntMethodV()

jint

CallLongMethod()

CallLongMethodA()

CallLongMethodV()

jlong

CallFloatMethod()

CallFloatMethodA()

CallFloatMethodV()

jfloat

CallDoubleMethod()

CallDoubleMethodA()

CallDoubleMethodV()

jdouble

参数:

obj:某个 Java 对象实例
methodID:指定方法的ID
args:输入参数列表
示例:

jclass clazz = env->FindClass("com/afei/jnidemo/Test");
jmethodID show_method = env->GetMethodID(clazz, "show", "(Ljava/lang/String;I)I");
jint result = env->CallIntMethod(clazz, show_method, "Hello JNI!", 0);


5. jmethodID GetStaticMethodID(jclass clazz, const char name, const char sig)
说明:同 GetMethodID,只不过操作的是静态方法



6. NativeType CallStaticMethod(jclass clazz, jmethodID methodID, …)
7. NativeType CallStaticMethodA(jclass clazz, jmethodID methodID, jvalue* args)
8. NativeType CallStaticMethodV(jclass clazz, jmethodID methodID, va_list args)
说明:同 NativeType CallMethod,只不过操作的是静态方法,参数也由 jobject 变成了 jclass。



七、全局引用和局部引用
1. jobject NewGlobalRef(jobject obj)
说明:创建一个全局引用,不用时必须调用 DeleteGlobalRef() 方法释放。
参数:

obj:某个 Java 对象实例,可以是局部引用或全局引用
示例:

jclass mPointFClass; // global reference to PointF class

...
jclass clazz = env->FindClass("android/graphics/PointF");
mPointFClass = (jclass) env->NewGlobalRef(clazz);


2. void DeleteGlobalRef(jobject globalRef)
说明:释放某个全局引用
参数:

globalRef:某全局引用


3. jobject NewLocalRef(jobject ref)
说明:创建一个局部引用。这个方法一般很少用。
参数:

ref:某个引用,可以是全局引用或者局部引用


4. void DeleteLocalRef(jobject localRef)
说明:释放某个局部引用
参数:

localRef:某局部引用
注意:
局部引用在方法执行完后也会自动释放,不过当你在执行一个很大的循环时,里面会产生大量临时的局部引用,那么建议的做法是手动的调用该方法去释放这个局部引用。



5. jweak NewWeakGlobalRef(jobject obj)
说明:创建一个全局的弱引用
参数:

obj:某个 Java 对象实例
注意:
弱引用不会阻止 GC 回收它引用的对象,在内存不足时,弱引用的对象往往会被回收掉,使用时一定要多加小心。



6. void DeleteWeakGlobalRef(jweak obj)
说明:释放某个全局的弱引用
参数:

obj:某个全局弱引用


八、字符串操作
1. jstring NewString(const jchar* unicodeChars, jsize len)
说明:以 UTF-16 的编码方式创建一个 Java 的字符串(jchar 的定义为 uint16_t)
参数:

unicodeChars:指向字符数组的指针
len:字符数组的长度


2. jstring NewStringUTF(const char* bytes)
说明:以 UTF-8 的编码方式创建一个 Java 的字符串
参数:

bytes:指向字符数组的指针


3. jsize GetStringLength(jstring string)
4. jsize GetStringUTFLength(jstring string)
说明:获取字符串的长度,GetStringLength 是 UTF-16 编码,GetStringUTFLength 是 UTF-8 编码
参数:

string:字符串


5. const jchar GetStringChars(jstring string, jboolean isCopy)
6. const char GetStringUTFChars(jstring string, jboolean isCopy)
说明:将 Java 风格的 jstring 对象转换成 C 风格的字符串,同上一个是 UTF-16 编码,一个是 UTF-8 编码
参数:

string:Java 风格的字符串
isCopy:是否进行拷贝操作,0 为不拷贝


7. void ReleaseStringChars(jstring string, const jchar* chars)
8. void ReleaseStringUTFChars(jstring string, const char* utf)
说明:释放指定的字符串指针,通常来说,Get 和 Release 是成对出现的
参数:

string:Java 风格的字符串
chars/utf:对应的 C 风格的字符串
示例:

JNIEXPORT void JNICALL
Java_com_afei_jnidemo_MainActivity_test(JNIEnv *env, jobject instance, jstring msg_) {
const char *msg = env->GetStringUTFChars(msg_, 0);
// Do Something
env->ReleaseStringUTFChars(msg_, msg);
}


九、数组操作
1. jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement)
说明:创建引用数据类型的数组
参数:

length:数组的长度
elementClass:数组的元素所属的类
initialElement:使用什么样的对象来初始化,可以选择 NULL
示例:

int points_count = 21;
jclass pointFClass = env->FindClass("android/graphics/PointF");
jobjectArray point_array = env->NewObjectArray(points_count, pointFClass, NULL);


2.ArrayType NewArray(jsize length)
说明:创建基本数据类型的数组。这里的基本数据类型有:

New<PrimitiveType>Array Routines

Array Type

NewBooleanArray()

jbooleanArray

NewByteArray()

jbyteArray

NewCharArray()

jcharArray

NewShortArray()

jshortArray

NewIntArray()

jintArray

NewLongArray()

jlongArray

NewFloatArray()

jfloatArray

NewDoubleArray()

jdoubleArray

参数:

length:数组的长度


3. jsize GetArrayLength(jarray array)
说明:获取数组的长度
参数:

array:指定的数组对象。jarray 是 jbooleanArray、jbyteArray、jcharArray 等的父类。


4. jobject GetObjectArrayElement(jobjectArray array, jsize index)
说明:获取引用数据类型数组指定索引位置处的对象
参数:

array:引用数据类型数组
index:目标索引值


5. void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)
说明:设置引用数据类型数组指定索引位置处的值
参数:

array:需要设置的引用数据类型数组
index:目标索引值
value:需要设置的值


6. NativeType GetArrayElements(ArrayType array, jboolean isCopy)
说明:获取基本数据类型数组的头指针
参数:

array:基本数据类型数组
isCopy:是否进行拷贝操作,0 为不拷贝


7. void ReleaseArrayElements(ArrayType array, NativeType* elems, jint mode)
说明:释放基本数据类型数组指针。通常来说,Get 和 Release 是成对出现的
参数:

array:基本数据类型数组
elems:对应的 C 风格的基本数据类型指针
mode:释放模式,通常我们都是使用 0,有三种,如下
0

将内容复制回来并释放原生数组

JNI_COMMIT

将内容复制回来但不释放原生数组,一般用于周期性更新数组

JNI_ABORT

释放原生数组但不将内容复制回来

示例:

Java_com_afei_jnidemo_MainActivity_test(JNIEnv *env, jobject instance, jintArray array_) {
jint *array = env->GetIntArrayElements(array_, 0);
// Do Something
env->ReleaseIntArrayElements(array_, array, 0);
}


8. void GetArrayRegion(ArrayType array, jsize start, jsize len, NativeType* buf)
说明:返回基本数据类型数组的部分副本。这里的基本数据类型有:

Get<PrimitiveType>ArrayRegion Routine

Array Type

Native Type

GetBooleanArrayRegion()

jbooleanArray

jboolean

GetByteArrayRegion()

jbyteArray

jbyte

GetCharArrayRegion()

jcharArray

jchar

GetShortArrayRegion()

jshortArray

jhort

GetIntArrayRegion()

jintArray

jint

GetLongArrayRegion()

jlongArray

jlong

GetFloatArrayRegion()

jfloatArray

jloat

GetDoubleArrayRegion()

jdoubleArray

jdouble

参数:

array:基本数据类型数组
start:起始的索引值
len:拷贝的长度
buf:拷贝到的目标数组


9. void SetArrayRegion(ArrayType array, jsize start, jsize len, const NativeType* buf)
说明:设置基本数据类型数组元素。类型和上面的表类似。
参数:

array:需要设置的基本数据类型数组
start:起始的索引值
len:需要设置的 buf 的长度
buf:需要设置的值数组


十、异常操作
1. jint Throw(jthrowable obj)
说明:抛出一个异常,需要手动创建异常的实例,调用较复杂,一般不使用这个方法
参数:

obj:异常对象
示例:

jclass ioExceptionClazz = env->FindClass("java/io/IOException");
jmethodID ioExceptionConstructor = env->GetMethodID(ioExceptionClazz, "<init>", "(Ljava/lang/String;)V");
jthrowable exceptionObj = static_cast<jthrowable>(env->NewObject(ioExceptionClazz,
ioExceptionConstructor, "IO异常"));
if (env->Throw(exceptionObj) == JNI_OK) {
// 创建成功
} else {
// 创建失败
}


2. jint ThrowNew(jclass clazz, const char* message)
说明:抛出一个异常。使用起来比三个方法方便
参数:

clazz:指定的异常类
message:异常信息
示例:

if (env->ThrowNew(env->FindClass("java/io/IOException"), "IO异常") == JNI_OK) {
// 创建成功
} else {
// 创建失败
}


3. jthrowable ExceptionOccurred()
4. jboolean ExceptionCheck()
说明:检查是否有异常,如果本地函数有异常抛出,ExceptionOccurred 会返回这个异常的示例,ExceptionCheck 只返回是否有异常



5. void ExceptionDescribe()
说明:将异常和堆栈信息推送到错误流



6. void ExceptionClear()
说明:清除掉发生的异常



以上几个方法的示例:

Java 部分代码为:

public class MainActivity extends AppCompatActivity {

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

@Override
protected void
onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
test();
} catch (Exception e) {
Log.e("MainActivity", "onCreate: " + e);
}
}

private native void test() throws IllegalArgumentException;

private void callNullPointerException() throws NullPointerException {
throw new NullPointerException("MainActivity NullPointerException");
}

}
JNI 部分代码为:

JNIEXPORT void JNICALL
Java_com_afei_jnidemo_MainActivity_test(JNIEnv *env, jobject instance) {
jclass clazz = env->GetObjectClass(instance);
jmethodID mid =env->GetMethodID(clazz, "callNullPointerException", "()V");
env->CallVoidMethod(instance, mid); // will throw a NullPointerException
jthrowable exc = env->ExceptionOccurred(); // 检测是否发生异常
if (exc) {
LOGD("============");
env->ExceptionDescribe(); // 打印异常信息
LOGD("============");
env->ExceptionClear(); // 清除掉发生的异常
jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(newExcCls, "throw from JNI"); // 返回一个新的异常到 Java
}
}
运行结果为:

07-12 14:57:07.443 26623-26623/com.afei.jnidemo D/FaceAPI: ============
07-12 14:57:07.443 26623-26623/com.afei.jnidemo W/System.err: java.lang.NullPointerException: MainActivity NullPointerException
at com.afei.jnidemo.MainActivity.callNullPointerException(MainActivity.java:34)
at com.afei.jnidemo.MainActivity.test(Native Method)
at com.afei.jnidemo.MainActivity.onCreate(MainActivity.java:25)
at android.app.Activity.performCreate(Activity.java:6857)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1125)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2702)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2810)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1532)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:181)
at android.app.ActivityThread.main(ActivityThread.java:6288)
at java.lang.reflect.Method.invoke(Native Method)
07-12 14:57:07.443 26623-26623/com.afei.jnidemo W/System.err: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:900)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:790)
07-12 14:57:07.443 26623-26623/com.afei.jnidemo D/FaceAPI: ============
07-12 14:57:07.444 26623-26623/com.afei.jnidemo E/MainActivity: onCreate: java.lang.IllegalArgumentException: throw from JNI


7. void FatalError(const char* msg)
说明:抛出一个致命异常,并且不希望JVM处理
参数:

msg:致命异常的信息


十一、其它
其他还有有关 Monitor Operations、NIO Support、Reflection Support 等一些方法,由于我也没使用过,就不再这里解释了。
可以参考 JNI 的官网的介绍:https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html#wp9502



其它:

NDK 学习系列:Android NDK 从入门到精通(汇总篇)
————————————————
版权声明:本文为CSDN博主「阿飞__」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/afei__/article/details/81016413

本文转载于: https://blog.csdn.net/afei__/article/details/81016413