一、前语

本系列文章是对音视频技能入门常识的整理和复习,为进一步深入系统研究音视频技能巩固根底。文章列表:

  • 01-音视频技能中心常识|了解音频技能【移动通信技能的发展、声响的实质、深入了解音频】
  • 02-音视频技能中心常识|建立开发环境【FFmpeg与Qt、Windows开发环境建立、Mac开发环境建立、Qt开发根底】
  • 03-音视频技能中心常识|Qt开发根底【.pro文件的装备、Qt控件根底、信号与槽】
  • 04-音视频技能中心常识|音频录制【指令行、C++编程】
  • 05-音视频技能中心常识|音频播映【播映PCM、WAV、PCM转WAV、PCM转WAV、播映WAV】
  • 06-音视频技能中心常识|音频重采样【音频重采样简介、用指令行进行重采样、经过编程重采样】
  • 07-音视频技能中心常识|AAC编码【AAC编码器解码器、编译FFmpeg、AAC编码实战、AAC解码实战】
  • 08-音视频技能中心常识|成像技能【重识图片、详解YUV、视频录制、显现BMP图片、显现YUV图片】
  • 09-音视频技能中心常识|视频编码解码【了解H.264编码、H.264编码、H.264编码解码】
  • 10-音视频技能中心常识|RTMP服务器建立【流媒体、服务器环境】

二、播映PCM

2.1 ffplay

能够运用ffplay播映《音频录制02_编程》中录制好的PCM文件,测试一下是否录制成功。

播映PCM需求指定相关参数:

  • ar:采样率
  • ac:声道数
  • f:采样格局
    • s16le:PCM signed 16-bit little-endian
    • 更多PCM的采样格局能够运用指令检查
      • Windows:ffmpeg -formats | findstr PCM
      • Mac:ffmpeg -formats | grep PCM
ffplay -ar 44100 -ac 2 -f s16le out.pcm

接下来演示一下,怎么经过编程的办法播映PCM数据。

2.2 SDL

ffplay是根据FFmpeg、SDL两个库完成的。经过编程的办法播映音视频,也是需求用到这2个库。FFmpeg我们都现已清楚了,比较陌生的是SDL。

05-音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】

2.2.1 简介

SDL(Simple DirectMedia Layer),是一个跨渠道的C语言多媒体开发库。

  • 支撑Windows、Mac OS X、Linux、iOS、Android
  • 供给对音频、键盘、鼠标、游戏操纵杆、图形硬件的底层访问
  • 很多的视频播映软件、模拟器、受欢迎的游戏都在运用它
  • 目前最新的稳定版是:2.0.14
  • API文档:wiki

2.2.2 下载

SDL官网下载地址:download-sdl2。

05-音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】

2.2.2.1 Windows

由于我们运用的是MinGW编译器,所以选择下载SDL2-devel-2.0.14-mingw.tar.gz。

解压后的目录结构如下图所示,跟FFmpeg的目录结构相似,因而就不再赘述每个文件夹的效果。

05-音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】

2.2.2.2 Mac

从brew官网能够看得出来:之前执行brew install ffmpeg时,现已顺带装置了SDL,装置目录是:/usr/local/Cellar/sdl2

05-音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】

假如没有这个目录,就执行brew install sdl2进行装置即可。

2.3 HelloWorld

来个简略的SDL HelloWorld吧,打印一下SDL的版本号。

2.3.1 .pro文件

win32 {
    FFMPEG_HOME = F:/Dev/ffmpeg-4.3.2
    SDL_HOME = F:/Dev/SDL2-2.0.14/x86_64-w64-mingw32
}
macx {
    FFMPEG_HOME = /usr/local/Cellar/ffmpeg/4.3.2
    SDL_HOME = /usr/local/Cellar/sdl2/2.0.14_1
}
INCLUDEPATH += $${FFMPEG_HOME}/include
LIBS += -L$${FFMPEG_HOME}/lib \
        -lavdevice \
        -lavcodec \
        -lavformat \
        -lavutil
INCLUDEPATH += $${SDL_HOME}/include
LIBS += -L$${SDL_HOME}/lib \
        -lSDL2

