android—binder进程间通讯流程分析


binder是android的基础,是学习android的重中之重,理解binder的运行机制,对阅读和理解android framework源代码非常有帮助,关于如何学习binder,建议首先不要去关注binder内核驱动的实现,先学会上层如何去使用,在熟悉后再进一步结合内核代码去学习,否则会有事倍功半的效果,特别推荐下面2篇文章:
1.Android深入浅出之Binder机制
http://www.cnblogs.com/innost/archive/2011/01/09/1931456.html,这篇文章主要是从上层去分析binder的使用,入门的首选;
2.红茶一杯话Binder
http://my.oschina.net/youranhongcha/blog/149575?fromerr=kZq036OO ,这篇文章是一个系列,对于深入学习帮助很大,包含了内核binder驱动的实现。
下面主要把自己理解的整个binder进程间通讯的流程进行梳理,将各个数据结构联系起来。

进程

进程是unix类操作系统抽象概念中最基本的一种,在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。
在系统中同时会存在很多进程,这些进程共用一个处理器,而虚拟处理器给进程一种抽象,让进程感觉自己在独享处理器;
而虚拟内存则让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源,所以上层应用程序所使用的内存地址都是虚拟地址,而不同应用程序之间(不同进程)的地址是隔离的。例如在进程A中新建了一个对象ObjectA,而在进程B中肯定是无法去使用进程A中的ObjectA对象,这时候就需要一种实现进程间通讯的机制。
android是基于linux内核,虽然已经淡化了进程的概念(说的最多的就是四大组件),但是上层归根结底都是一个个独立的进程。而binder是android上进程间通讯的主要手段,由于android上层代码既有java,又有c++,所以binder对两种语言都需要有对应的实现。
java语言使用binder时一般都是借助AIDL来简化工作,网上有很多文章专门介绍如何用AIDL(创建和使用service),而java层的实现其实最终都会调用c++的实现,所以这里只介绍binder的c++相关的东西。参考前面的例子,android binder最终的作用就是让进程B可以去“使用”进程A中的ObjectA对象,“使用”ObjectA的方法,这里使用都用的引号,因为进程的地址隔离,肯定不是直接去调用,而是需要binder这个粘合剂作为中间件,将进程A和B联系起来,达到间接调用ObjectA对象方法的目的,造成的假象就好像在进程B中执行了ObjectA对象的方法,这就实现了进程间通讯。

binder的c++实现

这里写图片描述

binder c++实现所涉及的类之间的关系如上图所示,其中xxx代表具体的service的名字,例如MediaPlayerService这个service,相应的类会有IMediaPlayerService,BpInterface<MediaPlayerService>,BnInterface< MediaPlayerService >,BpMediaPlayerService,BnMediaPlayerService,那么这些类之间到底是如何关系起来的?
这里先说几个内核中的概念。
1.在某一进程中,如果去打开/dev/binder设备,binder内核驱动在内核中会为该进程创建一个binder_proc结构,一个进程有唯一的binder_proc结构;
2.对于一般的service,例如MediaPlayerService,在创建一个对象后,即 SfObj = new MediaPlayerService()后,都会将这个对象注册到service manager管家中,如果对象SfObj 是在进程A中new的,我们就说进程A中拥有MediaPlayerService这个service的实体,也就是service的真实对象。当进程A向service manager去注册SfObj 时,binder内核驱动会为SfObj 这个对象创建一个binder_node结构,并且会把该binder_node结构保存在进程A内核中的binder_proc结构中;
3.binder内核驱动同时会为service manager进程创建一个binder_ref结构,表示SfObj 这个对象的代理,相当于真实对象的一个代表,该结构中的node成员会指向实际的binder_node结构(内核中的内存地址不是隔离的),同时将这个binder_ref结构保存在service manager进程的binder_proc结构中(service manager进程在启动的时候会去打开binder驱动,因此也会创建一个binder_proc结构)。
4.service manager进程中,binder内核驱动为SfObj 这个对象的binder_ref结构返回一个handle,其实就是个int值,从1开始,然后在service manager进程中将这个handle和SfObj这个对象代表的service的名字绑定起来,保存在一个链表中,例如(1,surfaceflinger)->(2,serviceA)…,而这个handle,即1,2等只是为了和内核binder驱动中binder_ref结构对应起来,利用handle,可以找到内核的binder_ref结构,然后就能获取其node成员,即真实service所对应的binder_node结构,然后就能找到这个binder_node所对应的进程所对应的binder_proc,从而找到了上层进程中的真实SfObj 这个对象。
在所有servcie启动前,首先会去启动service manager这个管家,下面继续分析。

