android binder 基础实例及解析(二)

原文网址(转载请注明出处): (http://blog.csdn.net/newchenxf/article/details/49425079

虽然已经写了一个【android binder 基础实例及解析(一)】,但是觉得还是不够,为啥?因为那个例子太基础了,就一个client,一个server,调用是单向的,也就是,只有client能主动调用server,但server没法主动调用client。怎么办?

这时候,就需要匿名binder了。
匿名binder就是不需要向servicemanager注册的binder。
思路就是,再写一个binder,比如接口名字为ICallback,然后BpCallback放在server端,而BnCallback放在client端,那就可以做到server也能主动调用client了。

我们就基于前文的XXXXService修改吧。
首先,我们要在interface目录添加ICallback.h和ICallback.cpp。
然后在client目录实现Callback,继承BnCallback。然后IXXXXService添加Callback的使用。
目录结果如下
Android.mk
client->Android.mk, main_client.cpp, Callback.h, Callback.cpp
interface->IXXXXService.cpp, IXXXXService.h, ICallback.h, ICallback.cpp
server->Android.mk, XXXXService.cpp, XXXXService.h, main_XXXXService.cpp

1,添加ICallback.h

该头文件干2件事,一个是定义接口ICallback,接口函数是notifyCallback。一个是声明BnCallback。
千万别忘了DECLARE_META_INTERFACE。

#ifndef Icallback_H
#define Icallback_H
#include <binder/IInterface.h>

namespace android {

class ICallback : public IInterface {
public:
    DECLARE_META_INTERFACE(Callback);
    virtual int notifyCallback(int a) = 0;
};

class BnCallback : public BnInterface<ICallback> {
public:
virtual status_t    onTransact( uint32_t code,
                                const Parcel& data,
                                Parcel* reply,
                                uint32_t flags = 0);
};
}
#endif

2,添加ICallback.cpp

#include "IXXXXService.h"
#include <binder/Parcel.h>
#include <binder/IInterface.h>
#include <utils/Log.h>


#define LOG_NDEBUG 0
#define LOG_TAG "chenxf: ICallback"

namespace android {

enum {
    NOTIFY_CALLBACK,
};

//////////////////client
class BpCallback : public BpInterface<ICallback> {
public:
    BpCallback(const sp<IBinder>& impl) : BpInterface<ICallback>(impl) {
    }

    virtual int notifyCallback(int a) {
        ALOGD(" BpCallback::notifyCallback, a = %d", a);
        Parcel data,reply;
        data.writeInt32(a);
        remote()->transact(NOTIFY_CALLBACK,data,&reply);
        return reply.readInt32();
    }
};

IMPLEMENT_META_INTERFACE(Callback, "chenxf.binder.ICallback");


////////////////server
status_t BnCallback::onTransact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
    switch (code) {
        case NOTIFY_CALLBACK: {
            ALOGD("BnCallback::onTransact>>NOTIFY_CALLBACK\n");
            reply->writeInt32(notifyCallback((int) data.readInt32()) );
            return NO_ERROR;
        } break;
    }

    return BBinder::onTransact(code, data, reply, flags);
}
}

很简单,实现BpCallback的notifyCallback和BnCallback的onTransact。千万记得写IMPLEMENT_META_INTERFACE。

3,添加Callback.h

现在callback的服务端是在client进程了,因此其继承BnCallback的Callbac类l就该放在client目录。

#include "../interface/IXXXXService.h"
#include "../interface/ICallback.h"
#include <binder/BinderService.h>
namespace android {
class Callback: public BnCallback {
    public:
      virtual int notifyCallback(int a);
};
}

4,添加Callback.cpp

#include <binder/Parcel.h>
#include <binder/IPCThreadState.h>
#include <utils/String16.h>
#include <utils/threads.h>
#include <utils/Atomic.h>