在Windows环境中,还需求处理一下dll文件,参阅:《dll文件处理》。

2.3.2 cpp代码

#include <SDL2/SDL.h>
SDL_version v;
SDL_VERSION(&v);
// 2 0 14
qDebug() << v.major << v.minor << v.patch;

2.4 播映PCM

2.4.1 初始化子系统

SDL分红好多个子系统(subsystem):

  • Video:显现和窗口办理
  • Audio:音频设备办理
  • Joystick:游戏摇杆操控
  • Timers:定时器

目前只用到了音频功用,所以只需求经过SDL_init函数初始化Audio子系统即可。

// 初始化Audio子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
    // 返回值不是0,就代表失利
    qDebug() << "SDL_Init Error" << SDL_GetError();
    return;
}

2.4.2 翻开音频设备

/* 一些宏界说 */
// 采样率
#define SAMPLE_RATE 44100
// 采样格局
#define SAMPLE_FORMAT AUDIO_S16LSB
// 采样巨细
#define SAMPLE_SIZE SDL_AUDIO_BITSIZE(SAMPLE_FORMAT)
// 声道数
#define CHANNELS 2
// 音频缓冲区的样本数量
#define SAMPLES 1024
// 用于存储读取的音频数据和长度
typedef struct {
    int len = 0;
    int pullLen = 0;
    Uint8 *data = nullptr;
} AudioBuffer;
// 音频参数
SDL_AudioSpec spec;
// 采样率
spec.freq = SAMPLE_RATE;
// 采样格局(s16le)
spec.format = SAMPLE_FORMAT;
// 声道数
spec.channels = CHANNELS;
// 音频缓冲区的样本数量(这个值必须是2的幂)
spec.samples = SAMPLES;
// 回调
spec.callback = pull_audio_data;
// 传递给回调的参数
AudioBuffer buffer;
spec.userdata = &buffer;
// 翻开音频设备
if (SDL_OpenAudio(&spec, nullptr)) {
    qDebug() << "SDL_OpenAudio Error" << SDL_GetError();
    // 铲除一切初始化的子系统
    SDL_Quit();
    return;
}

2.4.3 翻开文件

#define FILENAME "F:/in.pcm"
// 翻开文件
QFile file(FILENAME);
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "文件翻开失利" << FILENAME;
    // 封闭音频设备
    SDL_CloseAudio();
    // 铲除一切初始化的子系统
    SDL_Quit();
    return;
}

2.4.4 开端播映

// 每个样本占用多少个字节
#define BYTES_PER_SAMPLE ((SAMPLE_SIZE * CHANNELS) / 8)
// 文件缓冲区的巨细
#define BUFFER_SIZE (SAMPLES * BYTES_PER_SAMPLE)
// 开端播映
SDL_PauseAudio(0);
// 存放文件数据
Uint8 data[BUFFER_LEN];
while (!isInterruptionRequested()) {
    // 只要从文件中读取的音频数据,还没有填充结束,就跳过
    if (buffer.len > 0) continue;
    buffer.len = file.read((char *) data, BUFFER_SIZE);
    // 文件数据现已读取结束
    if (buffer.len <= 0) {
        // 剩下的样本数量
        int samples = buffer.pullLen / BYTES_PER_SAMPLE;
        int ms = samples * 1000 / SAMPLE_RATE;
        SDL_Delay(ms);
        break;
    }
    // 读取到了文件数据
    buffer.data = data;
}

2.4.5 回调函数

// userdata:SDL_AudioSpec.userdata
// stream:音频缓冲区(需求将音频数据填充到这个缓冲区)
// len:音频缓冲区的巨细(SDL_AudioSpec.samples * 每个样本的巨细)
void pull_audio_data(void *userdata, Uint8 *stream, int len) {
    // 清空stream
    SDL_memset(stream, 0, len);
    // 取出缓冲信息
    AudioBuffer *buffer = (AudioBuffer *) userdata;
    if (buffer->len == 0) return;
    // 取len、bufferLen的最小值(为了确保数据安全,防止指针越界)
    buffer->pullLen = (len > buffer->len) ? buffer->len : len;
    // 填充数据
    SDL_MixAudio(stream,
                 buffer->data,
                 buffer->pullLen,
                 SDL_MIX_MAXVOLUME);
    buffer->data += buffer->pullLen;
    buffer->len -= buffer->pullLen;
}