service manager

service manager,顾名思义,就是管理service的管家。

service servicemanager /system/bin/servicemanager
    class core
    user system
    group system
    critical
    onrestart restart healthd
    onrestart restart zygote
    onrestart restart media
    onrestart restart surfaceflinger
    onrestart restart drm

在init.rc中可以看出,servicemanager是一个进程,在init.rc中启动,其代码在frameworks\native\cmds\servicemanager下。
在service_manager.c函数的main()函数中,首先会去打开binder驱动设备,这样内核会servicemanager进程创建一个binder_proc结构,后续servicemanager就会去处理其他进程的注册service消息。

bs = binder_open(128*1024);

这里写图片描述

注册service

下面以MediaPlayerService服务的注册为例子,MediaPlayerService是在mediaserver进程中注册的。类MediaPlayerService的继承关系如下,下面的分析可以参考。

这里写图片描述

mediaserver进程首先会去调用下面的代码打开binder设备,然后内核会为mediaserver进程创建一个binder_proc结构。

sp<ProcessState> proc(ProcessState::self());

frameworks\av\media\mediaserver\main_mediaserver.c的main()中,

MediaPlayerService::instantiate();

在mediaserver进程中,首先新建一个MediaPlayerService对象,关于 defaultServiceManager()的实现前面的两篇文章中都有介绍,

void MediaPlayerService::instantiate() {
    defaultServiceManager()->addService(
            String16("media.player"), new MediaPlayerService());
}

defaultServiceManager()->addService(),实际是去调用BpServiceManager的addService(),

    virtual status_t addService(const String16& name, const sp<IBinder>& service,
            bool allowIsolated)
    {
        Parcel data, reply;
        data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
        data.writeString16(name);
        //mediaserver进程中的MediaPlayerService对象指针,
        data.writeStrongBinder(service);
        data.writeInt32(allowIsolated ? 1 : 0);
        //remote返回的就是BpBinder(0)
        status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
        return err == NO_ERROR ? reply.readExceptionCode() : err;
    }

writeStrongBinder()非常重要,将mediaserver进程中的MediaPlayerService对象指针要去打包,

status_t Parcel::writeStrongBinder(const sp<IBinder>& val)
{
    return flatten_binder(ProcessState::self(), val, this);
}

参数binder,就是MediaPlayerService对象指针,而MediaPlayerService继承自BBinder,也就是个IBinder,

status_t flatten_binder(const sp<ProcessState>& proc,
    const sp<IBinder>& binder, Parcel* out)
{
    flat_binder_object obj;

    obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
    if (binder != NULL) {
         //是否是binder实体,即真实的service的对象的指针,如果是返回非空
        IBinder *local = binder->localBinder();
        if (!local) {
            BpBinder *proxy = binder->remoteBinder();
            if (proxy == NULL) {
                ALOGE("null proxy");
            }
            const int32_t handle = proxy ? proxy->handle() : 0;
            obj.type = BINDER_TYPE_HANDLE;
            obj.handle = handle;
            obj.cookie = NULL;
        } else {
         //binder实体,真实service对象的指针会保存在cookie中
            obj.type = BINDER_TYPE_BINDER;
            obj.binder = local->getWeakRefs();
            obj.cookie = local;
        }
    } else {
        obj.type = BINDER_TYPE_BINDER;
        obj.binder = NULL;
        obj.cookie = NULL;
    }

    return finish_flatten_binder(binder, obj, out);
}

