【Android RTMP】RTMPDump 封装 RTMPPacket 数据包 ( 封装 SPS / PPS 数据包 )

简介: 【Android RTMP】RTMPDump 封装 RTMPPacket 数据包 ( 封装 SPS / PPS 数据包 )

文章目录

一、 基本封装数据格式说明

二、 封装 SPS PPS 数据总体说明

三、 封装头数据

四、 封装 SPS 数据

五、 封装 PPS 数据

六、 设置 RTMP 数据包其它参数

七、 SPS PPS 数据封装代码示例



Android 直播推流流程 : 手机采集视频 / 音频数据 , 视频数据使用 H.264 编码 , 音频数据使用 AAC 编码 , 最后将音视频数据都打包到 RTMP 数据包中 , 使用 RTMP 协议上传到 RTMP 服务器中 ;



Android 端中主要完成手机端采集视频数据操作 , 并将视频数据传递给 JNI , 在 NDK 中使用 x264 将图像转为 H.264 格式的视频 , 最后将 H.264 格式的视频打包到 RTMP 数据包中 , 上传到 RTMP 服务器中 ;



本篇博客中介绍如下内容 , Java 层将 Camera 采集的 NV21 格式的数据传入 JNI 层 , 在 JNI 中使用 x264 编码器将 NV21 图像数据编码为 H.264 视频数据 ;



本篇博客中主要封装 AVC 序列头数据 , 将 帧类型 , AVC 数据类型 , 合成时间 , 版本信息 , 编码规格 , NALU 长度 , SPS 个数 , SPS 长度 , SPS 数据 , PPS 个数 , PPS 长度 , PPS 数据 , 封装到 RTMP 包中 ;






一、 基本封装数据格式说明


1 . 这是完整的视频标签数据内容 : 这是 FLV 中完整视频标签数据 ;


0x00000182  :   09 00 00 2E 00 00 00 00 
0x0000018a  :   00 00 00 17 00 00 00 00 
0x00000192  :   01 64 00 32 FF E1 00 19 
0x0000019a  :   67 64 00 32 AC D9 80 78 
0x000001a2  :   02 27 E5 84 00 00 03 00 
0x000001aa  :   04 00 00 1F 40 3C 60 C6 
0x000001b2  :   68 01 00 05 68 E9 7B 2C 
0x000001ba  :   8B 00 00 00 39


2 . 标签头 : 前 11 1111 个字节是标签头数据 , 存储有 标签类型 , 标签数据大小 , 时间戳 , 时间戳扩展位 , 流编号 等 11 1111 字节信息 ;


0x00000182  :   09 00 00 2E 00 00 00 00 
0x0000018a  :   00 00 00



3 . 标签数据 ( 重点 ) : 这就是本篇博客要封装的内容 , 基本上是封装一个格式一模一样的 RTMP 数据包 ,


   

17 00 00 00 00 
0x00000192  :   01 64 00 32 FF E1 00 19 
0x0000019a  :   67 64 00 32 AC D9 80 78 
0x000001a2  :   02 27 E5 84 00 00 03 00 
0x000001aa  :   04 00 00 1F 40 3C 60 C6 
0x000001b2  :   68 01 00 05 68 E9 7B 2C 
0x000001ba  :   8B 00 00 00 39



参考博客 : 参考之前的两篇分析 RTMP 数据格式的博客 , 分析了与 RTMP 格式几乎一致的 FLV 视频数据格式 ;


【Android RTMP】RTMP 数据格式 ( FLV 视频格式分析 | 文件头 Header 分析 | 标签 Tag 分析 | 视频标签 Tag 数据分析 )


【Android RTMP】RTMP 数据格式 ( FLV 视频格式分析 | AVC 序列头格式解析 )



这两篇博客一定要 , 并且明白 FLV 视频标签数据格式 , 才能看懂今天写的 RTMP 数据包封装的内容 ;






二、 封装 SPS PPS 数据总体说明


1 . 数据示例 :