2.4.6 开释资源

// 封闭文件
file.close();
// 封闭音频设备
SDL_CloseAudio();
// 整理一切初始化的子系统
SDL_Quit();

三、PCM转WAV

播映器是无法直接播映PCM的,由于播映器并不知道PCM的采样率、声道数、位深度等参数。当PCM转成某种特定的音频文件格局后(比方转成WAV),就能够被播映器辨认播映了。

本文经过2种办法(指令行、编程)演示一下:怎么将PCM转成WAV。

1. WAV文件格局

在进行PCM转WAV之前,先再来认识一下WAV的文件格局。

  • WAV、AVI文件都是根据RIFF标准的文件格局
  • RIFF(Resource Interchange File Format,资源交流文件格局)由Microsoft和IBM提出
  • 所以WAV、AVI文件的最前面4个字节都是RIFF四个字符

找遍了全网,并没有找到令我十分满意的WAV文件格局图,于是按照自己的理解画了一张表格,个人觉得还是极端通俗易懂的。

05-音视频技术核心知识|音频播放【播放PCM、WAV、PCM转WAV、PCM转WAV、播放WAV】

每一个chunk(数据块)都由3部分组成:

  • id:chunk的标识
  • data size:chunk的数据部分巨细,字节为单位
  • data,chunk的数据部分

整个WAV文件是一个RIFF chunk,它的data由3部分组成:

  • format:文件类型
  • fmt chunk
    • 音频参数相关的chunk
    • 它的data里边有采样率、声道数、位深度等参数信息
  • data chunk
    • 音频数据相关的chunk
    • 它的data就是真正的音频数据(比方PCM数据)

RIFF chunk除去data chunk的data(音频数据)后,剩下的内容能够称为:WAV文件头,一般是44字节。

四、PCM转WAV

1. 指令行

经过下面的指令能够将PCM转成WAV。

ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm out.wav

需求注意的是:上面指令生成的WAV文件头有78字节。比照44字节的文件头,它多添加了一个34字节巨细的LIST chunk。

关于LIST chunk的参阅资料:

  • What is a “LIST” chunk in a RIFF/Wav header?
  • List chunk (of a RIFF file)

加上一个输出文件参数*-bitexact*能够去掉LIST Chunk。

ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm -bitexact out2.wav

2. 编程

在PCM数据的前面插入一个44字节的WAV文件头,就能够将PCM转成WAV。

2.1 WAV的文件头结构

WAV的文件头结构大概如下所示:

#define AUDIO_FORMAT_PCM 1
#define AUDIO_FORMAT_FLOAT 3
// WAV文件头(44字节)
typedef struct {
    // RIFF chunk的id
    uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'};
    // RIFF chunk的data巨细,即文件总长度减去8字节
    uint32_t riffChunkDataSize;
    // "WAVE"
    uint8_t format[4] = {'W', 'A', 'V', 'E'};
    /* fmt chunk */
    // fmt chunk的id
    uint8_t fmtChunkId[4] = {'f', 'm', 't', ' '};
    // fmt chunk的data巨细:存储PCM数据时,是16
    uint32_t fmtChunkDataSize = 16;
    // 音频编码,1表明PCM,3表明Floating Point
    uint16_t audioFormat = AUDIO_FORMAT_PCM;
    // 声道数
    uint16_t numChannels;
    // 采样率
    uint32_t sampleRate;
    // 字节率 = sampleRate * blockAlign
    uint32_t byteRate;
    // 一个样本的字节数 = bitsPerSample * numChannels >> 3
    uint16_t blockAlign;
    // 位深度
    uint16_t bitsPerSample;
    /* data chunk */
    // data chunk的id
    uint8_t dataChunkId[4] = {'d', 'a', 't', 'a'};
    // data chunk的data巨细:音频数据的总长度,即文件总长度减去文件头的长度(一般是44)
    uint32_t dataChunkDataSize;
} WAVHeader;

