Android SurfaceFlinger导读(10)理解DisplayDevice

该系列文章总纲链接:Android GUI系统之SurfaceFlinger 系列文章目录


本章关键点总结 & 说明:

本章节思维导图如上。主要讲述了DisplayDevice的概念,这里 主要DisplayDevice的创建流程和 两个关键方法makeCurrent  和swapBuffer。


在之前的分析中,我们知道 每个应用端对应一个SurfaceControl,每个SurfaceControl都对应多个Surface,即 应用端使用Surface来管理Buffer。实际上在SurfaceFlinger中 DisplayDevice也是通过Surface来管理Buffer。这里简单说明下两者的异同:

  1. 应用端的Buffer是向Ashmem申请的;DisplayDevice端的Buffer是向FrameBuffer申请的
  2. 应用端的Buffer是用来填充的;DisplayDevice端的Buffer是用来推给Framebuffer 渲染的

了解了这些后,我们开始从DisplayDevice创建流程开始分析。

1 DisplayDevice创建流程开始分析

DisplayDevice是在SurfaceFlinger的init中创建的,代码实现如下:

void SurfaceFlinger::init() {
    //...
    // initialize our non-virtual displays
    for (size_t i=0 ; i<DisplayDevice::NUM_BUILTIN_DISPLAY_TYPES ; i++) {
        DisplayDevice::DisplayType type((DisplayDevice::DisplayType)i);
        // set-up the displays that are already connected
        if (mHwc->isConnected(i) || type==DisplayDevice::DISPLAY_PRIMARY) {
            // All non-virtual displays are currently considered secure.
            bool isSecure = true;
            createBuiltinDisplayLocked(type);
            wp<IBinder> token = mBuiltinDisplays[i];
            //BufferQueue相关
            sp<IGraphicBufferProducer> producer;
            sp<IGraphicBufferConsumer> consumer;
            BufferQueue::createBufferQueue(&producer, &consumer,
                    new GraphicBufferAlloc());
            //关键点1:FramebufferSurface创建,定位为 消费者身份
            sp<FramebufferSurface> fbs = new FramebufferSurface(*mHwc, i,
                    consumer);
            int32_t hwcId = allocateHwcDisplayId(type);
            //关键点2:DisplayDevice创建,定位为 生产者身份
            sp<DisplayDevice> hw = new DisplayDevice(this,
                    type, hwcId, mHwc->getFormat(hwcId), isSecure, token,
                    fbs, producer,
                    mRenderEngine->getEGLConfig());
            if (i > DisplayDevice::DISPLAY_PRIMARY) {
                hw->setPowerMode(HWC_POWER_MODE_NORMAL);
            }
            mDisplays.add(token, hw);
        }
    }
    //...
    // start boot animation
    startBootAnim();
}

1.1 FramebufferSurface创建流程分析

FramebufferSurface构造函数实现如下:

FramebufferSurface::FramebufferSurface(HWComposer& hwc, int disp,
        const sp<IGraphicBufferConsumer>& consumer) :
    ConsumerBase(consumer),
    mDisplayType(disp),
    mCurrentBufferSlot(-1),
    mCurrentBuffer(0),
    mHwc(hwc)
{
    mName = "FramebufferSurface";
    mConsumer->setConsumerName(mName);
    /*这里设置了标志位GRALLOC_USAGE_HW_FB,借此参数分辨
    向Ashmem申请->APP;向Framebuffer申请->DisplayDevice
    */
    mConsumer->setConsumerUsageBits(GRALLOC_USAGE_HW_FB |
                                       GRALLOC_USAGE_HW_RENDER |
                                       GRALLOC_USAGE_HW_COMPOSER);
    mConsumer->setDefaultBufferFormat(mHwc.getFormat(disp));
    mConsumer->setDefaultBufferSize(mHwc.getWidth(disp),  mHwc.getHeight(disp));
    mConsumer->setDefaultMaxBufferCount(NUM_FRAMEBUFFER_SURFACE_BUFFERS);
}

专注分析setConsumerUsageBits,属于BufferQueueConsumer类型,因此代码实现如下:


status_t BufferQueueConsumer::setConsumerUsageBits(uint32_t usage) {
    ATRACE_CALL();
    BQ_LOGV("setConsumerUsageBits: %#x", usage);
    Mutex::Autolock lock(mCore->mMutex);
    mCore->mConsumerUsageBits = usage;
    return NO_ERROR;
}

这里将 参数usgae 传递给 mCore->mConsumerUsageBits。

1.2 DisplayDevice构造函数分析

DisplayDevice的构造函数实现如下:

DisplayDevice::DisplayDevice(
        const sp<SurfaceFlinger>& flinger,
        DisplayType type,
        //...各种初始化
{
    //创建surface对象
    mNativeWindow = new Surface(producer, false);
    ANativeWindow* const window = mNativeWindow.get();

    //创建OpenGLES 使用的surface对象
    EGLSurface surface;
    EGLint w, h;
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (config == EGL_NO_CONFIG) {
        config = RenderEngine::chooseEglConfig(display, format);
    }
    surface = eglCreateWindowSurface(display, config, window, NULL);
    eglQuerySurface(display, surface, EGL_WIDTH,  &mDisplayWidth);
    eglQuerySurface(display, surface, EGL_HEIGHT, &mDisplayHeight);

    if (mType >= DisplayDevice::DISPLAY_VIRTUAL)
        window->setSwapInterval(window, 0);//虚拟设备不支持 图像合成

    mConfig = config;
    mDisplay = display;
    mSurface = surface;
    mFormat  = format;
    mPageFlipCount = 0;
    mViewport.makeInvalid();
    mFrame.makeInvalid();

    //虚拟设备的屏幕 默认不关闭
    mPowerMode = (mType >= DisplayDevice::DISPLAY_VIRTUAL) ?
                  HWC_POWER_MODE_NORMAL : HWC_POWER_MODE_OFF;

    // Name the display.  The name will be replaced shortly if the display
    // was created with createDisplay().
    switch (mType) {
        case DISPLAY_PRIMARY:
            mDisplayName = "Built-in Screen";
            break;
        case DISPLAY_EXTERNAL:
            mDisplayName = "HDMI Screen";
            break;
        default:
            mDisplayName = "Virtual Screen";    // e.g. Overlay #n
            break;
    }

    // initialize the display orientation transform.
    setProjection(DisplayState::eOrientationDefault, mViewport, mFrame);
}

该构造函数主要是 创建了Surface,也就是一个NativeWindow。

1.2.1 surface构造函数分析

这里看下 surface的构造函数,代码如下:

Surface::Surface(
        const sp<IGraphicBufferProducer>& bufferProducer,
        bool controlledByApp)
    : mGraphicBufferProducer(bufferProducer)
{
    // Initialize the ANativeWindow function pointers.
    ANativeWindow::setSwapInterval  = hook_setSwapInterval;
    ANativeWindow::dequeueBuffer    = hook_dequeueBuffer;
    ANativeWindow::cancelBuffer     = hook_cancelBuffer;
    ANativeWindow::queueBuffer      = hook_queueBuffer;
    ANativeWindow::query            = hook_query;
    ANativeWindow::perform          = hook_perform;
    //...
}

这里对其中的一个hook_queueBuffer进行分析,代码如下:

int Surface::hook_queueBuffer(ANativeWindow* window,
        ANativeWindowBuffer* buffer, int fenceFd) {
    Surface* c = getSelf(window);
    return c->queueBuffer(buffer, fenceFd);
}

最后是调用回了Surface的queueBuffer方法,其他几个hook方法也是类似的。即NativeWindow 和Surface访问的方法是一样的。

同时通过上面的分析得出Buffer的流程:Surface->BufferQueue->FramebufferSurface->HWComposer->Gralloc->显示设备Framebuffer

1.2.2 eglCreateWindowSurface分析

eglCreateWindowSurface的代码实现如下:

EGLSurface eglCreateWindowSurface(  EGLDisplay dpy, EGLConfig config,
                                    NativeWindowType window,
                                    const EGLint *attrib_list)
{
    return createWindowSurface(dpy, config, window, attrib_list);
}

继续分析createWindowSurface,代码实现如下:

static EGLSurface createWindowSurface(EGLDisplay dpy, EGLConfig config,
        NativeWindowType window, const EGLint* /*attrib_list*/)
{
    if (egl_display_t::is_valid(dpy) == EGL_FALSE)
        return setError(EGL_BAD_DISPLAY, EGL_NO_SURFACE);
    if (window == 0)
        return setError(EGL_BAD_MATCH, EGL_NO_SURFACE);

    EGLint surfaceType;
    if (getConfigAttrib(dpy, config, EGL_SURFACE_TYPE, &surfaceType) == EGL_FALSE)
        return EGL_FALSE;

    if (!(surfaceType & EGL_WINDOW_BIT))
        return setError(EGL_BAD_MATCH, EGL_NO_SURFACE);

    if (static_cast<ANativeWindow*>(window)->common.magic !=
            ANDROID_NATIVE_WINDOW_MAGIC) {
        return setError(EGL_BAD_NATIVE_WINDOW, EGL_NO_SURFACE);
    }
        
    EGLint configID;
    if (getConfigAttrib(dpy, config, EGL_CONFIG_ID, &configID) == EGL_FALSE)
        return EGL_FALSE;

    int32_t depthFormat;
    int32_t pixelFormat;
    if (getConfigFormatInfo(configID, pixelFormat, depthFormat) != NO_ERROR) {
        return setError(EGL_BAD_MATCH, EGL_NO_SURFACE);
    }
	
    egl_surface_t* surface;
    //创建egl_window_surface_v2_t对象
    surface = new egl_window_surface_v2_t(dpy, config, depthFormat,
            static_cast<ANativeWindow*>(window));

    if (!surface->initCheck()) {
        delete surface;
        surface = 0;
    }
    return surface;
}

这里专注分析egl_window_surface_v2_t,构造函数代码实现如下:

egl_window_surface_v2_t::egl_window_surface_v2_t(EGLDisplay dpy,
        EGLConfig config,
        int32_t depthFormat,
        ANativeWindow* window)
    : egl_surface_t(dpy, config, depthFormat), 
    nativeWindow(window), buffer(0), previousBuffer(0), module(0),
    bits(NULL)
{
    hw_module_t const* pModule;
    hw_get_module(GRALLOC_HARDWARE_MODULE_ID, &pModule);
    module = reinterpret_cast<gralloc_module_t const*>(pModule);

    pixelFormatTable = gglGetPixelFormatTable();
    
    // keep a reference on the window
    nativeWindow->common.incRef(&nativeWindow->common);
    nativeWindow->query(nativeWindow, NATIVE_WINDOW_WIDTH, &width);
    nativeWindow->query(nativeWindow, NATIVE_WINDOW_HEIGHT, &height);
}

这里主要是一些初始化操作,从nativeWindow看出跟surface有关,从GRALLOC_HARDWARE_MODULE_ID来看也和 gralloc模块有关。会涉及 BufferQueue的一些操作。

2 DisplayDevice的两个关键方法

之所以分析这两个关键方法是因为 后面的章节 服务端分析4-handleMessageRefresh处理中 会有所涉及。

2.1 DisplayDevice的makeCurrent方法分析

makeCurrent代码实现如下:

EGLBoolean DisplayDevice::makeCurrent(EGLDisplay dpy, EGLContext ctx) const {
    EGLBoolean result = EGL_TRUE;
    EGLSurface sur = eglGetCurrentSurface(EGL_DRAW);
    if (sur != mSurface) {
        result = eglMakeCurrent(dpy, mSurface, mSurface, ctx);
        //...
    }
    setViewportAndProjection();
    return result;
}

这里关注eglMakeCurrent的实现,代码如下:

EGLBoolean eglMakeCurrent(  EGLDisplay dpy, EGLSurface draw,
                            EGLSurface read, EGLContext ctx)
{
    if (egl_display_t::is_valid(dpy) == EGL_FALSE)
        return setError(EGL_BAD_DISPLAY, EGL_FALSE);
    if (draw) {
        egl_surface_t* s = (egl_surface_t*)draw;// 1
        //...
    }
    //...
    ogles_context_t* gl = (ogles_context_t*)ctx;
    if (makeCurrent(gl) == 0) {
        if (ctx) {
            //...
            if (d) {
                if (d->connect() == EGL_FALSE) {
                    return EGL_FALSE;
                }
                d->ctx = ctx;
                d->bindDrawSurface(gl);
            }
            //...
        } else {
            //...
        }
        return EGL_TRUE;
    }
    return setError(EGL_BAD_ACCESS, EGL_FALSE);
}

这里主要关注一个connect方法,这个所谓的d类型是 egl_window_surface_v2_t,是egl_surface_t的子类,对应的connect代码实现如下:

EGLBoolean egl_window_surface_v2_t::connect() 
{
    // we're intending to do software rendering
    native_window_set_usage(nativeWindow, 
            GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN);

    // dequeue a buffer
    int fenceFd = -1;
    if (nativeWindow->dequeueBuffer(nativeWindow, &buffer,
            &fenceFd) != NO_ERROR) {
        return setError(EGL_BAD_ALLOC, EGL_FALSE);
    }

    // wait for the buffer
    sp<Fence> fence(new Fence(fenceFd));
    if (fence->wait(Fence::TIMEOUT_NEVER) != NO_ERROR) {
        nativeWindow->cancelBuffer(nativeWindow, buffer, fenceFd);
        return setError(EGL_BAD_ALLOC, EGL_FALSE);
    }

    // allocate a corresponding depth-buffer
    width = buffer->width;
    height = buffer->height;
    if (depth.format) {
        depth.width   = width;
        depth.height  = height;
        depth.stride  = depth.width; // use the width here
        depth.data    = (GGLubyte*)malloc(depth.stride*depth.height*2);
        if (depth.data == 0) {
            return setError(EGL_BAD_ALLOC, EGL_FALSE);
        }
    }

    // keep a reference on the buffer
    buffer->common.incRef(&buffer->common);

    // pin the buffer down
    if (lock(buffer, GRALLOC_USAGE_SW_READ_OFTEN | 
            GRALLOC_USAGE_SW_WRITE_OFTEN, &bits) != NO_ERROR) {
        ALOGE("connect() failed to lock buffer %p (%ux%u)",
                buffer, buffer->width, buffer->height);
        return setError(EGL_BAD_ACCESS, EGL_FALSE);
        // FIXME: we should make sure we're not accessing the buffer anymore
    }
    return EGL_TRUE;
}

这里会发现,调用了nativeWindow的dequeueBuffer操作,在之前的分析中我们知道这个dequeuBuffer实际上是Surface的dequeuBuffer方法(通过hook方式)。最后会进入到 BufferQueueproducer相关的操作。

2.2 DisplayDevice的swapbuffer方法分析

swapbuffer代码实现如下:

void DisplayDevice::swapBuffers(HWComposer& hwc) const {
    if (hwc.initCheck() != NO_ERROR ||
            (hwc.hasGlesComposition(mHwcDisplayId) &&
             (hwc.supportsFramebufferTarget() || mType >= DISPLAY_VIRTUAL))) {
        EGLBoolean success = eglSwapBuffers(mDisplay, mSurface);
        //...错误处理
    }

    status_t result = mDisplaySurface->advanceFrame();
	//...错误处理
}

这里专注分析eglSwapBuffers,代码实现如下:

EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface draw)
{
    if (egl_display_t::is_valid(dpy) == EGL_FALSE)
        return setError(EGL_BAD_DISPLAY, EGL_FALSE);

    egl_surface_t* d = static_cast<egl_surface_t*>(draw);
    if (!d->isValid())
        return setError(EGL_BAD_SURFACE, EGL_FALSE);
    if (d->dpy != dpy)
        return setError(EGL_BAD_DISPLAY, EGL_FALSE);

    // post the surface
    d->swapBuffers();

    // if it's bound to a context, update the buffer
    if (d->ctx != EGL_NO_CONTEXT) {
        d->bindDrawSurface((ogles_context_t*)d->ctx);
        egl_context_t* c = egl_context_t::context(d->ctx);
        if (c->read == draw) {
            d->bindReadSurface((ogles_context_t*)d->ctx);
        }
    }

    return EGL_TRUE;
}

