Android MediaPlayer整体架构源码分析 -【MediaCodec编解码器插件模块化注册和创建处理流程】【Part 7】【02】

承接上一章节分析:Android MediaPlayer整体架构源码分析 -【MediaCodec编解码器插件模块化注册和创建处理流程】【Part 7】【01】
本系列文章分析的安卓源码版本:【Android 10.0 版本】

推荐涉及到的知识点:
Binder机制实现原理:Android C++底层Binder通信机制原理分析总结【通俗易懂】
ALooper机制实现原理:Android native层媒体通信架构AHandler/ALooper机制实现源码分析
Binder异常关闭监听:Android native层DeathRecipient对关联进程(如相关Service服务进程)异常关闭通知事件的监听实现源码分析

【此章节小节编号就接着上一章节排列】
3.3、OMXClient类声明和构造函数实现:
OMXClient类声明【该类非常重要,因此全部代码给出】
可以看到它提供的功能比较少,主要就是为了连接和断开连接OMX服务端的服务接口实现

// [frameworks/av/media/libstagefright/include/media/stagefright/OMXClient.h]
namespace android {

class IOMX;

class OMXClient {
public:
    OMXClient();

    status_t connect();
    status_t connect(const char* name);
    void disconnect();

    sp<IOMX> interface();

private:
	// 缓存OMX服务接口
    sp<IOMX> mOMX;

	// 不允许类对象之间赋值
    OMXClient(const OMXClient &) = delete;
    OMXClient &operator=(const OMXClient &) = delete;
};

}  // namespace android

OMXClient类构造函数实现
其实际是一个空实现

// [frameworks/av/media/libstagefright/OMXClient.cpp]
OMXClient::OMXClient() {
}

3.4、client.connect(owner.c_str())实现分析:
从上面可以知道有两个connect方法实现,第一个为无参实现,如下,实际上也是默认传递owner值即组件归属者名为"default"的调用第二个方法。

// [frameworks/av/media/libstagefright/OMXClient.cpp]
status_t OMXClient::connect() {
    return connect("default");
}

connect()有参实现:

// [frameworks/av/media/libstagefright/OMXClient.cpp]
status_t OMXClient::connect(const char* name) {
	// 引入该命名空间
    using namespace ::android::hardware::media::omx::V1_0;
    // 若传入owner即组件归属者名为空,则修正为默认值
    if (name == nullptr) {
        name = "default";
    }
    // 获取该服务Bp代理对象
    // 其实看到这里我们已然知晓,该服务的实现获取流程在前面章节中已分析过了,
    // 该服务是通过HIDL语言实现的HAL层Binder服务交互接口,IOmx.h头文件是编译自动生成的。请查看前面相关章节分析。
    // 而由前面章节分析可知,它的功能实现文件是在Omx.h和Omx.cpp中,即它们是客户端这边的实现为了和HAL服务端那边交互。
    sp<IOmx> tOmx = IOmx::getService(name);
    if (tOmx.get() == nullptr) {
    	// 若为空,则获取该服务失败,返回未初始化状态错误码
        ALOGE("Cannot obtain IOmx service.");
        return NO_INIT;
    }
    // 此方法实际上是头文件中的默认实现,而该IOmx.h头文件是编译自动生成的。
    // 并且此处的判断含义就是:获取的IOmx服务是否为HIDL语言实现的HAL直通模式实现的Binder服务。
    // 即如下分析,我们的系统不再支持直通式HAL即不支持旧版HAL实现。并且在它的功能实现文件Omx.h和Omx.cpp中没有覆写该方法。
    // HAL类型包括绑定式HAL和直通式HAL:
    // 1、绑定式 HAL。以 HAL 接口定义语言 (HIDL) 或 Android 接口定义语言 (AIDL) 表示的 HAL。
    // 2、直通式 HAL。以 HIDL 封装的传统 HAL 或旧版 HAL。
    // 关于HIDL和HAL更多信息请查看官方介绍:
    // https://source.android.google.cn/devices/architecture/hal-types
    // 见3.4.1小节分析
    if (!tOmx->isRemote()) {
    	// IOmx服务以直通模式运行时则失败,返回未初始化状态错误码
        ALOGE("IOmx service running in passthrough mode.");
        return NO_INIT;
    }
    // 根据这个名字,其实我们可猜测它肯定是个IOmx代理对象的一个wrapper对象,其实也是一个封装代理对象实现。
    // 见3.4.2小节分析
    mOMX = new utils::LWOmx(tOmx);
    ALOGI("IOmx service obtained");
    // 然后返回成功状态
    return OK;
}

