MediaPlayer源码简单分析

1、MediaPlayer.java
位于/frameworks/base/media/java/android/media目录,主要是对底层media player的封装,主要的方法如seek、pause、play等也都有对应的jni方法。在MediaPlayer类加载时,调用了native_init()方法。该方法主要是jni层的初始化,将jni层的一些变量和java层的一些方法、变量对应起来。在创建MediaPlayer对象时,调用了native_setup()方法。该方法将java层对象和jni层的对象一一对应起来,并绑定jni层的事件回调对象。

2、com_android_MediaPlayer.cpp
位于/frameworks/base/media/jni目录。起到一个承上启下的作用,相当于java层MediaPlayer和native层MediaPlayer之间的桥梁。有几个重要的jni方法和结构体:

// jni层的初始化
android_media_MediaPlayer_native_init(JNIEnv *env)
{
    jclass clazz;

    clazz = env->FindClass("android/media/MediaPlayer"); // 获取java MediaPlayer类对象
    if (clazz == NULL) {
        return;
    }

    // 获取java MediaPlayer的字段指针,用于保存native MediaPlayer的对象指针。通过该指针,
    // 所有的java MediaPlayer对象的native方法,都调用到了native MediaPlayer对象的对应方法
    fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
    if (fields.context == NULL) {
        return;
    }

    // java MediaPlayer的事件回调方法
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");
    if (fields.post_event == NULL) {
        return;
    }

    // 和mNativeContext类似,将native SurfaceTexture对象指针保存在java对象的一个字段中
    fields.surface_texture = env->GetFieldID(clazz, "mNativeSurfaceTexture", "I");
    if (fields.surface_texture == NULL) {
        return;
    }
}

// java层native_setup(weak reference)的实现,thiz是java层MediaPlayer,weak_this则是java层MediaPlayer的weak reference。
static void
android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    ALOGV("native_setup");
    // 创建native media player,即BnMediaPlayer对象。MediaPlayer继承与BnMediaPlayer
    sp<MediaPlayer> mp = new MediaPlayer();
    if (mp == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    // create new listener and give it to MediaPlayer
    // 使用java层MediaPlayer对象和其弱引用,创建一个事件回调listener对象
    sp<JNIMediaPlayerListener> listener = new JNIMediaPlayerListener(env, thiz, weak_this);
    mp->setListener(listener); // 给MediaPlayer设置MediaPlayerListener事件回调

    // Stow our new C++ MediaPlayer in an opaque field in the Java object.
    // 将native MediaPlayer对象指针存入java MediaPlayer的一个字段中
    setMediaPlayer(env, thiz, mp);  
}

// thiz参数是一个java层MediaPlayer对象,而weak_thiz参数则是这个java对象的weak reference
JNIMediaPlayerListener::JNIMediaPlayerListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{

    // Hold onto the MediaPlayer class for use in calling the static method
    // that posts events to the application thread.
    jclass clazz = env->GetObjectClass(thiz); // 获取java MediaPlayer的类对象
    if (clazz == NULL) {
        ALOGE("Can't find android/media/MediaPlayer");
        jniThrowException(env, "java/lang/Exception", NULL);
        return;
    }
    mClass = (jclass)env->NewGlobalRef(clazz); // 保存java MediaPlayer的类对象

    // We use a weak reference so the MediaPlayer object can be garbage collected.
    // The reference is only used as a proxy for callbacks.
    mObject  = env->NewGlobalRef(weak_thiz);  // 保存java MediaPlayer的弱引用
}

JNIMediaPlayerListener::~JNIMediaPlayerListener()
{
    // remove global references
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->DeleteGlobalRef(mObject);
    env->DeleteGlobalRef(mClass);
}