这里专注分析d->swapBuffers(),而这个d类型是 egl_window_surface_v2_t,是egl_surface_t的子类,对应的swapBuffers代码实现如下:

EGLBoolean egl_window_surface_v2_t::swapBuffers()
{
    if (!buffer) {
        return setError(EGL_BAD_ACCESS, EGL_FALSE);
    }

    if (!dirtyRegion.isEmpty()) {
        dirtyRegion.andSelf(Rect(buffer->width, buffer->height));
        if (previousBuffer) {
            // This was const Region copyBack, but that causes an
            // internal compile error on simulator builds
            /*const*/ Region copyBack(Region::subtract(oldDirtyRegion, dirtyRegion));
            if (!copyBack.isEmpty()) {
                void* prevBits;
                if (lock(previousBuffer, 
                        GRALLOC_USAGE_SW_READ_OFTEN, &prevBits) == NO_ERROR) {
                    // copy from previousBuffer to buffer
                    copyBlt(buffer, bits, previousBuffer, prevBits, copyBack);
                    unlock(previousBuffer);
                }
            }
        }
        oldDirtyRegion = dirtyRegion;
    }

    if (previousBuffer) {
        previousBuffer->common.decRef(&previousBuffer->common); 
        previousBuffer = 0;
    }
    
    unlock(buffer);
    previousBuffer = buffer;
    nativeWindow->queueBuffer(nativeWindow, buffer, -1);
    buffer = 0;

    // dequeue a new buffer
    int fenceFd = -1;
    if (nativeWindow->dequeueBuffer(nativeWindow, &buffer, &fenceFd) == NO_ERROR) {
        sp<Fence> fence(new Fence(fenceFd));
        if (fence->wait(Fence::TIMEOUT_NEVER)) {
            nativeWindow->cancelBuffer(nativeWindow, buffer, fenceFd);
            return setError(EGL_BAD_ALLOC, EGL_FALSE);
        }

        // reallocate the depth-buffer if needed
        if ((width != buffer->width) || (height != buffer->height)) {
            // TODO: we probably should reset the swap rect here
            // if the window size has changed
            width = buffer->width;
            height = buffer->height;
            if (depth.data) {
                free(depth.data);
                depth.width   = width;
                depth.height  = height;
                depth.stride  = buffer->stride;
                depth.data    = (GGLubyte*)malloc(depth.stride*depth.height*2);
                if (depth.data == 0) {
                    setError(EGL_BAD_ALLOC, EGL_FALSE);
                    return EGL_FALSE;
                }
            }
        }

        // keep a reference on the buffer
        buffer->common.incRef(&buffer->common);

        // finally pin the buffer down
        if (lock(buffer, GRALLOC_USAGE_SW_READ_OFTEN |
                GRALLOC_USAGE_SW_WRITE_OFTEN, &bits) != NO_ERROR) {
            ALOGE("eglSwapBuffers() failed to lock buffer %p (%ux%u)",
                    buffer, buffer->width, buffer->height);
            return setError(EGL_BAD_ACCESS, EGL_FALSE);
            // FIXME: we should make sure we're not accessing the buffer anymore
        }
    } else {
        return setError(EGL_BAD_CURRENT_SURFACE, EGL_FALSE);
    }

    return EGL_TRUE;
}