17 00 00 00 00
0x00000192  :   01 64 00 32 FF E1 00 19
0x0000019a  :   67 64 00 32 AC D9 80 78
0x000001a2  :   02 27 E5 84 00 00 03 00
0x000001aa  :   04 00 00 1F 40 3C 60 C6
0x000001b2  :   68 01 00 05 68 E9 7B 2C
0x000001ba  :   8B 00 00 00 39


17 帧类型, 1 字节

00 数据类型, 1 字节

00 00 00 合成时间, 3 字节

01 版本信息, 1 字节

64 00 32 编码规则, 3 字节

FF NALU 长度, 1 字节

E1 SPS 个数, 1 字节

00 19 SPS 长度, 2 字节

截止到当前位置有 13 字节数据


spsLen 字节数据, 这里是 25 字节
                67 64 00 32 AC D9 80 78
0x000001a2  :   02 27 E5 84 00 00 03 00
0x000001aa  :   04 00 00 1F 40 3C 60 C6
0x000001b2  :   68


01 PPS 个数, 1 字节
00 05 PPS 长度, 2 字节
ppsLen 字节的 PPS 数据
                            68 E9 7B 2C
0x000001ba  :   8B


后面的 00 00 00 39 是视频标签的总长度 , 这里在 RTMP 标签中可以不用封装 ;



2 . 计算整个 SPS 和 PPS 数据的大小 :



① 封装头 : 帧类型 , 数据类型 , 合成时间 , 版本信息 , 编码规则 , NALU 长度 , 总共有 10 1010 字节 ;


② 封装 SPS 数据 : SPS 个数 , SPS 长度 , SPS 数据 , 分别有 1 + 2 + s p s L e n 1 + 2 + spsLen1+2+spsLen 字节 ;


③ 封装 PPS 数据 : PPS 个数 , PPS 长度 , PPS 数据 , 分别有 1 + 2 + p p s L e n 1 + 2 + ppsLen1+2+ppsLen 字节 ;


int rtmpPackagesize = 10 + 3 + spsLen + 3 + ppsLen;






三、 封装头数据


向 RTMP 数据包中 , 封装 帧类型 , 数据类型 , 合成时间 , 版本信息 , 编码规则 , NALU 长度 , 总共有 10 1010 字节 ;


 

// 帧类型数据 : 分为两部分;
    // 前 4 位表示帧类型, 1 表示关键帧, 2 表示普通帧
    // 后 4 位表示编码类型, 7 表示 AVC 视频编码
    rtmpPacket->m_body[nextPosition++] = 0x17;
    // 数据类型, 00 表示 AVC 序列头
    rtmpPacket->m_body[nextPosition++] = 0x00;
    // 合成时间, 一般设置 00 00 00
    rtmpPacket->m_body[nextPosition++] = 0x00;
    rtmpPacket->m_body[nextPosition++] = 0x00;
    rtmpPacket->m_body[nextPosition++] = 0x00;
    // 版本信息
    rtmpPacket->m_body[nextPosition++] = 0x01;
    // 编码规格
    rtmpPacket->m_body[nextPosition++] = sps[1];
    rtmpPacket->m_body[nextPosition++] = sps[2];
    rtmpPacket->m_body[nextPosition++] = sps[3];
    // NALU 长度
    rtmpPacket->m_body[nextPosition++] = 0xFF;




四、 封装 SPS 数据


将 SPS 数据封装到 RTMP 数据包中 , 包含 SPS 个数 , SPS 长度 , SPS 数据 ;


// SPS 个数
    rtmpPacket->m_body[nextPosition++] = 0xE1;
    // SPS 长度, 占 2 字节
    // 设置长度的高位
    rtmpPacket->m_body[nextPosition++] = (spsLen >> 8) & 0xFF;
    // 设置长度的低位
    rtmpPacket->m_body[nextPosition++] = spsLen & 0xFF;
    // 拷贝 SPS 数据
    // 将 SPS 数据拷贝到 rtmpPacket->m_body[nextPosition] 地址中
    memcpy(&rtmpPacket->m_body[nextPosition], sps, spsLen);
    // 累加 SPS 长度信息
    nextPosition += spsLen;