3.4.1、tOmx->isRemote()实现分析:
根据方法英文注释可知:返回值表示,返回此对象的实现是否在当前进程之外。默认为false。

// [out/<vendor name>/.intermediates/hardware/interfaces/media/omx/1.0/android.hardware.media.omx@1.0_genc++_headers/gen/android/hardware/media/omx/1.0/IOmx.h]
/**
 * Ref: frameworks/av/include/media/IOMX.h: IOMX
 *
 * IOmx has the ability to create OMX nodes.
 */
struct IOmx : public ::android::hidl::base::V1_0::IBase {
    /**
     * Returns whether this object's implementation is outside of the current process.
     */
    virtual bool isRemote() const override { return false; }
}

如上发现好像不对,返回false,其实不然,上面讲过默认声明的IOmx.hal文件编译自动生成 IOmx.h头文件,但是HIDL还做了另外一件事情,就是编译时自动实现了Binder机制,从而自动生成了很多个 BpHwXXX.h 和 BsXXX.h 子类头文件实现,其中BpHwXXX.h其实就是对应此前Binder机制章节分析过的Bp代理端实现,BsXXX.h对应Bn服务实现端,因此我们也必须看他们的对应功能方法是否有覆写实现,如下在系统该头文件路径下搜索该方法:
HIDL自动生成文件
因此可以看到在Bp代理对象中覆写了该方法并返回了true:

// [out/<vendor name>/.intermediates/hardware/interfaces/media/omx/1.0/android.hardware.media.omx@1.0_genc++_headers/gen/android/hardware/media/omx/1.0/BpHwOmx.h]
struct BpHwOmx : public ::android::hardware::BpInterface<IOmx>, public ::android::hardware::details::HidlInstrumentor {
    explicit BpHwOmx(const ::android::sp<::android::hardware::IBinder> &_hidl_impl);
	
	virtual bool isRemote() const override { return true; }
}

3.4.2、new utils::LWOmx(tOmx)实现分析:
根据这个名字,其实我们可猜测它肯定是个IOmx代理对象的一个wrapper对象,其实也是一个封装代理对象实现。
注意:如下其实为什么需要这种对象转换封装类实现呢,原因在于使用了HIDL实现导致的,因此可以看到在stagefright框架中大量使用了这种转换封装类。
LWOmx类声明:

// [frameworks/av/media/libmedia/include/media/omx/1.0/WOmx.h]
namespace android {
namespace hardware {
namespace media {
namespace omx {
namespace V1_0 {
namespace utils {

// 根据英文注释可知,这是个对象转换封装类,其实就是相当于代理类实现。还讲述了命名约定,对象互相转换实现。
/**
 * Wrapper classes for conversion
 * ==============================
 *
 * Naming convention:
 * - LW = Legacy Wrapper --- It wraps a Treble object inside a legacy object.
 * - TW = Treble Wrapper --- It wraps a legacy object inside a Treble object.
 */

struct LWOmx : public IOMX {
    sp<IOmx> mBase;
    LWOmx(sp<IOmx> const& base);
    // 遍历编解码器节点组件对象列表
    status_t listNodes(List<IOMX::ComponentInfo>* list) override;
    // 分配当前指定组件名节点组件对象
    status_t allocateNode(
            char const* name,
            sp<IOMXObserver> const& observer,
            sp<IOMXNode>* omxNode) override;
    // 创建输入Surface对象
    status_t createInputSurface(
            sp<::android::IGraphicBufferProducer>* bufferProducer,
            sp<::android::IGraphicBufferSource>* bufferSource) override;
};

}  // namespace utils
}  // namespace V1_0
}  // namespace omx
}  // namespace media
}  // namespace hardware
}  // namespace android

// [frameworks/av/media/libmedia/include/media/IOMX.h]
// 父类接口
class IOMX : public RefBase {}

