JNI/NDK入门指南之JNI多线程回调Java方法

     JNI/NDK入门指南之JNI多线程回调Java方法



背景需求

  假设现在有这么一个业务需求,我们需要通过JNI在本地方法中干一件耗时操作,干完以后再通知Java层。这个实现逻辑非常简单,就是我们可以在本地方法中开启一个线程做函数操作,然后通过JNI回调Java方法。好了,架构已经定下来了,那么我们一步步实现。在实现过程中我也会将错误思路和实现代码提供出来,让大家对正确的写法更加刻骨铭心。



代码实现探索

我想绝大部分读者刚开始的时候,实现该逻辑的办法是初始化的时候保存JNIEnv和jobject为全局变量,然后在需要的时候直接使用。那我们就先按照该思路进行。

1. 子线程中使用全局的JNIEnv和jobject

重要的事情说三篇,这个方法行不通,行不通,行不通!

Java端代码:
Java本地方法类NativeThread.java代码:

package com.pax.api.thread;
import android.util.Log;

public class NativeThread {
    private static final String TAG = "NativeThread";
    public native void nativeInit();//Native方法

    //供JNI端回调的Java方法
    public void onNativeCallBack(int count) {
        Log.e(TAG, "onNativeCallBack : " + count);
    }    
    static {
        System.loadLibrary("native_thread");
    }
}

Java测试代码:

    private void operateNativeThread(){
        NativeThread mNativeThread = new NativeThread();
        mNativeThread.nativeInit();
    }

JNI端代码
Java中Native方法对应com_pax_api_thread_NativeThread.h代码如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_pax_api_thread_NativeThread */

#ifndef _Included_com_pax_api_thread_NativeThread
#define _Included_com_pax_api_thread_NativeThread
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_pax_api_thread_NativeThread
 * Method:    nativeInit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_pax_api_thread_NativeThread_nativeInit
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

对应的com_pax_api_thread_NativeThread.cpp代码如下:

#include "com_pax_api_thread_NativeThread.h"
#include <stdio.h>
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#define TAG "NativeThread"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)

JavaVM *gJavaVM;//全局JavaVM 变量
jobject gJavaObj;//全局Jobject变量
JNIEnv * gEnv;	//全局的JNIEnv变量
jmethodID nativeCallback;//全局的方法ID
static int count = 0;


static void* native_thread_exec(void *arg)
{
	//线程循环
    for(int i = 0 ; i < 5; i++)
    {
        usleep(20);
		//跨线程回调Java层函数
        gEnv->CallVoidMethod(gJavaObj,nativeCallback,count++);
    }
}