五、 封装 PPS 数据


将 PPS 数据封装到 RTMP 数据包中 , 包含 PPS 个数 , PPS 长度 , PPS 数据 ;


// PPS 个数
    rtmpPacket->m_body[nextPosition++] = 0x01;
    // PPS 数据的长度, 占 2 字节
    // 设置长度的高位
    rtmpPacket->m_body[nextPosition++] = (ppsLen >> 8) & 0xFF;
    // 设置长度的低位
    rtmpPacket->m_body[nextPosition++] = (ppsLen) & 0xFF;
    // 拷贝 SPS 数据
    memcpy(&rtmpPacket->m_body[nextPosition], pps, ppsLen);





六、 设置 RTMP 数据包其它参数


设置 RTMP 包类型 , RTMP 包长度 , RTMP 通道 , 时间戳 等信息 ;


 

// 设置 RTMP 包类型, 视频类型数据
    rtmpPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    // 设置 RTMP 包长度
    rtmpPacket->m_nBodySize = rtmpPackagesize;
    // 分配 RTMP 通道, 随意分配
    rtmpPacket->m_nChannel = 10;
    // 设置视频时间戳, 如果是 SPP PPS 数据, 没有时间戳
    rtmpPacket->m_nTimeStamp = 0;
    // 设置绝对时间, 对于 SPS PPS 赋值 0 即可
    rtmpPacket->m_hasAbsTimestamp = 0;
    // 设置头类型, 随意设置一个
    rtmpPacket->m_headerType = RTMP_PACKET_SIZE_MEDIUM;





七、 SPS PPS 数据封装代码示例


/**
 * 将 SPS / PPS 数据发送到 RTMP 服务器端
 * @param sps       SPS 数据
 * @param pps       PPS 数据
 * @param spsLen    SPS 长度
 * @param ppsLen    PPS 长度
 */
