版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/gb702250823/article/details/81627503
            </div><!--一个博主专栏付费入口--><!--一个博主专栏付费入口结束--><link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-4a3473df85.css"><div id="content_views" class="markdown_views"><!-- flowchart 箭头图标 勿删 --><svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg><p>希望我们尊重每个人的成果,转载请标明出处:  <br>

https://blog.csdn.net/gb702250823/article/details/81627503

本文出自小口锅的博客

Android 官方的 MediaCodec API

MediaCodec 是Android 4.1(api 16)版本引入的编解码接口,Developer 官网上描述的已经很清楚了。可以配合中文翻译一起看。理解更深刻。

MediaCodec 基本介绍

  • MediaCodec类可用于访问Android底层的多媒体编解码器,例如,编码器/解码器组件。它是Android底层多媒体支持基础架构的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)。
  • Android 底层多媒体模块采用的是 OpenMax 框架,任何 Android 底层编解码模块的实现,都必须遵循 OpenMax 标准。Google 官方默认提供了一系列的软件编解码器:包括:OMX.google.h264.encoder,OMX.google.h264.encoder, OMX.google.aac.encoder, OMX.google.aac.decoder 等等,而硬件编解码功能,则需要由芯片厂商依照 OpenMax 框架标准来完成,所以,一般采用不同芯片型号的手机,硬件编解码的实现和性能是不同的

  • Android 应用层统一由 MediaCodec API 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等

MediaCodec的工作流程:

从上图可以看出 MediaCodec 架构上采用了2个缓冲区队列,异步处理数据,并且使用了一组输入输出缓存。
你请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终,你请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。
具体工作如下:
1. Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
2. Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]
3. MediaCodec 模块从 input 缓冲区队列取一帧数据进行编解码处理
4. 编解码处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列
5. Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
6. Client 对编解码后的 buffer 进行渲染/播放
7. 渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]

MediaCodec的基本调用流程是:

createEncoderByType/createDecoderByType
configure
start
while(true) {dequeueInputBuffer  //从输入流队列中取数据进行编码操作 getInputBuffers     //获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 queueInputBuffer    //输入流入队列 dequeueOutputBuffer //从输出队列中取出编码操作之后的数据getOutPutBuffers    // 获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组releaseOutputBuffer //处理完成,释放ByteBuffer数据
}
stop
release
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

1.初始化MediaCodec,方法有两种,分别是通过名称和类型来创建,对应的方法为:

MediaCodec createByCodecName (String name);
MediaCodec createDecoderByType (String type);
  • 1
  • 2
  • 选择第一种创建方式
    根据 mineType 以及是否为编码器,选择出一个 MediaCodecInfo,然后使用第一种方式初始化MediaCodec;
    private MediaCodecInfo selectSupportCodec(String mimeType) {int numCodecs = MediaCodecList.getCodecCount();for (int i = 0; i < numCodecs; i++) {MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);// 判断是否为编码器,否则直接进入下一次循环if (!codecInfo.isEncoder()) {continue;}// 如果是编码器,判断是否支持Mime类型String[] types = codecInfo.getSupportedTypes();for (int j = 0; j < types.length; j++) {if (types[j].equalsIgnoreCase(mimeType)) {return codecInfo;}}}return null;}MediaCodecInfo codecInfo = selectSupportCodec(config.mMime);if (codecInfo == null) return;mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 第二种方式比较简单
mMediaCodec = MediaCodec.createDecoderByType (MIME_TYPE);
  • 1

2.配置编码器,设置各种编码器参数(MediaFormat),这个类包含了比特率、帧率、关键帧间隔时间等。然后再调用 mMediaCodec .configure,对于 API 19 以上的系统,我们可以选择 Surface 输入:mMediaCodec .createInputSurface,

format= MediaFormat.createVideoFormat(MIME_TYPE, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //关键帧间隔时间 单位s
mMediaCodec .configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mMediaCodec.createInputSurface();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.打开编码器,获取输入输出缓冲区

mMediaCodec .start();
mInputBuffers = mMediaCodec .getInputBuffers();
mOutputBuffers = mMediaCodec .getOutputBuffers();
  • 1
  • 2
  • 3

获取输入输出缓冲区在api19 上是以上方式获取,api21以后 可以使用直接获取ByteBuffer

ByteBuffer intputBuffer = mMediaCodec.getOutputBuffer(inputBufferIndex);
ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
  • 1
  • 2

4.输入数据,有2种方式,一种是普通输入,一种是Surface 输入
普通输入又可区分为两种情况,一种是配合MediaExtractor ,一种是取原数据;

  • 获取可使用的缓冲区索引
 int outputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMES_OUT);
  • 1

返回一个填充了有效数据的input buffer的索引,如果没有可用的buffer则返回-1,参数为超时时间(TIMES_OUT),单位是微秒,当timeoutUs==0时,该方法立即返回;当timeoutUs<0时,无限期地等待一个可用的input buffer,当timeoutUs>0时,
等待时间为传入的微秒值。

  • 普通输入之获取原数据方式
ByteBuffer inputBuffer = mInputBuffers[inputbufferindex];
inputBuffer.clear();//清除原来的内容以接收新的内容
inputBuffer.put(bytes, 0, len);//len是传进来的有效数据长度
mMediaCodec .queueInputBuffer(inputbufferindex, 0, len, timestamp, 0);
  • 1
  • 2
  • 3
  • 4

上面输入缓存的index,通过getInputBuffers()得到的是输入缓存数组,通过index和输入缓存数组可以得到当前请求的输入缓存,在使用之前要clear一下,避免之前的缓存数据影响当前数据,接着就是把数据添加到输入缓存中,并调用queueInputBuffer(…)把缓存数据入队;

  • 普通输入之配合MediaExtractor 解码其他的音视频数据
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
int chunkSize = SDecoder.extractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {SDecoder.decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {SDecoder.decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, SDecoder.extractor.getSampleTime(), 0);                SDecoder.extractor.advance();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 使用Surface输入
    Surface输入是Android 4.3(api 18)引入。但用在某些 API 18 的机型上会导致编码器输出数据量特别小,画面是黑屏,所以 Surface 输入模式从 API 19 启用比较好。
//Requests a Surface to use as the input to an encoder, in place of input buffers. This may only be
//called after configure(MediaFormat, Surface, MediaCrypto, int) and before start().
//调用此方法,官方有这么一段话,意思是必须在configure之后 start()之前调用。
mInputSurface =  mMediaCodec.createInputSurface();
  • 1
  • 2
  • 3
  • 4

5.输出数据
通常编码传输时每个关键帧头部都需要带上编码配置数据(PPS,SPS),但 MediaCodec 会在首次输出时专门输出编码配置数据,后面的关键帧里是不携带这些数据的,所以需要我们手动做一个拼接;

  • 获取可使用的缓冲区
    获取输出缓存和获取输入缓存类似,首先通过dequeueOutputBuffer(BufferInfo info, long timeoutUs)来请求一个输出缓存,这里需要传入一个BufferInfo对象,用于存储ByteBuffer的信息,TIMES_OUT为超时时间。TIMES_OUT传的是 0,表示不会等待,由于这里并没有一个单独的线程不停调用,所以这样没什么问题,反倒可以防止阻塞,但如果我们单独起了一个线程专门取输出数据,那这就会导致 CPU 资源的浪费了,可以加上一个合适的值,例如 3~10ms;
BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
  • 1
  • 2
  • 获取数据
ByteBuffer outputBuffer = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {outputBuffer = outputBuffers[outputBufferIndex];
} else {outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {MediaFormat format = mMediaCodec.getOutputFormat();format.setByteBuffer("csd-0",outputBuffer);mBufferInfo.size = 0;
}// 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
// 并且限定将要读取缓存区数据的长度,否则输出数据会混乱
if (mBufferInfo.size != 0) {if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {outputBuffer.position(mBufferInfo.offset);outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);}// mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 释放缓冲区
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);
  • 1

6.使用完MediaCodec后释放资源
要告知编码器我们要结束编码,Surface 输入的话调用 mMediaCodec .signalEndOfInputStream,普通输入则可以为在 queueInputBuffer 时指定 MediaCodec.BUFFER_FLAG_END_OF_STREAM 这个 flag;告知编码器后我们就可以等到编码器输出的 buffer 带着 MediaCodec.BUFFER_FLAG_END_OF_STREAM 这个 flag 了,等到之后我们调用 mMediaCodec .release 销毁编码器

if (mMediaCodec != null) {mMediaCodec.stop();mMediaCodec.release();mMediaCodec = null;
}
  • 1
  • 2
  • 3
  • 4
  • 5

MediaCodec 流控

流控就是流量控制。为什么要控制,就是为了在一定的限制条件下,收益最大化!
涉及到了 TCP 和视频编码:
对 TCP 来说就是控制单位时间内发送数据包的数据量,对编码来说就是控制单位时间内输出数据的数据量。

TCP 的限制条件是网络带宽,流控就是在避免造成或者加剧网络拥塞的前提下,尽可能利用网络带宽。带宽够、网络好,我们就加快速度发送数据包,出现了延迟增大、丢包之后,就放慢发包的速度(因为继续高速发包,可能会加剧网络拥塞,反而发得更慢)。

视频编码的限制条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,限制条件变成了传输带宽/文件大小,我们希望在控制数据量的前提下,画面质量尽可能高。
一般编码器都可以设置一个目标码率,但编码器的实际输出码率不会完全符合设置,因为在编码过程中实际可以控制的并不是最终输出的码率,而是编码过程中的一个量化参数(Quantization Parameter,QP),它和码率并没有固定的关系,而是取决于图像内容。 这一点不在这里展开,感兴趣的朋友可以阅读视频压缩编码和音频压缩编码的基本原理。

无论是要发送的 TCP 数据包,还是要编码的图像,都可能出现“尖峰”,也就是短时间内出现较大的数据量。TCP 面对尖峰,可以选择不为所动(尤其是网络已经拥塞的时候),这没有太大的问题,但如果视频编码也对尖峰不为所动,那图像质量就会大打折扣了。如果有几帧数据量特别大,但仍要把码率控制在原来的水平,那势必要损失更多的信息,因此图像失真就会更严重。这种情况通常的表现是画面出现很多小方块,看上去像是打了马赛克一样,导致画面的局部或者整体看不清楚的情况

  • Android 硬编码流控
    MediaCodec 流控相关的接口并不多,一是配置时设置目标码率和码率控制模式,二是动态调整目标码率(Android 19+)。

配置时指定目标码率和码率控制模式:

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  • 1
  • 2
  • 3

码率控制模式有三种:
码率控制模式在 MediaCodecInfo.EncoderCapabilities类中定义了三种,在 framework 层有另一套名字和它们的值一一对应:

  • CQ 对应于 OMX_Video_ControlRateDisable,它表示完全不控制码率,尽最大可能保证图像质量;
  • CBR 对应于 OMX_Video_ControlRateConstant,它表示编码器会尽量把输出码率控制为设定值,即我们前面提到的“不为所动”;
  • VBR 对应于 OMX_Video_ControlRateVariable,它表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;

动态调整目标码率:

Bundle param = new Bundle();
param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
mediaCodec.setParameters(param);
  • 1
  • 2
  • 3

Android 流控策略选择

  • 质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择 CQ 码率控制策略。
  • VBR 输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择。

编码栗子

下面展示使用MediaExtractor获取数据后,用MediaMuxer重新写成一个MP4文件的简单栗子

private void doExtract() throws IOException {MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();boolean outputDone = false;boolean inputDone = false;while (!outputDone) {if (!inputDone) {int inputBufIndex = mMediaCodec.dequeueInputBuffer(10000);if (inputBufIndex >= 0) {ByteBuffer inputBuf = mMediaCodec.getInputBuffers()[inputBufIndex];int chunkSize = mMediaExtractor.readSampleData(inputBuf, 0);if (chunkSize < 0) {mMediaCodec.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);inputDone = true;} else {mMediaCodec.queueInputBuffer(inputBufIndex, 0, chunkSize, mMediaExtractor.getSampleTime(), 0);mMediaExtractor.advance();}}}if (!outputDone) {int decoderStatus =mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000);if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {Log.d(TAG, "no output from decoder available");} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {Log.d(TAG, "decoder output buffers changed");} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {MediaFormat newFormat = SDecoder.decoder.getOutputFormat();Log.d(TAG, "decoder output format changed: " + newFormat);} else {if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);outputDone = true;break;}// 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据ByteBuffer outputBuffer = null;if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {outputBuffer = mMediaCodec.getOutputBuffers()[outputBufferIndex];} else {outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);}if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {MediaFormat format = mMediaCodec.getOutputFormat();format.setByteBuffer("csd-0",outputBuffer);mBufferInfo.size = 0;}// 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置// 并且限定将要读取缓存区数据的长度,否则输出数据会混乱if (mBufferInfo.size != 0) {if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {outputBuffer.position(mBufferInfo.offset);outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);}// mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);}mMediaCodec.releaseOutputBuffer(decoderStatus, false);}}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
            <link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-b6c3c6d139.css" rel="stylesheet"></div>

Android原生编解码接口 MediaCodec 之——完全解析相关推荐

  1. zbar android解码错误,Android原生编解码接口 MediaCodec 之——踩坑

    关键帧 MediaCodec 有两种方式触发输出关键帧,一是由配置时设置的 KEY_FRAME_RATE和KEY_I_FRAME_INTERVAL参数自动触发,二是运行过程当中经过 setParame ...

  2. Android硬编解码接口MediaCodec使用完全解析(一)

    使用异步读取编码(解码)后的数据,效率会大增. 可以直接起一个线程不断地读. ------------------------------------------------------------- ...

  3. Android原生编解码接口MediaCodec详解

    作者:躬行之 了解了音视频的相关知识,可以先阅读同系列文章: 音视频开发基础知识 音频帧.视频帧及其同步 Camera2.MediaCodec录制mp4 MediaCodec 是 Android 中的 ...

  4. mediacodec.java_Android原生编解码接口 MediaCodec 之——踩坑

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/gb702250823/article/ ...

  5. Android视频编解码之MediaCodec简单入门

    本篇只是简单入门,后面会继续写文章详细讲解: 由于MediaCodec涉及内容众多,原本想一篇文章把所有内容概括,但是后来发现不太可能,限于自己能力,想要考虑全面太难,我也是刚开始学习需要借助网上的代 ...

  6. Android 音视频编解码(一) -- MediaCodec 初探

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  7. android amr-wb 编解码

    平台  PX30 + Android 9.0 + AndroidStudio 4.1.3 概述  在Android 平台上实现AMR-WB的编解码, 要求不高, JAVA也行, C/CPP也行, 可惜 ...

  8. android 解码webp动画,android webp编解码详解

    key words:android decode webp sample 当我敲下键盘的时候有种深深的耻辱感,看到android 4.0支持webp格式的图像,于是我狠命的找提供了什么样的api,nn ...

  9. Android音频开发(七)音频编解码之MediaCodec编解码AAC下

    在上一篇初识MediaCodec中,我们认识了MediaCodec,知道了MediaCodec的基本工作流程和开发注意事项,这一篇我将讲述如何利用MediaCodec编解码AAC. 1:MediaCo ...

最新文章

  1. 02-JDBC学习手册:JDBC编程步骤【重点重点】
  2. java报错误设置属性值_java – 设置属性值时出错;嵌套异常是org.springframework.beans.NotWritablePropertyException:...
  3. SQL嵌套语句执行顺序
  4. c语言无符号扩展,C语言无符号和有符号的区别
  5. proguard java enum,Proguard没有这么说就不会混淆课堂
  6. 抓linux肉鸡教程视频,抓肉鸡的教程和软件免费分享(2018一天抓1000只电脑肉鸡视频)...
  7. 三.VirtualBox中安装Centos7.5.1804
  8. spring data jpa 使用@Query 不确定参数查询
  9. Python学习笔记(六)函数(Function)
  10. python加减乘除运算代码_四则运算python版
  11. 你想要的WinForm界面开发教程在这里 - 如何使用自定义用户控件
  12. 阿里巴巴的业务范畴/文化和价值观
  13. mysql数据库下载、安装、使用
  14. 空间尺寸对迭代次数的影响
  15. order by 1含义
  16. 仿照jQuery进行一些简单的框架封装(欢迎指教~)
  17. 【已解决】winmm.dll被报病毒的解决方案
  18. ubuntu10.04下设置桌面特效
  19. android电池系统
  20. 电脑软件:推荐八款电脑必备效率软件

热门文章

  1. 35、基于51单片机的金属探测器
  2. robocopy 遷移共享文件夾
  3. DWL-2100AP 默认登录帐号密码
  4. 2018.9-江苏电赛省赛-基于STM32F103RCT6和FDC2214的手势识别装置
  5. Android电视设置密码,Android TV 输入登录账号和登录密码后首页页面焦点找不到原因...
  6. 2013——自我反思
  7. Excel实用技巧分享:Excel如何跨工作表求和
  8. 热成型容器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  9. CCNA实验三十八 ZFW(区域防火墙)
  10. 云服务器建站原理_第一篇博客---阿里云服务器建站过程(小菜鸟的第一次尝试)...