接下来主要对旧Buffer 和新Buffer 进行分析:

2.2.1 旧Buffer操作

这里对于旧Buffer,调用了nativeWindow的queueBuffer操作,在之前的分析中我们知道这个queuBuffer实际上是Surface的queueBuffer方法(通过hook方式),最后会进入到 BufferQueueProducer相关的操作。在执行了queueBuffer操作后,通过BufferQueue的接口最后会调用一个 onFrameAvailable的回调函数,代码如下:

void FramebufferSurface::onFrameAvailable(const BufferItem& /* item */) {
    sp<GraphicBuffer> buf;
    sp<Fence> acquireFence;
    status_t err = nextBuffer(buf, acquireFence);
    //...错误处理
    err = mHwc.fbPost(mDisplayType, acquireFence, buf);
    //...错误处理
}

这里关注 nextBuffer 和后面fbPost方法。

@1 nextBuffer代码实现如下:

status_t FramebufferSurface::nextBuffer(sp<GraphicBuffer>& outBuffer, sp<Fence>& outFence) {
    Mutex::Autolock lock(mMutex);

    BufferQueue::BufferItem item;
    status_t err = acquireBufferLocked(&item, 0);
    if (err == BufferQueue::NO_BUFFER_AVAILABLE) {
        outBuffer = mCurrentBuffer;
        return NO_ERROR;
    } 
	//...
    if (mCurrentBufferSlot != BufferQueue::INVALID_BUFFER_SLOT &&
        item.mBuf != mCurrentBufferSlot) {
        // Release the previous buffer.
        err = releaseBufferLocked(mCurrentBufferSlot, mCurrentBuffer,
                EGL_NO_DISPLAY, EGL_NO_SYNC_KHR);
        if (err < NO_ERROR) {
            ALOGE("error releasing buffer: %s (%d)", strerror(-err), err);
            return err;
        }
    }
    mCurrentBufferSlot = item.mBuf;
    mCurrentBuffer = mSlots[mCurrentBufferSlot].mGraphicBuffer;
    outFence = item.mFence;
    outBuffer = mCurrentBuffer;
    return NO_ERROR;
}