LWOmx类构造函数实现:
其实就是缓存IOmx对象,代理它的实现。

// [frameworks/av/media/libmedia/omx/1.0/WOmx.cpp]
// LWOmx
LWOmx::LWOmx(sp<IOmx> const& base) : mBase(base) {
}

3.5、client.interface()实现分析:
获取IOMX接口对象。其实也就是返回了3.3小节中的【LWOmx】转换封装代理【IOmx】HAL层接口的代理实现类对象。

// [frameworks/av/media/libstagefright/OMXClient.cpp]
sp<IOMX> OMXClient::interface() {
    return mOMX;
}

3.6、omx->allocateNode(componentName.c_str(), observer, &omxNode)实现分析:
分配编解码器组件节点信息对象
由于该部分内容篇幅过长,因此放入另一章节分析,请查看:
Android MediaPlayer整体架构源码分析 -【MediaCodec编解码器插件模块化注册和创建处理流程】【Part 8】

3.7、omxNode->getHalInterface()实现分析:
由上一个流程分析可知,omxNode对象其实是LWOmxNode类型代理对象实现。但该方法通过查找发现并没有在该代理类中实现,因此肯定在父类实现,而它的父类为【H2BConverter】,该父类是在【system/libhidl/transport/token/1.0/utils/include/hidl/HybridInterface.h】头文件中声明定义的。
父类H2BConverter声明定义的头文件:【省略无关代码】
关于HIDL具体实现是比较复杂的,所以我们这里只目前需要简单看一下关于它的声明。
它的实现原理就如它的类名一样,就是将HAL层接口类转换为Binder层接口类型。即对于模板的两个类型类。

// [system/libhidl/transport/token/1.0/utils/include/hidl/HybridInterface.h]
template <typename HINTERFACE,
          typename BNINTERFACE>
class H2BConverter : public BNINTERFACE {
public:
	// 类型别名定义,此处比较重要
    typedef H2BConverter<HINTERFACE, BNINTERFACE> CBase; // Converter Base
    typedef typename BNINTERFACE::BaseInterface BaseInterface;
    // HAL层接口的别名
    typedef HINTERFACE HalInterface;
    typedef typename BaseInterface::HalVariant HalVariant;
    using BaseInterface::sGetHalTokenTransactionCode;

    // 构造函数默认实现
    H2BConverter(const sp<HalInterface>& base) : mBase{base} {}
    virtual status_t onTransact(uint32_t code,
            const Parcel& data, Parcel* reply, uint32_t flags = 0);
    virtual status_t linkToDeath(
            const sp<IBinder::DeathRecipient>& recipient,
            void* cookie = nullptr,
            uint32_t flags = 0);
    virtual status_t unlinkToDeath(
            const wp<IBinder::DeathRecipient>& recipient,
            void* cookie = nullptr,
            uint32_t flags = 0,
            wp<IBinder::DeathRecipient>* outRecipient = nullptr);
    // 获取的是HalInterface对象即缓存HAL层类型的接口类
    virtual HalVariant getHalVariant() const override { return { mBase }; }
    HalInterface* getBase() { return mBase.get(); }

protected:
	// 缓存HAL层类型的接口类
    sp<HalInterface> mBase;
}

从上面声明发现该类中没有直接定义【getHalInterface】方法,其实该方法是通过宏定义来实现的,如下
【省略无关代码】

// [system/libhidl/transport/token/1.0/utils/include/hidl/HybridInterface.h]

#define DECLARE_HYBRID_META_INTERFACE(INTERFACE, ...)                     \
        DECLARE_HYBRID_META_INTERFACE_WITH_CODE(                          \
            ::android::DEFAULT_GET_HAL_TOKEN_TRANSACTION_CODE,            \
            INTERFACE, __VA_ARGS__)                                       \

#define DECLARE_HYBRID_META_INTERFACE_WITH_CODE(GTKCODE, INTERFACE, ...)  \
private:                                                                  \
    typedef ::std::variant<::std::monostate, __VA_ARGS__> _HalVariant;    \
    template <typename... Types>                                          \
    using _SpVariant =                                                    \
            ::std::variant<::std::monostate, ::android::sp<Types>...>;    \