//#include <cutils/bitops.h>
#include <cutils/properties.h>
#include <cutils/compiler.h>
#include "Callback.h"
#include <utils/Log.h>
#define LOG_NDEBUG 0
#define LOG_TAG "chenxf: client-Callback"

namespace android {
  int Callback::notifyCallback(int a) {
    ALOGD("Callback::notifyCallback, Actually, come from XXXXService.., the callback value:  %d", a);
    return 1;
  }

}

实现真正的notifyCallback。本例做很简单的事情,把callback的返回参数打印出来。

5,修改IXXXXService.h

添加一个接口函数。
virtual int setCallback(const sp& callback) = 0;
client进程可以通过setCallback函数,告诉server进程,有这么一个回调可以用,这样server有啥事,就可以通过callback告诉client进程。

#ifndef IXXXXService_H  
#define IXXXXService_H 

#include <binder/IInterface.h>
#include "ICallback.h"

namespace android {

class IXXXXService : public IInterface {
public:
    DECLARE_META_INTERFACE(XXXXService);
    virtual int setSomething(int a) = 0;
    virtual int getSomething() = 0;
    virtual int setCallback(const sp<ICallback>& callback) = 0;
};

class BnXXXXService : public BnInterface<IXXXXService> {
public:
    virtual status_t    onTransact( uint32_t code,
                                    const Parcel& data,
                                    Parcel* reply,
                                    uint32_t flags = 0);
};

}

#endif

6,修改IXXXXService.cpp

实现BpXXXXService的setCallback函数。
并在BnXXXXService的onTransact添加对setCallback的处理。
这是最关键的一步,后文讲详细分析。这里先给出代码。

#include "IXXXXService.h"
#include <binder/Parcel.h>
#include <binder/IInterface.h>
#include "ICallback.h"
#include <utils/Log.h>

#define LOG_NDEBUG 0
#define LOG_TAG "chenxf: IXXXXService"

namespace android {

enum {
    SET_SOMETHING = IBinder::FIRST_CALL_TRANSACTION,
    GET_SOMETHING,
    SET_CALLBACK
};

//------------------------------------proxy side--------------------------------

class BpXXXXService : public BpInterface<IXXXXService> {
public:
    BpXXXXService(const sp<IBinder>& impl)
        : BpInterface<IXXXXService>(impl) {
    }
    virtual int setSomething(int a) {
        ALOGD(" BpXXXXService::setSomething a = %d ", a);
        Parcel data,reply;
        data.writeInt32(a);
        remote()->transact(SET_SOMETHING,data,&reply);
        return reply.readInt32();
    }
    virtual int getSomething() {
        ALOGD(" BpXXXXService::getSomething ");
        Parcel data,reply;
        data.writeInterfaceToken(IXXXXService::getInterfaceDescriptor());
        remote()->transact(GET_SOMETHING,data,&reply);
        return reply.readInt32();
    }
    virtual int setCallback(const sp<ICallback>& callback) {
      ALOGD("BpXXXXService::setCallback");
      Parcel data, reply;
      data.writeStrongBinder(callback->asBinder());// TODO: important
      remote()->transact(SET_CALLBACK, data, &reply);
      return reply.readInt32();
    }

};

IMPLEMENT_META_INTERFACE(XXXXService, "chenxf.binder.IXXXXService");

//------------------------------------server side--------------------------------
status_t BnXXXXService::onTransact (
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags){
    switch (code) {
        case SET_SOMETHING: {
            ALOGD("BnXXXXService::onTransact  SET_SOMETHING ");
            reply->writeInt32(setSomething((int) data.readInt32()));
            return NO_ERROR;
        } break;
        case GET_SOMETHING: {
            ALOGD("BnXXXXService::onTransact  GET_SOMETHING ");
            reply->writeInt32(getSomething());
            return NO_ERROR;
        } break;
        case SET_CALLBACK: {
            ALOGD("BnXXXXService::onTransact  SET_CALLBACK ");
            sp<ICallback> callback = interface_cast<ICallback>(data.readStrongBinder());// TODO: important!
            reply->writeInt32(setCallback(callback));
            return NO_ERROR;
        }
    }

    return BBinder::onTransact(code, data, reply, flags);
}

}

