由浅入深 学习 Android Binder(十一) binder线程池

本文详细剖析了Android Binder线程池的工作原理,包括其数据结构、启动过程、线程管理方式以及线程池大小等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android Binder系列文章:
由浅入深 学习 Android Binder(一)- AIDL
由浅入深 学习 Android Binder(二)- bindService流程
由浅入深 学习 Android Binder(三)- java binder深究(从java到native)
由浅入深 学习 Android Binder(四)- ibinderForJavaObject 与 javaObjectForIBinder
由浅入深 学习 Android Binder(五)- binder如何在进程间传递
由浅入深 学习 Android Binder(六)- IPC 调用流程
由浅入深 学习 Android Binder(七)- IServiceManager与ServiceManagerNative(java层)
由浅入深 学习 Android Binder(八)- IServiceManager与BpServiceManager(native层)
由浅入深 学习 Android Binder(九)- service_manager 与 svclist
由浅入深 学习 Android Binder(十)- 总结
由浅入深 学习 Android Binder(十一) binder线程池

概述

此篇是补充篇。
“binder线程池”也是binder中一个较常见的知识点,作此文以记之。

binder线程池的数据结构

刚接触这个知识点最先想到的一定是数据结构。然而,实际上binder线程池并非一个传统的数据结构。
它的大致逻辑如下:

  1. 每个进程中只有一个类名为“PoolThread”的数据结构,它继承自"Thread"。
  2. binder_driver控制每个进程会启动多少个线程来与binder_driver通信。

PoolThread的源码在ProcessState.cp文件中,源码如下:

class PoolThread : public Thread
{
public:
    explicit PoolThread(bool isMain)
        : mIsMain(isMain)
    {
    }
    
protected:
    virtual bool threadLoop()
    {
        IPCThreadState::self()->joinThreadPool(mIsMain);
        return false;
    }
    
    const bool mIsMain;
};

binder线程池的启动

启动binder线程池的代码在ProcessState.cp文件中。

startThreadPool中的逻辑如下:

  1. 设置了线程池的启动状态
  2. 调用了spawnPooledThread方法,传入参数为true。
void ProcessState::startThreadPool()
{
    AutoMutex _l(mLock);
    if (!mThreadPoolStarted) {
        mThreadPoolStarted = true;
        spawnPooledThread(true);
    }
}

线程池中线程的启动

spawnPooledThread逻辑如下:

  1. 获取binder线程名
  2. 创建PoolThread并且run,最终其实会执行到PoolThread的threadLoop()方法,threadLoop()中会执行IPCThreadState的joinThreadPool方法。
void ProcessState::spawnPooledThread(bool isMain)
{
    if (mThreadPoolStarted) {
        String8 name = makeBinderThreadName();
        ALOGV("Spawning new pooled thread, name=%s\n", name.string());
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}
class PoolThread : public Thread
{
public:
    explicit PoolThread(bool isMain)
        : mIsMain(isMain)
    {
    }
    
protected:
    virtual bool threadLoop()
    {
        IPCThreadState::self()->joinThreadPool(mIsMain);
        return false;
    }
    
    const bool mIsMain;
};

joinThreadPool的逻辑如下:

  1. 根据isMain来判断写入哪个cmd。两个cmd与binderDriver通信时分别代表不同的行为。
  2. 通过执行getAndExecuteCommand(),将cmd发送给binderDriver,并且在getAndExecuteCommand中会等待binderDriver的返回并处理。

isMain不同值的情况:

  • isMain=true。代表是主线程。
    BC_ENTER_LOOPER发送给binderDriver后,binderDriver确认能否启动线程,如果不能会报error。
  • isMain=false。代表是普通的binder线程。
    BC_REGISTER_LOOPER发送给binderDriver后,binderDriver会确认能否启动线程,如果不能会报error。
void IPCThreadState::joinThreadPool(bool isMain)
{
    LOG_THREADPOOL("**** THREAD %p (PID %d) IS JOINING THE THREAD POOL\n", (void*)pthread_self(), getpid());

    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);

    status_t result;
    mIsLooper = true;
    do {
        processPendingDerefs();
        // now get the next command to be processed, waiting if necessary
        result = getAndExecuteCommand();

        if (result < NO_ERROR && result != TIMED_OUT && result != -ECONNREFUSED && result != -EBADF) {
            ALOGE("getAndExecuteCommand(fd=%d) returned unexpected error %d, aborting",
                  mProcess->mDriverFD, result);
            abort();
        }

        // Let this thread exit the thread pool if it is no longer
        // needed and it is not the main process thread.
        if(result == TIMED_OUT && !isMain) {
            break;
        }
    } while (result != -ECONNREFUSED && result != -EBADF);

    LOG_THREADPOOL("**** THREAD %p (PID %d) IS LEAVING THE THREAD POOL err=%d\n",
        (void*)pthread_self(), getpid(), result);

    mOut.writeInt32(BC_EXIT_LOOPER);
    mIsLooper = false;
    talkWithDriver(false);
}

