Android图形显示系统5 图像缓冲区(上)

一 概述

前面两篇文章介绍了图像生产者和图像消费者,终于到了最后一篇——图像缓冲区,这三者一起构成了一个完整的 Android 图像显示系统。Android 中的图像生产者 OpenGL,Skia,Vulkan 将绘制的数据存放在图像缓冲区中,Android 中的图像消费 SurfaceFlinger 从图像缓冲区将数据取出,进行加工及合成。那么图像缓冲区是什么呢?它是如何创建出来的呢?又要如何使用它呢?它的存储原理是什么呢?读完这篇文章,你就能回答这些问题了。

1.1 图像缓冲区

在讲解图像的生产者时,多次提到了 Surface,我们知道 Surface 可以存储用来绘制的图形数据。在硬件加速中,需要调用 ThreadedRenderer.initialize(mSurface) 函数将 Surface 缓冲区传递到 OpenGL 或者 Vulkan 的渲染管线;在软件绘制中调用 drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty) 函数将 Surface 缓冲区传递到 native 层的 SkiaCanvas。当OpenGL,Vulkan 或 Skia 拥有 Surface 后,就可以获取到图形缓冲区,将需要绘制的内容存储在图形缓冲区内了。

同时,在讲解图像的消费者时,也多次提到了 Layer,我们知道通过 Layer 可以取出缓冲区的图形数据。在 SurfaceFlinger 需要消费图像数据,进行图层混合时,需要遍历 Layer,取出 Layer 中缓冲区的图形数据后,再加过或合成处理。

我们也知道一个 Layer 唯一对应了一个 Surface,他们的关系如下。

在这里插入图片描述
所以,Surface 和 Layer 都属于图形缓冲区的组成部分。那么 Android 图形缓冲区由哪些部分组成呢?Surface 和 Layer 又是什么呢?他们加粗样式是怎么关联起来的呢?他们又分别是如何创建、获取以及使用缓冲区的呢?这篇文章主要的内容都是围绕这四个问题来讲解,在深入讲解这四个问题之前,这里我们先高屋建瓴的了解这个四个问题的答案,这样在接下面的篇幅中,才不会迷失在冗长的代码中。


1.Android 图形缓冲区由哪些部分组成

  • Android 的图形缓冲区由 Surface,BufferQueue,Layer,GraphicBuffer 四部分组成。BufferQueue 中的 slots 数组最多能存储 64 个 GraphicBuffer,而 GraphicBuffer 就是真正被分配内存,并能存储图形数据的缓冲区

2.Surface 和 Layer 分别是什么

  • Surface 是提供给图形生产者控制缓冲区的类,Surface 持有 GraphicBufferProducer,简称 gbp,gbp 专门用来创建或获取可用的 GraphicBuffer 以及提交绘制后的 GraphicBuffer
  • Layer 是提供给图像消费者获取缓冲区的类,Layer 持有 GraphicBufferConsumer,简称 gbc,通过 gbc 用来获取和释放 GraphicBuffer

3.他们是怎么关联起来的

  • Surface 和 Layer 通过 BufferQueue 关联起来,Surface 持有 BufferQueue 中的 gbp,Layer 持有 BufferQueue 中的 gbc,gbp 和 gbc 的 GraphicBuffer 都存储在 GraphicBufferCore 的 slots 数组中

4.如何创建、获取以及使用缓冲区

  • Surface 通过调用 gbp 的 dequeue 函数获取 GraphicBuffer,调用 queue 函数提交使用完毕的 GraphicBuffer
  • Layer 通过调用 gbc 的 acquire 函数获取有数据的 GraphicBuffer,调用 release 释放 GraphicBuffer

总结:当我们想要绘制图像时,需要创建 Surface 和 Layer,图像生产者如 Skia,OpenGL 通过 Surface 调用 dequeue 函数获取一块缓冲区 GraphicBuffer,有了这块缓冲区,就可以在缓冲区上绘制图像了,当绘制完毕后,通过 Surface 调用 queue 函数,将 GraphicBuffer 归还到 BufferQueue。之后,Vsync 通知图像消费者 SurfaceFlinger 调用 Layer 的 acquire 函数,获取绘制好内容的 GraphicBuffer 进行合成与处理,处理完毕后通过 release 函数释放这块缓冲区。

了解了大概的背景和流程,我们接着开始对图像缓冲区的深入学习。

二 缓冲区的创建

我们已经知道缓冲区由 Surface,Layer,BufferQueue 和 GraphicBuffer 组成。这一节主要讲这四个部分是如何创建的,先从 Surface 和 Layer 的创建开始。

2.1 Surface和Layer的创建

为了更容易的理解如何创建 Surface 和 Layer,这里先从开机动画这个案例讲起。

在前面图像生产者中讲如何通过 OpenGL ES 播放开启动画时,已经提到了创建 Surface 的流程,它的流程很简单,这里再回顾一下 Android 开机动画的启动流程。开机动画的对象为 BootAnimation,它的构造函数如下。

/frameworks/base/cmds/bootanimation/BootAnimation.cpp

BootAnimation::BootAnimation() : Thread(false), mClockEnabled(true), mTimeIsAccurate(false),
        mTimeFormat12Hour(false), mTimeCheckThread(NULL) {
    //创建SurfaceComposerClient
    mSession = new SurfaceComposerClient();
    ......
}

可以看到构造函数里面创建了 SurfaceComposerClient。我们接着看 BootAnimation 的初始化函数—— readyToRun 函数