上面的binder是binder实体,则是调用BBinder的localBinder,返回的是真实的service的对象的指针,

BBinder* BBinder::localBinder()
{
    return this;
}

remote函数就是BpServiceManager所继承的BpRefBase的remote函数,直接返回mRemote,也就是BpBinder(0),其handle为0。

inline  IBinder*        remote()                { return mRemote; }

接着会去调用BpBinder的transact,将参数打包发送到内核,通过上面的步骤在数据中打包了一个BINDER_TYPE_BINDER类型的数据。
当binder内核驱动收到mediaserver打包的数据,在内核的binder_transaction函数中,

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply)
{
        fp = (struct flat_binder_object *)(t->buffer->data + *offp);
        switch (fp->type) {
        case BINDER_TYPE_BINDER:
        case BINDER_TYPE_WEAK_BINDER: {
            struct binder_ref *ref;
            struct binder_node *node = binder_get_node(proc, fp->binder);
            if (node == NULL) {
                //新建binder_node结构
                node = binder_new_node(proc, fp->binder, fp->cookie);
                if (node == NULL) {
                    return_error = BR_FAILED_REPLY;
                    goto err_binder_new_node_failed;
                }
                node->min_priority = fp->flags & FLAT_BINDER_FLAG_PRIORITY_MASK;
                node->accept_fds = !!(fp->flags & FLAT_BINDER_FLAG_ACCEPTS_FDS);
            }
            if (fp->cookie != node->cookie) {
                binder_user_error("binder: %d:%d sending u%p "
                    "node %d, cookie mismatch %p != %p\n",
                    proc->pid, thread->pid,
                    fp->binder, node->debug_id,
                    fp->cookie, node->cookie);
                goto err_binder_get_ref_for_node_failed;
            }
            //这里的目标进程target_proc是servicemanager的binder_proc
            ref = binder_get_ref_for_node(target_proc, node);
            if (ref == NULL) {
                return_error = BR_FAILED_REPLY;
                goto err_binder_get_ref_for_node_failed;
            }
            if (fp->type == BINDER_TYPE_BINDER)
                fp->type = BINDER_TYPE_HANDLE;
            else
                fp->type = BINDER_TYPE_WEAK_HANDLE;
            fp->handle = ref->desc;
            binder_inc_ref(ref, fp->type == BINDER_TYPE_HANDLE,
                       &thread->todo);

            binder_debug(BINDER_DEBUG_TRANSACTION,
                     "        node %d u%p -> ref %d desc %d\n",
                     node->debug_id, node->ptr, ref->debug_id,
                     ref->desc);
        } break;
}

如果数据类型为BINDER_TYPE_BINDER,首先会在内核中新建一个binder_node结构,这里的参数cookie就是mediaserver进程中的MediaPlayerService的对象指针,

static struct binder_node *binder_new_node(struct binder_proc *proc,
                       void __user *ptr,
                       void __user *cookie)
{
    struct rb_node **p = &proc->nodes.rb_node;
    struct rb_node *parent = NULL;
    struct binder_node *node;

    while (*p) {
        parent = *p;
        node = rb_entry(parent, struct binder_node, rb_node);

        if (ptr < node->ptr)
            p = &(*p)->rb_left;
        else if (ptr > node->ptr)
            p = &(*p)->rb_right;
        else
            return NULL;
    }

    node = kzalloc(sizeof(*node), GFP_KERNEL);
    if (node == NULL)
        return NULL;
    binder_stats_created(BINDER_STAT_NODE);
    //将binder_node插入到binder_proc中的节点中
    rb_link_node(&node->rb_node, parent, p);
    rb_insert_color(&node->rb_node, &proc->nodes);
    node->debug_id = ++binder_last_id;
    node->proc = proc;
    node->ptr = ptr;
    node->cookie = cookie;
    node->work.type = BINDER_WORK_NODE;
    INIT_LIST_HEAD(&node->work.entry);
    INIT_LIST_HEAD(&node->async_todo);
    return node;
}

接着调用binder_get_ref_for_node(target_proc, node);,在目标进程,即service manager进程所对应的binder_proc中建立一个binder_ref结构,指向binder实体所对应的binder_node结构。其中binder_ref的desc成员是个int,多一个ref,这个参数就增加1,对应不同的binder实体,其作用就是将代理和binder实体一一对应起来,

static struct binder_ref *binder_get_ref_for_node(struct binder_proc *proc,
                          struct binder_node *node)
{
    struct rb_node *n;
    struct rb_node **p = &proc->refs_by_node.rb_node;
    struct rb_node *parent = NULL;
    struct binder_ref *ref, *new_ref;

    while (*p) {
        parent = *p;
        ref = rb_entry(parent, struct binder_ref, rb_node_node);

        if (node < ref->node)
            p = &(*p)->rb_left;
        else if (node > ref->node)
            p = &(*p)->rb_right;
        else
            return ref;
    }
    //新建binder_ref
    new_ref = kzalloc(sizeof(*ref), GFP_KERNEL);
    if (new_ref == NULL)
        return NULL;
    binder_stats_created(BINDER_STAT_REF);
    new_ref->debug_id = ++binder_last_id;
    new_ref->proc = proc;
    //将binder_ref结构中的node指向binder实体
    new_ref->node = node;
    rb_link_node(&new_ref->rb_node_node, parent, p);
    rb_insert_color(&new_ref->rb_node_node, &proc->refs_by_node);

    //如果不是servicemanager,即binder_context_mgr_node,则初始值为1,这里肯定不是
    new_ref->desc = (node == binder_context_mgr_node) ? 0 : 1;
    //遍历目标进程中的所有代理节点,对new_ref->desc赋,其实就是在现有节点的最大值上+1,一直递增
    for (n = rb_first(&proc->refs_by_desc); n != NULL; n = rb_next(n)) {
        ref = rb_entry(n, struct binder_ref, rb_node_desc);
        if (ref->desc > new_ref->desc)
            break;
        new_ref->desc = ref->desc + 1;
    }

    p = &proc->refs_by_desc.rb_node;
    while (*p) {
        parent = *p;
        ref = rb_entry(parent, struct binder_ref, rb_node_desc);

        if (new_ref->desc < ref->desc)
            p = &(*p)->rb_left;
        else if (new_ref->desc > ref->desc)
            p = &(*p)->rb_right;
        else
            BUG();
    }
    rb_link_node(&new_ref->rb_node_desc, parent, p);
    rb_insert_color(&new_ref->rb_node_desc, &proc->refs_by_desc);
    if (node) {
        hlist_add_head(&new_ref->node_entry, &node->refs);

        binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                 "binder: %d new ref %d desc %d for "
                 "node %d\n", proc->pid, new_ref->debug_id,
                 new_ref->desc, node->debug_id);
    } else {
        binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                 "binder: %d new ref %d desc %d for "
                 "dead node\n", proc->pid, new_ref->debug_id,
                  new_ref->desc);
    }
    return new_ref;
}