7,修改XXXXService.h

server进程得实现setCallback,在头文件声明一下,并添加一个本地变量mCallback。

#include "../interface/IXXXXService.h"
#include <binder/BinderService.h>
#include "../interface/ICallback.h"

namespace android {
class XXXXService : public BinderService<XXXXService>, public BnXXXXService {
public:
    XXXXService();
    static const char* getServiceName() { return "XXXXService"; }//will be the service name
    virtual int setSomething(int a);
    virtual int getSomething();
    virtual int setCallback(const sp<ICallback>& callback);
protected:
    int myParam;
    sp<ICallback> mCallback;
  };
}

8,修改XXXXService.cpp

实现server进程的setCallback函数。
这里就是让本地的mCallback等于setCallback的参数。
如此一来,我们server进程想主动发消息给client进程,只要执行mCallback->notifyCallback就行啦!有了callback,妥妥的全双工通信。

本例就简单点,发现客户端调用了setSomething,就notifyCallback一下,告诉客户端,收到事情了,给你办好了,你安心吧。^^

#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <utils/Log.h>
#include <binder/Parcel.h>
#include <binder/IPCThreadState.h>
#include <utils/threads.h>

#include <cutils/properties.h>
#include "XXXXService.h"
#define LOG_NDEBUG 0
#define LOG_TAG "chenxf: XXXXService"

namespace android {
    XXXXService::XXXXService() {
        myParam = 0;
    }

    int XXXXService::setSomething(int a) {
        ALOGD(" XXXXService::setSomething a = %d myParam %d", a, myParam);
        myParam += a;
        //Let's trigger callback
        if(mCallback != NULL) {
            ALOGD("will notify???");
            mCallback->notifyCallback(myParam);
        } else {
            ALOGW("mCallback is NULL");
        }
        return 0;//OK
    }
    int XXXXService::getSomething() {
        ALOGD("#XXXXService::getSomething myParam = %d", myParam);
        return myParam;
    }

    int XXXXService::setCallback(const sp<ICallback>& callback) {
      ALOGD(" XXXXService::setCallback");
      mCallback = callback;
      return 0;
    }

}

9,修改main_client.cpp

好了,现在client进程可以先调用setCallback注册callback。

#include <stdio.h>
#include "Callback.h"
#include "../interface/IXXXXService.h"

#define LOG_NDEBUG 0
#define LOG_TAG "chenxf: Client-main"

using namespace android;

sp<IXXXXService> mXXXXService;

void initXXXXServiceClient() {
    int count = 10;
    if (mXXXXService == 0) {
        sp<IServiceManager> sm = defaultServiceManager();
        sp<IBinder> binder;
        do {
            binder = sm->getService(String16("XXXXService"));
            if (binder != 0)
            break;
            ALOGW("XXXXService not published, waiting...");
            sleep(1); // 1 s
            count++;
        } while (count < 20);
        mXXXXService = interface_cast<IXXXXService>(binder);
    }
}

int main(int argc, char* argv[]) {
    initXXXXServiceClient();
    if(mXXXXService ==NULL) {
        ALOGW("cannot find XXXXService");
        return 0;
    }
    sp<Callback> c = new Callback();
    mXXXXService->setCallback(c);
    while(1) {
        mXXXXService->setSomething(1);
        sleep(1);
        ALOGD("getSomething %d", mXXXXService->getSomething());
    }
    return 0;
}

10,main_XXXXService.cpp,不改

server进程啥也不用该,因为他就是启动了XXXXService,具体callback的事情,都在XXXXService完成了。不过为了代码的完整性,我还是把代码贴出来。

#include <binder/IPCThreadState.h>
#include <binder/ProcessState.h>
#include <binder/IServiceManager.h>
#include <utils/Log.h>