binderDriver对BC_ENTER_LOOPER的处理

binderDriver对BC_ENTER_LOOPER的处理逻辑如下:

  1. 判断是否已经是BINDER_LOOPER_STATE_REGISTERED的状态,即,对应的client进程中的线程,是否是binder主线程。如果是,那么就会报错。
  2. 最终会把当前的状态设置成BINDER_LOOPER_STATE_ENTERED,即,对应的client进程中的线程,是非主线程。
static int binder_thread_write(struct binder_proc *proc,
			struct binder_thread *thread,
			binder_uintptr_t binder_buffer, size_t size,
			binder_size_t *consumed)
{
————————————————省略
		case BC_ENTER_LOOPER:
			binder_debug(BINDER_DEBUG_THREADS,
				     "%d:%d BC_ENTER_LOOPER\n",
				     proc->pid, thread->pid);
			if (thread->looper & BINDER_LOOPER_STATE_REGISTERED) {
				thread->looper |= BINDER_LOOPER_STATE_INVALID;
				binder_user_error("%d:%d ERROR: BC_ENTER_LOOPER called after BC_REGISTER_LOOPER\n",
					proc->pid, thread->pid);
			}
			thread->looper |= BINDER_LOOPER_STATE_ENTERED;
			break;
————————————————省略
}

binderDriver对BC_REGISTER_LOOPER的处理

binderDriver对BC_REGISTER_LOOPER的处理的处理逻辑如下:

  1. 首先判断下当前是否是BINDER_LOOPER_STATE_ENTERED的状态,即,对应的client进程中的线程,是否是binder主线程,如果是就报错。
  2. 判断当前可以请求的线程是否为0,如果没有可以请求的线程,那么就报错。
  3. 如果没有报错,那么会把当前“可以请求的线程”减一,当前“开始请求的线程”加一。
    并且将状态设置成BINDER_LOOPER_STATE_REGISTERED,即,对应的client进程中的线程,是非主线程。
static int binder_thread_write(struct binder_proc *proc,
			struct binder_thread *thread,
			binder_uintptr_t binder_buffer, size_t size,
			binder_size_t *consumed)
{
————————————————省略
		case BC_REGISTER_LOOPER:
			binder_debug(BINDER_DEBUG_THREADS,
				     "%d:%d BC_REGISTER_LOOPER\n",
				     proc->pid, thread->pid);
			if (thread->looper & BINDER_LOOPER_STATE_ENTERED) {
				thread->looper |= BINDER_LOOPER_STATE_INVALID;
				binder_user_error("%d:%d ERROR: BC_REGISTER_LOOPER called after BC_ENTER_LOOPER\n",
					proc->pid, thread->pid);
			} else if (proc->requested_threads == 0) {
				thread->looper |= BINDER_LOOPER_STATE_INVALID;
				binder_user_error("%d:%d ERROR: BC_REGISTER_LOOPER called without request\n",
					proc->pid, thread->pid);
			} else {
				proc->requested_threads--;
				proc->requested_threads_started++;
			}
			thread->looper |= BINDER_LOOPER_STATE_REGISTERED;
			break;
————————————————省略
}