2.2 PCM转WAV中心完成

封装到了FFmpegs类的pcm2wav函数中。

#include <QFile>
#include <QDebug>
class FFmpegs {
public:
    FFmpegs();
    static void pcm2wav(WAVHeader &header,
                        const char *pcmFilename,
                        const char *wavFilename);
};
void FFmpegs::pcm2wav(WAVHeader &header,
                      const char *pcmFilename,
                      const char *wavFilename) {
    header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
    header.byteRate = header.sampleRate * header.blockAlign;
    // 翻开pcm文件
    QFile pcmFile(pcmFilename);
    if (!pcmFile.open(QFile::ReadOnly)) {
        qDebug() << "文件翻开失利" << pcmFilename;
        return;
    }
    header.dataChunkDataSize = pcmFile.size();
    header.riffChunkDataSize = header.dataChunkDataSize
                               + sizeof (WAVHeader) - 8;
    // 翻开wav文件
    QFile wavFile(wavFilename);
    if (!wavFile.open(QFile::WriteOnly)) {
        qDebug() << "文件翻开失利" << wavFilename;
        pcmFile.close();
        return;
    }
    // 写入头部
    wavFile.write((const char *) &header, sizeof (WAVHeader));
    // 写入pcm数据
    char buf[1024];
    int size;
    while ((size = pcmFile.read(buf, sizeof (buf))) > 0) {
        wavFile.write(buf, size);
    }
    // 封闭文件
    pcmFile.close();
    wavFile.close();
}

2.3 调用函数

// 封装WAV的头部
WAVHeader header;
header.numChannels = 2;
header.sampleRate = 44100;
header.bitsPerSample = 16;
// 调用函数
FFmpegs::pcm2wav(header, "F:/in.pcm", "F:/out.wav");

五、播映WAV

关于WAV文件来说,能够直接运用ffplay指令播映,并且不必像PCM那样添加额外的参数。由于WAV的文件头中现已包含了相关的音频参数信息。

ffplay in.wav

接下来演示一下怎么运用SDL播映WAV文件。

1. 初始化子系统

// 初始化Audio子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
    qDebug() << "SDL_Init error:" << SDL_GetError();
    return;
}

2. 加载WAV文件

// 存放WAV的PCM数据和数据长度
typedef struct {
    Uint32 len = 0;
    int pullLen = 0;
    Uint8 *data = nullptr;
} AudioBuffer;
// WAV中的PCM数据
Uint8 *data;
// WAV中的PCM数据巨细(字节)
Uint32 len;
// 音频参数
SDL_AudioSpec spec;
// 加载wav文件
if (!SDL_LoadWAV(FILENAME, &spec, &data, &len)) {
    qDebug() << "SDL_LoadWAV error:" << SDL_GetError();
    // 铲除一切的子系统
    SDL_Quit();
    return;
}
// 回调
spec.callback = pull_audio_data;
// 传递给回调函数的userdata
AudioBuffer buffer;
buffer.len = len;
buffer.data = data;
spec.userdata = &buffer;

假如想要轻松加载MP3、Ogg、FLAC等格局的音频文件,能够运用第三方库:SDL_mixer。

3. 翻开音频设备

// 翻开设备
if (SDL_OpenAudio(&spec, nullptr)) {
    qDebug() << "SDL_OpenAudio error:" << SDL_GetError();
    // 开释文件数据
    SDL_FreeWAV(data);
    // 铲除一切的子系统
    SDL_Quit();
    return;
}

开端播映

// 开端播映(0是取消暂停)
SDL_PauseAudio(0);
while (!isInterruptionRequested()) {
    if (buffer.len > 0) continue;
    // 每一个样本的巨细
    int size = spec.channels * SDL_AUDIO_BITSIZE(spec.format) / 8;
    // 最终一次播映的样本数量
    int samples = buffer.pullLen / size;
    // 最终一次播映的时长
    int ms = samples * 1000 / spec.freq;
    SDL_Delay(ms);
    break;
}

4. 回调函数