#include "XXXXService.h"
#define LOG_NDEBUG 0
#define LOG_TAG "chenxf: XXXXService-main"

#define EASY_START_BINDER_SERVICE 0
using namespace android;

int main(int argc, char** argv)
{
#if EASY_START_BINDER_SERVICE
    XXXXService::publishAndJoinThreadPool();
#else
    sp<ProcessState> proc(ProcessState::self());
    sp<IServiceManager> sm(defaultServiceManager());
    sm->addService(String16(XXXXService::getServiceName()), new XXXXService());
    ProcessState::self()->startThreadPool();
    IPCThreadState::self()->joinThreadPool();
#endif

    return 0;
}

关键技术分析

这个的例子,我们用了2个binder实例,第一个IXXXXService是公开的,向ServiceManager注册过的。而ICallback就没有,我们称为匿名binder。
那么,2个binder实例是否要打开2次binder设备呢?不用喔,再次强调一下,每个进程都只有一个ProcessState对象,它打开binder设备就行了,不管你写多少个binder实例,最终的数据传输都可以让他处理。

本例最重要的细节,是IXXXXServce的setCallback。
client进程已经new了Callback(继承BnCallback),所以setCallback的根本目的就是,让server端建立对应的BpCallback.

来看一下client如何注册callback到服务端。
1. client进程BpXXXXService的setCallback。
代码如下

    virtual int setCallback(const sp<ICallback>& callback) {
      ALOGD("BpXXXXService::setCallback");
      Parcel data, reply;
      data.writeStrongBinder(callback->asBinder());// TODO: important
      remote()->transact(SET_CALLBACK, data, &reply);
      return reply.readInt32();
    }

参数callback是main_client.cpp初始化的Callback对象。
writeStrongBinder干了什么?

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

status_t flatten_binder(const sp<ProcessState>& /*proc*/,
    const sp<IBinder>& binder, Parcel* out)
{
    //输入的binder,有可能是BpBinder或BBinder,准备构造flat_binder_object
    flat_binder_object obj;

    obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
    if (binder != NULL) {
        //localBinder()被IBinder定义且实现(返回NULL),被BBinder重写(返回this),因此,为NULL说明是BpBinder,不是NULL则是BBinder。对本例来说,就是BBinder
        IBinder *local = binder->localBinder();
        if (!local) {//参数是BpBinder,传递服务代理信息,客户端A把它得到的服务对象的BpBinder告诉客户端B,客户端B不查询ServiceManager也能使用服务,实现服务信息共享
            BpBinder *proxy = binder->remoteBinder();
            //remoteBinder()被IBinder定义且实现(返回NULL),被BpBinder重写(返回this)
            if (proxy == NULL) {
                ALOGE("null proxy");
            }
            const int32_t handle = proxy ? proxy->handle() : 0;
            obj.type = BINDER_TYPE_HANDLE;
            obj.binder = 0; /* Don't pass uninitialized stack data to a remote process */
            obj.handle = handle;//服务对象的handle
            obj.cookie = 0;
            ALOGD("chenxf binder->localBinder() is  null, handle = %08x", handle);
        } else {//参数是BBinder,传递服务对象信息,本例将进入这个判断
            obj.type = BINDER_TYPE_BINDER;
            obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());//服务对象的引用记录的地址
            obj.cookie = reinterpret_cast<uintptr_t>(local);//服务对象的地址
        }
    } else {
        obj.type = BINDER_TYPE_BINDER;
        obj.binder = 0;
        obj.cookie = 0;
    }
    //把flat_binder_object写到Parcel
    return finish_flatten_binder(binder, obj, out);
}

//finish_flatten_binder
inline static status_t finish_flatten_binder(
    const sp<IBinder>& /*binder*/, const flat_binder_object& flat, Parcel* out)
{
    return out->writeObject(flat, false);
}