status_t BootAnimation::readyToRun() {
    mAssets.addDefaultAssets();

    sp<IBinder> dtoken(SurfaceComposerClient::getBuiltInDisplay(
            ISurfaceComposer::eDisplayIdMain));
    DisplayInfo dinfo;
    //获取屏幕信息
    status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &dinfo);
    if (status)
        return -1;

    // 通知SurfaceFlinger创建Surface,创建成功会返回一个SurfaceControl代理
    sp<SurfaceControl> control = session()->createSurface(String8("BootAnimation"),
            dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565);

    SurfaceComposerClient::openGlobalTransaction();
    //设置这个layer在SurfaceFlinger中的层级顺序
    control->setLayer(0x40000000);

    //获取surface
    sp<Surface> s = control->getSurface();

    // 以下是EGL的初始化流程
    const EGLint attribs[] = {
            EGL_RED_SIZE,   8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE,  8,
            EGL_DEPTH_SIZE, 0,
            EGL_NONE
    };
    EGLint w, h;
    EGLint numConfigs;
    EGLConfig config;
    EGLSurface surface;
    EGLContext context;

    //步骤1:获取Display
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    //步骤2:初始化EGL
    eglInitialize(display, 0, 0);
    //步骤3:选择参数
    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
    //步骤4:传入SurfaceFlinger生成的surface,并以此构造EGLSurface
    surface = eglCreateWindowSurface(display, config, s.get(), NULL);
    //步骤5:构造egl上下文
    context = eglCreateContext(display, config, NULL, NULL);
    //步骤6:绑定EGL上下文
    if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)
        return NO_INIT;
    ......
}

通过 BootAnimation 的构造函数和 readyToRun 函数可以看到,创建 Surface 的步骤如下:

  • 创建 SurfaceComponentClient
  • 通过 SurfaceComponentClient 的 createSurface 函数创建 SurfaceControl,SurfaceControl 是 Surface 的控制类
  • 有了 SurfaceControl 之后,我们就可以通过 getSurface 获取到 Surface

只需要三步,Surface 的创建就完成了,非常的简单。但是不我们不能止步于如何创建 Surface,我们还需要了解 SurfaceComponentClient 是什么?createSurface 经历了哪些流程?getSurface 又经历了哪些流程?

2.1.1 SurfaceComponentClient

我们先看看 SurfaceComponentClien t对象,它的构造函数如下:

/frameworks/native/libs/gui/SurfaceComposerClient.cpp

ComposerService::ComposerService()
: Singleton<ComposerService>() {
    connectLocked();
}

void ComposerService::connectLocked() {
    const String16 name("SurfaceFlinger");
    //获取SurfaceFlinger
    while (getService(name, &mComposerService) != NO_ERROR) {
        usleep(250000);
    }    
    //注册binder death的通知
    ......        
}

从 SurfaceComposerClient 可以看到,它在 connectLocked 函数中获取了 SurfaceFlinger 的客户端 Binder 代理 mComposerService。接着看它的初始化函数 onFirstRef

void SurfaceComposerClient::onFirstRef() {
    sp<ISurfaceComposer> sm(ComposerService::getComposerService());
    if (sm != 0) {
        auto rootProducer = mParent.promote();
        sp<ISurfaceComposerClient> conn;
        conn = (rootProducer != nullptr) ? sm->createScopedConnection(rootProducer) :
                sm->createConnection();
        if (conn != 0) {
            mClient = conn;
            mStatus = NO_ERROR;
        }
    }
}

onFirstRef 函数中通过 mComposerService 调用 createConnection 方法。mComposerService 就是 SurfaceFlinger 的 binder 代理,所以这里会最终调用 SurfaceFlinger 的 createConnection 函数。

接着看 SurfaceFlinger 的 createConnection 函数

/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

sp<ISurfaceComposerClient> SurfaceFlinger::createConnection() {
    return initClient(new Client(this));
}

static sp<ISurfaceComposerClient> initClient(const sp<Client>& client) {
    status_t err = client->initCheck();
    if (err == NO_ERROR) {
        return client;
    }
    return nullptr;
}

可以看到 createConnection 方法创建了 Client,这个 Client 封装了对 Layer 和 Surface 的操作,我们看一下 Client 的头文件

/frameworks/native/services/surfaceflinger/Client.h

class Client : public BnSurfaceComposerClient
{
public:
    explicit Client(const sp<SurfaceFlinger>& flinger);
    Client(const sp<SurfaceFlinger>& flinger, const sp<Layer>& parentLayer);
    ~Client();

    status_t initCheck() const;

    // protected by SurfaceFlinger::mStateLock
    void attachLayer(const sp<IBinder>& handle, const sp<Layer>& layer);

    void detachLayer(const Layer* layer);

    sp<Layer> getLayerUser(const sp<IBinder>& handle) const;

    void setParentLayer(const sp<Layer>& parentLayer);

private:
    // ISurfaceComposerClient interface
    virtual status_t createSurface(
            const String8& name,
            uint32_t w, uint32_t h,PixelFormat format, uint32_t flags,
            const sp<IBinder>& parent, uint32_t windowType, uint32_t ownerUid,
            sp<IBinder>* handle,
            sp<IGraphicBufferProducer>* gbp);

    virtual status_t destroySurface(const sp<IBinder>& handle);

    virtual status_t clearLayerFrameStats(const sp<IBinder>& handle) const;

    virtual status_t getLayerFrameStats(const sp<IBinder>& handle, FrameStats* outStats) const;

    virtual status_t onTransact(
        uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags);

    sp<Layer> getParentLayer(bool* outParentDied = nullptr) const;

    // constant
    sp<SurfaceFlinger> mFlinger;