public:                                                                   \
    typedef _SpVariant<__VA_ARGS__> HalVariant;                           \
    virtual HalVariant getHalVariant() const;                             \
    size_t getHalIndex() const;                                           \
    template <size_t Index>                                               \
    using HalInterface = ::std::variant_alternative_t<Index, _HalVariant>;\
    // 宏定义该方法,并传入一个HAL层接口类类型名
    template <typename HAL>                                               \
    sp<HAL> getHalInterface() const {                                     \
        // 此次其实将会获取到上面的 mBase缓存对象 
        HalVariant halVariant = getHalVariant();                          \
        const sp<HAL>* hal = std::get_if<sp<HAL>>(&halVariant);           \
        return hal ? *hal : nullptr;                                      \
    }  

但通过父类声明发现并没有使用该宏定义【DECLARE_HYBRID_META_INTERFACE】,其实注意上面的父类声明还继承了类型参量【BNINTERFACE】父类类型,因此我们肯定可以断定在【BNINTERFACE】即Binder层接口类中,而从上一流程中分析可知,其继承声明为【H2BConverter<IOmxNode, BnOMXNode>】,即BnOMXNode就是【BNINTERFACE】。
因此再看BnOMXNode类声明:

// [frameworks/av/media/libmedia/include/media/IOMX.h]
class BnOMXNode : public BnInterface<IOMXNode> {}

// [frameworks/av/media/libmedia/include/media/IOMX.h]
class IOMXNode : public IInterface {
public:
	// 此处发现确实实现了该宏定义。因此才有该小节分析的方法调用。
    DECLARE_HYBRID_META_INTERFACE(OMXNode, IOmxNode);
}

3.8、mCodec->mRenderTracker.setComponentName(componentName)实现分析:
向帧渲染追踪器对象设置组件名
mRenderTracker变量声明为:
帧渲染追踪器对象追踪的是:被ACodec渲染的渲染信息缓冲区buffer

// [frameworks/av/media/libstagefright/include/media/stagefright/ACodec.h]
    FrameRenderTracker mRenderTracker; // render information for buffers rendered by ACodec

FrameRenderTracker类声明:

// [frameworks/av/media/libstagefright/include/media/stagefright/FrameRenderTracker.h]
struct FrameRenderTracker {
    typedef RenderedFrameInfo Info;
private:

    // Render information for buffers. Regular surface buffers are queued in the order of
    // rendering. Tunneled buffers are queued in the order of receipt.
    // 已被渲染帧信息列表
    std::list<Info> mRenderQueue;
    // 最后渲染帧时间戳,单位ns纳秒
    nsecs_t mLastRenderTimeNs;
    // 缓存的组件名
    AString mComponentName;
}

构造函数实现:

// [frameworks/av/media/libstagefright/FrameRenderTracker.cpp]
FrameRenderTracker::FrameRenderTracker()
    : mLastRenderTimeNs(-1),
      mComponentName("unknown component") {
}

setComponentName() 方法实现:
设置组件名

// [frameworks/av/media/libstagefright/FrameRenderTracker.cpp]
void FrameRenderTracker::setComponentName(const AString &componentName) {
    mComponentName = componentName;
}

3.9、mCodec->mCallback->onComponentAllocated(mCodec->mComponentName.c_str())实现分析:
回调给MediaCodec编解码器组件实例对象已分配(成功)方法,并传入组件名参数
ACodec的mCallback该回调监听对象赋值见前面流程中分析,是在MediaCodec中设置进来的,因此会调用其该方法实现,如下

// [frameworks/av/media/libstagefright/MediaCodec.cpp]
void CodecCallback::onComponentAllocated(const char *componentName) {
	// 由前面的分析可知,此处mNotify为【kWhatCodecNotify】事件消息对象给MediaCodec接收处理
    sp<AMessage> notify(mNotify->dup());
    // 该消息子事件类型
    notify->setInt32("what", kWhatComponentAllocated);
    notify->setString("componentName", componentName);
    notify->post();
}

【kWhatCodecNotify】事件消息的子事件【kWhatComponentAllocated】接收处理:

// [frameworks/av/media/libstagefright/MediaCodec.cpp]
void MediaCodec::onMessageReceived(const sp<AMessage> &msg) {
    switch (msg->what()) {
        case kWhatCodecNotify:
        {
            int32_t what;
            CHECK(msg->findInt32("what", &what));

            switch (what) {
                case kWhatComponentAllocated:
                {
                    // 检查此刻MediaCodec内部状态是否有效,无效则直接跳过该处理。注意:前面执行init最后进行了AMessage的wait事件,
                    // 但该wait的是NuPlayerDecoder调用端消息线程,而不是MediaCodec内部消息线程,所以MediaCodec内部状态可能会被修改。
                    if (mState == RELEASING || mState == UNINITIALIZED) {
                        // In case a kWhatError or kWhatRelease message came in and replied,
                        // we log a warning and ignore.
                        ALOGW("allocate interrupted by error or release, current state %d",
                              mState);
                        break;
                    }
                    // 上一个状态必须是 INITIALIZING
                    CHECK_EQ(mState, INITIALIZING);
                    // 切换状态为已初始化完成状态,该方法此前已分析过
                    setState(INITIALIZED);
                    // 添加bit位标记:组件已分配完成标记
                    mFlags |= kFlagIsComponentAllocated;

                    // 获取组件名参数
                    CHECK(msg->findString("componentName", &mComponentName));

					 // 缓存组件名在媒体统计数据项中
                    if (mComponentName.c_str()) {
                        mAnalyticsItem->setCString(kCodecCodec, mComponentName.c_str());
                    }
                    const char *owner = "default";
                    if (mCodecInfo !=NULL)
                        owner = mCodecInfo->getOwnerName();
                    if (mComponentName.startsWith("OMX.google.")
                            && (owner == nullptr || strncmp(owner, "default", 8) == 0)) {
                        // 组件名以 "OMX.google." 字符串开头即软编解码器,并且当owner编解码器归属者名为空或者为"default"时,
                        // 添加使用软渲染器渲染处理  
                        mFlags |= kFlagUsesSoftwareRenderer;
                    } else {
                    	// 否则不使用软渲染器渲染处理,即去掉该位标记值。关于位运算不再分析,请自行先了解
                        mFlags &= ~kFlagUsesSoftwareRenderer;
                    }
                    // 缓存
                    mOwnerName = owner;

                    // 当前媒体资源类型
                    MediaResource::Type resourceType;
                    if (mComponentName.endsWith(".secure")) {
                    	// 安全编解码器时
                        mFlags |= kFlagIsSecure;
                        resourceType = MediaResource::kSecureCodec;
                        // 设置该参数
                        mAnalyticsItem->setInt32(kCodecSecure, 1);
                    } else {
                    	// 非安全编解码器时
                        mFlags &= ~kFlagIsSecure;
                        resourceType = MediaResource::kNonSecureCodec;
                        mAnalyticsItem->setInt32(kCodecSecure, 0);
                    }

                    if (mIsVideo) {
                    	// 若是视频编解码器时,添加当前媒体资源类型信息
                    	// 注意:只有视频才被系统媒体资源管理器管理视频编解码器资源,而音频时将不会。也就是说音频理论上不会被该策略限制。
                    	// 见下面的分析
                        // audio codec is currently ignored.
                        addResource(resourceType, MediaResource::kVideoCodec, 1);
                    }

					// 【mReplyID】为此前章节分析时全局缓存的需要应答请求端回复处理结果应答信息,
					// 也就是请求端前面分析的NuPlayerDecoder消息线程现在还是wait等待该应答结果(MediaCodec的init结尾wait),
					// 因此此处将应答消息并唤醒NuPlayerDecoder的MediaCodec.createByType调用处。
					// 备注:这后面的处理流程不属于该系列MediaCodec内容章节,因此请查看其它系列章节。
                    (new AMessage)->postReply(mReplyID);
                    break;
                }

            }
        }
    }
}

addResource(resourceType, MediaResource::kVideoCodec, 1)实现分析:
若是视频编解码器时,添加当前媒体资源类型信息。
注意:只有视频才被系统媒体资源管理器管理视频编解码器资源,而音频时将不会。也就是说音频理论上不会被该策略限制。