// 等候音频设备回调(会回调屡次)
void pull_audio_data(void *userdata,
                     // 需求往stream中填充PCM数据
                     Uint8 *stream,
                     // 希望填充的巨细(samples * format * channels / 8)
                     int len
                    ) {
    // 清空stream
    SDL_memset(stream, 0, len);
    AudioBuffer *buffer = (AudioBuffer *) userdata;
    // 文件数据还没准备好
    if (buffer->len <= 0) return;
    // 取len、bufferLen的最小值
    buffer->pullLen = (len > (int) buffer->len) ? buffer->len : len;
    // 填充数据
    SDL_MixAudio(stream,
                 buffer->data,
                 buffer->pullLen,
                 SDL_MIX_MAXVOLUME);
    buffer->data += buffer->pullLen;
    buffer->len -= buffer->pullLen;
}

5. 开释资源

// 开释WAV文件数据
SDL_FreeWAV(data);
// 封闭设备
SDL_CloseAudio();
// 铲除一切的子系统
SDL_Quit();

专题系列文章

1. 前常识

  • 01-探求iOS底层原理|综述
  • 02-探求iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
  • 03-探求iOS底层原理|LLDB
  • 04-探求iOS底层原理|ARM64汇编

2. 根据OC语言探求iOS底层原理

  • 05-探求iOS底层原理|OC的实质
  • 06-探求iOS底层原理|OC目标的实质
  • 07-探求iOS底层原理|几种OC目标【实例目标、类目标、元类】、目标的isa指针、superclass、目标的办法调用、Class的底层实质
  • 08-探求iOS底层原理|Category底层结构、App启动时Class与Category装载进程、load 和 initialize 执行、关联目标
  • 09-探求iOS底层原理|KVO
  • 10-探求iOS底层原理|KVC
  • 11-探求iOS底层原理|探求Block的实质|【Block的数据类型(实质)与内存布局、变量捕获、Block的种类、内存办理、Block的修饰符、循环引证】
  • 12-探求iOS底层原理|Runtime1【isa详解、class的结构、办法缓存cache_t】
  • 13-探求iOS底层原理|Runtime2【消息处理(发送、转发)&&动态办法解析、super的实质】
  • 14-探求iOS底层原理|Runtime3【Runtime的相关应用】
  • 15-探求iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
  • 16-探求iOS底层原理|RunLoop的应用
  • 17-探求iOS底层原理|多线程技能的底层原理【GCD源码剖析1:主行列、串行行列&&并行行列、全局并发行列】
  • 18-探求iOS底层原理|多线程技能【GCD源码剖析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
  • 19-探求iOS底层原理|多线程技能【GCD源码剖析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
  • 20-探求iOS底层原理|多线程技能【GCD源码剖析3:线程调度组dispatch_group、事件源dispatch Source】
  • 21-探求iOS底层原理|多线程技能【线程锁:自旋锁、互斥锁、递归锁】
  • 22-探求iOS底层原理|多线程技能【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
  • 23-探求iOS底层原理|内存办理【Mach-O文件、Tagged Pointer、目标的内存办理、copy、引证计数、weak指针、autorelease

3. 根据Swift语言探求iOS底层原理

关于函数枚举可选项结构体闭包特点办法swift多态原理StringArrayDictionary引证计数MetaData等Swift根本语法和相关的底层原理文章有如下几篇:

  • 01-Swift5常用中心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
  • 02-Swift5常用中心语法|根底语法【Playground、常量与变量、常见数据类型、字面量、元组、流程操控、函数、枚举、可选项、guard句子、区间】
  • 03-Swift5常用中心语法|面向目标【闭包、结构体、类、枚举】
  • 04-Swift5常用中心语法|面向目标【特点、inout、类型特点、单例模式、办法、下标、承继、初始化】
  • 05-Swift5常用中心语法|高档语法【可选链、协议、过错处理、泛型、String与Array、高档运算符、扩展、访问操控、内存办理、字面量、模式匹配】
  • 06-Swift5常用中心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、呼应式编程、Swift源码剖析】

