我的编程空间,编程开发者的网络收藏夹
学习永远不晚

BufferQueue的设计思想和内部实现方法是什么

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

BufferQueue的设计思想和内部实现方法是什么

这篇文章主要介绍“BufferQueue的设计思想和内部实现方法是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“BufferQueue的设计思想和内部实现方法是什么”文章能帮助大家解决问题。

背景

对业务开发来说,无法接触到BufferQueue,甚至不知道BufferQueue是什么东西。对系统来说,BufferQueue是很重要的传递数据的组件,Android显示系统依赖于BufferQueue,只要显示内容到“屏幕”(此处指抽象的屏幕,有时候还可以包含编码器),就一定需要用到BufferQueue,可以说在显示/播放器相关的领域中,BufferQueue无处不在。即使直接调用Opengl ES来绘制,底层依然需要BufferQueue才能显示到屏幕上。

弄明白BufferQueue,不仅可以增强对Android系统的了解,还可以弄明白/排查相关的问题,如为什么Mediacodec调用dequeueBuffer老是返回-1?为什么普通View的draw方法直接绘制内容即可,SurfaceView在draw完毕后还需要unlockCanvasAndPost?

注:本文分析的代码来自于Android6.0.1。

2. BufferQueue内部运作方式

BufferQueue是Android显示系统的核心,它的设计哲学是生产者-消费者模型,只要往BufferQueue中填充数据,则认为是生产者,只要从BufferQueue中获取数据,则认为是消费者。有时候同一个类,在不同的场景下既可能是生产者也有可能是消费者。如SurfaceFlinger,在合成并显示UI内容时,UI元素作为生产者生产内容,SurfaceFlinger作为消费者消费这些内容。而在截屏时,SurfaceFlinger又作为生产者将当前合成显示的UI内容填充到另一个BufferQueue,截屏应用此时作为消费者从BufferQueue中获取数据并生产截图。

以下是常见的BufferQueue使用步骤:

  1. 初始化一个BufferQueue

  2. 图形数据的生产者通过BufferQueue申请一块GraphicBuffer,对应图中的dequeueBuffer方法

  3. 申请到GraphicBuffer后,获取GraphicBuffer,通过函数requestBuffer获取

  4. 获取到GraphicBuffer后,通过各种形式往GraphicBuffer中填充图形数据后,然后将GraphicBuffer入队到BufferQueue中,对应上图中的queueBuffer方法

  5. 在新的GraphicBuffer入队BufferQueue时,BufferQueue会通过回调通知图形数据的消费者,有新的图形数据被生产出来了

  6. 然后消费者从BufferQueue中出队一个GraphicBuffer,对应图中的acquireBuffer方法

  7. 待消费者消费完图形数据后,将空的GraphicBuffer还给BufferQueue以便重复利用,此时对应上图中的releaseBuffer方法

  8. 此时BufferQueue再通过回调通知图形数据的生产者有空的GraphicBuffer了,图形数据的生产者又可以从BufferQueue中获取一个空的GraphicBuffer来填充数据

  9. 一直循环2-8步骤,这样就有条不紊的完成了图形数据的生产-消费

当然图形数据的生产者可以不用等待BufferQueue的回调再生产数据,而是一直生产数据然后入队到BufferQueue,直到BufferQueue满为止。图形数据的消费者也可以不用等BufferQueue的回调通知,每次都从BufferQueue中尝试获取数据,获取失败则尝试,只是这样效率比较低,需要不断的轮训BufferQueue(因为BufferQueue有同步阻塞和非同步阻塞两种机种,在非同步阻塞机制下获取数据失败不会阻塞该线程直到有数据才唤醒该线程,而是直接返回-1)。

同时使用BufferQueue的生产者和消费者往往处在不同的进程,BufferQueue内部使用共享内存和Binder在不同的进程传递数据,减少数据拷贝提高效率。

和BufferQueue有关的几个类分别是:

  1. BufferBufferCore:BufferQueue的实际实现

  2. BufferSlot:用来存储GraphicBuffer

  3. BufferState:表示GraphicBuffer的状态

  4. IGraphicBufferProducer:BufferQueue的生产者接口,实现类是BufferQueueProducer

  5. IGraphicBufferConsumer:BufferQueue的消费者接口,实现类是BufferQueueConsumer

  6. GraphicBuffer:表示一个Buffer,可以填充图像数据

  7. ANativeWindow_Buffer:GraphicBuffer的父类

  8. ConsumerBase:实现了ConsumerListener接口,在数据入队列时会被调用到,用来通知消费者

