Android codec2 视频框架之输出端的内存管理

news2024/11/17 13:46:40

文章目录

      • 前言
      • setSurface
      • start
      • 从哪个pool中申请buffer
      • 解码后框架的处理流程
      • renderOutbuffer 输出显示

前言

在这里插入图片描述

输出buffer整体的管理流程主要可以分为三个部分:

  1. MediaCodc 和 应用之间的交互 包括设置Surface、解码输出回调到MediaCodec。将输出buffer render或者releas到surface。
  2. MediaCodec到CCodecBufferChannel,主要是传递控制命令
  3. CCodecbufferChannel到componet buffer的封装 传递 控制等等。
  4. componet到bufferqueuepool buffer的申请

外部设置Surface进来,然后把输入buffer 输入,等待输出buffer 的回调,回调回来后 根据音视频同步的策略。在合适的时机renderOutput 送到MediaCodec。

需要了解的几个方面

  1. setSurface内部做了什么处理。
  2. 什么时候有输出的buffer可用?
  3. 输出buffer render到MediaCodec.内部做了什么处理。

setSurface

外部的setSurface调用到 MediaCodec的kWhatSetSurface

  1. MediaCodec::setSurface
    调用下面的connetToSurface 对surface进行连接
 nativeWindowConnect(surface.get(), "connectToSurface(reconnect)");

  1. CCodecBufferChannel::setSurface

根据bufferChanned的信息配置surface,比如配置deuque buffer 的超时时间、
dequeue最大的buffer数,当然这些值在后续可能还会改变,后续在解码器中解码出来的delay改变的话 回重新设置这个delay,
然后在handlework 重新设置最大的可dequeue的buffer 数。赋值mOutputSurface的相关变量。

        Mutexed<OutputSurface>::Locked output(mOutputSurface);
        output->surface = newSurface;
        output->generation = generation;
  1. 设置surface到 C2BufferQueueBlockPool 用于后续的解码buffer的申请

在ccodecbufferchannel的start中调用configureProducer设置外部surface的GraphicBufferProducer到
bufferQueueBlockpopl中。

          outputSurface = output->surface ?
                    output->surface->getIGraphicBufferProducer() : nullptr;


        if (outputSurface) {
            mComponent->setOutputSurface(
                    outputPoolId_,
                    outputSurface,
                    outputGeneration,
                    maxDequeueCount);
        }

Return<Status> Component::setOutputSurface(
        uint64_t blockPoolId,
        const sp<HGraphicBufferProducer2>& surface) {
    std::shared_ptr<C2BlockPool> pool;
    GetCodec2BlockPool(blockPoolId, mComponent, &pool);
    if (pool && pool->getAllocatorId() == C2PlatformAllocatorStore::BUFFERQUEUE) {
        if (bqPool) {
            bqPool->setRenderCallback(cb);
            bqPool->configureProducer(surface);
        }
    }
    return Status::OK;
}

void configureProducer(const sp<HGraphicBufferProducer> &producer,
                       native_handle_t *syncHandle,
                       uint64_t producerId,
                       uint32_t generation,
                       uint64_t usage,
                       bool bqInformation) {        
          if (producer) {
                mProducer = producer;
                mProducerId = producerId;
                mGeneration = bqInformation ? generation : 0;
         }

}

start

start 中跟输出buffer 相关的主要是两个方面

  1. 可以从surface最大能够dequeue出的buffer 数。由4个值组成 其中
    kSmoothnessFactor为4 kRenderingDepth为3。outputDelay由各个解码组件进行设置
    比如h264的默认设置为8, 同时会在解码过程handlework进行重新设置。

具体来说:

  • 在解码组件中解析到相关的reorder系数变化时 将系统放到输出的work中携带出去。
    在外部的ccodebufferchannel 中取出系统设置到surface中。
  • 实现动态的控制surface最大可以dequeue的buffer 数量。 外部通过dequeue申请的最大的buffer数是通过surface的setMaxDequeuedBufferCount
    设置到bufferqueueproducter 中,后续调用dequeue的时候会进行判断。
  • 比如解码器会重新设置为i4_reorder_depth。i4_reorder_depth 是什么? 怎么赋值的?(显示次序在某帧图像之后,解码次序在某帧图像之前的图像数量的最大值。因为编码器中的B帧不仅有前向参考,还有后向参考。 后向参考要求当前图像编码前,参考的后向图像已经编码完成,所以会导致图像的编码顺序和显示顺序不一样。)hevc 是存储在sps的sps_max_num_reorder_pics语言当中。