函数的功能,请看注释。
本例,由于Callback继承BnCallback,而BnCallback继承BBinder,所以进入的判断是
obj.type = BINDER_TYPE_BINDER;
obj.binder = reinterpret_cast(local->getWeakRefs());
obj.cookie = reinterpret_cast(local);

接着,执行
remote()->transact(SET_CALLBACK, data, &reply);
看一下做什么。
如上一篇文章说的,remote()就是BpBinder,而BpBinder的transact将调用IPCThreadState::transact()。
IPCThreadState先调用writeTransactionData()把数据写入mOut,
再调用waitForResponse()写入数据并等待应答。
下面给出一个transact的主要工作内容

status_t IPCThreadState::transact(int32_t handle,....)
{
//1. 构造写入数据mOut
IPCThreadState::writeTransactionData(BC_TRANSACTION,...)
//2. 写入命令并等待应答
IPCThreadState::waitForResponse();
}

IPCThreadState::waitForResponse() {
//1. talkWithDriver,通过ioctl(mProcess->mDriverFD,  BINDER_WRITE_READ, &bwr)把mOut写入driver,然后读取返回值,写到mIn
talkWithDriver();
//2. 处理返回值
cmd = mIn.readInt32();
switch(cmd){
    case BR_TRANSACTION_COMPLETE:
    ......
}
}

client进程写数据到driver后,基本就完事了。

2. server进程BnXXXXService的onTransact 。
server进程,不断的从binder驱动读取数据,如果是给自己的,将在onTransact处理。来看一下对callback的处理。

status_t BnXXXXService::onTransact (
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags){
    switch (code) {
        case SET_CALLBACK: {
            ALOGD("BnXXXXService::onTransact  SET_CALLBACK ");
            sp<ICallback> callback = interface_cast<ICallback>(data.readStrongBinder());
            reply->writeInt32(setCallback(callback));
            return NO_ERROR;
        }
    }

}

显而易见,下面这句话最重要。
sp callback = interface_cast(data.readStrongBinder());
看一下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)
{
//out可能是BpBinder或BBinder,会把handle转换成BpBinder
//从Parcel缓冲区读取flat_binder_object
    const flat_binder_object* flat = in.readObject(false);

    if (flat) {
        switch (flat->type) {
            case BINDER_TYPE_BINDER:
                 //客户端和服务在同一进程,返回BBinder???
                *out = reinterpret_cast<IBinder*>(flat->cookie);
                return finish_unflatten_binder(NULL, *flat, in);
            case BINDER_TYPE_HANDLE:
                //客户端和服务不在同一进程,返回BpBinder,本例将走到这里。ProcessState根据handle创建BpBinder对象
                *out = proc->getStrongProxyForHandle(flat->handle);
                return finish_unflatten_binder(
                    static_cast<BpBinder*>(out->get()), *flat, in);
        }
    }
    return BAD_TYPE;
}

如注释所说,函数将走到getStrongProxyForHandle,获得一个BpBinder对象。如果你有疑问为啥client进程发的type明明是BINDER_TYPE_BINDER,为啥这里处理确实BINDER_TYPE_HANDLE?好像是因为,在kernel,做了转换。
linux/drivers/staging/android/binder.c

static void binder_transaction(......)
{
    ......
    if (fp->type == BINDER_TYPE_BINDER)
        fp->type = BINDER_TYPE_HANDLE;
    else
        fp->type = BINDER_TYPE_WEAK_HANDLE;
    fp->handle = ref->desc;
}