4. C++中心语法

  • 01-C++中心语法|C++概述【C++简介、C++起源、可移植性和标准、为什么C++会成功、从一个简略的程序开端认识C++】
  • 02-C++中心语法|C++对C的扩展【::效果域运算符、名字操控、struct类型加强、C/C++中的const、引证(reference)、函数】
  • 03-C++中心语法|面向目标1【 C++编程规范、类和目标、面向目标程序设计事例、目标的构造和析构、C++面向目标模型初探】
  • 04-C++中心语法|面向目标2【友元、内部类与局部类、强化训练(数组类封装)、运算符重载、仿函数、模板、类型转化、 C++标准、过错&&异常、智能指针】
  • 05-C++中心语法|面向目标3【 承继和派生、多态、静态成员、const成员、引证类型成员、VS的内存窗口】

5. Vue全家桶

  • 01-Vue全家桶中心常识|Vue根底【Vue概述、Vue根本运用、Vue模板语法、根底事例、Vue常用特性、归纳事例】
  • 02-Vue全家桶中心常识|Vue常用特性【表单操作、自界说指令、计算特点、侦听器、过滤器、生命周期、归纳事例】
  • 03-Vue全家桶中心常识|组件化开发【组件化开发思维、组件注册、Vue调试东西用法、组件间数据交互、组件插槽、根据组件的
  • 04-Vue全家桶中心常识|多线程与网络【前后端交互模式、promise用法、fetch、axios、归纳事例】
  • 05-Vue全家桶中心常识|Vue Router【根本运用、嵌套路由、动态路由匹配、命名路由、编程式导航、根据vue-router的事例】
  • 06-Vue全家桶中心常识|前端工程化【模块化相关规范、webpack、Vue 单文件组件、Vue 脚手架、Element-UI 的根本运用】
  • 07-Vue全家桶中心常识|Vuex【Vuex的根本运用、Vuex中的中心特性、vuex事例】

6. 音视频技能中心常识

  • 01-音视频技能中心常识|了解音频技能【移动通信技能的发展、声响的实质、深入了解音频】
  • 02-音视频技能中心常识|建立开发环境【FFmpeg与Qt、Windows开发环境建立、Mac开发环境建立、Qt开发根底】
  • 03-音视频技能中心常识|Qt开发根底【.pro文件的装备、Qt控件根底、信号与槽】
  • 04-音视频技能中心常识|音频录制【指令行、C++编程】
  • 05-音视频技能中心常识|音频播映【播映PCM、WAV、PCM转WAV、PCM转WAV、播映WAV】
  • 06-音视频技能中心常识|音频重采样【音频重采样简介、用指令行进行重采样、经过编程重采样】
  • 07-音视频技能中心常识|AAC编码【AAC编码器解码器、编译FFmpeg、AAC编码实战、AAC解码实战】
  • 08-音视频技能中心常识|成像技能【重识图片、详解YUV、视频录制、显现BMP图片、显现YUV图片】
  • 09-音视频技能中心常识|视频编码解码【了解H.264编码、H.264编码、H.264编码解码】
  • 10-音视频技能中心常识|RTMP服务器建立【流媒体、服务器环境】

其它底层原理专题

1. 底层原理相关专题

  • 01-计算机原理|计算机图形烘托原理这篇文章
  • 02-计算机原理|移动终端屏幕成像与卡顿

2. iOS相关专题

  • 01-iOS底层原理|iOS的各个烘托结构以及iOS图层烘托原理
  • 02-iOS底层原理|iOS动画烘托原理
  • 03-iOS底层原理|iOS OffScreen Rendering 离屏烘托原理
  • 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决计划

3. webApp相关专题

  • 01-Web和类RN大前端的烘托原理

4. 跨渠道开发计划相关专题

5. 阶段性总结:Native、WebApp、跨渠道开发三种计划功能比较

  • 01-Native、WebApp、跨渠道开发三种计划功能比较

6. Android、HarmonyOS页面烘托专题

  • 01-Android页面烘托原理
  • 02-HarmonyOS页面烘托原理 (待输出)

7. 小程序页面烘托专题

  • 01-小程序结构烘托原理