mediaserver进程向service manager进程去注册MediaPlayerService对象指针,前面已经完成了mediaserver进程的binder实体(binder_node)和service manager进程binder代理的建立(binder_ref),这时候内核会将数据类型修改为BINDER_TYPE_HANDLE,然后会将这个desc返回给servicemanager进程,其实就是上层的handle。

            if (fp->type == BINDER_TYPE_BINDER)
                fp->type = BINDER_TYPE_HANDLE;
            else
                fp->type = BINDER_TYPE_WEAK_HANDLE;
            fp->handle = ref->desc;

当service manager进程收到数据,在frameworks\native\cmds\servicemanager\service_manager.c的svcmgr_handler函数中,

int svcmgr_handler(struct binder_state *bs,
                   struct binder_txn *txn,
                   struct binder_io *msg,
                   struct binder_io *reply)
{
    case SVC_MGR_ADD_SERVICE:
        s = bio_get_string16(msg, &len);
        //返回handle,这里不过是用指针表示的
        ptr = bio_get_ref(msg);
        allow_isolated = bio_get_uint32(msg) ? 1 : 0;
        if (do_add_service(bs, s, len, ptr, txn->sender_euid, allow_isolated))
            return -1;
        break;
}
void *bio_get_ref(struct binder_io *bio)
{
    struct binder_object *obj;

    obj = _bio_get_obj(bio);
    if (!obj)
        return 0;
    //内核给servicemanager返回的type就是BINDER_TYPE_HANDLE,返回handle
    if (obj->type == BINDER_TYPE_HANDLE)
        return obj->pointer;

    return 0;
}