    // protected by mLock
    DefaultKeyedVector< wp<IBinder>, wp<Layer> > mLayers;
    wp<Layer> mParentLayer;

    // thread-safe
    mutable Mutex mLock;
};
}

可以看到,Client 继承自 BnSurfaceComposerClient,并提供了创建和销毁 Layer 和 Surface 的操作函数。

2.1.2 createSurface

创建好了 SurfaceComponentClinet,并有了一个在 SurfaceFlinger 对应的 Clinet。我们可以接着看第二步:createSurface 函数。

/frameworks/native/libs/gui/SurfaceComposerClient.cpp

sp<SurfaceControl> SurfaceComposerClient::createSurface(
        const String8& name,
        uint32_t w,
        uint32_t h,
        PixelFormat format,
        uint32_t flags,
        SurfaceControl* parent,
        uint32_t windowType,
        uint32_t ownerUid)
{
    sp<SurfaceControl> sur;
    if (mStatus == NO_ERROR) {
        sp<IBinder> handle;
        sp<IBinder> parentHandle;
        sp<IGraphicBufferProducer> gbp;

        if (parent != nullptr) {
            parentHandle = parent->getHandle();
        }
        status_t err = mClient->createSurface(name, w, h, format, flags, parentHandle,
                windowType, ownerUid, &handle, &gbp);
        if (err == NO_ERROR) {
            sur = new SurfaceControl(this, handle, gbp);
        }
    }
    return sur;
}

createSurface 函数主要做了两件事情

  • 调用 Client 的 createSurface 方法
  • 创建 SurfaceControl

先看第一件事情:调用 Client 的 createSurface 函数

status_t Client::createSurface(
        const String8& name,
        uint32_t w, uint32_t h, PixelFormat format, uint32_t flags,
        const sp<IBinder>& parentHandle, uint32_t windowType, uint32_t ownerUid,
        sp<IBinder>* handle,
        sp<IGraphicBufferProducer>* gbp)
{
    ......    
    class MessageCreateLayer : public MessageBase {
        SurfaceFlinger* flinger;
        Client* client;
        sp<IBinder>* handle;
        sp<IGraphicBufferProducer>* gbp;
        status_t result;
        const String8& name;
        uint32_t w, h;
        PixelFormat format;
        uint32_t flags;
        sp<Layer>* parent;
        uint32_t windowType;
        uint32_t ownerUid;
    public:
        MessageCreateLayer(SurfaceFlinger* flinger,
                const String8& name, Client* client,
                uint32_t w, uint32_t h, PixelFormat format, uint32_t flags,
                sp<IBinder>* handle, uint32_t windowType, uint32_t ownerUid,
                sp<IGraphicBufferProducer>* gbp,
                sp<Layer>* parent)
            : flinger(flinger), client(client),
              handle(handle), gbp(gbp), result(NO_ERROR),
              name(name), w(w), h(h), format(format), flags(flags),
              parent(parent), windowType(windowType), ownerUid(ownerUid) {
        }
        status_t getResult() const { return result; }
        virtual bool handler() {
            result = flinger->createLayer(name, client, w, h, format, flags,
                    windowType, ownerUid, handle, gbp, parent);
            return true;
        }
    };

    sp<MessageBase> msg = new MessageCreateLayer(mFlinger.get(),
            name, this, w, h, format, flags, handle,
            windowType, ownerUid, gbp, &parent);
    mFlinger->postMessageSync(msg);
    return static_cast<MessageCreateLayer*>( msg.get() )->getResult();
}

可以看到,createSurface 实际是通过 SurfaceFlinger 的消息队列调用 createLayer 函数,接着看 SurfaceFlinger 中 createLayer 函数的实现。

/frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

status_t SurfaceFlinger::createLayer(
        const String8& name,
        const sp<Client>& client,
        uint32_t w, uint32_t h, PixelFormat format, uint32_t flags,
        uint32_t windowType, uint32_t ownerUid, sp<IBinder>* handle,
        sp<IGraphicBufferProducer>* gbp, sp<Layer>* parent)
{
    status_t result = NO_ERROR;
    sp<Layer> layer;
    String8 uniqueName = getUniqueLayerName(name);

    switch (flags & ISurfaceComposerClient::eFXSurfaceMask) {
        case ISurfaceComposerClient::eFXSurfaceNormal:
            //创建普通的Layer
            result = createNormalLayer(client,
                    uniqueName, w, h, flags, format,
                    handle, gbp, &layer);
            break;
        case ISurfaceComposerClient::eFXSurfaceDim:
            //创建有遮罩效果的Layer
            result = createDimLayer(client,
                    uniqueName, w, h, flags,
                    handle, gbp, &layer);
            break;
        default:
            result = BAD_VALUE;
            break;
    }

    if (result != NO_ERROR) {
        return result;
    }

    layer->setInfo(windowType, ownerUid);

    result = addClientLayer(client, *handle, *gbp, layer, *parent);
    if (result != NO_ERROR) {
        return result;
    }
    mInterceptor.saveSurfaceCreation(layer);

    setTransactionFlags(eTransactionNeeded);
    return result;
}

