文章目录
本文参考很多资料和源码,每一小结附上参考资源。
组件介绍
- SurfaceFlinger:系统服务,接收多个源的数据,对它们进行合成,然后发送到显示设备进行显示。
- HWComposer:在没有HWComposer之前,SurfaceFlinger将各个Layer的内容用OpenGL渲染到暂存缓冲区中,最后将暂存缓冲区传送到显示硬件。HWComposer是硬件合成器,帮助GPU做一些工作,SurfaceFlinger把多个Surface输出给hwc, hwc按照Surface的属性, 把多个Surface混合成一个Surface, 最后输出到Display。
- Gralloc:层中提供了一个Gralloc模块,Hgralloc库负责了申请图形缓冲区的所有工作,用户空间中的进程申请图形缓冲区,可以是普通的缓存区,也可以是显存缓冲区,并且将缓冲区映射到应用程序的地址空间。
GraphicBuffer
GraphicBuffer是图形内存块,是UI绘制的缓存数据。Surface向GraphicBufferProducer申请一个GraphicBuffer,再绘制到GraphicBuffer中,提供给SurfaceFlinger进行消费合成。
那么GraphicBuffer是什么?
// GraphicBuffer.h
GraphicBufferMapper& mBufferMapper; // 辅助类
ssize_t mInitCheck; // 记录图形缓冲区的状态
sp<ANativeWindowBuffer> mWrappedBuffer; // 描述Native Buffer
uint64_t mId; // 图形缓冲区的标识
sp<IBinder> mBufferRef;
uint32_t mGenerationNumber; // 记录 generation number
其中,GraphicBufferAllocator 负责GraphicBuffer创建和释放工作;
GraphicBufferMapper负责lock和unlock操作,lock 操作将图形内存块映射到应用程序进程的虚拟地址空间内。而unlock就是归还内存。
struct ANativeWindowBuffer
{
int width;
int height ;
.....
buffer_handle_t handle ; // 硬件驱动层生成的图像缓存句柄
}
更多GraphicBuffer可以参考:
https://www.wolfcstech.com/2017/09/20/android_graphics_bufferalloc/
GraphicBufferAllocator
GraphicBufferAllocator 管理分配显示的内存,提供了申请和释放图像内存块的方法:
status_t GraphicBufferAllocator::alloc(uint32_t width, uint32_t height, PixelFormat format, uint32_t usage, buffer_handle_t* handle, uint32_t* stride);
status_t GraphicBufferAllocator::free(buffer_handle_t handle);
GraphicBufferAllocator是单例模式,每个进程只有一个GraphicBufferAllocator对象;
GraphicBufferAllocator对接了gralloc,而gralloc模块是Android硬件抽象层提供的一个操作帧缓冲区的模块。
gralloc 提供了两种缓存区:
- FrameBuffer帧缓冲区,映射到用户空间,应用进程可以操作FrameBuffer,来让显示设备显示;
- 普通的数据缓冲区,内核中创建一块匿名共享内存,映射到用户空间。
// gralloc
gralloc_alloc(){
if ( usage & GRALLOC_USAGE_HW_FB ) {
err = gralloc_alloc_framebuffer (dev, size, usage , pHandle);
} else {
err = gralloc_alloc_buffer (dev, size, usage , pHandle);
}
}
GraphicBufferAllocator::get()
提供获取GraphicBufferAllocator的方法
GraphicBufferAllocator分配缓存Buffer,GraphicBufferMapper mmap映射到应用程序的进程
更多Gralloc参考:
https://blog.csdn.net/fu_shuwu/article/details/53048047
共享内存
由上面我们可以知道,GraphicBuffer指向的是一个图像缓存区,而应用程序执行draw动作的时候,将一帧帧的数据通过Surface写入到GraphicBuffer,等到vsync信号来了,GraphicBuffer被SurfaceFlinger消费,通过某种手段混合生成最后的帧数据,写入FrameBuffer交由显示设备显示。
其中应用进程、SurfaceFlinger都是在不同的进程,binder通信不适传递大量的图像数据,因此采用了匿名共享内存的IPC手段,在tmpfs临时文件系统中创建一个临时文件。
参考:
https://www.jianshu.com/p/d9bc9c668ba6
GraphicBuffer和FrameBuffer的关系
二者之间其实没有很直接的关系,但是自己的学习过程中,一直搞不懂之间的猫腻。
GraphicBuffer
GraphicBuffer继承自ANativeWindowBuffer,Surface继承自ANativeWindow
BufferQueue操作的具体对象,表示图像缓冲区,Surface可能包含申请多个GraphicBuffer;
FrameBuffer
帧缓冲是Linux 系统为显示设备提供的一个接口,每个显示设备被抽象为一个帧缓冲区,注册到FrameBuffer模块中,并在/dev/graphics目录下创建对应的fbX设备。屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。
http://blog.csdn.net/ear5cm/article/details/45458683
BufferQueue
GraphicBuffer是表示基本的显示内存单元,而GraphicBufferAllocator负责真正的申请和释放内存,那BufferQueue就是管理GraphicBuffer的管理者。
SurfaceFlinger在Surface创建的时候,对应创建了Layer,Layer会创建BufferQueue。
BufferQueue的原理很简单:
- 生产者(Producer)向BufferQueue申请出队(dequeue) GraphicBuffer,生产者向GraphicBuffer中填充图形数据后,然后将GraphicBuffer入队(queue)到BufferQueue;
- GraphicBuffer入队BufferQueue时,BufferQueue会通知消费者,新的图形数据生产了;
- 消费者(Consumer)向BufferQueue获取(acquire) GraphicBuffer,消费者消费图形数据,然后将GraphicBuffer释放交回(release)给BufferQueue;
- 空的GraphicBuffer交回给BufferQueue,BufferQueue又会通知到生产者有新的可利用的GraphicBuffer。
BufferQueueCore
BufferQueue 内部有一个重要的变量BufferQueueCore。BufferQueue中用BufferSlot来存储GraphicBuffer,使用数组来存储一系列BufferSlot,数组默认大小为64。
- mSlots
定义了64个BufferSlot, BufferSlot直接持有GraphicBuffer - std::set mFreeSlots
mFreeSlots里面的slot值表明当前slot的BufferSlot是FREE状态, 并且没有GraphicBuffer - std::list mUnusedSlots
mSlots的大小是64个,而mUnusedSlots就是除掉mFreeSlots剩下的BufferSlot - std::list mFreeBuffers
mFreeBuffers里面的slot表明当前slot对应的BufferSlot是FREE状态,并且有GraphicBuffer - std::set mActiveBuffers
mActiveBuffers里面的slot表明当前slot对应的BufferSlot都有GraphicBuffer,并且是NON FREE状态
GraphicBuffer用BufferState来表示其状态,有以下状态:
- FREE:表示该Buffer当前可用,允许被dequeued,此时Buffer属于BufferQueue;
- DEQUEUED:表示该Buffer被生产者获取了,属于生产者,BufferQueue不可以对这块缓冲区进行操作;
- QUEUED:表示该Buffer被生产者填充了数据,并且入队到BufferQueue了,该Buffer的所有权属于BufferQueue
- ACQUIRED:表示该Buffer被消费者获取了,该Buffer的所有权属于消费者
BufferQueue创建
BufferQueue是由BufferLayer创建,而BufferLayer是在SurfaceFlinger创建,BufferQueue为BufferLayer管理GrapicBuffer。
void bufferqueue::createbufferqueue(sp<igraphicbufferproducer>* outproducer,
sp<igraphicbufferconsumer>* outconsumer,
const sp<igraphicbufferalloc>& allocator) {
//allocator == null
...
//创建bufferqueuecore
sp<bufferqueuecore> core(new bufferqueuecore(allocator));
...
//创建生产者
sp<igraphicbufferproducer> producer(new bufferqueueproducer(core));
...
//创建消费者
sp<igraphicbufferconsumer> consumer(new bufferqueueconsumer(core));
...
*outproducer = producer;
*outconsumer = consumer;
}
BufferQueueProducer
BufferQueue 创建了生产者BufferQueueProducer,并赋给外部的IGraphicBufferProducer,供外部调用。
来看一下BufferQueueProducer,他继承自了BnGraphicBufferProducer,是一个binder的服务端,接收来自客户端的消息。
status_t dequeueBuffer(int* outSlot, sp<Fence>* outFence, uint32_t width,
uint32_t height, PixelFormat format, uint64_t usage,
uint64_t* outBufferAge, FrameEventHistoryDelta* outTimestamps);
status_t queueBuffer(int slot, const QueueBufferInput& input, QueueBufferOutput* output);
status_t requestBuffer(int slot, sp<GraphicBuffer>* buf);
参考:https://zhuanlan.zhihu.com/p/62813895
BufferQueueConsumer
BufferQueue 创建了消费者者BufferQueueConsumer,并赋给外部的IGraphicBufferConsumer,供外部调用。
同样,BufferQueueConsumer也继承了BnGraphicBufferConsumer,同样也是个binder的服务端。
status_t acquireBuffer(BufferItem* outBuffer,nsecs_t expectedPresent, uint64_t maxFrameNumber = 0);
status_t releaseBuffer(int slot, uint64_t frameNumber, const sp<Fence>& releaseFence,
EGLDisplay display, EGLSyncKHR fence);
更多参考:
https://www.jianshu.com/p/af5858c06d5d
https://blog.csdn.net/yangwen123/article/details/12234931
Surface 生产数据
Surface生产数据经历的流程:
- allocateBuffer
- dequeueBuffer
- queueBuffer
//Surface.cpp
void Surface::allocateBuffers() {
uint32_t reqWidth = mReqWidth ? mReqWidth : mUserWidth;
uint32_t reqHeight = mReqHeight ? mReqHeight : mUserHeight;
mGraphicBufferProducer->allocateBuffers(reqWidth, reqHeight,
mReqFormat, mReqUsage);
}
int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {
...
status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, reqWidth, reqHeight,
reqFormat, reqUsage, &mBufferAge,
enableFrameTimestamps ? &frameTimestamps : nullptr);
...
Mutex::Autolock lock(mMutex);
sp<GraphicBuffer>& gbuf(mSlots[buf].buffer);
...
*buffer = gbuf.get();
...
return OK;
}
int Surface::queueBuffer(android_native_buffer_t* buffer, int fenceFd) {
Rect crop(Rect::EMPTY_RECT);
mCrop.intersect(Rect(buffer->width, buffer->height), &crop);
IGraphicBufferProducer::QueueBufferOutput output;
IGraphicBufferProducer::QueueBufferInput input(timestamp, isAutoTimestamp,
static_cast<android_dataspace>(mDataSpace), crop, mScalingMode,
mTransform ^ mStickyTransform, fence, mStickyTransform,
mEnableFrameTimestamps);
// 处理图像数据
...
nsecs_t now = systemTime();
status_t err = mGraphicBufferProducer->queueBuffer(i, input, &output);
...
return err;
}
status_t BufferQueueProducer::queueBuffer(int slot,
const QueueBufferInput &input, QueueBufferOutput *output) {
input.deflate(&requestedPresentTimestamp, &isAutoTimestamp, &dataSpace,
&crop, &scalingMode, &transform, &acquireFence, &stickyTransform,
&getFrameTimestamps);
sp<IConsumerListener> frameAvailableListener;
sp<IConsumerListener> frameReplacedListener;
uint64_t currentFrameNumber = 0;
BufferItem item;
{
const sp<GraphicBuffer>& graphicBuffer(mSlots[slot].mGraphicBuffer);
Rect bufferRect(graphicBuffer->getWidth(), graphicBuffer->getHeight());
Rect croppedRect(Rect::EMPTY_RECT);
crop.intersect(bufferRect, &croppedRect);
mSlots[slot].mFence = acquireFence;
mSlots[slot].mBufferState.queue();
++mCore->mFrameCounter;
currentFrameNumber = mCore->mFrameCounter;
mSlots[slot].mFrameNumber = currentFrameNumber;
item.mAcquireCalled = mSlots[slot].mAcquireCalled;
item.mGraphicBuffer = mSlots[slot].mGraphicBuffer;
item.mCrop = crop;
...
item.mQueuedBuffer = true;
…
if (mCore->mQueue.empty()) {
mCore->mQueue.push_back(item);
frameAvailableListener = mCore->mConsumerListener;
} else {
const BufferItem& last = mCore->mQueue.itemAt(mCore->mQueue.size() - 1);
if (last.mIsDroppable) {
...
mCore->mQueue.editItemAt(mCore->mQueue.size() - 1) = item;
frameReplacedListener = mCore->mConsumerListener;
} else {
mCore->mQueue.push_back(item);
frameAvailableListener = mCore->mConsumerListener;
}
}
}
{
Mutex::Autolock lock(mCallbackMutex);
while (callbackTicket != mCurrentCallbackTicket) {
mCallbackCondition.wait(mCallbackMutex);
}
if (frameAvailableListener != NULL) {
frameAvailableListener->onFrameAvailable(item);
} else if (frameReplacedListener != NULL) {
frameReplacedListener->onFrameReplaced(item);
}
}
return NO_ERROR;
}
SurfaceFlinger 消费数据
上面的frameAvailableListener->onFrameAvailable(),会回调通知Layer,Layer通知SurfaceFlinger消费,SurfaceFlinger利用EventThread,在VSYNC信号来时,执行INVALIDATE的操作,触发对所有Layer执行操作。SurfaceFlinger又触发REFRESH消息
void BufferLayer::onFrameAvailable(const BufferItem& item) {
// Add this buffer from our internal queue tracker
...
mFlinger->signalLayerUpdate();
}
void SurfaceFlinger::signalLayerUpdate() {
mEventQueue->invalidate();
}
#define INVALIDATE_ON_VSYNC 1
/* when INVALIDATE_ON_VSYNC is set SF only processes
* buffer updates on VSYNC and performs a refresh immediately
* after.
*
* when INVALIDATE_ON_VSYNC is set to false, SF will instead
* perform the buffer updates immediately, but the refresh only
* at the next VSYNC.
* THIS MODE IS BUGGY ON GALAXY NEXUS AND WILL CAUSE HANGS
*/
void MessageQueue::invalidate() {
#if INVALIDATE_ON_VSYNC
mEvents->requestNextVsync();
#else
mHandler->dispatchInvalidate();
#endif
}
void MessageQueue::Handler::handleMessage(const Message& message) {
switch (message.what) {
case INVALIDATE:
android_atomic_and(~eventMaskInvalidate, &mEventMask);
mQueue.mFlinger->onMessageReceived(message.what);
break;
case REFRESH:
android_atomic_and(~eventMaskRefresh, &mEventMask);
mQueue.mFlinger->onMessageReceived(message.what);
break;
}
}
当VSync信号到来时,SurfaceFlinger处理最重要的两个操作INVALIDATE和REFRESH消息
-
INVALIDATE
依次调用handleMessageTransaction() 处理之前对屏幕和应用程序窗口的改动;handleMessageInvalidate()获取各Layer对应的BufferQueue缓冲数据,更新脏区域;触发REFRESH消息。 -
REFRESH
调用handleMessageRefresh()合并和渲染输出。
void SurfaceFlinger::onMessageReceived(int32_t what) {
switch (what) {
case MessageQueue::INVALIDATE: {
...
if (frameMissed) {
mTimeStats.incrementMissedFrames();
if (mPropagateBackpressure) {
signalLayerUpdate();
break;
}
}
updateVrFlinger();
bool refreshNeeded = handleMessageTransaction();
refreshNeeded |= handleMessageInvalidate();
refreshNeeded |= mRepaintEverything;
if (refreshNeeded) {
signalRefresh();
}
break;
}
case MessageQueue::REFRESH: {
handleMessageRefresh();
break;
}
}
}
handleMessageRefresh()
void SurfaceFlinger::handleMessageRefresh() {
mRefreshPending = false;
nsecs_t refreshStartTime = systemTime(SYSTEM_TIME_MONOTONIC);
preComposition(refreshStartTime); // 预处理显示和Layer的改变
rebuildLayerStacks(); // 根据Layer层级改变次数
setUpHWComposer(); // 更新 HWComposer层级
doDebugFlashRegions();
doTracing("handleRefresh");
logLayerStats();
doComposition();// 生成 OpenGL texture纹理
postComposition(refreshStartTime); // 推送到物理显示设备
mPreviousPresentFence = getBE().mHwc->getPresentFence(HWC_DISPLAY_PRIMARY);
...
mVsyncModulator.onRefreshed(mHadClientComposition);
mLayersWithQueuedFrames.clear();
}
参考:
https://juejin.im/post/5baf275f5188255c9a7740ba
http://gityuan.com/2017/02/18/surface_flinger_2/
小结
Surface创建过程中,SurfaceFlinger对应创建一个BufferLayer,BufferLayer创建对应的BufferQueue。
上面大致领略了BufferQueue的生产者消费者模式,由GraphicBufferAllocator分配buffer内存,其中Surface生产图像数据,SurfaceFlinger消费图像数据。
Surface
剩下的问题:
- 上一文章中最后Surface创建的时候赋值了一个GraphicBufferProducer,而SurfaceFlinger创建对应的Layer,Surface中的GraphicBufferProducer是哪里来的?
- Layer是什么?怎么创建的?
- Native层的Surface到底是什么? 怎么创建的
GraphicBufferProducer的创建
其实是利用了SurfaceFlinger的Client创建传递过来的。
// Surface.cpp
err = mClient->createSurface(name, w, h, format, flags, parentHandle,
windowType, ownerUid, &handle, &gbp);
// gbp就是GraphicBufferProducer
Layer
Layer 有两种类型:
- BufferLayer 具备Buffer可缓存显示数据的Layer;
- ColorLayer 可绘制指定颜色和透明度的Layer
BufferLayer,通过BufferQueue的createBufferQueue,创建了一个buffer队列,一个buffer队列,有一个生产者producer,和一个消费者consumer。
BufferLayer实现ContentsChangedListener和FrameAvailableListener两个接口类
private:
sp<BufferLayerConsumer> mConsumer;
sp<IGraphicBufferProducer> mProducer;
// constants
uint32_t mTextureName; // from GLES
PixelFormat mFormat;
// main thread
uint32_t mCurrentScalingMode;
bool mBufferLatched = false; // TODO: Use mActiveBuffer?
uint64_t mPreviousFrameNumber; // Only accessed on the main thread.
// The texture used to draw the layer in GLES composition mode
mutable Texture mTexture;
bool mUpdateTexImageFailed; // This is only accessed on the main thread.
bool mRefreshPending;
BufferLayer是什么
void BufferLayer::onFirstRef() {
Layer::onFirstRef();
// Creates a custom BufferQueue for SurfaceFlingerConsumer to use
sp<IGraphicBufferProducer> producer;
sp<IGraphicBufferConsumer> consumer;
BufferQueue::createBufferQueue(&producer, &consumer, true);
mProducer = new MonitoredProducer(producer, mFlinger, this);
{
// Grab the SF state lock during this since it's the only safe way to access RenderEngine
Mutex::Autolock lock(mFlinger->mStateLock);
mConsumer = new BufferLayerConsumer(consumer, mFlinger->getRenderEngine(), mTextureName,
this);
}
mConsumer->setConsumerUsageBits(getEffectiveUsage(0));
mConsumer->setContentsChangedListener(this);
mConsumer->setName(mName);
if (mFlinger->isLayerTripleBufferingDisabled()) {
mProducer->setMaxDequeuedBufferCount(2);
}
const sp<const DisplayDevice> hw(mFlinger->getDefaultDisplayDevice());
updateTransformHint(hw);
}
- Producer生产完后,会通过BufferQueueCore中的mConsumerListener通知ConsumerBase
- ConsumerBase,接受到BufferQueueConsumer的通知,再通过BufferLayer传下来的信使mFrameAvailableListener,通知BufferLayer。
- BufferLayer接受到通知后,就可以去消费生产完的Buffer了。
Layer创建流程:
SurfaceFlinger为Surface创建了一个Layer与之对应,
// Client.cpp
status_t Client::createSurface(... , sp<IGraphicBufferProducer>* gbp)
{
result = flinger->createLayer(name, client, w, h, format, flags,
windowType, ownerUid, handle, gbp, parent);
}
// SurfaceFlinger.cpp
status_t SurfaceFlinger::createLayer(
const String8& name,
const sp<Client>& client,
uint32_t w, uint32_t h, PixelFormat format, uint32_t flags,
int32_t windowType, int32_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:
result = createBufferLayer(client,
uniqueName, w, h, flags, format,
handle, gbp, &layer);
break;
case ISurfaceComposerClient::eFXSurfaceColor:
result = createColorLayer(client,
uniqueName, w, h, flags,
handle, &layer);
break;
default:
result = BAD_VALUE;
break;
}
...
layer->setInfo(windowType, ownerUid);
result = addClientLayer(client, *handle, *gbp, layer, *parent);
if (result != NO_ERROR) {
return result;
}
mInterceptor->saveSurfaceCreation(layer);
setTransactionFlags(eTransactionNeeded);
return result;
}
看createBufferLayer和addClientLayer
status_t SurfaceFlinger::createBufferLayer(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;
}
sp<BufferLayer> layer = new BufferLayer(this, client, name, w, h, flags);
status_t err = layer->setBuffers(w, h, format, flags);
if (err == NO_ERROR) {
*handle = layer->getHandle();
*gbp = layer->getProducer();
*outLayer = layer;
}
return err;
}
addClientLayer
status_t SurfaceFlinger::addClientLayer(const sp<Client>& client,
const sp<IBinder>& handle,
const sp<IGraphicBufferProducer>& gbc,
const sp<Layer>& lbc,
const sp<Layer>& parent)
{
// add this layer to the current state list
{
Mutex::Autolock _l(mStateLock);
if (mNumLayers >= MAX_LAYERS) {
ALOGE("AddClientLayer failed, mNumLayers (%zu) >= MAX_LAYERS (%zu)", mNumLayers,
MAX_LAYERS);
return NO_MEMORY;
}
if (parent == nullptr) {
mCurrentState.layersSortedByZ.add(lbc);
} else {
if (parent->isPendingRemoval()) {
ALOGE("addClientLayer called with a removed parent");
return NAME_NOT_FOUND;
}
parent->addChild(lbc);
}
if (gbc != nullptr) {
mGraphicBufferProducerList.insert(IInterface::asBinder(gbc).get());
LOG_ALWAYS_FATAL_IF(mGraphicBufferProducerList.size() >
mMaxGraphicBufferProducerListSize,
"Suspected IGBP leak: %zu IGBPs (%zu max), %zu Layers",
mGraphicBufferProducerList.size(),
mMaxGraphicBufferProducerListSize, mNumLayers);
}
mLayersAdded = true;
mNumLayers++;
}
// attach this layer to the client
client->attachLayer(handle, lbc);
return NO_ERROR;
}
SurfaceFlinger工作流程总结
app进程向WMS申请创建对应的RootView
WMS向SurfaceFlinger申请创建Surface
SurfaceFlinger对应创建Layer,Layer内部创建一个BufferQueue,app负责申请GraphicBuffer利用匿名共享内存方式写入图像数据,SurfaceFlinger负责从BufferQueue取出消费图像数据。
Surfaceflinger把所有的显示的buffer做图层合并处理,利用HWC或者OpenGL,合并到FrameBuffer中。
framebuffer本身申请的内存能存两个屏幕的数据量还大的内存,所以采样交替送显的方式进行eglSwapBuffers交换(即fb_pan_display指定切换到另外framebuffer的另一部分地址),即framebuffer的A部分用于merge处理,framebuffer的B部分用于送显显示,
下一个节拍例如vsync时,进行切换,framebuffer的A部分送显,framebuffer的B部分用于merge。送显的内容除了framebuffer外,还有overlay的内容,硬件会把他们进行合并,再送到显示屏幕。
参考:
https://blog.csdn.net/happylishang/article/details/78282527
https://www.jianshu.com/p/af5858c06d5d
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/6247.html