接着调用do_add_service(),在添加service之前,先会判断是否可以注册,如果未注册,则新建一个svcinfo结构,该结构中最重要的就是ptr和service对应的名字,这里的prt其实就是内核中的handle,所有添加的service通过next构成一个链表。

struct svcinfo 
{
    //链表
    struct svcinfo *next;
    // handle
    void *ptr;
    struct binder_death death;
    int allow_isolated;
    unsigned len;
    //service名字
    uint16_t name[0];
};
int do_add_service(struct binder_state *bs,
                   uint16_t *s, unsigned len,
                   void *ptr, unsigned uid, int allow_isolated)
{
    struct svcinfo *si;
    //ALOGI("add_service('%s',%p,%s) uid=%d\n", str8(s), ptr,
    //        allow_isolated ? "allow_isolated" : "!allow_isolated", uid);

    if (!ptr || (len == 0) || (len > 127))
        return -1;

    if (!svc_can_register(uid, s)) {
        ALOGE("add_service('%s',%p) uid=%d - PERMISSION DENIED\n",
             str8(s), ptr, uid);
        return -1;
    }

    si = find_svc(s, len);
    if (si) {
        if (si->ptr) {
            ALOGE("add_service('%s',%p) uid=%d - ALREADY REGISTERED, OVERRIDE\n",
                 str8(s), ptr, uid);
            svcinfo_death(bs, si);
        }
        si->ptr = ptr;
    } else {
        si = malloc(sizeof(*si) + (len + 1) * sizeof(uint16_t));
        if (!si) {
            ALOGE("add_service('%s',%p) uid=%d - OUT OF MEMORY\n",
                 str8(s), ptr, uid);
            return -1;
        }
        //填充handle及其他参数
        si->ptr = ptr;
        si->len = len;
        memcpy(si->name, s, (len + 1) * sizeof(uint16_t));
        si->name[len] = '\0';
        si->death.func = svcinfo_death;
        si->death.ptr = si;
        si->allow_isolated = allow_isolated;
        si->next = svclist;
        svclist = si;

    return 0;
}

至此,MediaPlayerService(MediaPlayerService对象指针)服务注册完成,在service manager的链表中(svclist)添加了handle和名字,下面的图显示了注册完成后的结果。

这里写图片描述

client 使用service

当在service manager中注册完服务后,其他进程就能使用这个服务了。
frameworks\base\media\jni\android_media_MediaPlayer.cpp中,有使用MediaPlayerService服务的方法,如下

static jint
android_media_MediaPlayer_pullBatteryData(JNIEnv *env, jobject thiz, jobject java_reply)
{
    //①,获取media.player服务的BpBinder(handle)
    sp<IBinder> binder = defaultServiceManager()->getService(String16("media.player"));
    //②,由BpBinder(handle)转换为BpMediaPlayerService
    sp<IMediaPlayerService> service = interface_cast<IMediaPlayerService>(binder);
    if (service.get() == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "cannot get MediaPlayerService");
        return UNKNOWN_ERROR;
    }

    Parcel *reply = parcelForJavaObject(env, java_reply);
    //调用BpMediaPlayerService代理的方法
    return service->pullBatteryData(reply);
}