status_t SurfaceFlinger::createNormalLayer(const sp<Client>& client,
        const String8& name, uint32_t w, uint32_t h, uint32_t flags, PixelFormat& format,
        sp<IBinder>* handle, sp<IGraphicBufferProducer>* gbp, sp<Layer>* outLayer)
{
    // initialize the surfaces
    switch (format) {
    case PIXEL_FORMAT_TRANSPARENT:
    case PIXEL_FORMAT_TRANSLUCENT:
        format = PIXEL_FORMAT_RGBA_8888;
        break;
    case PIXEL_FORMAT_OPAQUE:
        format = PIXEL_FORMAT_RGBX_8888;
        break;
    }

    *outLayer = new Layer(this, client, name, w, h, flags);
    status_t err = (*outLayer)->setBuffers(w, h, format, flags);
    if (err == NO_ERROR) {
        *handle = (*outLayer)->getHandle();
        *gbp = (*outLayer)->getProducer();
    }

    ALOGE_IF(err, "createNormalLayer() failed (%s)", strerror(-err));
    return err;
}

到这里我们可以发现,原来 createSurface 函数实际上并不是创建 Surface,而是创建了 Layer。接着看 Layer 的初始化函数 onFirstRef

/frameworks/native/services/surfaceflinger/Layer.cpp

void Layer::onFirstRef() {    
    sp<IGraphicBufferProducer> producer;
    sp<IGraphicBufferConsumer> consumer;
    //创建BufferQueue
    BufferQueue::createBufferQueue(&producer, &consumer, true);
    mProducer = new MonitoredProducer(producer, mFlinger, this);
    mSurfaceFlingerConsumer = new SurfaceFlingerConsumer(consumer, mTextureName, this);
    mSurfaceFlingerConsumer->setConsumerUsageBits(getEffectiveUsage(0));
    mSurfaceFlingerConsumer->setContentsChangedListener(this);
    mSurfaceFlingerConsumer->setName(mName);

    if (mFlinger->isLayerTripleBufferingDisabled()) {
        mProducer->setMaxDequeuedBufferCount(2);
    }

    const sp<const DisplayDevice> hw(mFlinger->getDefaultDisplayDevice());
    updateTransformHint(hw);
}

可以看到,Layer 的初始化函数里通过 BufferQueue::createBufferQueue 创建了 BufferQueue,而 BufferQueue 又会创建 GraphicBufferProducer 和 GraphicBufferConsumer,关于 BufferQueue 后面详讲。

在看第二件事情:创建 SurfaceControl

/frameworks/native/libs/gui/SurfaceControl.cpp

SurfaceControl::SurfaceControl(
        const sp<SurfaceComposerClient>& client,
        const sp<IBinder>& handle,
        const sp<IGraphicBufferProducer>& gbp)
    : mClient(client), mHandle(handle), mGraphicBufferProducer(gbp)
{
}

SurfaceControl 的构造函数没有任何操作,仅仅只是传入 Client,以及在 BufferQueue 中创建的 GraphicBufferProducer。

2.1.3 getSurface

Layer 和 SurfaceControl 都创建好了,就差最后一步了:调用 getSurface 函数获取 Surface。

/frameworks/native/libs/gui/SurfaceControl.cpp

sp<Surface> SurfaceControl::getSurface() const
{
    Mutex::Autolock _l(mLock);
    if (mSurfaceData == 0) {
        return generateSurfaceLocked();
    }
    return mSurfaceData;
}

sp<Surface> SurfaceControl::generateSurfaceLocked() const
{
    
    mSurfaceData = new Surface(mGraphicBufferProducer, false);

    return mSurfaceData;
}

可以看到,这里会创建一个 Surface。为了对 Surface 有一个了解,我们看一下 Surface 的头文件

/frameworks/native/include/gui/Surface.h

class Surface
    : public ANativeObjectBase<ANativeWindow, Surface, RefBase>
{
public:
    explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer,
            bool controlledByApp = false);
   ......
protected:
    virtual int dequeueBuffer(ANativeWindowBuffer** buffer, int* fenceFd);
    virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd);
   ......
protected:
    uint32_t mReqWidth;
    uint32_t mReqHeight;
    PixelFormat mReqFormat;
    uint32_t mDefaultWidth;
    uint32_t mDefaultHeight;
    ......
}

从头文件可以看到,Surface 继承自 ANativeWindow,并且封装了许多对 Buffer 进行操作的方法,以及维护了 Surface 的大小即属性等参数数据。到这里,Surface 也创建完成了,Surface 的构造函数的入参就是 GraphicBufferProducer。有了 Surface,就可以交给 OpenGL 进行开机动画的绘制了,绘制的过程在图像生产者中已经讲了,这里就不再说了。

通过开机动画这个案例,我们可以发现,通过简单三步,就能创建好 Surface 和 Layer 了。下面接着来看看位于 Java 层的 Activity 是如何创建 Surface 的,由于 Activity 的的界面显示流程涉及到很多 JNI 的通信以及对界面的测量布局等流程,所以它创建 Surface 和 Layer 并没有开机动画这么直观易懂。

2.2 Activity创建Surface和Layer

Activity 创建 Surface 和 Layer 的流程会和界面绘制、AMS 对界面 Z 轴排序等流程耦合在一块,所以在讲 Surface 和 Layer 的创建过程中,会将这些流程一起讲,Activity 的界面我都以 Window,即窗口来表示。

先看看 Java 层的 Surface,它位于 ViewRootImpl 中,并且声明成员变量就创建好了。

/frameworks/base/core/java/android/view/ViewRootImpl.java

public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks,
      ThreadedRenderer.DrawCallbacks {
    //……
    final Surface mSurface = new Surface();
    //……  
}

这里的 Surface 只是 Java 层的 Surface,它需要绑定 Native 的 Surface,我们一般在构造函数中,可以看到 jni 方法创建 native 层对应的对象,比如图像生产者中提到的 DisplayListCanvas 就会在构造函数中创建 native 层的 RecordingCanvas,所以我们先看看 Surface 的构造函数

/frameworks/base/core/java/android/view/Surface.java