先不在意这个细节,来看最关键的函数getStrongProxyForHandle。

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;
    AutoMutex _l(mLock);
    //如果Service代理对象(BpBinder)未创建,将在列表中增加一个entry,以保存下面将要创建的代理对象,但初始化e->binder=null
    handle_entry* e = lookupHandleLocked(handle);
    if (e != NULL) {
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            if (handle == 0) {//servicemanager proxy的handle=0,其他正常的不会走到这
                Parcel data;
                status_t status = IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                   return NULL;
            }
            //Callback的调用,handle不是0,走到这 
            b = new BpBinder(handle); 
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

所以,
sp callback = interface_cast(data.readStrongBinder());
相当于,
sp callback = interface_cast(new BpBinder(handle));
而interface_cast将会new BpCallback
所以又相当于
sp callback = new BpCallback(new BpBinder(handle));

所以到最后,XXXXService的mCallback = new BpCallback(new BpBinder(handle))。

所以我们明白了,其实client进程的setCallback的参数是一个BnCallback对象,但传递到server进程后,变成一个BpCallback对象。
server进程想要发送消息给client进程,相当于是让ICallback的客户端(BpCallback)发给ICallback的服务端(BnCallback)。

总结

经过这个例子,我们发现,A进程和B进程,既可以有binder的客户端,也可以有binder的服务端,如A进程有BpXXXXService和BnCallback。B进程有BnXXXXService和BpCallback。并且A和B都还含有ServiceManager的客户端,因为他们都要与ServiceManager服务端通信,如调用addService、getService。
所以我刚才的称呼client进程和server进程,也不是很严肃的,或者说,我是以XXXXService为标准来称呼的。

测试结果

启动2个进程。
#test_bidner_server &
#test_bidner_client &
server的processID是1947
client的processID是2136
下面是log。

10-12 00:00:41.078  2136  2136 D chenxf: IXXXXService: BpXXXXService::setCallback
10-12 00:00:41.078  1947  1947 D chenxf: IXXXXService: BnXXXXService::onTransact  SET_CALLBACK
10-12 00:00:41.078  1947  1947 D chenxf: XXXXService:  XXXXService::setCallback
10-12 00:00:41.078  2136  2136 D chenxf: IXXXXService:  BpXXXXService::setSomething a = 1
10-12 00:00:41.078  1947  1948 D chenxf: IXXXXService: BnXXXXService::onTransact  SET_SOMETHING
10-12 00:00:41.078  1947  1948 D chenxf: XXXXService:  XXXXService::setSomething a = 1 myParam 0
10-12 00:00:41.078  1947  1948 D chenxf: XXXXService: will notify???
10-12 00:00:41.078  1947  1948 D chenxf: ICallback:  BpCallback::notifyCallback, a = 1
10-12 00:00:41.079  2136  2136 D chenxf: ICallback: BnCallback::onTransact>>NOTIFY_CALLBACK
10-12 00:00:41.079  2136  2136 D chenxf: client-Callback: Callback::notifyCallback, Actually, come from XXXXService.., the callback value:  1
10-12 00:00:42.079  2136  2136 D chenxf: IXXXXService:  BpXXXXService::getSomething
10-12 00:00:42.079  1947  1947 D chenxf: IXXXXService: BnXXXXService::onTransact  GET_SOMETHING
10-12 00:00:42.079  1947  1947 D chenxf: XXXXService: #XXXXService::getSomething myParam = 1
10-12 00:00:42.080  2136  2136 D chenxf: Client-main: getSomething 1
10-12 00:00:42.080  2136  2136 D chenxf: IXXXXService:  BpXXXXService::setSomething a = 1
10-12 00:00:42.080  1947  1948 D chenxf: IXXXXService: BnXXXXService::onTransact  SET_SOMETHING
10-12 00:00:42.080  1947  1948 D chenxf: XXXXService:  XXXXService::setSomething a = 1 myParam 1
10-12 00:00:42.081  1947  1948 D chenxf: XXXXService: will notify???
10-12 00:00:42.081  1947  1948 D chenxf: ICallback:  BpCallback::notifyCallback, a = 2
10-12 00:00:42.081  2136  2136 D chenxf: ICallback: BnCallback::onTransact>>NOTIFY_CALLBACK
10-12 00:00:42.081  2136  2136 D chenxf: client-Callback: Callback::notifyCallback, Actually, come from XXXXService.., the callback value:  2
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

newchenxf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值