BufferQueue中用BufferSlot来存储GraphicBuffer,使用数组来存储一系列BufferSlot,数组默认大小为64。

GraphicBuffer用BufferState来表示其状态,有以下状态:

  1. FREE:表示该Buffer没有被生产者-消费者所使用,该Buffer的所有权属于BufferQueue

  2. DEQUEUED:表示该Buffer被生产者获取了,该Buffer的所有权属于生产者

  3. QUEUED:表示该Buffer被生产者填充了数据,并且入队到BufferQueue了,该Buffer的所有权属于BufferQueue

  4. ACQUIRED:表示该Buffer被消费者获取了,该Buffer的所有权属于消费者

为什么需要这些状态呢? 假设不需要这些状态,实现一个简单的BufferQueue,假设是如下实现:

BufferQueue{
   vector<GraphicBuffer> slots;
   void push(GraphicBuffer slot){
       slots.push(slot);
   }

   GraphicBuffer pull(){
       return slots.pull();
   }
}

生产者生产完数据后,通过调用BufferQueue的push函数将数据插入到vector中。消费者调用BufferQueue的pull函数出队一个Buffer数据。

上述实现的问题在于,生产者每次都需要自行创建GraphicBuffer,而消费者每次消费完数据后的GraphicBuffer就被释放了,GraphicBuffer没有得到循环利用。而在Android中,由于BufferQueue的生产者-消费者往往处于不同的进程,GraphicBuffer内部是需要通过共享内存来连接生成者-消费者进程的,每次创建GraphicBuffer,即意味着需要创建共享内存,效率较低。

而BufferQueue中用BufferState来表示GraphicBuffer的状态则解决了这个问题。每个GraphicBuffer都有当前的状态,通过维护GraphicBuffer的状态,完成GraphicBuffer的复用。

由于BufferQueue内部实现是BufferQueueCore,下文均用BufferQueueCore代替BufferQueue。先介绍下BufferQueueCore内部相应的数据结构,再介绍BufferQueue的状态扭转过程和生产-消费过程。

以下是Buffer的入队/出队操作和BufferState的状态扭转的过程,这里只介绍非同步阻塞模式。

2.1 BufferQueueCore内部数据结构

核心数据结构如下:

BufferQueueDefs::SlotsType mSlots:用数组存放的Slot,数组默认大小为BufferQueueDefs::NUM_BUFFER_SLOTS,具体是64,代表所有的Slot
std::set<int> mFreeSlots:当前所有的状态为FREE的Slot,这些Slot没有关联上具体的GraphicBuffer,后续用的时候还需要关联上GraphicBuffer
std::list<int> mFreeBuffers:当前所有的状态为FREE的Slot,这些Slot已经关联上具体的GraphicBuffer,可以直接使用
Fifo mQueue:一个先进先出队列,保存了生产者生产的数据

在BufferQueueCore初始化时,由于此时队列中没有入队任何数据,按照上面的介绍,此时mFreeSlots应该包含所有的Slot,元素大小和mSlots一致,初始化代码如下:

for (int slot = 0; slot < BufferQueueDefs::NUM_BUFFER_SLOTS; ++slot) {
        mFreeSlots.insert(slot);
    }
2.2 生产者dequeueBuffer

当生产者可以生产图形数据时,首先向BufferQueue中申请一块GraphicBuffer。调用函数BufferQueueProducer.dequeueBuffer,如果当前BufferQueue中有可用的GraphicBuffer,则返回其对用的索引;如果不存在,则返回-1,代码在BufferQueueProducer,流程如下:

status_t BufferQueueProducer::dequeueBuffer(int *outSlot,
        sp<android::Fence> *outFence, bool async,
        uint32_t width, uint32_t height, PixelFormat format, uint32_t usage) {

             //1. 寻找可用的Slot,可用指Buffer状态为FREE
             status_t status = waitForFreeSlotThenRelock("dequeueBuffer", async,
                    &found, &returnFlags);
            if (status != NO_ERROR) {
                return status;
            }
            //2.找到可用的Slot,将Buffer状态设置为DEQUEUED,由于步骤1找到的Slot状态为FREE,因此这一步完成了FREE到DEQUEUED的状态切换
            *outSlot = found;
            ATRACE_BUFFER_INDEX(found);
            attachedByConsumer = mSlots[found].mAttachedByConsumer;
            mSlots[found].mBufferState = BufferSlot::DEQUEUED;
            //3. 找到的Slot如果需要申请GraphicBuffer,则申请GraphicBuffer,这里采用了懒加载机制,如果内存没有申请,申请内存放在生产者来处理
            if (returnFlags & BUFFER_NEEDS_REALLOCATION) {
                status_t error;
                sp<GraphicBuffer> graphicBuffer(mCore->mAllocator->createGraphicBuffer(width, height, format, usage, &error));
                graphicBuffer->setGenerationNumber(mCore->mGenerationNumber);
                mSlots[*outSlot].mGraphicBuffer = graphicBuffer;
            }
}

关键在于寻找可用Slot,waitForFreeSlotThenRelock的流程如下:

status_t BufferQueueProducer::waitForFreeSlotThenRelock(const char* caller,
        bool async, int* found, status_t* returnFlags) const {

    //1. mQueue 是否太多
    bool tooManyBuffers = mCore->mQueue.size()> static_cast<size_t>(maxBufferCount);
        if (tooManyBuffers) {

        } else {
            // 2. 先查找mFreeBuffers中是否有可用的,由2.1介绍可知,mFreeBuffers中的元素关联了GraphicBuffer,直接可用
            if (!mCore->mFreeBuffers.empty()) {
                auto slot = mCore->mFreeBuffers.begin();
                *found = *slot;
                mCore->mFreeBuffers.erase(slot);
            } else if (mCore->mAllowAllocation && !mCore->mFreeSlots.empty()) {
                // 3. 再查找mFreeSlots中是否有可用的,由2.1可知,初始化时会填充满这个列表,因此第一次调用一定不会为空。同时用这个列表中的元素需要关联上GraphicBuffer才可以直接使用,关联的过程由外层函数来实现
                auto slot = mCore->mFreeSlots.begin();
                // Only return free slots up to the max buffer count
                if (*slot < maxBufferCount) {
                    *found = *slot;
                    mCore->mFreeSlots.erase(slot);
                }
            }
        }

         tryAgain = (*found == BufferQueueCore::INVALID_BUFFER_SLOT) ||
                   tooManyBuffers;
        //4. 如果找不到可用的Slot或者Buffer太多(同步阻塞模式下),则可能需要等
        if (tryAgain) {
            if (mCore->mDequeueBufferCannotBlock &&
                    (acquiredCount <= mCore->mMaxAcquiredBufferCount)) {
                return WOULD_BLOCK;
            }
            mCore->mDequeueCondition.wait(mCore->mMutex);
        }
}

waitForFreeSlotThenRelock函数会尝试寻找一个可用的Slot,可用的Slot状态一定是FREE(因为是从两个FREE状态的列表中获取的),然后dequeueBuffer将状态改变为DEQUEUED,即完成了状态的扭转。

waitForFreeSlotThenRelock返回可用的Slot分为两种:

  1. 从mFreeBuffers中获取到的,mFreeBuffers中的元素关联了GraphicBuffer,直接可用

  2. 从mFreeSlots中获取到的,没有关联上GraphicBuffer,因此需要申请GraphicBuffer并和Slot关联上,通过createGraphicBuffer申请一个GraphicBuffer,然后赋值给Slot的mGraphicBuffer完成关联

小结dequeueBuffer:尝试找到一个Slot,并完成Slot与GraphicBuffer的关联(如果需要),然后将Slot的状态由FREE扭转成DEQUEUED,返回Slot在BufferQueueCore中mSlots对应的索引。

2.3 生产者requestBuffer

dequeueBuffer函数获取到了可用Slot的索引后,通过requestBuffer获取到对应的GraphicBuffer。流程如下:

status_t BufferQueueProducer::requestBuffer(int slot, sp<GraphicBuffer>* buf) {

    // 1. 判断slot参数是否合法
    if (slot < 0 || slot >= BufferQueueDefs::NUM_BUFFER_SLOTS) {
        BQ_LOGE("requestBuffer: slot index %d out of range [0, %d)",
                slot, BufferQueueDefs::NUM_BUFFER_SLOTS);
        return BAD_VALUE;
    } else if (mSlots[slot].mBufferState != BufferSlot::DEQUEUED) {
        BQ_LOGE("requestBuffer: slot %d is not owned by the producer "
                "(state = %d)", slot, mSlots[slot].mBufferState);
        return BAD_VALUE;
    }

    //2. 将mRequestBufferCalled置为true
    mSlots[slot].mRequestBufferCalled = true;
    *buf = mSlots[slot].mGraphicBuffer;
    return NO_ERROR;
}

这一步不是必须的,业务层可以直接通过Slot的索引获取到对应的GraphicBuffer。

2.4 生产者queueBuffer

上文dequeueBuffer获取到一个Slot后,就可以在Slot对应的GraphicBuffer上完成图像数据的生产了,可以是View的主线程Draw过程,也可以是SurfaceView的子线程绘制过程,甚至可以是MediaCodec的解码过程。

填充完图像数据后,需要将Slot入队BufferQueueCore(数据写完了,可以传给生产者-消费者队列,让消费者来消费了),入队调用queueBuffer函数。queueBuffer的流程如下:

status_t BufferQueueProducer::queueBuffer(int slot,
        const QueueBufferInput &input, QueueBufferOutput *output) {

        // 1. 先判断传入的Slot是否合法
        if (slot < 0 || slot >= maxBufferCount) {
            BQ_LOGE("queueBuffer: slot index %d out of range [0, %d)",
                    slot, maxBufferCount);
            return BAD_VALUE;
        }

        //2. 将Buffer状态扭转成QUEUED,此步完成了Buffer的状态由DEQUEUED到QUEUED的过程
        mSlots[slot].mFence = fence;
        mSlots[slot].mBufferState = BufferSlot::QUEUED;
        ++mCore->mFrameCounter;
        mSlots[slot].mFrameNumber = mCore->mFrameCounter;

        //3. 入队mQueue
        if (mCore->mQueue.empty()) {
            mCore->mQueue.push_back(item);
            frameAvailableListener = mCore->mConsumerListener;
        } 

        // 4. 回调frameAvailableListener,告知消费者有数据入队了
        if (frameAvailableListener != NULL) {
            frameAvailableListener->onFrameAvailable(item);
        } else if (frameReplacedListener != NULL) {
            frameReplacedListener->onFrameReplaced(item);
        }
}

从上面的注释可以看到,queueBuffer的主要步骤如下:

  1. 将Buffer状态扭转成QUEUED,此步完成了Buffer的状态由DEQUEUED到QUEUED的过程

  2. 将Buffer入队到BufferQueueCore的mQueue队列中

  3. 回调frameAvailableListener,告知消费者有数据入队,可以来消费数据了,frameAvailableListener是消费者注册的回调

小结queueBuffer:将Slot的状态扭转成QUEUED,并添加到mQueue中,最后通知消费者有数据入队。

2.5 消费者acquireBuffer

在消费者接收到onFrameAvailable回调时或者消费者主动想要消费数据,调用acquireBuffer尝试向BufferQueueCore获取一个数据以供消费。消费者的代码在BufferQueueConsumer中,acquireBuffer流程如下:

status_t BufferQueueConsumer::acquireBuffer(BufferItem* outBuffer,
        nsecs_t expectedPresent, uint64_t maxFrameNumber) {

        //1. 如果队列为空,则直接返回
        if (mCore->mQueue.empty()) {
            return NO_BUFFER_AVAILABLE;
        }

        //2. 取出mQueue队列的第一个元素,并从队列中移除
        BufferQueueCore::Fifo::iterator front(mCore->mQueue.begin());
           int slot = front->mSlot;
        *outBuffer = *front;
        mCore->mQueue.erase(front);

        //3. 处理expectedPresent的情况,这种情况可能会连续丢几个Slot的“显示”时间小于expectedPresent的情况,这种情况下这些Slot已经是“过时”的,直接走下文的releaseBuffer消费流程,代码比较长,忽略了
              

        //4. 更新Slot的状态为ACQUIRED
        if (mCore->stillTracking(front)) {
            mSlots[slot].mAcquireCalled = true;
            mSlots[slot].mNeedsCleanupOnRelease = false;
            mSlots[slot].mBufferState = BufferSlot::ACQUIRED;
            mSlots[slot].mFence = Fence::NO_FENCE;
        }

        //5. 如果步骤3有直接releaseBuffer的过程,则回调生产者,有数据被消费了
        if (listener != NULL) {
            for (int i = 0; i < numDroppedBuffers; ++i) {
                listener->onBufferReleased();
            }
        }

}

从上面的注释可以看到,acquireBuffer的主要步骤如下:

  1. 从mQueue队列中取出并移除一个元素

  2. 改变Slot对应的状态为ACQUIRED

  3. 如果有丢帧逻辑,回调告知生产者有数据被消费,生产者可以准备生产数据了

小结acquireBuffer:将Slot的状态扭转成ACQUIRED,并从mQueue中移除,最后通知生产者有数据出队。

2.6 消费者releaseBuffer

消费者获取到Slot后开始消费数据(典型的消费如SurfaceFlinger的UI合成),消费完毕后,需要告知BufferQueueCore这个Slot被消费者消费完毕了,可以给生产者重新生产数据,releaseBuffer流程如下:

status_t BufferQueueConsumer::releaseBuffer(int slot, uint64_t frameNumber,
        const sp<Fence>& releaseFence, EGLDisplay eglDisplay,EGLSyncKHR eglFence) {

         //1. 检查Slot是否合法
        if (slot < 0 || slot >= BufferQueueDefs::NUM_BUFFER_SLOTS ||         
            return BAD_VALUE;
        }

        //2. 容错处理:如果要处理的Slot存在于mQueue中,那么说明这个Slot的来源不合法,并不是从2.5的acquireBuffer获取的Slot,拒绝处理
        BufferQueueCore::Fifo::iterator current(mCore->mQueue.begin());
        while (current != mCore->mQueue.end()) {
            if (current->mSlot == slot) {
                return BAD_VALUE;
            }
            ++current;
        } 

         // 3. 将Slot的状态扭转为FREE,之前是ACQUIRED,并将该Slot添加到BufferQueueCore的mFreeBuffers列表中(mFreeBuffers的定义参考2.1的介绍)
         if (mSlots[slot].mBufferState == BufferSlot::ACQUIRED) {
                mSlots[slot].mEglDisplay = eglDisplay;
                mSlots[slot].mEglFence = eglFence;
                mSlots[slot].mFence = releaseFence;
                mSlots[slot].mBufferState = BufferSlot::FREE;
                mCore->mFreeBuffers.push_back(slot);
                listener = mCore->mConnectedProducerListener;
                BQ_LOGV("releaseBuffer: releasing slot %d", slot);
            }

           // 4. 回调生产者,有数据被消费了
           if (listener != NULL) {
               listener->onBufferReleased();
           }
}

从上面的注释可以看到,releaseBuffer的主要步骤如下:

  1. 将Slot的状态扭转为FREE

  2. 将被消费的Slot添加到mFreeBuffers供后续的生产者dequeueBuffer使用

  3. 回调告知生产者有数据被消费,生产者可以准备生产数据了

小结releaseBuffer:将Slot的状态扭转成FREE,并添加到BufferQueueCore mFreeBuffers队列中,最后通知生产者有数据出队。

总结下状态变化的过程:

BufferQueue的设计思想和内部实现方法是什么

关于“BufferQueue的设计思想和内部实现方法是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注编程网行业资讯频道,小编每天都会为大家更新不同的知识点。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

BufferQueue的设计思想和内部实现方法是什么

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

BufferQueue的设计思想和内部实现方法是什么

这篇文章主要介绍“BufferQueue的设计思想和内部实现方法是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“BufferQueue的设计思想和内部实现方法是什么”文章能帮助大家解决问题。1.
2023-06-05

Linux内核设计与实现的方法是什么