public Surface() {
}

这里发现他是一个空实现,找不到创建创建 native 层 Surface 的 JNI 方法调用。那么 native 层的 Surface 是如何创建的呢?又是如何和 java 层的 Surface 绑定起来的呢?带着这个疑问,接着往下看。

这需要从 Activity 界面的绘制流程开始讲起,当我们在 Activity 的 OnCreate 函数中通过 setContentView 设置界面后,最终会执行到ViewRootImpl 的 setView 方法,我们从这儿开始看起。

/frameworks/base/core/java/android/view/ViewRootImpl.java

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            ......
            // 开启硬件加速
            if (mSurfaceHolder == null) {
                enableHardwareAcceleration(attrs);
            }            
            //1,测量,布局和绘制流程
            requestLayout();
            if ((mWindowAttributes.inputFeatures
                 & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                //触摸事件回调信道
                mInputChannel = new InputChannel();
            }
            mForceDecorViewVisibility = (mWindowAttributes.privateFlags
                                         & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
         
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                //2,调用wms的addToDisplay函数
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                     getHostVisibility(), mDisplay.getDisplayId(),
                     mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                     mAttachInfo.mOutsets, mInputChannel);            

            //添加窗口错误的处理
            ......            
            //触摸事件监听初始化处理
            ......
        }
    }
}

setView 函数中关键流程主要是这两件事情:

  • 执行 requestLayout 函数,这个流程里会创建 Surface 和 Layer
  • 调用 wms 的 addTodisplay(),这个流程里会创建 SurfaceComponentClient

先看 requestLayout 函数

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

requestLayout 函数通过 mChoreographer 添加了回调对象 mTraversalRunnable,mChoreographer 会在收到 VSync 执行 mTraversalRunnable,这里就不介绍 mChoreographer 了。mTraversalRunnable 里做的事情主要是测量,布局和绘制流程,但是现在就进行这些工作还太早了,因为我们的 SurfaceComponentClient 还没创建好,所以 mTraversalRunnable 的回调会在 addTodisplay 后执行。

2.2.1 SurfaceComponentClinet

我们先看 mWindowSession.addToDisplay() 的流程,他会在 mTraversalRunnable 回调之前执行。在这里我们先了解一下 mWindowSession,凡是带 Session 的类都是用来通信的,WindowSession 就是 Java 层的窗口和 WMS 通信的会话对象,它在 ViewRootImp 的l构造函数中通过 WindowManagerGlobal.getWindowSession() 获取。

/frameworks/base/core/java/android/view/ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {
    ......
    mWindowSession = WindowManagerGlobal.getWindowSession();
    ......
}

public final class WindowManagerGlobal {
    ......        
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                    InputMethodManager imm = InputMethodManager.getInstance();
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                        new IWindowSessionCallback.Stub() {
                            @Override
                            public void onAnimatorScaleChanged(float scale) {
                                ValueAnimator.setDurationScale(scale);
                            }
                        },
                        imm.getClient(), imm.getInputContext());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }    
    ......
}

可以看到 getWindowSession 函数中获取了 WindowManagerService 的 Binder 代理,然后调用 WindowManagerService 的 openSession 方法来创建 Session。

public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
                                  IInputContext inputContext) {
    if (client == null) throw new IllegalArgumentException("null client");
    if (inputContext == null) throw new IllegalArgumentException("null inputContext");
    Session session = new Session(this, callback, client, inputContext);
    return session;
}

为什么不通过 WMS 的 Proxy 直接进行通信呢,而要创建 Session 来进行通信呢?我的理解是 WMS 要和多个窗口通信,如果直接通过 WMS 对话会不方便维护对应窗口的上下文,这个 Session 作用是表示当前这个窗口和 WMS 的对话。继续回到 mWindowSession.addToDisplay 函数上来。

/frameworks/base/services/core/java/com/android/server/wm/Session.java

public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
                        int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
                        Rect outOutsets, InputChannel outInputChannel) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                              outContentInsets, outStableInsets, outOutsets, outInputChannel);
}

addToDisplay 方法只是调用了 WMS 的 addWindow 方法