// native层事件通知到java层
void JNIMediaPlayerListener::notify(int msg, int ext1, int ext2, const Parcel *obj)
{
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    if (obj && obj->dataSize() > 0) {
        jobject jParcel = createJavaParcelObject(env);
        if (jParcel != NULL) {
            Parcel* nativeParcel = parcelForJavaObject(env, jParcel);
            nativeParcel->setData(obj->data(), obj->dataSize());
            env->CallStaticVoidMethod(mClass, fields.post_event, mObject,
                    msg, ext1, ext2, jParcel);
        }
    } else {
        env->CallStaticVoidMethod(mClass, fields.post_event, mObject,
                msg, ext1, ext2, NULL);
    }
    if (env->ExceptionCheck()) {
        ALOGW("An exception occurred while notifying an event.");
        LOGW_EX(env);
        env->ExceptionClear();
    }
}

static sp<MediaPlayer> getMediaPlayer(JNIEnv* env, jobject thiz)
{
    Mutex::Autolock l(sLock);

    // 从java MediaPlayer对象的字段中,获取native MediaPlayer的对象指针
    MediaPlayer* const p = (MediaPlayer*)env->GetIntField(thiz, fields.context);
    return sp<MediaPlayer>(p);
}

static sp<MediaPlayer> setMediaPlayer(JNIEnv* env, jobject thiz, const sp<MediaPlayer>& player)
{
    Mutex::Autolock l(sLock);
    sp<MediaPlayer> old = (MediaPlayer*)env->GetIntField(thiz, fields.context);
    if (player.get()) {
        player->incStrong(thiz);
    }   
    if (old != 0) {
        old->decStrong(thiz);
    }   

    // 将native MediaPlayer的对象指针保存到java MediaPlayer对象的字段中
    env->SetIntField(thiz, fields.context, (int)player.get());
    return old;
}

// 对应java MediaPlayer的seekTo(int msec)接口,其它接口pause、start等,调用流程和该接口类似
static void
android_media_MediaPlayer_seekTo(JNIEnv *env, jobject thiz, int msec)
{
    // 从java MediaPlayer对象中获取native MediaPlayer对象的指针
    sp<MediaPlayer> mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    ALOGV("seekTo: %d(msec)", msec);

    // 调用native MediaPlayer的seekTo()
    process_media_player_call( env, thiz, mp->seekTo(msec), NULL, NULL );
}

// 处理native MediaPlayer对象的接口返回值
static void process_media_player_call(JNIEnv *env, jobject thiz, status_t opStatus, const char* exception, const char *message)
{
    if (exception == NULL) {  // Don't throw exception. Instead, send an event.
        if (opStatus != (status_t) OK) {
            sp<MediaPlayer> mp = getMediaPlayer(env, thiz);
            if (mp != 0) mp->notify(MEDIA_ERROR, opStatus, 0);
        }
    } else {  // Throw exception!
        if ( opStatus == (status_t) INVALID_OPERATION ) {
            jniThrowException(env, "java/lang/IllegalStateException", NULL);
        } else if ( opStatus == (status_t) PERMISSION_DENIED ) {
            jniThrowException(env, "java/lang/SecurityException", NULL);
        } else if ( opStatus != (status_t) OK ) {
            if (strlen(message) > 230) {
               // if the message is too long, don't bother displaying the status code
               jniThrowException( env, exception, message);
            } else {
               char msg[256];
                // append the status code to the message
               sprintf(msg, "%s: status=0x%X", message, opStatus);
               jniThrowException( env, exception, msg);
            }
        }
    }
}

3、mediaplayer.h和mediaplayer.cpp
位于/frameworks/av/include/media和/frameworks/av/media/libmedia目录。MediaPlayer继承与BnMediaPlayerClient和IMediaDeathNotifier,并声明了prepare、start、pause、stop、seekTo等多媒体播放相关的控制方法。

class IMediaPlayerClient: public IInterface
{
public:
    // 声明构造、析构、asInterface()、getInterfaceDescriptor()方法和descriptor变量
    DECLARE_META_INTERFACE(MediaPlayerClient);

    // 事件回调接口
    virtual void notify(int msg, int ext1, int ext2, const Parcel *obj) = 0;
};

// ----------------------------------------------------------------------------

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

BnMediaPlayerClient在/frameworks/av/include/media/IMediaPlayerClient.h中定义。一眼看完,没有任何新鲜的玩意。
关于DECLARE_META_INTERFACE,可以参考native binder相关类,简单来说就是声明了几个方法和变量。