何时触发binder线程(非主线程)

binder主线程是在启动binder线程池的时候启动的。
那么非主线程的其他线程在什么时候会被调用呢?

先回到启动binder线程的方法,即ProcessState.cp中的spawnPooledThread():

void ProcessState::spawnPooledThread(bool isMain)
{
    if (mThreadPoolStarted) {
        String8 name = makeBinderThreadName();
        ALOGV("Spawning new pooled thread, name=%s\n", name.string());
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}

查找的方法非常简单,就是全局搜索调用spawnPooledThread(false)的地方,最终可以发现在IPCThreadState的executeCommand方法中,代码如下:

status_t IPCThreadState::executeCommand(int32_t cmd)
{
————————————————省略
    case BR_SPAWN_LOOPER:
        mProcess->spawnPooledThread(false);
        break;
————————————————省略
}

由此可知还是由binderDriver来通知的,因此在binderDriver的代码中搜索BR_SPAWN_LOOPER,于是可以定位到binder_thread_read()方法。
这个方法中的可以提取的逻辑主要如下:

  1. 只有放cmd为BINDER_WORK_TRANSACTION时,才会走到done中的逻辑。否则会直接continue。
  2. 在done中一定条件下,会发送BR_SPAWN_LOOPER来通知进程启动binder线程。

done中会执行的条件如下:

  1. 当前进程没有可请求的线程,也没有已经ready可用的线程。
  2. 当前进程已经开始请求的线程小于最大线程数。(最大线程数后面会探索)
  3. 当前线程的状态不能是BINDER_LOOPER_STATE_REGISTERED,也不能是BINDER_LOOPER_STATE_ENTERED。即,对应的client中的线程不能已经启动过。
static int binder_thread_read(struct binder_proc *proc,
			      struct binder_thread *thread,
			      binder_uintptr_t binder_buffer, size_t size,
			      binder_size_t *consumed, int non_block)
{
————————————————省略

		case BINDER_WORK_TRANSACTION: {
			t = container_of(w, struct binder_transaction, work);
		} break;
————————————————省略
		if (!t)
			continue;
————————————————省略
done:

	*consumed = ptr - buffer;
	if (proc->requested_threads + proc->ready_threads == 0 &&
	    proc->requested_threads_started < proc->max_threads &&
	    (thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
	     BINDER_LOOPER_STATE_ENTERED)) /* the user-space code fails to */
	     /*spawn a new thread if we leave this out */) {
		proc->requested_threads++;
		binder_debug(BINDER_DEBUG_THREADS,
			     "%d:%d BR_SPAWN_LOOPER\n",
			     proc->pid, thread->pid);
		if (put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer))
			return -EFAULT;
		binder_stat_br(proc, thread, BR_SPAWN_LOOPER);
	}
	return 0;
}

那么问题就继续转移成了“什么时候会调用到BINDER_WORK_TRANSACTION的cmd”。继续在binderDriver的代码中搜索,可以找到binder_transaction()方法。

static void binder_transaction(struct binder_proc *proc,
			       struct binder_thread *thread,
			       struct binder_transaction_data *tr, int reply)
{
————————————————省略
	t->work.type = BINDER_WORK_TRANSACTION;
	list_add_tail(&t->work.entry, target_list);
	tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
	list_add_tail(&tcomplete->entry, &thread->todo);
	if (target_wait)
		wake_up_interruptible(target_wait);
	return;
————————————————省略
}

再继续看下是哪些地方调用了binder_transaction()方法,可以找到binder_thread_write()中的代码如下。
也就是说当client进程发送BC_TRANSACTION或者BC_REPLY的cmd的时候,会启动binder线程。

static int binder_thread_write(struct binder_proc *proc,
			struct binder_thread *thread,
			binder_uintptr_t binder_buffer, size_t size,
			binder_size_t *consumed)
{
————————————————省略
		case BC_TRANSACTION:
		case BC_REPLY: {
			struct binder_transaction_data tr;

			if (copy_from_user(&tr, ptr, sizeof(tr)))
				return -EFAULT;
			ptr += sizeof(tr);
			binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
			break;
		}
————————————————省略
}