public int addWindow(Session session, IWindow client, int seq,
                     WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
                     Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
                     InputChannel outInputChannel) {
    int[] appOp = new int[1];
    //权限检查
    int res = mPolicy.checkAddPermission(attrs, appOp);
    if (res != WindowManagerGlobal.ADD_OKAY) {
        return res;
    }

    boolean reportNewConfig = false;
    WindowState parentWindow = null;
    long origId;
    final int callingUid = Binder.getCallingUid();
    final int type = attrs.type;

    synchronized(mWindowMap) {
        if (!mDisplayReady) {
            throw new IllegalStateException("Display has not been initialialized");
        }

        //创建或获取DisplayContent
        final DisplayContent displayContent = mRoot.getDisplayContentOrCreate(displayId);
        if (displayContent == null) {
            Slog.w(TAG_WM, "Attempted to add window to a display that does not exist: "
                   + displayId + ".  Aborting.");
            return WindowManagerGlobal.ADD_INVALID_DISPLAY;
        }
        
        //窗口类型校验
        …… 

        //获取WindowToken
        AppWindowToken atoken = null;
        final boolean hasParent = parentWindow != null;
        WindowToken token = displayContent.getWindowToken(
            hasParent ? parentWindow.mAttrs.token : attrs.token);
       
        final int rootType = hasParent ? parentWindow.mAttrs.type : type;

        boolean addToastWindowRequiresToken = false;

        if (token == null) {
            
            // token为空的情况下,根据窗口类型判断是否返回错误,有些窗口,如系统窗口运行token为空,但是子窗口等窗口不允许
            ……
            
            //为允许token为空的窗口创建token
            final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
            token = new WindowToken(this, binder, type, false, displayContent,
                                    session.mCanAddInternalSystemWindow);
        } else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
            // APPLICATION类型的窗口检查,对应的WindowToken的类型也为APPLICATION
            ……
        } else if (rootType == TYPE_INPUT_METHOD) {
            // INPUT_METHOD类型的窗口检查
            ……
        } else if (rootType == TYPE_VOICE_INTERACTION) {
            // VOICE类型的窗口检查
            ……
        } else if (rootType == TYPE_WALLPAPER) {
            // WALLPAPER类型的窗口检查
            ……
        } else if (rootType == TYPE_DREAM) {
            // TYPE_DREAM类型的窗口检查
            ……
        } else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
            ……
        } else if (type == TYPE_TOAST) {
            ……
        } else if (type == TYPE_QS_DIALOG) {
            ……
        } else if (token.asAppWindowToken() != null) {
            …… 
        }

        //为这个窗口创建WindowState
        final WindowState win = new WindowState(this, session, client, token, parentWindow,
                                                appOp[0], seq, attrs, viewVisibility, session.mUid,
                                                session.mCanAddInternalSystemWindow);       
        ......
        
        //执行attach
        win.attach();
        //将window存入mWindowMap
        mWindowMap.put(client.asBinder(), win);  

        final AppWindowToken aToken = token.asAppWindowToken();
        
        //同一个token会有多个window
        win.mToken.addWindow(win);       
        ......
    }    
    return res;
}

addToDisplay 比较长,但主要做的事情只有这几件

  • 对添加窗口的 Token 和类型合法性校验,Token 是这个窗口的标识,Activity 窗口的 Token 会在 AMS 创建 Acitvity 的时候创建,系统窗口会在这里创建
  • 创建 WindowState,WindowState 会持有在上面创建的用于 WMS 和窗口通信的 Session 以及一些窗口状态
  • 存储 WindowState

这里只关心 SurfaceComponentClient 的创建,所以我们只看 win.attach() 函数

/frameworks/base/services/core/java/com/android/server/wm/WindowState.java

void attach() {
    mSession.windowAddedLocked(mAttrs.packageName);
}

这里调用了 Session 的 windowAddedLocked 函数,这个 session 就是我们在上面创建的 Session

/frameworks/base/services/core/java/com/android/server/wm/Session.java

void windowAddedLocked(String packageName) {
    mPackageName = packageName;   
    if (mSurfaceSession == null) {     
        mSurfaceSession = new SurfaceSession();      
        mService.mSessions.add(this);
        if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
            mService.dispatchNewAnimatorScaleLocked(this);
        }
    }
    mNumWindow++;
}

可以看到这里创建了 SurfaceSession,又遇到了一个以 Session 命名的类,它是和谁通信用的呢?带着疑问接着往下看
/frameworks/base/core/java/android/view/SurfaceSession.java

public SurfaceSession() {
    mNativeClient = nativeCreate();
}

SurfaceSession 的构造函数调用了 native 函数 nativeCreate

/frameworks/base/core/jni/android_view_SurfaceSession.cpp

static jlong nativeCreate(JNIEnv* env, jclass clazz) {
    SurfaceComposerClient* client = new SurfaceComposerClient();
    client->incStrong((void*)nativeCreate);
    return reinterpret_cast<jlong>(client);
}

到这里终于发现了 SurfaceComposerClient 的身影,在前面已经知道,只要有了 SurfaceComposerClient,就能通过 createSurface 和 getSurface 创建 Layer 和 Surface 了。

我们接着探索 Activity 的界面显示流程中是在哪儿调用这两个函数的。

2.2.2 createSurface

那么 createSurface 的调用在哪儿呢?它其实在 mTraversalRunnable 回调里

/frameworks/base/core/java/android/view/ViewRootImpl.java

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

mTraversalRunnable 回调函数执行了 performTraversals 函数,这个函数主要是用来进行布局,测量和绘制的,接着看这个函数。

private void performTraversals() {
    final View host = mView;
    if (host == null || !mAdded)
        return;

    // 窗口参数初始化和调整
    ……

    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
    if (layoutRequested) {
        ……

        //1,预测量
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
    }

    ……

    if (mApplyInsetsRequested) {
        mApplyInsetsRequested = false;
        if (mLayoutRequested) {
            // window窗口发生变化后重新测量
            windowSizeMayChange |= measureHierarchy(host, lp,
                    mView.getContext().getResources(),
                    desiredWindowWidth, desiredWindowHeight);
        }
    }

    //判断窗口是否需要重新调整大小
    ……


    final int surfaceGenerationId = mSurface.getGenerationId();

    final boolean isViewVisible = viewVisibility == View.VISIBLE;
    final boolean windowRelayoutWasForced = mForceNextWindowRelayout;
    //第一次加载,或者 window 需要重新调整大小,或者 insets 发生了改变,或者 view 可见性变化了
    if (mFirst || windowShouldResize || insetsChanged ||
            viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
        mForceNextWindowRelayout = false;

        ……

        boolean hadSurface = mSurface.isValid();

        try {
            ……

            //2,调用wms,创建native Surface等工作。
            relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);

            
            // 判断 surface 大小等数据是否发生变化
            ……

            //Surface的处理
            …… 

        } catch (RemoteException e) {
        }
        if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                //touch mode以及之前预测量的宽高不一样,则再次进行测量
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                     //3,进行测量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    ……
                    layoutRequested = true;
                }
            }
    } else {
       
        maybeHandleWindowMove(frame);
    }
    ……

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        //4,执行布局工作
        performLayout(lp, mWidth, mHeight);
        //计算透明区域
        ……

    }

    //回调,请求焦点,输入法处理等工作
    …… 

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw && !newSurface) {
        ……

        //4,绘制工作
        performDraw();
    } else {
        ……
    }

    mIsInTraversal = false;
}

