iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >BufferQueue的设计思想和内部实现方法是什么
  • 861
分享到

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

2023-06-05 02:06:54 861人浏览 独家记忆
摘要

这篇文章主要介绍“BufferQueue的设计思想和内部实现方法是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“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的设计思想和内部实现方法是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注编程网精选频道,小编每天都会为大家更新不同的知识点。

--结束END--

本文标题: BufferQueue的设计思想和内部实现方法是什么

本文链接: https://www.lsjlt.com/news/240754.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

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

下载Word文档
猜你喜欢
  • BufferQueue的设计思想和内部实现方法是什么
    这篇文章主要介绍“BufferQueue的设计思想和内部实现方法是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“BufferQueue的设计思想和内部实现方法是什么”文章能帮助大家解决问题。 背...
    99+
    2023-06-05
  • Linux内核设计与实现的方法是什么
    今天小编给大家分享一下Linux内核设计与实现的方法是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。 Unix...
    99+
    2023-06-16
  • 关系数据库的设计思想是什么
    本篇内容介绍了“关系数据库的设计思想是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在计算机领域有许多...
    99+
    2024-04-02
  • java内部类实例化的方法是什么
    Java内部类实例化的方法有两种:1. 非静态内部类实例化:外部类实例名.内部类实例名 = 外部类实例名.new 内部类构造方法()...
    99+
    2023-09-26
    java
  • 责任链设计模式的实现方法是什么
    本篇内容介绍了“责任链设计模式的实现方法是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!责任链设计模式...
    99+
    2024-04-02
  • Go连接池设计与实现的方法是什么
    这篇“Go连接池设计与实现的方法是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Go连接池设计与实现的方法是什么”文章吧...
    99+
    2023-07-06
  • Kubernetes中锁机制的设计与实现方法是什么
    这篇文章主要讲解了“Kubernetes中锁机制的设计与实现方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Kubernetes中锁机制的设计与实现...
    99+
    2024-04-02
  • Android顶部标题栏的布局设计方法是什么
    这篇“Android顶部标题栏的布局设计方法是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Android顶部标题栏的布...
    99+
    2023-06-26
  • Java内部类在GUI设计中的作用是什么
    这篇文章主要讲解了“Java内部类在GUI设计中的作用是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java内部类在GUI设计中的作用是什么”吧!Java内部类其实在J2EE编程中使用...
    99+
    2023-06-17
  • Ajax的内部实现机制是什么
    Ajax的内部实现机制主要涉及以下几个方面:1. XMLHttpRequest对象:Ajax通过XMLHttpRequest对象与服...
    99+
    2023-08-16
    Ajax
  • C++高并发内存池的整体设计和实现思路
    目录一、整体设计1、需求分析2、总体设计思路3、申请内存流程图二、详细设计1、各个模块内部结构详细剖析2、设计细节三、测试一、整体设计 1、需求分析 池化技术是计算机中的一种设计模式...
    99+
    2024-04-02
  • redis计数器实现的方法是什么
    Redis计数器可以通过以下几种方法实现: 使用INCR命令:Redis提供了INCR命令来对一个键的值进行递增操作,可以用来实现...
    99+
    2024-03-11
    redis
  • java内部类调用的方法是什么
    在Java中,内部类调用的方法可以是外部类的方法,也可以是内部类自身的方法。如果内部类想要调用外部类的方法,可以使用以下语法:```...
    99+
    2023-10-08
    java
  • java创建内部类的方法是什么
    在Java中创建内部类的方法有两种:1. 非静态内部类(成员内部类):在外部类的成员方法中创建内部类的对象。```public cl...
    99+
    2023-10-12
    java
  • Mysql锁的内部实现机制是什么
    这篇“Mysql锁的内部实现机制是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Mys...
    99+
    2024-04-02
  • 数据库设计的方法是什么
    本篇内容主要讲解“数据库设计的方法是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“数据库设计的方法是什么”吧!数据库设计1. 为什么要设计数据库节省数据的存储空间保证数据的完整性方便根据数据...
    99+
    2023-06-16
  • Java泛型的设计方法是什么
    这篇文章主要讲解了“Java泛型的设计方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java泛型的设计方法是什么”吧!引言泛型是Java中一个非常重要的知识点,在Java集合类框架...
    99+
    2023-06-17
  • 实现Runnable接口的多线程程序设计方法是什么
    这篇文章将为大家详细讲解有关实现Runnable接口的多线程程序设计方法是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。实现Runnable接口的多线程程序设计方法  Java语言中提供...
    99+
    2023-06-03
  • PHP分布式架构的设计思路与实现方法
    随着互联网业务的不断发展和用户量的不断增加,单机架构已经无法满足业务需求,分布式架构成为了目前互联网行业的主流趋势之一。PHP作为一种流行的后端开发语言,也需要在分布式架构中发挥作用。本文将介绍PHP分布式架构的设计思路和实现方法。 一、...
    99+
    2023-09-03
    分布式 npm numy
  • springboot修改内部文件的方法是什么
    在Spring Boot中,可以使用以下方法来修改内部文件:1. 使用`java.nio.file.Files`类的`write`方...
    99+
    2023-09-25
    springboot
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作