void VedioChannel::sendSpsPpsToRtmpServer(uint8_t *sps, uint8_t *pps, int spsLen, int ppsLen) {
    // 创建 RTMP 数据包, 将数据都存入该 RTMP 数据包中
    RTMPPacket *rtmpPacket = new RTMPPacket;
    /*
        计算整个 SPS 和 PPS 数据的大小
        数据示例 :
                                 17 00 00 00 00
        0x00000192  :   01 64 00 32 FF E1 00 19
        0x0000019a  :   67 64 00 32 AC D9 80 78
        0x000001a2  :   02 27 E5 84 00 00 03 00
        0x000001aa  :   04 00 00 1F 40 3C 60 C6
        0x000001b2  :   68 01 00 05 68 E9 7B 2C
        0x000001ba  :   8B 00 00 00 39
        17 帧类型, 1 字节
        00 数据类型, 1 字节
        00 00 00 合成时间, 3 字节
        01 版本信息, 1 字节
        64 00 32 编码规则, 3 字节
        FF NALU 长度, 1 字节
        E1 SPS 个数, 1 字节
        00 19 SPS 长度, 2 字节
        截止到当前位置有 13 字节数据
        spsLen 字节数据, 这里是 25 字节
                        67 64 00 32 AC D9 80 78
        0x000001a2  :   02 27 E5 84 00 00 03 00
        0x000001aa  :   04 00 00 1F 40 3C 60 C6
        0x000001b2  :   68
        01 PPS 个数, 1 字节
        00 05 PPS 长度, 2 字节
        ppsLen 字节的 PPS 数据
                                    68 E9 7B 2C
        0x000001ba  :   8B
        后面的 00 00 00 39 是视频标签的总长度
        这里再 RTMP 标签中可以不用封装
     */
    int rtmpPackagesize = 10 + 3 + spsLen + 3 + ppsLen;
    // 为 RTMP 数据包分配内存
    RTMPPacket_Alloc(rtmpPacket, rtmpPackagesize);
    // 记录下一个要写入数据的索引位置
    int nextPosition = 0;
    // 帧类型数据 : 分为两部分;
    // 前 4 位表示帧类型, 1 表示关键帧, 2 表示普通帧
    // 后 4 位表示编码类型, 7 表示 AVC 视频编码
    rtmpPacket->m_body[nextPosition++] = 0x17;
    // 数据类型, 00 表示 AVC 序列头
    rtmpPacket->m_body[nextPosition++] = 0x00;
    // 合成时间, 一般设置 00 00 00
    rtmpPacket->m_body[nextPosition++] = 0x00;
    rtmpPacket->m_body[nextPosition++] = 0x00;
    rtmpPacket->m_body[nextPosition++] = 0x00;
    // 版本信息
    rtmpPacket->m_body[nextPosition++] = 0x01;
    // 编码规格
    rtmpPacket->m_body[nextPosition++] = sps[1];
    rtmpPacket->m_body[nextPosition++] = sps[2];
    rtmpPacket->m_body[nextPosition++] = sps[3];
    // NALU 长度
    rtmpPacket->m_body[nextPosition++] = 0xFF;
    // SPS 个数
    rtmpPacket->m_body[nextPosition++] = 0xE1;
    // SPS 长度, 占 2 字节
    // 设置长度的高位
    rtmpPacket->m_body[nextPosition++] = (spsLen >> 8) & 0xFF;
    // 设置长度的低位
    rtmpPacket->m_body[nextPosition++] = spsLen & 0xFF;
    // 拷贝 SPS 数据
    // 将 SPS 数据拷贝到 rtmpPacket->m_body[nextPosition] 地址中
    memcpy(&rtmpPacket->m_body[nextPosition], sps, spsLen);
    // 累加 SPS 长度信息
    nextPosition += spsLen;
    // PPS 个数
    rtmpPacket->m_body[nextPosition++] = 0x01;
    // PPS 数据的长度, 占 2 字节
    // 设置长度的高位
    rtmpPacket->m_body[nextPosition++] = (ppsLen >> 8) & 0xFF;
    // 设置长度的低位
    rtmpPacket->m_body[nextPosition++] = (ppsLen) & 0xFF;
    // 拷贝 SPS 数据
    memcpy(&rtmpPacket->m_body[nextPosition], pps, ppsLen);
    // 设置 RTMP 包类型, 视频类型数据
    rtmpPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    // 设置 RTMP 包长度
    rtmpPacket->m_nBodySize = rtmpPackagesize;
    // 分配 RTMP 通道, 随意分配
    rtmpPacket->m_nChannel = 10;
    // 设置视频时间戳, 如果是 SPP PPS 数据, 没有时间戳
    rtmpPacket->m_nTimeStamp = 0;
    // 设置绝对时间, 对于 SPS PPS 赋值 0 即可
    rtmpPacket->m_hasAbsTimestamp = 0;
    // 设置头类型, 随意设置一个
    rtmpPacket->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
}