performTraversals 函数非常的长,做的事情也非常多,它的关键流程只有下面这件事

  • 预测量
  • relayoutWindow,这一步会创建 Layer 和 Surface
  • 重新进行测量
  • 布局
  • 绘制

这篇文章主要讲图像缓冲区,所以测量,布局和绘制的流程都不讲了,我们这里只关心第二步,relayoutWindow 是如何创建 Surface 的。

/frameworks/base/core/java/android/view/ViewRootImpl.java

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
                           boolean insetsPending) throws RemoteException {

    float appScale = mAttachInfo.mApplicationScale;
    boolean restore = false;

    int relayoutResult = mWindowSession.relayout(
        mWindow, mSeq, params,
        (int) (mView.getMeasuredWidth() * appScale + 0.5f),
        (int) (mView.getMeasuredHeight() * appScale + 0.5f),
        viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
        mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
        mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame,
        mPendingMergedConfiguration, mSurface);

    return relayoutResult;
}

relayoutWindow 方法里面 mWindowSession.relayout 函数

/frameworks/base/services/core/java/com/android/server/wm/Session.java

public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs,
                    int requestedWidth, int requestedHeight, int viewFlags,
                    int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
                    Rect outVisibleInsets, Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
                    MergedConfiguration mergedConfiguration, Surface outSurface) {

    int res = mService.relayoutWindow(this, window, seq, attrs,
           requestedWidth, requestedHeight, viewFlags, flags,
           outFrame, outOverscanInsets, outContentInsets, outVisibleInsets,
           outStableInsets, outsets, outBackdropFrame, mergedConfiguration, outSurface);
    return res;
}

WindowSession 的 relayout 函数调用了 wms 的 relayoutWindow 函数

/frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

public int relayoutWindow(Session session, IWindow client, int seq,
       WindowManager.LayoutParams attrs, int requestedWidth,
       int requestedHeight, int viewVisibility, int flags,
       Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
       Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
       MergedConfiguration mergedConfiguration, Surface outSurface) {
    //权限校验
    ......
    synchronized(mWindowMap) {
        WindowState win = windowForClientLocked(session, client, false);
        ......
        if (viewVisibility == View.VISIBLE &&
            (win.mAppToken == null || win.mAttrs.type == TYPE_APPLICATION_STARTING
             || !win.mAppToken.isClientHidden())) {

            // 进行布局,以分配正确大小的surface
            if (win.mLayoutSeq == -1) {
                win.setDisplayLayoutNeeded();
                mWindowPlacerLocked.performSurfacePlacement(true);
            }
            result = win.relayoutVisibleWindow(mergedConfiguration, result, attrChanges,
                                               oldVisibility);

            try {
                //创建SurfaceControl
                result = createSurfaceControl(outSurface, result, win, winAnimator);
            } catch (Exception e) {
                // 错误处理
                ......
                return 0;
            }
           
        } else {
           ......
        }
      ......
    }    
    return result;
}

relayoutWindow 函数主要做了两件事情

  • 对所有 Window 进行测量布局和排序,前面讲到 performTraversals 会进行测量布局的流程,为什么这里还要进行一次呢?因为 WMS 中需要对所有的窗口进行一次整体的测量,比如 activity 的窗口会位于状态栏窗口和虚拟按键窗口之间,所以确定 activity 窗口的实际大小需要 wms 中结合其他的窗口才能进行
  • 执行 createSurfaceControl

测量和排序的流程不在这儿详说了,还是只看和 Surface 创建相关函数 createSurfaceControl

/frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

private int createSurfaceControl(Surface outSurface, int result, WindowState win,
                                 WindowStateAnimator winAnimator) {
    if (!win.mHasSurface) {
        result |= RELAYOUT_RES_SURFACE_CHANGED;
    }

    WindowSurfaceController surfaceController;
    try { 
        //1,创建SurfaceControl
        surfaceController = winAnimator.createSurfaceLocked(win.mAttrs.type, win.mOwnerUid);
    } finally {
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    }
    if (surfaceController != null) {
        //2,创建Surface
        surfaceController.getSurface(outSurface);     
    } else {       
        outSurface.release();
    }

    return result;
}

createSurfaceControl 做了两件事情

  • 通过 createSurfaceLocked 创建 SurfaceController,这个过程会创建 Layer
  • 通过 surfaceController.getSurface 创建 Surface

createSurfaceControl 实际是调用了 winAnimator 的 createSurfaceLocked 函数。winAnimator 是 WinState 的成员变量,是主要负责窗口动画的类。我们接着看 createSurfaceLocked 函数

/frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java

WindowSurfaceController createSurfaceLocked(int windowType, int ownerUid) {
        final WindowState w = mWin;
        ......
        mSurfaceController = new WindowSurfaceController(mSession.mSurfaceSession,
            attrs.getTitle().toString(),
            width, height, format, flags, this, windowType, ownerUid);
        ......
        return mSurfaceController;
}

可以看到,这里以 SurfaceSession 为入参创建了 WindowSurfaceController,SurfaceSession 在前面 addWindow 中已经提到了,它其实就是 SurfaceCompserClient。接着看 WindowSurfaceController 的构造函数。

/frameworks/base/services/core/java/com/android/server/wm/WindowSurfaceController.java