hevc'解码为例
ps_dec_op->i4_reorder_depth =
ps_sps->ai1_sps_max_num_reorder_pics[ps_sps->i1_sps_max_sub_layers - 1];

mOutputDelay = ps_decode_op->i4_reorder_depth;
ALOGV("New Output delay %d ", mOutputDelay);
C2PortActualDelayTuning::output outputDelay(mOutputDelay);
std::vector<std::unique_ptr<C2SettingResult>> failures;
c2_status_t err =
    mIntf->config({&outputDelay}, C2_MAY_BLOCK, &failures);
if (err == OK) {
    work->worklets.front()->output.configUpdate.push_back(
        C2Param::Copy(outputDelay));
 }
 
 
bool CCodecBufferChannel::handleWork(
        std::unique_ptr<C2Work> work,
        const sp<AMessage> &outputFormat,
        const C2StreamInitDataInfo::output *initData) {
        
while (!worklet->output.configUpdate.empty()) {
        std::unique_ptr<C2Param> param;
        worklet->output.configUpdate.back().swap(param);
        worklet->output.configUpdate.pop_back();

                if (param->forOutput()) {
                    C2PortActualDelayTuning::output outputDelay;
                    if (outputDelay.updateFrom(*param)) {
                        ALOGE("[%s] onWorkDone: updating output delay %u",
                              mName, outputDelay.value);
                        (void)mPipelineWatcher.lock()->outputDelay(outputDelay.value);
                        newOutputDelay = outputDelay.value;
                        needMaxDequeueBufferCountUpdate = true;
                    }
                }
                break;

    if (needMaxDequeueBufferCountUpdate) {
        int maxDequeueCount = 0;
        {
            Mutexed<OutputSurface>::Locked output(mOutputSurface);
            maxDequeueCount = output->maxDequeueBuffers =
                    numOutputSlots + reorderDepth + kRenderingDepth;
            if (output->surface) {
                output->surface->setMaxDequeuedBufferCount(output->maxDequeueBuffers);
            }
        }
        if (maxDequeueCount > 0) {
            mComponent->setOutputSurfaceMaxDequeueCount(maxDequeueCount);
        }
    }





}

constexpr size_t kSmoothnessFactor = 4;
constexpr size_t kRenderingDepth = 3;

    C2PortActualDelayTuning::output outputDelay(0);
    c2_status_t err = mComponent->query(
            {
                &iStreamFormat,
                &oStreamFormat,
                &kind,
                &reorderDepth,
                &reorderKey,
                &inputDelay,
                &pipelineDelay,
                &outputDelay,
                &secureMode,
            },
            {},
            C2_DONT_BLOCK,
            nullptr);
        size_t numOutputSlots = outputDelayValue + kSmoothnessFactor

        sp<IGraphicBufferProducer> outputSurface;
        uint32_t outputGeneration;
        int maxDequeueCount = 0;
        {
            Mutexed<OutputSurface>::Locked output(mOutputSurface);
            maxDequeueCount = output->maxDequeueBuffers = numOutputSlots +
                    reorderDepth.value + kRenderingDepth;
            outputSurface = output->surface ?
                    output->surface->getIGraphicBufferProducer() : nullptr;
            if (outputSurface) {
                output->surface->setMaxDequeuedBufferCount(output->maxDequeueBuffers);
            }
            outputGeneration = output->generation;

constexpr uint32_t kDefaultOutputDelay = 8;

        addParameter(
                DefineParam(mActualOutputDelay, C2_PARAMKEY_OUTPUT_DELAY)
                .withDefault(new C2PortActualDelayTuning::output(kDefaultOutputDelay))
                .withFields({C2F(mActualOutputDelay, value).inRange(0, kMaxOutputDelay)})
                .withSetter(Setter<decltype(*mActualOutputDelay)>::StrictValueWithNoDeps)
                .build());



   if (ps_decode_op->i4_reorder_depth >= 0 && mOutputDelay != ps_decode_op->i4_reorder_depth) {
            mOutputDelay = ps_decode_op->i4_reorder_depth;
            ALOGV("New Output delay %d ", mOutputDelay);
            C2PortActualDelayTuning::output outputDelay(mOutputDelay);
            std::vector<std::unique_ptr<C2SettingResult>> failures;
            c2_status_t err =
                mIntf->config({&outputDelay}, C2_MAY_BLOCK, &failures);
            if (err == OK) {
                work->worklets.front()->output.configUpdate.push_back(
                    C2Param::Copy(outputDelay));
            } else {
                ALOGE("Cannot set output delay");
                mSignalledError = true;
                work->workletsProcessed = 1u;
                work->result = C2_CORRUPTED;
                return;
            }
        }


从哪个pool中申请buffer

在CCodecBufferChannel::start的时候决定,在下面代码中将pools的allocatedID转为
C2BufferQueueBlockPool。 在这之后调用mComponent->createBlockPool。Codec2Client::Component::createBlockPool调用c2store的c2_status_t createBlockPool()然后调用_createBlockPool,在之前设置了是BUFFERQUEUE,这边就保存了创建好的C2BufferQueueBlockPool。 在后面解码的流程中fetchGrallocBlock,使用的是这个类型的
C2BufferQueueBlockPool。

poolmask的默认值:

int GetCodec2PoolMask() {
    return property_get_int32(
            "debug.stagefright.c2-poolmask",
            1 << C2PlatformAllocatorStore::ION |
            1 << C2PlatformAllocatorStore::BUFFERQUEUE);
}

int poolMask = GetCodec2PoolMask();

申请的buffer的类型函数是bufferqueue

if (pools->outputAllocatorId == C2PlatformAllocatorStore::GRALLOC
&& err != C2_OK
&& ((poolMask >> C2PlatformAllocatorStore::BUFFERQUEUE) & 1)) {
pools->outputAllocatorId = C2PlatformAllocatorStore::BUFFERQUEUE;
}
}


bufferqueue的申请调用的是C2PlatformAllocatorStoreImpl的fetchAllocator

case C2PlatformAllocatorStore::BUFFERQUEUE:
res = allocatorStore->fetchAllocator(
C2PlatformAllocatorStore::BUFFERQUEUE, &allocator);
if (res == C2_OK) {
std::shared_ptr<C2BlockPool> ptr(
new C2BufferQueueBlockPool(allocator, poolId), deleter);
*pool = ptr;
mBlockPools[poolId] = ptr;
mComponents[poolId].insert(
mComponents[poolId].end(),
components.begin(), components.end());
}
break;

fetchAllocator返回gralloc的allocator。

std::shared_ptr<C2Allocator> C2PlatformAllocatorStoreImpl::fetchBufferQueueAllocator() {
    static std::mutex mutex;
    static std::weak_ptr<C2Allocator> grallocAllocator;
    std::lock_guard<std::mutex> lock(mutex);
    std::shared_ptr<C2Allocator> allocator = grallocAllocator.lock();
    if (allocator == nullptr) {
        allocator = std::make_shared<C2AllocatorGralloc>(
                C2PlatformAllocatorStore::BUFFERQUEUE, true);
        grallocAllocator = allocator;
    }
    return allocator;
}


### fetchGraphicBlock 流程

  • fetchGraphicBlock

fetch经过一系列判断和处理 最终调用mProducer的dequeueBuffer

    c2_status_t fetchGraphicBlock(
            uint32_t width,
            uint32_t height,
            uint32_t format,
            C2MemoryUsage usage,
            std::shared_ptr<C2GraphicBlock> *block /* nonnull */,
            C2Fence *fence) {

c2_status_t status = fetchFromIgbp_l(width, height, format, usage, block, fence);
             c2Status = dequeueBuffer(width, height, format, usage,
                              &slot, &bufferNeedsReallocation, &fence);                         
        if (fence) {
            static constexpr int kFenceWaitTimeMs = 10;
            status_t status = fence->wait(kFenceWaitTimeMs);
     }
               
    其中 dequeueBuffer
    
        Return<void> transResult = mProducer->dequeueBuffer(
                Input{
                    width,
                    height,
                    format,
                    androidUsage.asGrallocUsage()},
                [&status, slot, needsRealloc,
                 fence](HStatus hStatus,
                         int32_t hSlot,
                         Output const& hOutput) {
                    *slot = static_cast<int>(hSlot);
                    if (!h2b(hStatus, &status) ||
                            !h2b(hOutput.fence, fence)) {
                        status = ::android::BAD_VALUE;
                    } else {
                        *needsRealloc =
                                hOutput.bufferNeedsReallocation;
                    }
                });




  • dequebuffer中的fence 有什么作用

Fence是一种同步机制,用于GraphicBuffer的同步。用来处理跨硬件平台不同的情况(CPU和GPU),尤其是CPU、GPU和HWC之间的同步。另外,也可用于多个时间点之间的同步,当Graphics Buffer的生产者或消费者在对buffer处理完之后,通过fence发出信号,这样系统可以异步queue当前不需要但有可能接下来会使用读写的buffer。

简言之,在合适的时间发一种信号,将先到的buffer拦住,等后来的到达,两者步调一致再一起走。也就是dequeuebuffer 之后并不能直接用这块buffer,需要等待buffer的fence发送上来之后 才可以使用这块buffer。

  • 完整一个获取fetch buffer的流程

dequeueBuffer ---->(获取到slot或fence) fence->wait -----> mProducer->requestBuffer(通过slot 获取到buffer)

将从gralloc 获取到的buffer (native_handle_t)通过调用android::WrapNativeCodec2GrallocHandle转化为C2Handle
这个C2Handle 会生成C2AllocationGralloc,这个alloc最后会new 封装成C2GraphicBlock。这个block就是返回给外部解码申请的地方。

经过上面的这个流程 解码要的共享的buffer 就从gralloc这边申请出来了,然后这个buffer就可以给到后面的解码器使用了,如果是软解就map出虚拟地址,然后将软解后的数据拷贝到里面。但一般厂商不会用软解,正常的实现是这块buffer给到硬件,硬解数据直接写到这块buffer。

解码的buffer准备好之后,会把grallocblock的buffer 转换为c2buffer 然后会放到c2work中output buffers里面。

std::shared_ptr<C2Buffer> buffer
= createGraphicBuffer(std::move(entry->outblock),
C2Rect(mWidth, mHeight).at(left, top));

解码后框架的处理流程

解码后的哪些信息是携带在work里面的, 解码的buffer,

work->worklets.front()->output.flags = (C2FrameData::flags_t)0;
work->worklets.front()->output.buffers.clear();
work->worklets.front()->output.buffers.push_back(buffer);
work->worklets.front()->output.ordinal = work->input.ordinal;
work->workletsProcessed = 1u;

  • 从应用开始, 应用调用的是dequeueOutputBuffer返回的是index 时间戳等等信息,这个调用到mediacodec, mediacodec 从 mAvailPortBuffers 取出可用的buffer。
  • mAvailPortBuffers是通过解码那边 BufferCallback onOutputBufferAvailable来把解码buffer push 到mAvailPortBuffers。这个回调是simpleC2Componet 的finish的listener->onWorkDone_nb调用到CCodec的onWorkDone。
  • onWorkDone调用到mChannel->onWorkDone。 在mChannel的workDone 中 调用handleWork。
  • handlework 里面将解码器传递在work 中outputbuffer 转换为mediacodec的用的index 和 mediaCodecbuffer。同时返回到MediaCodec之前设置的callback。这个最后会返回应用设置callback的地方。
mCallback->onOutputBufferAvailable(index, outBuffer);

这个callback 是从何而来的。 在mediacodec的init的时候会新建一个codec 并将codec设置到codec2里面。mCodec->setCallback(
std::unique_ptrCodecBase::CodecCallback(
new CodecCallback(new AMessage(kWhatCodecNotify, this))));

  • 各个buffer 直接的转换

首先从解码这边出来的是C2GraphicBlock,会在codecbufferchannel中转为index 传递出去给mediacodec 转换过程是 内部有一个
mBuffers数组,在handlework先pushToStash到这里面。然后从这里面取出来。popFromStashAndRegister是这个里面去转换为mediacodec的buffer 和index 的。转换的MediaCodecBuffer, 就是把c2buffer的一个结构体赋值到Codec2Buffer中。c2Buffer->copy(buffer)。

renderOutbuffer 输出显示

  • render的时候传递的是index,同样也是mAvailPortBuffers 取出可用的buffer。
  • 这个buffer 通过status_t err = mBufferChannel->renderOutputBuffer(buffer, renderTimeNs)。将MediaCodecBuffer转换为C2buffer。
  • 从这个C2buffer 中取出C2ConstGraphicBlock, block 在转换为bqslot。 这个slot最后queue到surface那边。
getBufferQueueAssignment(block, &generation, &bqId, &bqSlot)
status = outputIgbp->queueBuffer(static_cast<int>(bqSlot),
input, output);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1235293.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

可用于短期风速预测及光伏预测的LSTM/ELM预测程序

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 程序内容&#xff1a; 该程序是预测类的基础性代码&#xff0c;程序对河北某地区的气象数据进行详细统计&#xff0c;程序最终得到pm2.5的预测结果&#xff0c;通过更改数据很容易得到风速预测结果。程序主要…

U盘系统制作

一、简介 目标&#xff1a;将Linux和Windows系统装进U盘&#xff0c;linux称为LTG、Windows称为WTG 环境&#xff1a; 1、使用Rufus工具进行操作 2、基于windows系统进行Rufus软件进行制作 3、使用联想Y7000作为测试U盘系统启动测试机器&#xff08;无系统盘&#xff09; 优点…

CAD文件转奥维 转shapefile

之前写过一篇CAD转ArcGIS 其实万变不离其宗&#xff0c;都是经纬度知识的应用。 背景是当我们拿到一份带有坐标的CAD文件如何转换为矢量文件。 首先我们要明白XY坐标系的含义。 X—real X-500000 为近距离标准经线的距离。 y 为距离赤道的距离。 X 429174.3048 Y 32313…

Java 多线程之 volatile(可见性/重排序)

文章目录 一、概述二、使用方法三、测试程序3.1 验证可见性的示例3.2 验证指令重排序的示例 一、概述 在Java中&#xff0c;volatile 关键字用于修饰变量&#xff0c;其作用是确保多个线程之间对该变量的可见性和禁止指令重排序优化。 当一个变量被声明为volatile时&#xff0…

暴力求解欲哭无泪之保安问题

身为程序员哪一个瞬间让你最奔溃&#xff1f; > 提醒&#xff1a;在发布作品前&#xff0c;请把不需要的内容删掉。 方向一&#xff1a;身为程序员遇到过的奔溃瞬间 写题目想到第一个方法便是暴力求解,然后少情况 题目如下: 方向二&#xff1a;如何解决遇到的奔溃瞬间 不…

Arduino驱动Si7021温湿度传感器(温湿度传感器)

目录 1、传感器特性 2、控制器和传感器连线图 3、驱动程序 Si7021温湿度传感器,应用了专用的数字模块采集技术和温湿度传感技术,具有极高的可靠性与卓越的长期稳定性。同时其体积小巧、精度高,特别是拥有毫秒级测试转换时间(DHT系列需要约2s的转换时间),启动测量与读…

2023.11.19使用flask制作一个文件夹生成器

2023.11.19使用flask制作一个文件夹生成器 实现功能&#xff1a; &#xff08;1&#xff09;在指定路径上建立文件夹 &#xff08;2&#xff09;返回文件夹的路径和建立成功与否的提示 main.py import os from flask import Flask, request, jsonify, render_templateapp F…

生态系统NPP及碳源、碳汇模拟实践技术应用

由于全球变暖、大气中温室气体浓度逐年增加等问题的出现&#xff0c;“双碳”行动特别是碳中和已经在世界范围形成广泛影响。碳中和可以从碳排放&#xff08;碳源&#xff09;和碳固定&#xff08;碳汇&#xff09;这两个侧面来理解。陆地生态系统在全球碳循环过程中有着重要作…

微信关键词自动回复有什么用?

微信关键词自动回复有什么用&#xff1f; 关键词回复可以帮助解答客户的高频次问题。 假如&#xff0c;微信可以设置自动回复。。。 你还在担心一个个通过好友手动发欢迎语吗&#xff1f; 遇到常规问题&#xff0c;不用再复制粘贴那个已经回答了一百遍的答案吗&#xff1f;…

小诺2.0开源版工程启动

小诺是一款开源的前后端开发框架&#xff0c;同若依、SpringBladex一样可作为私活、外包脚手架。 开源地址&#xff1a;Snowy: 最新&#xff1a;&#x1f496;国内首个国密前后分离快速开发平台&#x1f496;&#xff0c;采用Vue3AntDesignVue3 ViteSpringBootMpHuToolSaToke…

【链表的说明、方法---顺序表与链表的区别】

文章目录 前言什么是链表链表的结构带头和不带头的区别 链表的实现&#xff08;方法&#xff09;遍历链表头插法尾插法任意位置插入一个节点链表中是否包含某个数字删除链表某个节点删除链表中所有关键字key清空链表所有节点 ArrayList 和 LinkedList的区别总结 前言 什么是链…

Week-T10 数据增强

文章目录 一、准备环境和数据1.环境2. 数据 二、数据增强&#xff08;增加数据集中样本的多样性&#xff09;三、将增强后的数据添加到模型中四、开始训练五、自定义增强函数六、一些增强函数 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f…

数据结构(c语言版) 树的遍历

作业要求 以如下图为例&#xff0c;完成树的遍历&#xff1a; 1、利用孩子兄弟表示法的存储结构 2、利用先根序列创建树 3、先根遍历树 4、后根遍历树 思考 预期的结果应该为&#xff1a; 1、先根创建树时需要输入的数据为&#xff1a; A B E 0 F 0 0 C 0 D G 0 0 0 0 2、…

第3关:图的广度优先遍历

任务要求参考答案评论2 任务描述相关知识编程要求测试说明 任务描述 本关任务&#xff1a;以邻接矩阵存储图&#xff0c;要求编写程序实现图的广度优先遍历。 相关知识 广度优先遍历类似于树的按层次遍历的过程。 假设从图中某顶点v出发&#xff0c;在访问了v之后依次访问…

基于Vue+SpringBoot的考研专业课程管理系统

项目编号&#xff1a; S 035 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S035&#xff0c;文末获取源码。} 项目编号&#xff1a;S035&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 考研高校模块2.3 高…

多目标应用:基于非支配排序的鲸鱼优化算法NSWOA求解微电网多目标优化调度(MATLAB代码)

一、微网系统运行优化模型 微电网优化模型介绍&#xff1a; 微电网多目标优化调度模型简介_IT猿手的博客-CSDN博客 二、基于非支配排序的鲸鱼优化算法NSWOA 基于非支配排序的鲸鱼优化算法NSWOA简介&#xff1a; 三、基于非支配排序的鲸鱼优化算法NSWOA求解微电网多目标优化…

设计模式-解析器-笔记

“领域规则”模式 在特定领域中&#xff0c;某些变化虽然频繁&#xff0c;但可以抽象为某种规则。这时候&#xff0c;结合特定领域&#xff0c;将稳日抽象为语法规则&#xff0c;从而给出在该领域下的一般性解决方案。 典型模式&#xff1a;Interpreter 动机(Motivation) 在…

测试用例的8大设计原则

我们看到的大部分关于测试用例设计的文章&#xff0c;都在讲等价类、因果图、流程法等内容&#xff0c;这是关于测试用例的具体设计方法层面。本文想讨论的重点是&#xff0c;测试用例设计该遵循什么原则&#xff0c;有哪些思维和观点有助于产出更好的测试设计&#xff0c;这些…

ChatGLM2 大模型微调过程中遇到的一些坑及解决方法(更新中)

1. 模型下载问题 OSError: We couldnt connect to https://huggingface.co to load this file, couldnt find it in the cached files and it looks like bert-base-uncased is not the path to a directory containing a file named config.json. Checkout your internet con…

【C++】set和map的底层结构(AVL树红黑树)

文章目录 一、前言二、AVL 树1.AVL树的概念2.AVL树节点的定义3.AVL树的插入4.AVL树的旋转5.AVL树的验证6.AVL树的删除、AVL树的性能 三、红黑树1.红黑树的概念2.红黑树的性质3.红黑树节点的定义4.红黑树结构5.红黑树的插入操作6.红黑树的验证7.红黑树与AVL树比较 四、红黑树模拟…