那么BC_TRANSACTION与BC_REPLY在client进程中,又分别对应什么操作呢?
在IPCThreadState中分别可以找到对应的方法,代码如下。
分别对应两种场景:

  • BC_TRANSACTION:client进程向binderDriver发送IPC调用请求的时候。
  • BC_REPLY:client进程收到了binderDriver的IPC调用请求,逻辑执行结束后发送返回值。

对binder IPC流程有兴趣的读者可以看下笔者的前文:
由浅入深 学习 Android Binder(六)- IPC 调用流程

status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
————————————————省略
    err = writeTransactionData(BC_TRANSACTION_SG, flags, handle, code, data, NULL);
————————————————省略
}
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags)
{
    status_t err;
    status_t statusBuffer;
    err = writeTransactionData(BC_REPLY_SG, flags, -1, 0, reply, &statusBuffer);
    if (err < NO_ERROR) return err;

    return waitForResponse(NULL, NULL);
}

binder线程池大小

前面分析的时候有接触到线程池的最大线程数,即proc->max_threads。
在binderDriver源码中可以搜索到定义这个值的地方,即binder_ioctl()方法:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
————————————————省略
	case BINDER_SET_MAX_THREADS:
		if (copy_from_user(&proc->max_threads, ubuf, sizeof(proc->max_threads))) {
			ret = -EINVAL;
			goto err;
		}
		break;
————————————————省略
}

接着全局搜索下BINDER_SET_MAX_THREADS这个cmd,可以在ProcessState中找到open_driver()方法:

static int open_driver(const char *driver)
{
    int fd = open(driver, O_RDWR | O_CLOEXEC);
    if (fd >= 0) {
        int vers = 0;
        status_t result = ioctl(fd, BINDER_VERSION, &vers);
        if (result == -1) {
            ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
            close(fd);
            fd = -1;
        }
        if (result != 0 || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
          ALOGE("Binder driver protocol(%d) does not match user space protocol(%d)! ioctl() return value: %d",
                vers, BINDER_CURRENT_PROTOCOL_VERSION, result);
            close(fd);
            fd = -1;
        }
        size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
        if (result == -1) {
            ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
        }
    } else {
        ALOGW("Opening '%s' failed: %s\n", driver, strerror(errno));
    }
    return fd;
}

可以看到默认的binder线程池的大小是DEFAULT_MAX_BINDER_THREADS:

#define DEFAULT_MAX_BINDER_THREADS 15

于是可以得到结论,binder非主线程的最大个数是15,加上主线程的话,总共就是16个线程。

总结

binder线程池相关的内容还是挺多的,笔者在此就记录一下个人认为重要的知识点,如对其他细节有兴趣的读者可以自行阅读下源码:

  • binder线程池并非一个传统意义上的线程池结构,它在client进程中只有一个继承自Thread的PoolThread类。而线程的启动以及管理都是由binderDriver来控制的。
  • binder线程有主线程和非主线程之分,主线程是启动的时候才会有的,每个binder线程池只有一个。其他情况下申请的都是非主线程。
  • binder线程池启动的时候,实际上只是启动了client中的binder主线程。
  • binder线程(非主线程)有两种情况启动:client进程向binderDriver发送IPC请求,以及 client进程向binderDriver回复IPC请求结果。
  • binder线程池的默认大小是16,1个主线程和15个非主线程。