/*
 * Class:     com_pax_api_thread_NativeThread
 * Method:    nativeInit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_pax_api_thread_NativeThread_nativeInit
  (JNIEnv * env, jobject object)
{
	LOGE(TAG,"Java_com_pax_api_thread_NativeThread_nativeInit\n");
    
	gJavaObj = object;//保存object到全局gJavaObj中
	gEnv = env;
	jclass clazz = env->GetObjectClass(object);
	nativeCallback = env->GetMethodID(clazz,"onNativeCallBack","(I)V");
    pthread_t id;
	//通过pthread库创建线程
	LOGE(TAG,"create native thread\n");
    if(pthread_create(&id,NULL,native_thread_exec,NULL)!=0)
    {
        LOGE(TAG,"native thread create fail");
        return;
    }
    for(int i = 0 ; i < 5; i++)
    {
        usleep(20);
		//跨线程回调Java层函数
        gEnv->CallVoidMethod(gJavaObj,nativeCallback,count++);
    }
    LOGE(TAG,"native thread creat success");
}


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
	JNIEnv* env = NULL;
	//获取JNI_VERSION版本
	if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
		LOGE(TAG,"checkversion error\n");
		return -1;
	}
	//返回jni 的版本
	return JNI_VERSION_1_6;
}

运行演示

5I/NativeThread(13225): JNI_OnLoad
I/NativeThread(13225): Java_com_pax_api_thread_NativeThread_nativeInit
I/NativeThread(13225): create native thread
E/NativeThread(13225): onNativeCallBack : 0
E/NativeThread(13225): onNativeCallBack : 1
E/NativeThread(13225): onNativeCallBack : 3
E/NativeThread(13225): onNativeCallBack : 4
E/NativeThread(13225): onNativeCallBack : 5
I/NativeThread(13225): native thread creat success
I/DEBUG   (  302): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   (  302): Build fingerprint: 'PAX/CB03/CB03:5.1.1/LMY47V/CB03_CH_V4.70_S:user/release-keys'
I/DEBUG   (  302): Revision: '0'
I/DEBUG   (  302): ABI: 'arm'
I/DEBUG   (  302): pid: 13225, tid: 13246, name: com.pax.jni  >>> com.pax.jni <<<
I/DEBUG   (  302): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x98
I/DEBUG   (  302):     r0 00000000  r1 a5627dd0  r2 fffffa94  r3 00000007
I/DEBUG   (  302):     r4 b6e9ede4  r5 b4ba0000  r6 00000001  r7 fffffa98
I/DEBUG   (  302):     r8 b4b8ba28  r9 b4b9dc84  sl 00000000  fp a5627d44
I/DEBUG   (  302):     ip b4b9dc9c  sp a5627a30  lr b4960057  pc b495fb18  cpsr 800f0030
I/DEBUG   (  302):
I/DEBUG   (  302): backtrace:
I/DEBUG   (  302):     #00 pc 000afb18  /system/lib/libart.so (_ZN3artL8JniAbortEPKcS1_+47)
I/DEBUG   (  302):     #01 pc 000b046f  /system/lib/libart.so (_ZN3art9JniAbortFEPKcS1_z+58)
I/DEBUG   (  302):     #02 pc 000b316f  /system/lib/libart.so (_ZN3art11ScopedCheckC2EP7_JNIEnviPKc+334)
I/DEBUG   (  302):     #03 pc 000ba841  /system/lib/libart.so (_ZN3art8CheckJNI15CallVoidMethodVEP7_JNIEnvP8_jobjectP10_jmethodIDSt9__va_list+32)
I/DEBUG   (  302):     #04 pc 00000d79  /data/app/com.pax.jni-2/lib/arm/libnative_thread.so (_ZN7_JNIEnv14CallVoidMethodEP8_jobjectP10_jmethodIDz+16)
I/DEBUG   (  302):     #05 pc 00000db3  /data/app/com.pax.jni-2/lib/arm/libnative_thread.so
I/DEBUG   (  302):     #06 pc 000132b3  /system/lib/libc.so (_ZL15__pthread_startPv+30)
I/DEBUG   (  302):     #07 pc 000111df  /system/lib/libc.so (__start_thread+6)
I/DEBUG   (  302):
I/DEBUG   (  302): Tombstone written to: /data/tombstones/tombstone_06

很不幸的事情发生了,程序直接闪退并打印出异常堆栈信息。这是为什么呢,为什么子线程中使用全局的JNIEnv和jobject不行呢?下面让我们来分析一番。

异常分析
通过前面的代码我们可以看到在C++的本地方法和开启线程中回调Java方法是有所不同的。而造成这种局面的原因是由于JNIEnv *env的使用限制和局部引用造成的。JNIEnv *env是接口指针,通过它能调用JNI所有函数来使用虚拟机的各种功能,但是它是一个指向线程的局部引用数据,不能被保存起来供其它线程使用,它与线程是一一对应的关系,每个线程都有一个属于自己的JNIEnv * env。既然JNIEnv * env不能被多线程共享,而JavaVM是可以的(可以参见篇章JNI/NDK入门指南之JavaVM和JNIEnv的介绍),并且JavaVM的的结构中可以通过AttachCurrentThread获取当前线程中的JNIEnv指针,那么我让我们试试通过保存JavaVM,然后提供给其它线程使用,然后在其它线程中通过JavaVM拿到env。


2. 在主线程中保存JavaVM和jobject,然后在子线程中调用

重要的事情说三篇,这个方法行不通,行不通,行不通!
在前面的子线程中使用全局的JNIEnv和jobject已经验证失败了,这里我们将继续尝试在主线程中保存JavaVM和jobject,然后在子线程中调用。

#include "com_pax_api_thread_NativeThread.h"
#include <stdio.h>
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>

#define TAG "NativeThread"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)

JavaVM *gJavaVM = NULL;//全局JavaVM 变量
jobject gJavaObj = NULL;//全局Jobject变量
JNIEnv * gEnv = NULL;	//全局的JNIEnv变量
jmethodID nativeCallback = NULL;//全局的方法ID
static int count = 0;


static void* native_thread_exec(void *arg)
{
	JNIEnv * env;
	if(gJavaVM != NULL)
	{
		if(gJavaVM->AttachCurrentThread(&env, NULL) == JNI_OK)
		{
			if(env != NULL)
			{
				//线程循环
			    for(int i = 0 ; i < 5; i++)
			    {
			        usleep(20);
					//跨线程回调Java层函数
			        env->CallVoidMethod(gJavaObj,nativeCallback,count++);
			    }
			}

		}
	}
}


/*
 * Class:     com_pax_api_thread_NativeThread
 * Method:    nativeInit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_pax_api_thread_NativeThread_nativeInit
  (JNIEnv * env, jobject object)
{
	LOGE(TAG,"Java_com_pax_api_thread_NativeThread_nativeInit\n");
    
	gJavaObj = object;//保存object到全局gJavaObj中
	gEnv = env;
	jclass clazz = env->GetObjectClass(object);
	nativeCallback = env->GetMethodID(clazz,"onNativeCallBack","(I)V");


	//操作方式二,调用JNI函数保存JavaVM
	env->GetJavaVM(&gJavaVM);
    pthread_t id;
	//通过pthread库创建线程
	LOGE(TAG,"create native thread\n");
    if(pthread_create(&id,NULL,native_thread_exec,NULL)!=0)
    {
        LOGE(TAG,"native thread create fail");
        return;
    }
    for(int i = 0 ; i < 5; i++)
    {
        usleep(20);
		//跨线程回调Java层函数
        gEnv->CallVoidMethod(gJavaObj,nativeCallback,count++);
    }
    LOGE(TAG,"native thread creat success");
}


//SO动态库加载一定会在该流程
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
	LOGE(TAG,"JNI_OnLoad\n");
	JNIEnv* env = NULL;	
	//获取JNI_VERSION版本
	if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
		LOGE(TAG,"checkversion error\n");
		return -1;
	}

	//操作方式一,通过SO加载时保存全局JavaVM
	//gJavaVM = vm;

	//返回jni 的版本
	return JNI_VERSION_1_6;
}

在这里我们定义了一个全局的JavaVM和jobject

JavaVM *gJavaVM = NULL;//全局JavaVM 变量
jobject gJavaObj = NULL;//全局Jobject变量

然后我们可以使用如下两种方式保存全局的JavaVM:

  • 第一种实在加载SO库的时候,在默认函数JNI_OnLoad里面保存,代码如下:
//SO动态库加载一定会在该流程
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
	LOGE(TAG,"JNI_OnLoad\n");
	JNIEnv* env = NULL;	
	//获取JNI_VERSION版本
	if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
		LOGE(TAG,"checkversion error\n");
		return -1;
	}

	//操作方式一,通过SO加载时保存全局JavaVM
	//gJavaVM = vm;

	//返回jni 的版本
	return JNI_VERSION_1_6;
}
  • 第二种就是在调用本地方法初始化中,如下:
/*
 * Class:     com_pax_api_thread_NativeThread
 * Method:    nativeInit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_pax_api_thread_NativeThread_nativeInit
  (JNIEnv * env, jobject object)
{
	...
	//操作方式二,调用JNI函数保存JavaVM
	env->GetJavaVM(&gJavaVM);
	...
}

线程中使用:

static void* native_thread_exec(void *arg)
{
	JNIEnv * env;
	if(gJavaVM != NULL)
	{
		if(gJavaVM->AttachCurrentThread(&env, NULL) == JNI_OK)
		{
			if(env != NULL)
			{
				//线程循环
			    for(int i = 0 ; i < 5; i++)
			    {
			        usleep(20);
					//跨线程回调Java层函数
			        env->CallVoidMethod(gJavaObj,nativeCallback,count++);
			    }
			}
		}
	}
}

运行演示

I/NativeThread(13732): JNI_OnLoad
I/NativeThread(13732): Java_com_pax_api_thread_NativeThread_nativeInit
I/NativeThread(13732): create native thread
E/NativeThread(13732): onNativeCallBack : 0
E/NativeThread(13732): onNativeCallBack : 1
E/NativeThread(13732): onNativeCallBack : 2
E/NativeThread(13732): onNativeCallBack : 3
E/NativeThread(13732): onNativeCallBack : 4
I/NativeThread(13732): native thread creat success
I/DEBUG   (  302): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   (  302): Build fingerprint: 'PAX/CB03/CB03:5.1.1/LMY47V/CB03_CH_V4.70_S:user/release-keys'
I/DEBUG   (  302): Revision: '0'
I/DEBUG   (  302): ABI: 'arm'
I/DEBUG   (  302): pid: 13732, tid: 13751, name: Thread-457  >>> com.pax.jni <<<
I/DEBUG   (  302): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x14
I/DEBUG   (  302): Abort message: 'art/runtime/check_jni.cc:65] JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0xbed2deec'
I/DEBUG   (  302):     r0 29627015  r1 00000000  r2 00000000  r3 fffffa9c
I/DEBUG   (  302):     r4 b4b9dc84  r5 70e70420  r6 b6e9ede4  r7 00000000
I/DEBUG   (  302):     r8 70e7043c  r9 00000000  sl 00000000  fp b6e9ede4
I/DEBUG   (  302):     ip b4b9b608  sp a5627798  lr b4ad306d  pc b4a8746c  cpsr a0070030
I/DEBUG   (  302):
I/DEBUG   (  302): backtrace:
I/DEBUG   (  302):     #00 pc 001d746c  /system/lib/libart.so (_ZN3art6mirror9ArtMethod7ToDexPcEjb+31)
I/DEBUG   (  302):     #01 pc 00223069  /system/lib/libart.so (_ZN3art16StackDumpVisitor10VisitFrameEv+72)
I/DEBUG   (  302):     #02 pc 0021e3e9  /system/lib/libart.so (_ZN3art12StackVisitor9WalkStackEb+260)
I/DEBUG   (  302):     #03 pc 00223fab  /system/lib/libart.so (_ZNK3art6Thread13DumpJavaStackERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+170)
I/DEBUG   (  302):     #04 pc 002255d9  /system/lib/libart.so (_ZNK3art6Thread4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+156)
I/DEBUG   (  302):     #05 pc 0022e6c1  /system/lib/libart.so (_ZN3art10ThreadList10DumpLockedERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+104)
I/DEBUG   (  302):     #06 pc 002159cf  /system/lib/libart.so (_ZN3art10AbortState4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+238)
I/DEBUG   (  302):     #07 pc 00215c11  /system/lib/libart.so (_ZN3art7Runtime5AbortEv+72)
I/DEBUG   (  302):     #08 pc 000a62c5  /system/lib/libart.so (_ZN3art10LogMessageD1Ev+1312)
I/DEBUG   (  302):     #09 pc 000aff25  /system/lib/libart.so (_ZN3artL8JniAbortEPKcS1_+1084)
I/DEBUG   (  302):     #10 pc 000b046f  /system/lib/libart.so (_ZN3art9JniAbortFEPKcS1_z+58)
I/DEBUG   (  302):     #11 pc 000b27a1  /system/lib/libart.so (_ZN3art11ScopedCheck5CheckEbPKcz.constprop.129+668)
I/DEBUG   (  302):     #12 pc 000ba853  /system/lib/libart.so (_ZN3art8CheckJNI15CallVoidMethodVEP7_JNIEnvP8_jobjectP10_jmethodIDSt9__va_list+50)
I/DEBUG   (  302):     #13 pc 00000d81  /data/app/com.pax.jni-1/lib/arm/libnative_thread.so (_ZN7_JNIEnv14CallVoidMethodEP8_jobjectP10_jmethodIDz+16)
I/DEBUG   (  302):     #14 pc 00000dd5  /data/app/com.pax.jni-1/lib/arm/libnative_thread.so
I/DEBUG   (  302):     #15 pc 000132b3  /system/lib/libc.so (_ZL15__pthread_startPv+30)
I/DEBUG   (  302):     #16 pc 000111df  /system/lib/libc.so (__start_thread+6)
I/DEBUG   (  302):
I/DEBUG   (  302): Tombstone written to: /data/tombstones/tombstone_09

结果分析
可以看到依然直接crash失败,提示jobject reference无效。按道理说应该要OK了,但是为什么依然失败了呢?在这里我也不卖关子了,这是因为要想在新线程中使用jclass或者jobject就必须以全局引用方式保存,也许读者会说我们不是以前将jobject全局保存了吗,我们是全局保存了,但是我们保存的jobject是一个局部引用,一旦我们的函数Java_com_pax_api_thread_NativeThread_nativeInit返回,jobject就会被GC回收销毁,所以此时虽然我们的gJavaObj保存了全局引用,但是它现在指向的是一个非法地址,当然我们使用非法地址直接报crash。那么有什么解决办法呢,那就是根据局部引用创建全局引用,这样就不会被GC回收销毁了。


3. 在主线程中保存JavaVM和并且创建全局jobject引用,然后在子线程中调用

人狠话不多,直接上代码:

#include "com_pax_api_thread_NativeThread.h"
#include <stdio.h>
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>


#define TAG "NativeThread"
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)


JavaVM *gJavaVM = NULL;//全局JavaVM 变量
jobject gJavaObj = NULL;//全局Jobject变量

jmethodID nativeCallback = NULL;//全局的方法ID
static int count = 0;


static void* native_thread_exec(void *arg)
{

	LOGE(TAG,"nativeThreadExec");
	LOGE(TAG,"The pthread id : %d\n", pthread_self());
    JNIEnv *env;
    //从全局的JavaVM中获取到环境变量
    gJavaVM->AttachCurrentThread(&env,NULL);
	

	//线程循环
    for(int i = 0 ; i < 5; i++)
    {
        usleep(2);
		//跨线程回调Java层函数
        env->CallVoidMethod(gJavaObj,nativeCallback,count++);
    }
    gJavaVM->DetachCurrentThread();
    LOGE(TAG,"thread stoped");
	return ((void *)0);

}


/*
 * Class:     com_pax_api_thread_NativeThread
 * Method:    nativeInit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_pax_api_thread_NativeThread_nativeInit
  (JNIEnv * env, jobject object)
{
	LOGE(TAG,"Java_com_pax_api_thread_NativeThread_nativeInit\n");
    
	gJavaObj = env->NewGlobalRef(object);//创建全局引用
	jclass clazz = env->GetObjectClass(object);
	nativeCallback = env->GetMethodID(clazz,"onNativeCallBack","(I)V");


	//操作方式二,调用JNI函数保存JavaVM
	env->GetJavaVM(&gJavaVM);
    pthread_t id;
	//通过pthread库创建线程
	LOGE(TAG,"create native thread\n");
    if(pthread_create(&id,NULL,native_thread_exec,NULL)!=0)
    {
        LOGE(TAG,"native thread create fail");
        return;
    }
    for(int i = 0 ; i < 5; i++)
    {
        usleep(20);
		//跨线程回调Java层函数
        env->CallVoidMethod(gJavaObj,nativeCallback,count++);
    }

    LOGE(TAG,"native thread creat success");

}


//SO动态库加载一定会在该流程
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
	LOGE(TAG,"JNI_OnLoad\n");
	JNIEnv* env = NULL;	
	//获取JNI_VERSION版本
	if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
		LOGE(TAG,"checkversion error\n");
		return -1;
	}

	//操作方式一,通过SO加载时保存全局JavaVM
	//gJavaVM = vm;

	//返回jni 的版本
	return JNI_VERSION_1_6;
}

运行演示:

I/NativeThread(14716): JNI_OnLoad
I/NativeThread(14716): Java_com_pax_api_thread_NativeThread_nativeInit
I/NativeThread(14716): create native thread
E/NativeThread(14716): onNativeCallBack : 0
E/NativeThread(14716): onNativeCallBack : 1
E/NativeThread(14716): onNativeCallBack : 2
I/NativeThread(14716): nativeThreadExec
I/NativeThread(14716): The pthread id : -1205608352
E/NativeThread(14716): onNativeCallBack : 3
E/NativeThread(14716): onNativeCallBack : 4
E/NativeThread(14716): onNativeCallBack : 5
I/NativeThread(14716): native thread creat success
E/NativeThread(14716): onNativeCallBack : 6
E/NativeThread(14716): onNativeCallBack : 7
E/NativeThread(14716): onNativeCallBack : 8
E/NativeThread(14716): onNativeCallBack : 9
I/NativeThread(14716): thread stoped


总结思考

通过前面的一个坑一个脚印的摸索,读者应该对JNI多线程回调Java方法应该是了然于心了。老规矩还是总结一下,对于在JNI多线程中使用JNI应该注意哪些地方:

  • 线程之间不能直接传递JNIEnv和jobject这类通过JNI函数传递下来的属性值,因为他们和线程有关系,且属于局部引用在函数调用结束后会被GC回收并且销毁。
  • JavaVM是可以进行传递的,因为它属于JNI进程的,每个进程有且只有一个JavaVM所以可以被多线程共享,但是JNIEnv和jobject是属于线程私有的,不能共享。
  • 所以在多线程中需要使用jobject和JNIEnv的解决办法就是保存JavaVM,并且创建全局jobject引用,然后使用AttachCurrentThread从JavaVM中获取JNIEnv。


写在最后

  在最后麻烦读者朋友们,如果本篇对你有帮助,请关注和点赞一下,当然如果有错误和不足的地方也可以拍砖。

发布了89 篇原创文章 · 获赞 92 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/tkwxty/article/details/103814984