这里最主要的是 执行了Acquire操作,取出Buffer

@2 fbPost代码实现如下:

int HWComposer::fbPost(int32_t id,
        const sp<Fence>& acquireFence, const sp<GraphicBuffer>& buffer) {
    if (mHwc && hwcHasApiVersion(mHwc, HWC_DEVICE_API_VERSION_1_1)) {
        return setFramebufferTarget(id, acquireFence, buffer);
    } else {
        acquireFence->waitForever("HWComposer::fbPost");
        return mFbDev->post(mFbDev, buffer->handle);
    }
}

这里调用了mFbDev->post,和这个设备就是FrameBuffer的HAL层接口post方法(注册时被赋值为fb_post),因此最后会调用到FrameBuffer HAL层的方法fb_post,代码如下:


static int fb_post(struct framebuffer_device_t* dev, buffer_handle_t buffer)
{
    if (private_handle_t::validate(buffer) < 0)
        return -EINVAL;

    fb_context_t* ctx = (fb_context_t*)dev;

    private_handle_t const* hnd = reinterpret_cast<private_handle_t const*>(buffer);
    private_module_t* m = reinterpret_cast<private_module_t*>(
            dev->common.module);

    if (hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER) {
        const size_t offset = hnd->base - m->framebuffer->base;
        m->info.activate = FB_ACTIVATE_VBL;
        m->info.yoffset = offset / m->finfo.line_length;
        if (ioctl(m->framebuffer->fd, FBIOPUT_VSCREENINFO, &m->info) == -1) {
            ALOGE("FBIOPUT_VSCREENINFO failed");
            m->base.unlock(&m->base, buffer); 
            return -errno;
        }
        m->currentBuffer = buffer;
        
    } else {
        // If we can't do the page_flip, just copy the buffer to the front 
        // FIXME: use copybit HAL instead of memcpy
        
        void* fb_vaddr;
        void* buffer_vaddr;
        
        m->base.lock(&m->base, m->framebuffer, 
                GRALLOC_USAGE_SW_WRITE_RARELY, 
                0, 0, m->info.xres, m->info.yres,
                &fb_vaddr);

        m->base.lock(&m->base, buffer, 
                GRALLOC_USAGE_SW_READ_RARELY, 
                0, 0, m->info.xres, m->info.yres,
                &buffer_vaddr);

        memcpy(fb_vaddr, buffer_vaddr, m->finfo.line_length * m->info.yres);
        
        m->base.unlock(&m->base, buffer); 
        m->base.unlock(&m->base, m->framebuffer); 
    }
    
    return 0;
}

这里最主要的是 消费Buffer,执行Post操作将Buffer推给Framebuffer来显示了。

2.2.2 新Buffer操作

对于新Buffer,又重现调用了dequeueBuffer的操作,进入下一次的 处理。

2.3 两个关键方法的总结

  1. 这里makeCurrent 是对应dequeueBuffer操作。
  2. SwapBuffer对应旧Buffer的queueBuffer操作,新Buffer的dequeueBuffer操作。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

图王大胜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值