public WindowSurfaceController(SurfaceSession s, String name, int w, int h, int format,
       int flags, WindowStateAnimator animator, int windowType, int ownerUid) {
    mAnimator = animator;

    mSurfaceW = w;
    mSurfaceH = h;

    title = name;

    mService = animator.mService;
    final WindowState win = animator.mWin;
    mWindowType = windowType;
    mWindowSession = win.mSession;


    mSurfaceControl = new SurfaceControl(
        s, name, w, h, format, flags, windowType, ownerUid);


    if (mService.mRoot.mSurfaceTraceEnabled) {
        mSurfaceControl = new RemoteSurfaceTrace(
            mService.mRoot.mSurfaceTraceFd.getFileDescriptor(), mSurfaceControl, win);
    }
}

WindowSurfaceController 的构造函数里面实际是以 SurfaceSession 为入参创建了 SurfaceControl。SurfaceControl 的构造函数如下:

/frameworks/base/core/java/android/view/SurfaceControl.java

public SurfaceControl(SurfaceSession session, String name, int w, int h, int format, int flags,
                      SurfaceControl parent, int windowType, int ownerUid)
    throws OutOfResourcesException {

    mName = name;
    mNativeObject = nativeCreate(session, name, w, h, format, flags,
        parent != null ? parent.mNativeObject : 0, windowType, ownerUid);
    ......
}

这个 SurfaceControl 是 java 层的 SurfaceControl,它在构造函数中通过 jni 方法 nativeCreate 创建了 Native 层的 SurfaceControl。

接着看 nativeCreated 的实现。

/frameworks/base/core/jni/android_view_SurfaceControl.cpp

static jlong nativeCreate(JNIEnv* env, jclass clazz, jobject sessionObj,
                          jstring nameStr, jint w, jint h, jint format, jint flags, jlong parentObject,
                          jint windowType, jint ownerUid) {
    ScopedUtfChars name(env, nameStr);
    //通过SurfaceSession获取SurfaceComposerClient
    sp<SurfaceComposerClient> client(android_view_SurfaceSession_getClient(env, sessionObj));
    SurfaceControl *parent = reinterpret_cast<SurfaceControl*>(parentObject);
    //调用createSurface,创建Layer
    sp<SurfaceControl> surface = client->createSurface(
        String8(name.c_str()), w, h, format, flags, parent, windowType, ownerUid);
    if (surface == NULL) {
        jniThrowException(env, OutOfResourcesException, NULL);
        return 0;
    }

    surface->incStrong((void *)nativeCreate);
    return reinterpret_cast<jlong>(surface.get());
}

在 JNI 方法 nativeCreate 中,我们发现了 createSurface 函数,在这里,Layer 便创建好了,剩下的只剩调用 getSurface 创建 Surface了。

2.2.3 getSurface

接着回到 createSurfaceLocked 流程中的第二步:执行 surfaceController.getSurface() 函数

/frameworks/base/services/core/java/com/android/server/wm/WindowSurfaceController.java

void getSurface(Surface outSurface) {
    outSurface.copyFrom(mSurfaceControl);
}

这里的 outSurface 是前面 ViewRootImpl 创建的 Surface,它这个时候的还只是一个空的 Surface,需要 SurfaceComposerClient 在 native 层创建真正的 Surface 后,将 Native 层的 Surface 和这个 Java 层空的 Surface 绑定之后,ViewRootImpl 中的 Surface 才是一个可用的 Surface。

接着看 copyFrom 的实现

/frameworks/base/core/java/android/view/Surface.java

 public void copyFrom(SurfaceControl other) {
    
    long surfaceControlPtr = other.mNativeObject;
     //调用native方法创建Surface
    long newNativeObject = nativeGetFromSurfaceControl(surfaceControlPtr);

    synchronized (mLock) {
        if (mNativeObject != 0) {
            nativeRelease(mNativeObject);
        }
        //将native层的Surface和Java层的Surface绑定
        setNativeObjectLocked(newNativeObject);
    }
}

接着看 native 函数 nativeGetFromSurfaceControl

/frameworks/base/core/jni/android_view_Surface.cpp

static jlong nativeGetFromSurfaceControl(JNIEnv* env, jclass clazz,
        jlong surfaceControlNativeObj) {
   
    sp<SurfaceControl> ctrl(reinterpret_cast<SurfaceControl *>(surfaceControlNativeObj));
    //创建Surface
    sp<Surface> surface(ctrl->getSurface());
    if (surface != NULL) {
        surface->incStrong(&sRefBaseOwner);
    }
    return reinterpret_cast<jlong>(surface.get());
}

在 nativeGetFromSurfaceControl 函数的实现中,我们终于看到缓冲区创建流程的最后一步 SurfaceControl->getSurface(),至此,在 Activity 的绘制流程中,Layer 和 Surface 都已经创建完成了。

三 总结

  • ViewRootImpl 的 setView 函数中会执行 requestLayout() 和 mWindowSession.addToDisplay()
  • 其中,在 mWindowSession.addToDisplay() 流程中,会调用 WMS 来创建 SurfaceComponentClient
  • requestLayout() 流程中会使用 SurfaceComponentClient 来创建 Layer 和 Surface
  • Layer 的创建函数为 SurfaceCompoentClient.createSurface(),Surface 的创建函数为 SurfaceCompoentClient.getSurface
  • 创建好了图像缓冲区后,requestLayout() 就可以接着进行测量布局和绘制的操作流程了

讲完了 Surface 和 Layer 的创建,我们在下一篇文章中分析如何创建 BufferQueue。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值