上面代码主要分三部分:
1.向service manager进程获取media.player服务的BpBinder,这时候和注册服务时一样,上述代码所在的进程(记为进程P)首先会打开binder设备,内核为进程P创建一个binder_proc,当调用getService时,内核为进程P新建一个binder_ref结构,指向MediaPlayerService对象指针,即binder实体所对应的binder_node,然后给进程P返回一个handle,具体的细节和上面类似,不分析了。
在BpServiceManager的checkService()函数中,最重要的是内核给返回的东西reply.readStrongBinder();

    virtual sp<IBinder> getService(const String16& name) const
    {
        unsigned n;
        for (n = 0; n < 5; n++){
            sp<IBinder> svc = checkService(name);
            if (svc != NULL) return svc;
            ALOGI("Waiting for service %s...\n", String8(name).string());
            sleep(1);
        }
        return NULL;
    }

    virtual sp<IBinder> checkService( const String16& name) const
    {
        Parcel data, reply;
        data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
        data.writeString16(name);
        remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);
        //内核返回的东西
        return reply.readStrongBinder();
    }
sp<IBinder> Parcel::readStrongBinder() const
{
    sp<IBinder> val;
    unflatten_binder(ProcessState::self(), *this, &val);
    return val;
}

接着调用,

status_t unflatten_binder(const sp<ProcessState>& proc,
    const Parcel& in, sp<IBinder>* out)
{
    const flat_binder_object* flat = in.readObject(false);

    if (flat) {
        switch (flat->type) {
            case BINDER_TYPE_BINDER:
                *out = static_cast<IBinder*>(flat->cookie);
                return finish_unflatten_binder(NULL, *flat, in);
            //getservice,内核返回的时候,肯定是走到这
            case BINDER_TYPE_HANDLE:
             //创建BpBinder的关键,handle是内核传回来的int
                *out = proc->getStrongProxyForHandle(flat->handle);
                return finish_unflatten_binder(
                    static_cast<BpBinder*>(out->get()), *flat, in);
        }        
    }
    return BAD_TYPE;
}

下面代码是由内核返回的数据,创建BpBinder(handle)的关键,在使用服务的进程中(每个使用binder的进程都会有个ProcessState对象),其ProcessState对象中有个Vector<handle_entry>mHandleToObject;对象,这个vector就是用来按照handle保存BpBinder的。

            struct handle_entry {
                IBinder* binder;
                RefBase::weakref_type* refs;
            };
sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;

    AutoMutex _l(mLock);

    handle_entry* e = lookupHandleLocked(handle);

    if (e != NULL) {
        // We need to create a new BpBinder if there isn't currently one, OR we
        // are unable to acquire a weak reference on this current one.  See comment
        // in getWeakProxyForHandle() for more info about this.
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            if (handle == 0) {
                // Special case for context manager...
                // The context manager is the only object for which we create
                // a BpBinder proxy without already holding a reference.
                // Perform a dummy transaction to ensure the context manager
                // is registered before we create the first local reference
                // to it (which will occur when creating the BpBinder).
                // If a local reference is created for the BpBinder when the
                // context manager is not present, the driver will fail to
                // provide a reference to the context manager, but the
                // driver API does not return status.
                //
                // Note that this is not race-free if the context manager
                // dies while this code runs.
                //
                // TODO: add a driver API to wait for context manager, or
                // stop special casing handle 0 for context manager and add
                // a driver API to get a handle to the context manager with
                // proper reference counting.

                Parcel data;
                status_t status = IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                   return NULL;
            }
        //新建BpBinder,
            b = new BpBinder(handle); 
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
            // This little bit of nastyness is to allow us to add a primary
            // reference to the remote proxy when this team doesn't have one
            // but another team is sending the handle to us.
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

2.
接着通过interface_cast<IMediaPlayerService>(binder)将BpBinder(handle)转换为BpMediaPlayerService对象,前面的文章都有介绍。
3.
有了代理对象BpMediaPlayerService,我们就能开始调用对象中的方法了。
上面各个类之间的关系如下,
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值