NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。
音视频播放器系列:
音视频一二三节已经实现了音视频播放,本节主要是通过Profiler来检测工程存在的内存泄漏问题。
主要内容如下:
1.项目native层内存泄漏全面分析。
2.项目native层内存泄漏各个隐患补救方案。
用到的ffmpeg、rtmp等库资源:
https://wwgl.lanzout.com/iN21C0qiiija
一、项目native层内存泄漏全面分析
通过Profiler可以清楚的看出,视频在播放时,native层消耗的内存不断增加,最终达到1.5G,java层内存几乎没有变化,可以直观的分析出native层存在严重的内存泄漏问题。进一步定位native层代码,涉及到循环、线程的主要有:NdkPlayer.cpp、VideoChannel.cpp、AudioChannel.cpp;
接下来将从这几个类开始,将代码一行行进行分析定位问题。
NdkPlayer.cpp:
问题1:循环中存在明显的生产者生产速度远大于,消费者的消费速度,导致队列撑爆,段时间内存急速增到;
优化方案:放慢生产速度,等待消费者将队列数据消费差不多再生产。
// TODO 1.内存泄漏关键点(控制packet队列大小,等待队列中的数据被消费)
while (isPlaying) {
// 1.1内存泄漏点 解决方案:音频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
if (audio_channel && audio_channel->packets.size() > 100) {
av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
continue;
}
// 1.2内存泄漏点 解决方案:视频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
if (video_channel && video_channel->packets.size() > 100) {
av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
continue;
}
}
问题2:数据使用完后未及时释放;
优化方案:数据使用完后立即释放。
if (result == AVERROR_EOF) {
// end of file == 读到文件末尾了 == AVERROR_EOF
// 表示读完了,要考虑释放播放完成,并不代表播放完毕
LOGI("NdkPlayer::start_() end");
// 1.3内存泄漏点 解决方案:队列的数据被音频 视频 全部播放完毕了,退出
if (video_channel->packets.empty() && audio_channel->packets.empty()) {
break;
}
}
完整优化代码:
/**
* 循环获取压缩包AVPacket,并push压缩包到队列
*/
void NdkPlayer::start_() {
LOGI("NdkPlayer::start_()");
// TODO 1.内存泄漏关键点(控制packet队列大小,等待队列中的数据被消费)
while (isPlaying) {
// 1.1内存泄漏点 解决方案:音频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
if (audio_channel && audio_channel->packets.size() > 100) {
av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
continue;
}
// 1.2内存泄漏点 解决方案:视频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
if (video_channel && video_channel->packets.size() > 100) {
av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
continue;
}
// AVPacket 可能是音频 也可能是视频(压缩包)
AVPacket *packet = av_packet_alloc();
int result = av_read_frame(format_context, packet);
// @return 0 if OK
if (!result) {
// 把压缩包AVPacket 分别加入音频 和 视频队列
if (audio_channel && audio_channel->stream_index == packet->stream_index) {
// 音频
audio_channel->packets.insertToQueue(packet);
} else if (video_channel && video_channel->stream_index == packet->stream_index) {
// 视频
video_channel->packets.insertToQueue(packet);
}
} else if (result == AVERROR_EOF) {
// end of file == 读到文件末尾了 == AVERROR_EOF
// 表示读完了,要考虑释放播放完成,并不代表播放完毕
LOGI("NdkPlayer::start_() end");
// 1.3内存泄漏点 解决方案:队列的数据被音频 视频 全部播放完毕了,退出
if (video_channel->packets.empty() && audio_channel->packets.empty()) {
break;
}
} else {
// av_read_frame 出现了错误,结束当前循环
break;
}
} // end while
isPlaying = 0;
audio_channel->stop();
video_channel->stop();
}
VideoChannel.cpp:
同样存在上面问题。
/**
* 第一个线程: 视频:取出队列的压缩包 进行解码 解码后的原始包 再push队列中去
*/
void VideoChannel::video_decode() {
LOGI("VideoChannel::video_decode()");
// TODO 2.内存泄漏关键点(控制frames队列大小,等待队列中的数据被消费)
AVPacket *pkt = 0;
while (isPlaying) {
// 2.1内存泄漏点 解决方案:不丢弃数据,等待队列中原始包AVFrame被消费,再添加到队列
if (isPlaying && frames.size() > 100) {
av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
continue;
}
// 获取AVPacket * 压缩包
int result = packets.getQueueAndDel(pkt);
if (!isPlaying) {
// 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
break;
}
if (!result) {
// 获取失败,可能是压缩包数据还没有加入队列,继续获取
continue;
}
// 1.发送pkt(压缩包)给缓冲区,@return 0 on success
result = avcodec_send_packet(codecContext, pkt);
// FFmpeg源码缓存一份pkt,释放即可,放到后面释放
// releaseAVPacket(&pkt);
if (result) {
// avcodec_send_packet 出现了错误
break;
}
AVFrame *frame = av_frame_alloc();
// 2.从缓冲区拿出来(原始包),@return 0: success
result = avcodec_receive_frame(codecContext, frame);
if (result == AVERROR(EAGAIN)) {
// B帧 B帧参考前面成功 B帧参考后面失败 可能是P帧没有出来,再拿一次就行了
continue;
} else if (result != 0) {
// avcodec_receive_frame 出现了错误
// 2.2内存泄漏点 解决方案:解码视频的frame出错,马上释放,防止在堆区开辟了空间
if (frame) {
releaseAVFrame(&frame);
}
break;
}
// 拿到了原始包,并将原始包push到队列
frames.insertToQueue(frame);
// TODO 4.内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
}
// 解码获取原始包后,释放压缩包
// 4.1内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
}
/**
* 第二线线程:视频:从队列取出原始包,播放
*/
void VideoChannel::video_play() {
LOGI("VideoChannel::video_play()");
AVFrame *frame = 0;
uint8_t *dst_data[4]; // RGBA 播放文件
int dst_linesize[4]; // RGBA
//给 dst_data 申请内存 width * height * 4 xxxx
av_image_alloc(dst_data, dst_linesize,
codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);
// SWS_BILINEAR 适中算法
SwsContext *sws_ctx = sws_getContext(
// 下面是输入环节
codecContext->width,
codecContext->height,
codecContext->pix_fmt, // 自动获取 xxx.mp4 的像素格式 AV_PIX_FMT_YUV420P // 写死的
// 下面是输出环节
codecContext->width,
codecContext->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR, NULL, NULL, NULL);
while (isPlaying) {
int result = frames.getQueueAndDel(frame);
if (!isPlaying) {
break; // 如果关闭了播放,跳出循环,releaseAVFrame(&frame);
}
if (!result) { // ret == 0
continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
}
// 格式转换 yuv ---> rgba
sws_scale(sws_ctx,
// 下面是输入环节 YUV的数据
frame->data, frame->linesize,
0, codecContext->height,
// 下面是输出环节 成果:RGBA数据 dst_data
dst_data,
dst_linesize
);
/**
* ANatvieWindows 渲染工作
* SurfaceView ----- ANatvieWindows
* 这里拿不到Surface,只能函数指针renderCallback()将RGBA数据 dst_data 回调给 native-lib.cpp,显示
* 函数指针renderCallback()
* 参数1:RGBA数据 dst_data 数组被传递会退化成指针,默认就是取第1元素
* 参数2:视频宽
* 参数3:视频高
* 参数4:数据长度
*/
this->renderCallback(dst_data[0], codecContext->width, codecContext->height,
dst_linesize[0]);
// 释放原始包,因为已经被渲染完了,没用了
// TODO 6.内存泄漏点 原始包已经被播放了,安心释放原始包frame本身空间和frame成员指向的空间
av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
}
// 6.1内存泄漏点 原始包已经被播放了,安心释放原始包frame本身空间和frame成员指向的空间
av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
isPlaying = 0;
av_free(&dst_data[0]);
// free(sws_ctx); FFmpeg必须使用人家的函数释放,直接崩溃
sws_freeContext(sws_ctx);
}
AudioChannel.cpp:
同样存在上面问题。
/**
* 第一个线程: 音频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去(音频:PCM数据)
*/
void AudioChannel::audio_decode() {
LOGI("AudioChannel::audio_decode()");
// TODO 3.内存泄漏关键点(控制frames队列大小,等待队列中的数据被消费)
AVPacket *pkt = 0;
while (isPlaying) {
// 3.1内存泄漏点 解决方案:不丢弃数据,等待队列中原始包AVFrame被消费,再添加到队列
if (isPlaying && frames.size() > 100) {
av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
continue;
}
// 获取AVPacket * 压缩包
int result = packets.getQueueAndDel(pkt);
if (!isPlaying) {
// 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
break;
}
if (!result) {
// 获取失败,可能是压缩包数据还没有加入队列,继续获取
continue;
}
// 1.发送pkt(压缩包)给缓冲区,@return 0 on success
result = avcodec_send_packet(codecContext, pkt);
// FFmpeg源码缓存一份pkt,释放即可,放到后面释放
// releaseAVPacket(&pkt);
if (result) {
// avcodec_send_packet 出现了错误
break;
}
AVFrame *frame = av_frame_alloc();
// 2.从缓冲区拿出来(原始包),@return 0: success
result = avcodec_receive_frame(codecContext, frame);
if (result == AVERROR(EAGAIN)) {
// 有可能音频帧,也会获取失败,重新拿一次
continue;
} else if (result != 0) {
// avcodec_receive_frame 出现了错误
// 3.2内存泄漏点 解决方案:解码视频的frame出错,马上释放,防止在堆区开辟了空间
if (frame) {
releaseAVFrame(&frame);
}
break;
}
// 拿到了原始包,并将原始包push到队列 PCM数据
frames.insertToQueue(frame);
// TODO 5.内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
}
// 解码获取原始包后,释放压缩包
// 5.1内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
}
/**
* 1.out_buffers 给予数据
* 2.out_buffers 给予数据的大小计算工作
* @return 大小还要计算,因为我们还要做重采样工作,重采样之后,大小不同了
*/
int AudioChannel::getPCM() {
LOGI("AudioChannel::getPCM");
int pcm_data_size = 0;
// 从frames队列中,获取PCM数据,frame->data == PCM数据(待 重采样 32bit)
AVFrame *frame = 0;
while (isPlaying) {
int result = frames.getQueueAndDel(frame);
if (!isPlaying) {
break; // 如果关闭了播放,跳出循环,releaseAVPacket(&pkt);
}
if (!result) {
continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
}
/**
* 开始重采样
* 如:来源:10个48000 ----> 目标:44100 11个44100
* 获取单通道的样本数 (计算目标样本数: ? 10个48000 ---> 48000/44100因为除不尽 11个44100)
* 参数1:swr_get_delay(swr_ctx, frame->sample_rate) + frame->nb_samples 获取下一个输入样本相对于下一个输出样本将经历的延迟
* 参数2:out_sample_rate 输出采样率
* 参数3:frame->sample_rate 输入采样率
* 参数4:AV_ROUND_UP 先上取 取去11个才能容纳的上
*/
int dst_nb_samples = av_rescale_rnd(
swr_get_delay(swr_ctx, frame->sample_rate) + frame->nb_samples,
out_sample_rate, frame->sample_rate, AV_ROUND_UP);
/**
* pcm的处理逻辑
* 音频播放器的数据格式是我们自己在下面定义的
* 而原始数据(待播放的音频pcm数据)
* TODO 重采样工作
* 返回的结果:每个通道输出的样本数(注意:是转换后的) 重采样实验(通道基本上都是:1024)
* 参数1:swr_ctx SwrContext
* TODO 下面是输出区域
* 参数2:out_buffers 重采样后的成果的buff
* 参数3:dst_nb_samples 成果的 单通道的样本数 无法与out_buffers对应,所以有下面的pcm_data_size计算
* TODO 下面是输入区域
* 参数4:(const uint8_t **) frame->data 队列的AVFrame * 的PCM数据 未重采样的
* 参数5:frame->nb_samples 输入的样本数
* 参数6:
*/
int samples_per_channel = swr_convert(swr_ctx, &out_buffers, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
/**
* 由于out_buffers 和 dst_nb_samples 无法对应,所以pcm_data_size需要重新计算
* 941通道样本数 * 2样本格式字节数 * 2声道数 =3764
*/
pcm_data_size = samples_per_channel * out_sample_size * out_channels;
break;
} // while end
// TODO 7.内存泄漏点 原始包已经被播放了,安心释放原始包frame本身空间和frame成员指向的空间
av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
/**
* FFmpeg录制 Mac 麦克风 输出 每一个音频包的size == 4096
* 4096是单声道的样本数,44100是每秒钟采样的数
* 单通道样本数:1024 * 2声道 * 2(16bit) = 4,096 == 4096是单声道的样本数
* 采样率 44100是每秒钟采样的次数
* 样本数 = 采样率 * 声道数 * 位声
* 双声道的样本数 = (采样率 * 声道数 * 位声) * 2
*/
return pcm_data_size;
}
优化后再次使用Profiler检测内存:
内存平稳,保持在177M左右,基本解决内存泄漏问题,接下来。。。