<think>首先,用户的问题是关于AndroidBinder线程池大小限制和工作机制。用户明确提到了“Binder线程池大小限制及工作原理”,所以我需要基于提供的引用来回答。回顾系统指令:-所有行内数学表达式用$...$。-独立公式用$$...$$并单独成段。-LaTeX语法正确。-使用中文回答。-生成相关问题。-在回答中引用的段落末尾添加引用标识如[^1]。用户提供了引用[1]到[5],这些是关于Binder机制的引用。我得尽量参考这些引用来构建答案。关键点从引用中提取:-引用[4]:"当服务进程首次发布Binder服务时,系统会自动创建线程池:ProcessState::ProcessState():mMaxThreads(DEFAULT_MAX_BINDER_THREADS)//默认16个线程"。所以,Binder线程池默认大小是16个线程。-同样引用[4]有序列图描述线程创建流程:PoolThread创建,然后IPCThreadState处理消息。-引用[1]、[2]、[3]、[5]提供更多上下文,但焦点是线程池大小和机制。回答结构:-用户强调“回答结构清晰,帮助用户逐步解决问题”,所以我应该逻辑组织。-步骤:1.解释Binder线程池是什么。2.大小限制:从引用[4],默认是16。3.工作机制:从序列图描述。4.相关优化或问题:如引用[3]提到“内存不足异常”和“线程池优化策略”。生成相关问题:-在末尾添加相关问题,如系统指令要求。完整回答框架:1.引入:Binder线程池的背景。2.大小限制:具体数值。3.工作机制:线程创建、处理消息等。4.潜在问题和优化:如果有信息。5.结尾:相关问题。确保中文回答。添加引用:-任何直接引用的信息末尾加引用标识。LaTeX:用户问题中没有提到数学表达式,所以可能不需要,但如果提到并发或线程数,可以用行内表示,如$16$个线程。现在,提取相关部分:-大小限制:默认最大线程数为16(DEFAULT_MAX_BINDER_THREADS)。-工作机制:线程池在服务进程启动时创建(spawnPooledThread),每个线程有IPCThreadState处理事务(onTransact)。-从序列图:线程创建后注册到Binder驱动,处理BR_TRANSACTION。相关引用:-[^4]:提供线程池创建细节。-其他可能间接相关。回答内容:-Binder线程池是为处理IPC事务而设的线程集合。-大小限制:Android系统中,默认Binder线程池最大大小为16个线程。-工作机制:当进程启动并注册Binder服务时,系统创建线程池。每个线程(PoolThread)初始化IPCThreadState,与Binder驱动交互处理事务(如onTransact)。-优化:从引用[3],提到“线程池优化策略”,可以简述或忽略,因为用户焦点是机制。相关问题:基于主题,生成3-5个问题,如:1.Binder线程池大小如何调整?2.Binder机制如何处理高并发?3.Binder线程池的常见问题和解决方案?写回答。</think> ### Android Binder线程池大小限制及工作机制 在Android系统中,Binder作为一种高效的进程间通信(IPC)机制,依赖于线程池来处理事务请求。线程池负责管理多个线程,以并行执行Binder调用,确保系统性能。以下基于您提供的引用信息,我将逐步解释Binder线程池大小限制和工作机制。回答主要参考引用[4]和[5],因为这涉及线程池的具体实现细节。 #### 1. **Binder线程池大小限制** - **默认大小限制**:在Android系统中,Binder线程池的最大线程数默认设置为$16$个线程。这在引用[4]中有明确说明:当服务进程首次发布Binder服务时,`ProcessState`类初始化线程池,参数`mMaxThreads`被定义为`DEFAULT_MAX_BINDER_THREADS`,其值为$16$。以下是关键代码部分: ```cpp ProcessState::ProcessState() : mMaxThreads(DEFAULT_MAX_BINDER_THREADS) // 默认16个线程 { // 打开binder驱动 mDriverFD = open_driver(); // 初始化线程池 if (mDriverFD >= 0) { spawnPooledThread(true); } } ``` [^4] 这意味着,任何Android进程在初始化Binder机制时,都会创建最多$16$个线程来处理Binder事务。限制线程数可以防止资源耗尽(如内存或CPU),确保系统稳定性。 - **可调整性**:这个默认值可以被开发者或系统修改,通常在编译时或运行时通过配置参数调整。例如,在高并发场景下(如处理大量服务请求),可以增加线程数以提高吞吐量,但这会增加开销[^3]。线程池大小的上限通常受限于系统硬件的可用资源(如CPU核心数和内存)。 #### 2. **Binder线程池的工作机制** Binder线程池的工作流程包括线程创建、事务处理和数据传递。其机制基于**预创建的线程池**和**消息驱动模型**,以确保高效、低延迟的IPC。以下是详细步骤(参考引用[4]中的序列图和说明): - **线程创建阶段**: - 当服务进程启动时,系统会调用`ProcessState`构造函数初始化线程池,并创建第一个线程(主线程)[^4]。代码中`spawnPooledThread(true)`启动线程池创建过程,这会生成一个`PoolThread`对象。 - 每个`PoolThread`线程会初始化自身的`IPCThreadState`,后者负责与Binder内核驱动交互。`IPCThreadState`是Binder线程的核心对象,它使用线程本地存储(TLS)管理每个线程的状态,避免线程间竞争。 - **事务处理阶段**: - 线程池中的每个线程都进入一个循环监听状态,等待从Binder驱动接收事务请求(`BR_TRANSACTION`命令)。当客户端(如另一个进程)发起请求时,Binder驱动将数据封装为Parcel对象,并投递到服务端的线程池队列。 - 线程池中空闲的线程会拾取事务,并通过`IPCThreadState`解析请求。然后,它调用服务对象的`onTransact()`方法执行实际逻辑(如RPC调用)。完成后,结果通过Binder驱动返回给客户端。 - 序列图简化如下(基于引用[4]): ``` ProcessState --> PoolThread: 创建新线程(线程池初始化) PoolThread --> IPCThreadState: 初始化线程本地存储(TLS) IPCThreadState --> Binder驱动: 发送BC_REGISTER_LOOPER(注册线程) loop Binder驱动 --> IPCThreadState: 发送BR_TRANSACTION(请求数据) IPCThreadState --> 服务对象: 调用onTransact()执行事务 end ``` [^4] - 工作机制的核心是**消息队列和事件循环**。线程池管理多个线程同时处理事务,减少等待时间。在高负载时,如果所有线程忙,新请求将排队,直到线程空闲。 - **关键特性**: - **异步处理支持**:虽然Binder主要设计为同步调用(如`ServiceManager.getService()`会阻塞直到返回),但线程池机制支持异步操作。例如,服务端可以在`onTransact()`中触发回调或通知客户端响应[^2]。但线程池大小限制可能影响异步性能(e.g., 如果线程数不足,延迟会增加)。 - **内存管理**:线程池事务使用共享内存(Parcel对象传递数据)。如果事务负载过大(如数据包过大),可能触发内存不足异常(如引用[3]中的`Out of memory for transaction buffer`),这时线程池优化策略(如调整线程数)尤为重要。 #### 3. **潜在问题与优化建议** - **常见问题**: - **内存压力**:如果事务数据量过大或线程池线程过载,可能抛出`TransactionException`(引用[3]),这通常在Logcat中显示为错误消息。优化方案包括限制Parcel大小或增加线程池大小[^3]。 - **并发瓶颈**:默认的16线程上限可能导致高并发场景下性能下降(如多个客户端同时请求)。企业级解决方案(如引用[3]提到的线程池优化)可动态调整线程数,匹配硬件资源。 - **最佳实践**: - 监控线程池状态(通过工具如`adb shell dumpsys`),并根据应用负载调整`mMaxThreads`值(在系统代码或自定义Service中)。 - 在应用中,使用`Binder.flushPendingCommands()`等方法防止事务队列堆积。 ### 相关问题 1. Binder线程池大小如何动态调整以满足不同应用负载?[参考线程池优化策略] 2. AndroidBinder机制如何处理高并发IPC请求以避免线程阻塞?[涉及异步设计] 3. Binder线程池的常见异常(如内存不足或线程死锁)及其调试方法?[基于日志和工具] 4. ServiceManager在Binder线程池初始化中起什么作用?[引用服务注册流程] 如果您需要更多细节(如示例代码或深入分析机制),请随时告知!
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值