目录
相关文章
|
5月前
|
安全 API 开发工具
Android平台RTMP推送|轻量级RTSP服务如何实现麦克风|扬声器声音采集切换
Android平台扬声器播放声音的采集,在无纸化同屏等场景下,意义很大,早期低版本的Android设备,是没法直接采集扬声器audio的(从Android 10开始支持),所以,如果需要采集扬声器audio,需要先做系统版本判断,添加相应的权限。
122 0
|
5月前
|
编解码 开发工具 Android开发
Android平台实现屏幕录制(屏幕投影)|音频播放采集|麦克风采集并推送RTMP或轻量级RTSP服务
Android平台屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务的相关技术实现,做成高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器
|
5月前
|
Web App开发 网络协议 Android开发
Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【9月更文挑战第4天】本文详细对比了在Android平台上实现一对一音视频通话时常用的WebRTC、RTMP及RTSP三种技术方案。从技术原理、性能表现与开发难度等方面进行了深入分析,并提供了示例代码。WebRTC适合追求低延迟和高质量的场景,但开发成本较高;RTMP和RTSP则在简化开发流程的同时仍能保持较好的传输效果,适用于不同需求的应用场景。
290 1
|
6月前
|
Android开发 iOS开发
Android项目架构设计问题之将隐式跳转的逻辑进行抽象和封装如何解决
Android项目架构设计问题之将隐式跳转的逻辑进行抽象和封装如何解决
71 0
|
6月前
|
图形学 Android开发 iOS开发
穿越数字洪流,揭秘Unity3d中的视频魔法!Windows、Android和iOS如何征服RTSP与RTMP的终极指南!
【8月更文挑战第15天】在数字媒体的海洋中,实时视频流是连接世界的桥梁。对于那些渴望在Unity3d中搭建这座桥梁的开发者来说,本文将揭示如何在Windows、Android和iOS平台上征服RTSP与RTMP的秘密。我们将深入探讨这两种协议的特性,以及在不同平台上实现流畅播放的技巧。无论你是追求稳定性的RTSP拥趸,还是低延迟的RTMP忠实粉丝,这里都有你需要的答案。让我们一起穿越数字洪流,探索Unity3d中视频魔法的世界吧!
117 2
|
6月前
|
开发工具 Android开发 开发者
Android平台如何不推RTMP|不发布RTSP流|不实时录像|不回传GB28181数据时实时快照?
本文介绍了一种在Android平台上实现实时截图快照的方法,尤其适用于无需依赖系统接口的情况,如在RTMP推送、RTSP服务或GB28181设备接入等场景下进行截图。通过底层模块(libSmartPublisher.so)实现了截图功能,封装了`SnapShotImpl.java`类来管理截图流程。此外,提供了关键代码片段展示初始化SDK实例、执行截图、以及在Activity销毁时释放资源的过程。此方案还考虑到了快照数据的灵活处理需求,符合GB/T28181-2022的技术规范。对于寻求更灵活快照机制的开发者来说,这是一个值得参考的设计思路。
|
6月前
|
编解码 开发工具 Android开发
Android平台RTMP直播推送模块技术接入说明
大牛直播SDK跨平台RTMP直播推送模块,始于2015年,支持Windows、Linux(x64_64架构|aarch64)、Android、iOS平台,支持采集推送摄像头、屏幕、麦克风、扬声器、编码前、编码后数据对接,功能强大,性能优异,配合大牛直播SDK的SmartPlayer播放器,轻松实现毫秒级的延迟体验,满足大多数行业的使用场景。RTMP直播推送模块数据源,支持编码前、编码后数据对接
|
6月前
|
Web App开发 网络协议 Android开发
### 惊天对决!Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【8月更文挑战第14天】随着移动互联网的发展,实时音视频通信已成为移动应用的关键部分。本文对比分析了Android平台上WebRTC、RTMP与RTSP三种主流技术方案。WebRTC提供端到端加密与直接数据传输,适于高质量低延迟通信;RTMP适用于直播场景,但需服务器中转;RTSP支持实时流播放,但在复杂网络下稳定性不及WebRTC。三种方案各有优劣,WebRTC功能强大但集成复杂,RTMP和RTSP实现较简单但需额外编码支持。本文还提供了示例代码以帮助开发者更好地理解和应用这些技术。
186 0
|
Android开发
Android RTMP直播推流方案选择
1. 技术科普: RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。
2556 0
|
4天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
23 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    Android历史版本与APK文件结构
  • 3
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 6
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 7
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 8
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
  • 9
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 10
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 1
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    26
  • 2
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    34
  • 3
    Android历史版本与APK文件结构
    121
  • 4
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    29
  • 5
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
    23
  • 6
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
    57
  • 7
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    37
  • 8
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
    73
  • 9
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    120
  • 10
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
    29