// [frameworks/av/media/libstagefright/MediaCodec.cpp]
void MediaCodec::addResource(
        MediaResource::Type type, MediaResource::SubType subtype, uint64_t value) {
    Vector<MediaResource> resources;
    resources.push_back(MediaResource(type, subtype, value));
    mResourceManagerService->addResource(
            getId(mResourceManagerClient), mResourceManagerClient, resources);
}

// [frameworks/av/media/libstagefright/MediaCodec.cpp]
void MediaCodec::ResourceManagerServiceProxy::addResource(
        int64_t clientId,
        const sp<IResourceManagerClient> &client,
        const Vector<MediaResource> &resources) {
    Mutex::Autolock _l(mLock);
    if (mService == NULL) {
        return;
    }
    // 此处将不再分析其具体处理。
    // 根据前面流程分析可知,最终调用了该服务Bn实现端即ResourceManagerService服务的对应方法,并且前面分析中也阐述了该服务的工作大致原理,
    // 因此此方法也不再分析,简单的说就是添加当前进程(即mPid例如应用APP进程)正在使用的该媒体资源类型信息及其创建的ResourceManagerClient对象映射信息。
    mService->addResource(mPid, mUid, clientId, client, resources);
}

3.10、mCodec->changeState(mCodec->mLoadedState):
扭转状态机状态为已加载状态实现者,即前面分析的 LoadedState 状态实现者
改变状态方法 changeState 不再分析,它将会执行状态的状态进入方法,请参考前面分析。
此处直接分析如下:

// [frameworks/av/media/libstagefright/ACodec.cpp]
void ACodec::LoadedState::stateEntered() {
    ALOGV("[%s] Now Loaded", mCodec->mComponentName.c_str());
    // 以下时间又是一次初始化工作

    // 初始化输入输出端口索引对应的端口EOS状态为false
    mCodec->mPortEOS[kPortIndexInput] =
        mCodec->mPortEOS[kPortIndexOutput] = false;

    mCodec->mInputEOSResult = OK;

    mCodec->mDequeueCounter = 0;
    mCodec->mMetadataBuffersToSubmit = 0;
    mCodec->mRepeatFrameDelayUs = -1LL;
    mCodec->mInputFormat.clear();
    mCodec->mOutputFormat.clear();
    mCodec->mBaseOutputFormat.clear();
    mCodec->mGraphicBufferSource.clear();

    if (mCodec->mShutdownInProgress) {
    	// 若此前正在shutdown执行流程中时
		// 备注:目前我们暂不需要关注shutdown流程,因此暂不分析。

        bool keepComponentAllocated = mCodec->mKeepComponentAllocated;

        mCodec->mShutdownInProgress = false;
        mCodec->mKeepComponentAllocated = false;

        onShutdown(keepComponentAllocated);
    }
    // 显式shutdown标志位重置为false
    // 备注:其实从上面onShutdown处理中,该标记为true时将会往上层发送编解码器stop或者release的回调通知,否则不会发送通知。
    mCodec->mExplicitShutdown = false;

	// 执行延迟消息处理流程
	// 见下面的分析
    mCodec->processDeferredMessages();
}

mCodec->processDeferredMessages()实现分析:
其实从实现上看,就是将AMessage消息队列统一一次性循环执行,并清空全局缓存消息队列,并且最后调用了ACodec的onMessageReceived消息接收处理,onMessageReceived该方法其实就是前面章节中分析过的。

// [frameworks/av/media/libstagefright/ACodec.cpp]
void ACodec::processDeferredMessages() {
    List<sp<AMessage> > queue = mDeferredQueue;
    mDeferredQueue.clear();

    List<sp<AMessage> >::iterator it = queue.begin();
    while (it != queue.end()) {
        onMessageReceived(*it++);
    }
}

备注:在当前执行流程中,此时消息队列肯定为空的,必须MediaCodec等触发添加消息才能使ACodec继续往下工作。
因此关于本系列MediaCodec【编解码器插件模块化注册和创建处理流程】相关内容章节分析到此处才是真正的完成了。

接下来请查看NuPlayerDecoder创建MediaCodec之后的处理流程章节。
即继续【六】Android MediaPlayer整体架构源码分析 -【start请求播放处理流程】【Part 4】该章节的处理

本章节结束

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值