IMediaDeathNotifier则定义了一个重要方法getMediaPlayerService()。该方法就是获得一个BpMediaPlayerService对象。

// establish binder interface to MediaPlayerService
/*static*/const sp<IMediaPlayerService>&
IMediaDeathNotifier::getMediaPlayerService()
{
    ALOGV("getMediaPlayerService");
    Mutex::Autolock _l(sServiceLock);
    if (sMediaPlayerService == 0) {
        // 获取到一个BpServiceManager对象,在[native binder相关类](http://blog.csdn.net/mountains2001/article/details/51494203)一文中我们已经分析过
        sp<IServiceManager> sm = defaultServiceManager();
        sp<IBinder> binder;
        do {
            // 通过BpServiceManager获取到BpMediaPlayerService,在[media server分析](http://blog.csdn.net/mountains2001/article/details/51541753)一文中我们已经分析过MediaPlayerService是如何注册到ServiceManager的
            binder = sm->getService(String16("media.player"));
            if (binder != 0) {
                break;
            }
            ALOGW("Media player service not published, waiting...");
            usleep(500000); // 0.5 s
        } while (true);

        if (sDeathNotifier == NULL) {
        sDeathNotifier = new DeathNotifier();
    }
    binder->linkToDeath(sDeathNotifier);
    sMediaPlayerService = interface_cast<IMediaPlayerService>(binder);
    }
    ALOGE_IF(sMediaPlayerService == 0, "no media player service!?");
    return sMediaPlayerService;  // 保存BpMediaPlayerService对象
}

// binder对象销毁时,需要通知到对侧,避免内存泄露
void IMediaDeathNotifier::DeathNotifier::binderDied(const wp<IBinder>& who) {
    ALOGW("media server died");

    // Need to do this with the lock held
    SortedVector< wp<IMediaDeathNotifier> > list;
    {
        Mutex::Autolock _l(sServiceLock);
        sMediaPlayerService.clear();
        list = sObitRecipients;
    }

    // Notify application when media server dies.
    // Don't hold the static lock during callback in case app
    // makes a call that needs the lock.
    size_t count = list.size();
    for (size_t iter = 0; iter < count; ++iter) {
        sp<IMediaDeathNotifier> notifier = list[iter].promote();
        if (notifier != 0) {
            notifier->died();
        }
    }
}

以下分析MediaPlayer的setDataSource()方法。

status_t MediaPlayer::setDataSource(
        const char *url, const KeyedVector<String8, String8> *headers)
{
    ALOGV("setDataSource(%s)", url);
    status_t err = BAD_VALUE;
    if (url != NULL) {
        // IMediaDeathNotifier.cpp中定义了getMediaPlayerService(),获取到BpMediaPlayerService对象
        const sp<IMediaPlayerService>& service(getMediaPlayerService());

        if (service != 0) {
            // 使用BnMediaPlayerClient(this)对象创建一个BpMediaPlayer对象。此处的player实际上是一个BpMediaPlayer,player内部包含一个BnMediaPlayerClient对象。而在media player service端则保存这一个BnMediaPlayer对象,而这个BnMediaPlayer内部保存这一个BpMediaPlayerClient对象。应用层的start、pause等命令通过BpMediaPlayer的对应接口调用到服务端的BnMediaPlayer,服务端的BnMediaPlayer的状态、错误等信息则通过BpMediaPlayerClient的notify()方法最终回调到应用层。
            sp<IMediaPlayer> player(service->create(getpid(), this, mAudioSessionId));

            // 调用BpMediaPlayer的setDataSource()方法
            if ((NO_ERROR != doSetRetransmitEndpoint(player)) ||
                (NO_ERROR != player->setDataSource(url, headers))) {
                player.clear();
            }

            // 将BpMediaPlayer对象保存下来,初始化MediaPlayer状态,并销毁上一次创建的BpMediaPlayer信息
            err = attachNewPlayer(player);
        }
    }   
    return err;
}