今天小编给大家分享一下Linux内核设计与实现的方法是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。 Unix强大的根本
2023-06-16

java内部类实例化的方法是什么

Java内部类实例化的方法有两种:1. 非静态内部类实例化:外部类实例名.内部类实例名 = 外部类实例名.new 内部类构造方法();示例代码:```public class OuterClass {public class InnerCl
2023-09-26

Go连接池设计与实现的方法是什么

这篇“Go连接池设计与实现的方法是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Go连接池设计与实现的方法是什么”文章吧
2023-07-06

Android顶部标题栏的布局设计方法是什么

这篇“Android顶部标题栏的布局设计方法是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Android顶部标题栏的布
2023-06-26

redis计数器实现的方法是什么

Redis计数器可以通过以下几种方法实现:使用INCR命令:Redis提供了INCR命令来对一个键的值进行递增操作,可以用来实现计数器的功能。例如:INCR counter使用INCRBY命令:类似于INCR命令,但可以指定递增的步长。例如
redis计数器实现的方法是什么
2024-03-11

k8s部署redis集群实现的方法是什么

这篇文章主要介绍“k8s部署redis集群实现的方法是什么”,在日常操作中,相信很多人在k8s部署redis集群实现的方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”k8s部署redis集群实现的方法
2023-07-05

实现Runnable接口的多线程程序设计方法是什么

这篇文章将为大家详细讲解有关实现Runnable接口的多线程程序设计方法是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。实现Runnable接口的多线程程序设计方法  Java语言中提供
2023-06-03

PHP运行内存设置的方法和原理是什么

今天小编给大家分享一下PHP运行内存设置的方法和原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。一、什么是 PHP
2023-07-05

基于Linux内核新特性的网关设计方法是什么

今天小编给大家分享一下基于Linux内核新特性的网关设计方法是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。UCloud
2023-06-16

面试官:Redis中字符串的内部实现方式是什么?

在面试间里等候时,感觉这可真暖和呀,我那冰冷的出租屋还得盖两层被子才能睡着。正要把外套脱下来,我突然听到了门外的脚步声,随即门被打开,穿着干净满脸清秀的青年走了进来,一股男士香水的淡香扑面而来。面试官:Redis中基本的数据类型有哪些?我:Redis的基本数据
面试官:Redis中字符串的内部实现方式是什么?
2018-02-24

C++中内存池的原理及实现方法是什么

这篇文章主要讲解了“C++中内存池的原理及实现方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C++中内存池的原理及实现方法是什么”吧!为什么要用内存池C++程序默认的内存管理(ne
2023-07-05

Redis快速部署为Docker容器的实现方法是什么

Redis快速部署为Docker容器的实现方法是什么,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Redis是一种内存键值存储,可以保存高性能的抽象数据结构。开源软件通常用
2023-06-22

vue中实现全页面或局部刷新的方法是什么

今天就跟大家聊聊有关vue中实现全页面或局部刷新的方法是什么,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。vue中怎么实现页面刷新一.全页面刷新1.修改 App.vue,代码如下:<
2023-06-22

大数据报表合计需求的实现方法是什么

这篇文章的内容主要围绕大数据报表合计需求的实现方法是什么进行讲述,文章内容清晰易懂,条理清晰,非常适合新手学习,值得大家去阅读。感兴趣的朋友可以跟随小编一起阅读吧。希望大家通过这篇文章有所收获!一、常用合计方案在有专业报表工具之前,常用的实
2023-06-04

MD5算法原理及C#和JS实现的方法是什么

本篇内容主要讲解“MD5算法原理及C#和JS实现的方法是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“MD5算法原理及C#和JS实现的方法是什么”吧!一、简介MD5 是哈希算法(散列算法)的
2023-07-05

MySQL内连接、外连接及SQL JOINS的实现方法是什么

这篇文章主要讲解了“MySQL内连接、外连接及SQL JOINS的实现方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“MySQL内连接、外连接及SQL JOINS的实现方法是什么”吧
2023-07-05

Python操作xmind文件实现思维导图绘制的方法是什么

这篇文章主要讲解了“Python操作xmind文件实现思维导图绘制的方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Python操作xmind文件实现思维导图绘制的方法是什么”吧!思
2023-06-25

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录