以下是BnMediaPlayerClient的notify()接口的实现,

void MediaPlayer::notify(int msg, int ext1, int ext2, const Parcel *obj)
{
    ALOGV("message received msg=%d, ext1=%d, ext2=%d", msg, ext1, ext2);
    bool send = true;
    bool locked = false;

    // TODO: In the future, we might be on the same thread if the app is
    // running in the same process as the media server. In that case,
    // this will deadlock.
    //
    // The threadId hack below works around this for the care of prepare
    // and seekTo within the same process.
    // FIXME: Remember, this is a hack, it's not even a hack that is applied
    // consistently for all use-cases, this needs to be revisited.
    if (mLockThreadId != getThreadId()) {
        mLock.lock();
        locked = true;
    }

    // Allows calls from JNI in idle state to notify errors
    if (!(msg == MEDIA_ERROR && mCurrentState == MEDIA_PLAYER_IDLE) && mPlayer == 0) {
        ALOGV("notify(%d, %d, %d) callback on disconnected mediaplayer", msg, ext1, ext2);
        if (locked) mLock.unlock();   // release the lock when done.
        return;
    }

    switch (msg) {
    case MEDIA_NOP: // interface test message
        break;
    case MEDIA_PREPARED:
        ALOGV("prepared");
        // 。。。。省略
        break;
    case MEDIA_PLAYBACK_COMPLETE:
        ALOGV("playback complete");
        // 。。。。省略
        break;
    // 。。。省略
    case MEDIA_BUFFERING_UPDATE:
        ALOGV("buffering %d", ext1);
        break;
    case MEDIA_SET_VIDEO_SIZE:
        ALOGV("New video size %d x %d", ext1, ext2);
        mVideoWidth = ext1;
        mVideoHeight = ext2;
        break;
    case MEDIA_TIMED_TEXT:
        ALOGV("Received timed text message");
        break;
    default:
        ALOGV("unrecognized message: (%d, %d, %d)", msg, ext1, ext2);
        break;
    }

    // 还记得在jni层创建MediaPlayer时,创建完成后立即又创建了一个JNIMediaPlayerListener对象,并将这个对象设置给了MediaPlayer对象。这里的mListener就是一个JNIMediaPlayerListener对象。
    sp<MediaPlayerListener> listener = mListener;
    if (locked) mLock.unlock();

    // this prevents re-entrant calls into client code
    if ((listener != 0) && send) {
        Mutex::Autolock _l(mNotifyLock);
        ALOGV("callback application");

        // 事件通知到JNIMediaPlayerListener
        listener->notify(msg, ext1, ext2, obj);
        ALOGV("back from callback");
    }

结论:java层的MediaPlayer的命令,通过JNI到达mediaplayer.cpp中的MediaPlayer。此处的MediaPlayer实际上是和native层通讯的中间桥梁,是一个BpMediaPlayer和BnMediaPlayerClient的组合体。在创建BpMediaPlayer时,先调用了defaultServiceManager()获取一个BpServiceManager,在从BpServiceManager中获取到BpMediaPlayerService,然后通过IMediaPlayerService的create(pid, BnMediaPlayerClient, session_id)接口,最终得到了一个BpMediaPlayer对象。而它自身又实现了BnMediaPlayerClient接口。这样,java层命令通过BpMediaPlayer传递到ServiceManager一侧的BnMediaPlayer;而底层BnMediaPlayer的状态,又通过ServiceManager一侧的BpMediaPlayerClient,回调到了mediaplayer.cpp中的MediaPlayer的notify()接口,最终经过JNI,回调到上层。

3、MediaPlayerService.h / MediaPlayerService.cpp
位于/av/media/libmediaplayerservice/目录,实现了IMediaPlayService.h接口,继承与BnMediaPlayerService类(在/av/include/media/IMediaPlayerService.h中定义)。

首先,来分析BnMediaPlayer的创建过程:

sp<IMediaPlayer> MediaPlayerService::create(pid_t pid, const 
sp<IMediaPlayerClient>& client,// BpMediaPlayerClient,实际就是mediaplayer.cpp中的MediaPlayer
        int audioSessionId)
{
    int32_t connId = android_atomic_inc(&mNextConnId);

    // 创建一个BpMediaPlayerClient的封装
    sp<Client> c = new Client(
            this, pid, connId, client, audioSessionId,
            IPCThreadState::self()->getCallingUid());

    ALOGV("Create new client(%d) from pid %d, uid %d, ", connId, pid,
         IPCThreadState::self()->getCallingUid());

    wp<Client> w = c;
    {
        Mutex::Autolock lock(mLock);
        mClients.add(w);  // 保存client
    }
    return c;
}

// Client的构造方法,内部保存了BnMediaPlayerService、BpMediaPlayerClient等对象,还保存
// 了PID、UID、session id之类的数据
MediaPlayerService::Client::Client(
        const sp<MediaPlayerService>& service, pid_t pid,
        int32_t connId, const sp<IMediaPlayerClient>& client,
        int audioSessionId, uid_t uid)
{
    ALOGV("Client(%d) constructor", connId);
    mPid = pid;
    mConnId = connId;
    mService = service;
    mClient = client;
    mLoop = false;
    mStatus = NO_INIT;
    mAudioSessionId = audioSessionId;
    mUID = uid;
    mRetransmitEndpointValid = false;

#if CALLBACK_ANTAGONIZER
    ALOGD("create Antagonizer");
    mAntagonizer = new Antagonizer(notify, this);
#endif
}

这个Client是个什么东东?原来它继承与BnMediaPlayer,真相大白了。java层的MediaPlayer的接口,最终调用到了这里。

    class Client : public BnMediaPlayer {
        // IMediaPlayer interface
        virtual void            disconnect();
        virtual status_t        setVideoSurfaceTexture(
                                        const sp<ISurfaceTexture>& surfaceTexture);
        virtual status_t        prepareAsync();
        virtual status_t        start();
        virtual status_t        stop();
        virtual status_t        pause();
        // 。。。省略
    }

再看Client的setDataSource()接口:

status_t MediaPlayerService::Client::setDataSource(
        const char *url, const KeyedVector<String8, String8> *headers)
{
    //ALOGV("setDataSource(%s)", url);
    if (url == NULL)
        return UNKNOWN_ERROR;

    // 检查url,判断是否需要网络权限
    if ((strncmp(url, "http://", 7) == 0) ||
        (strncmp(url, "https://", 8) == 0) ||
        (strncmp(url, "rtsp://", 7) == 0)) {
        if (!checkPermission("android.permission.INTERNET")) {
            return PERMISSION_DENIED;
        }
    }

    // 使用content provider打开
    if (strncmp(url, "content://", 10) == 0) {
        // get a filedescriptor for the content Uri and
        // pass it to the setDataSource(fd) method

        String16 url16(url);
        int fd = android::openContentProviderFile(url16);
        if (fd < 0)
        {
            ALOGE("Couldn't open fd for %s", url);
            return UNKNOWN_ERROR;
        }
        setDataSource(fd, 0, 0x7fffffffffLL); // this sets mStatus
        close(fd);
        return mStatus;
    } else {
        // 使用url判断初player type
        player_type playerType = getPlayerType(url);

        // 使用player type创建不同类型的MediaPlayerBase
        sp<MediaPlayerBase> p = setDataSource_pre(playerType);
        if (p == NULL) {
            return NO_INIT;
        }

        // 调用MediaPlayerBase的setDataSource()接口
        setDataSource_post(p, p->setDataSource(url, headers));
        return mStatus;
    }
}

static player_type getDefaultPlayerType() {
    char value[PROPERTY_VALUE_MAX];
    if (check_prop_enable("media.amsuperplayer.enable"))
        return AMSUPER_PLAYER;

    if (property_get("media.stagefright.use-nuplayer", value, NULL)
            && (!strcmp("1", value) || !strcasecmp("true", value))) {
        return NU_PLAYER;
    }

    return STAGEFRIGHT_PLAYER;
}

player_type getPlayerType(const char* url)
{
    if (TestPlayerStub::canBeUsed(url)) {
        return TEST_PLAYER;
    }

    /* mp3 using STAGEFRIGHT_PLAYER default */
    if (strlen(url) >= 4 && !strcasecmp(".mp3", &url[strlen(url) - 4])) {
        LOGV("Create HiPlayer STAGEFRIGHT_PLAYER");
        return STAGEFRIGHT_PLAYER;
    }

    if (!check_prop_enable("media.amsuperplayer.enable")) {
        if (!strncasecmp("http://", url, 7)
                || !strncasecmp("https://", url, 8)) {
            size_t len = strlen(url);
            if (len >= 5 && !strcasecmp(".m3u8", &url[len - 5])) {
                return NU_PLAYER;
            }

            if (strstr(url,"m3u8")) {
                return NU_PLAYER;
            }
        }

        if (!strncasecmp("rtsp://", url, 7)) {
            return NU_PLAYER;
        }
    }

    return getDefaultPlayerType();
}

static sp<MediaPlayerBase> createPlayer(player_type playerType, void* cookie,
        notify_callback_f notifyFunc)
{
    sp<MediaPlayerBase> p;
    switch (playerType) {
        case SONIVOX_PLAYER:
            ALOGV(" create MidiFile");
            p = new MidiFile();
            break;
        case STAGEFRIGHT_PLAYER:
            ALOGV(" create StagefrightPlayer");
            p = new StagefrightPlayer;
            break;
        case NU_PLAYER:
            ALOGV(" create NuPlayer");
            p = new NuPlayerDriver;
            break;
        // 。。。。省略
        default:
            ALOGE("Unknown player type: %d", playerType);
            return NULL;
    }
    if (p != NULL) {
        if (p->initCheck() == NO_ERROR) {
            p->setNotifyCallback(cookie, notifyFunc);
        } else {
            p.clear();
        }
    }
    if (p == NULL) {
        ALOGE("Failed to create player object");
    }
    return p;
}

注意:部分厂家会对这一部分代码改动,以接入自己的播放器实现。但是基本流程是固定的,就是根据不同的url,返回不同的MediaPlayerBase对象。

4、MediaPlayerInterface.h
位于目录/av/include/media/。定义了MediaPlayerBase接口,以及上边创建MediaPlayerBase时使用的几个player type的常量。

// 定义了player type常量
enum player_type {
    PV_PLAYER = 1,
    SONIVOX_PLAYER = 2,
    STAGEFRIGHT_PLAYER = 3,
    NU_PLAYER = 4,
    // Test players are available only in the 'test' and 'eng' builds.
    // The shared library with the test player is passed passed as an
    // argument to the 'test:' url in the setDataSource call.
    TEST_PLAYER = 5,

    AAH_RX_PLAYER = 100,
    AAH_TX_PLAYER = 101,

    AMLOGIC_PLAYER = 110,
    AMSUPER_PLAYER = 111,
};

// 定义了回调方法
typedef void (*notify_callback_f)(void* cookie,
        int msg, int ext1, int ext2, const Parcel *obj);

// 。。。省略,MediaPlayerBase接口和上层的MediaPlayer接口基本类似

5、StagefrightPlayer.h和StagefrightPlayer.cpp
位于/av/media/libmediaplayerservice/目录下。

StagefrightPlayer::StagefrightPlayer()
    : mPlayer(new AwesomePlayer) {
    ALOGV("StagefrightPlayer");

    mPlayer->setListener(this);
}

StagefrightPlayer::~StagefrightPlayer() {
    ALOGV("~StagefrightPlayer");
    reset();

    delete mPlayer;
    mPlayer = NULL;
}

// 。。。。省略

status_t StagefrightPlayer::setDataSource(
        const char *url, const KeyedVector<String8, String8> *headers) {
    return mPlayer->setDataSource(url, headers);
}

status_t StagefrightPlayer::start() {
    ALOGV("start");

    return mPlayer->play();
}

// 。。。。省略

从上边的代码看,StagefrightPlayer只是AwesomePlayer的简单封装,不在做分析。

6、AwesomePlayer

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值