前言
Camera360应用录像预览在我们的设备上存在滞后的问题。
具体现象在你快速摄像头角度的时候,预览画面不能及时更新到当前摄像头拍摄的角度的画面,
或者你拍你自己的手,快速握拳展开,预览画面需要延迟一些时间才能显示展开的手
一、程序员的直觉
线索
一:原生Camera应用没有问题,只有Camera360的应用有问题。
二:降低Camera的输出帧率到15帧左右,可以缓解Camera360的问题
1.1 Camera360应用自身问题
很多工程师相信肯定有这个直觉,而且可能用以下这个句话就可以给领导一个交代
原生Camera应用没有问题,只有Camera360的应用有问题,判断是应用自己的问题。
由于Camera360没有源码,暂时无法进一步分析这个问题。
1.2 Camera360无法及时处理一帧画面
这个是我的第一直觉,虽然没有源码我也很好奇为什么,我希望通过Trace来验证我的直觉。
二、Camera APP架构
虽然我对Camera不是很熟悉,但是利用我掌握的知识,推测出Camera预览应用有两种架构。
2.1 架构A
CameraServer直接输出buffer给Camera APP创建的SurfaceView,然后SurfaceFlinger可以直接显示Camera的buffer。
优点:预览延迟少,因为整个预览帧到显示屏幕的过程,其实是跳过APP。
缺点:无法进一步对预览的效果进行加工处理
2.2架构B
CameraServer直接输出buffer给Camera APP创建的SurfaceTexture,APP再通过OpenGL等手段加工,然后在将加工后的buffer通过SurfaceView交给了SurfaceFlinger,SurfaceFlinger显示Camera的buffer。
优点:应用可以对预览的效果进行加工处理,例如美颜效果。
缺点:一旦加工超时,就会导致预览帧无法及时的显示到屏幕上,导致预览延迟。
进一步的直觉
看完上面的架构描述,加上上面的两个线索,再考考你的直觉,Camera360采用的是架构A还是架构B。
答案呼之欲出,Camera360采用的是架构B,因为架构B的缺点和线索完全吻合,而且大概率是Camera360的加工超时导致的这个问题。
以上所有推测是在我没有看Trace和代码之前完成,我比较喜欢提前推测问题的可能性,因为顺着推测更有利于分析Trace和代码。
三、一帧预览buffer到屏幕显示
接下来我们通过Trace来分析一帧预览buffer到屏幕显示,看看导致这个问题的终极元凶是谁。
3.1 CameraProvider->CameraServer->Camera360
箭头1:CameraProvider(HAL) 回调CallBack,然后通过Binder调用告诉CameraServer一帧已经准备好。
箭头2:CameraServer在3.1.1的Binder Reply阶段通过Binder调用将buffer传递到Camera360的SurfaceTexture。
3.2 Camera360->SurfaceFlinger
第一步:
Camera360的8233线程,acquireBuffer拿到3.1中CameraServer通过SurfaceTexture传递的buffer
第二步:
对buffer加工一下,例如美颜一下,处理一下饱和度,色温什么的。
第三步:
通过queuebuffer到SurfaceView,但是发现queuebuffer的时候被同一个bufferqueue的上一帧的GPU绘制卡主了,卡了接近40ms,直到上一帧完成渲染才完成了queuebuffer。
为什么会卡,我们后面分析,先把流程走完 ?
第四步:
GPU开始工作,完成渲染,消耗了接近60ms。
从第3步到第4步,这一帧完全完成GPU绘制就浪费了100ms以上,这还不算加工时间,还有Camera回调到APP的时间,最后SurfaceView显示到屏幕的时间,真正摄像头旋转到拍摄到第一帧到显示到屏幕就远远大于100ms。
四、新增知识点
通过这个Trace我发现两个之前没有掌握的知识点,请看下图。
4.1 Trace分析
知识点1
queuebuffer没有完成,SurfaceView的buffer数量就会增加1,但是实际上这一个buffer对于SurfaceFlinger是不可用。
知识点2
queuebuffer的过程会因为同一个bufferqueue的上一帧GPU绘制未完成而block。
4.2 源码分析
知识点1和2分别对应下列代码中注释的那行代码。
frameworks/native/libs/gui/BufferQueueProducer.cpp
status_t BufferQueueProducer::queueBuffer(int slot,
const QueueBufferInput &input, QueueBufferOutput *output) {
ATRACE_CALL();
...
BufferItem item;
{ // Autolock scope
...
output->width = mCore->mDefaultWidth;
output->height = mCore->mDefaultHeight;
output->transformHint = mCore->mTransformHintInUse = mCore->mTransformHint;
output->numPendingBuffers = static_cast<uint32_t>(mCore->mQueue.size());
output->nextFrameNumber = mCore->mFrameCounter + 1;
ATRACE_INT(mCore->mConsumerName.string(),
static_cast<int32_t>(mCore->mQueue.size()));//知识点1
...
} // Autolock scope
// Wait without lock held
if (connectedApi == NATIVE_WINDOW_API_EGL) {
// Waiting here allows for two full buffers to be queued but not a
// third. In the event that frames take varying time, this makes a
// small trade-off in favor of latency rather than throughput.
lastQueuedFence->waitForever("Throttling EGL Production");//知识点2
}
return NO_ERROR;
}
4.3 小知识
以后看Trace的之后注意,就算buffer size+1了,不代表这帧准备好了,因为可能会等上一帧的GPU渲染完成。就算queuebuffer的方法执行完了,也不代表这帧准备好了,需要等到这帧的GPU渲染完成。
这里都出现了一个相同的词,等待GPU渲染完成,GPU渲染完成就是通过Fence机制通知的。Fence机制,这里暂时不展开说了。
五、不对劲的点
问题还远没有结束,Camera360预览滞后的真实体验远大于我前面分析的100ms极限,滞后的感觉至少有500ms以上,继续跟踪。
六、显示的那一帧是什么时候拍的?
回过头来看这个架构图,大家会发现,其实我只是分析到了从CameraServer的buffer到SurfaceFlinger显示的流程,但是我没有跟踪这个buffer创建的时间点。
也就是从CameraServer将buffer传给CameraHal之后,CameraHal回传给CameraServer的时间
七、跟踪一帧拍摄
我随便选中一个 SurfaceTexture-0-11745-1: 9 跟踪一下。
7.1 从buffer传递给HAL拍摄到HAL回调CameraServer
不看不知道,一看吓一跳,没想到时间间隔竟然有600ms
7.2 buffer传递给HAL拍摄
这一步可以理解为摄像头转向某个角度时候拍摄到的画面。
7.3 HAL回调CameraServer
这一步可以理解为7.2中拍摄的buffer回传给应用用于显示
7.4 小结
没想到一帧拍摄buffer到回调竟然需要600多毫秒,加上显示需要消耗的100多ms,真实当摄像头转到某个角度,这个角度拍的照片到显示到屏幕上保守估计就需要700ms。
这才是真正导致Camera360录像预览滞后的原因
八、为什么每次滞后8帧
发现问题稳定后,看下面的trace从buffer给hal,再到hal回传buffer,中间正好有8个prepareHalRequests
8.1 SurfaceTexture的buffer size
通过搜索SurfaceTexture-0-11745-1: 0~SurfaceTexture-0-11745-1: 9发现SurfaceTexture的数量是10,我一开始是以为SurfaceTexture的上限,导致了buffer的堆积,但是我搜了一下源码SurfaceTexture的buffer的上限是64,而且10和8也没有太大关系,排除了这个可能性。
8.2 MAX_INFLIGHT_REQUESTS
cameraserver中MAX_INFLIGHT_REQUESTS是8,正好和trace吻合。
//我们平台上是8,不同的平台可能会设置不同的数值
#define MAX_INFLIGHT_REQUESTS 8
看到prepareHalRequests的过程中调用getBuffer的时候会去判断requests的数量是不是等于8,如果等于8的话,就会用mOutputBufferReturnedSignal休眠,等释放一个requests,就会唤醒。
frameworks/av/services/camera/libcameraservice/device3/Camera3Device.cpp
status_t Camera3Device::RequestThread::prepareHalRequests() {
...
res = outputStream->getBuffer(&outputBuffers->editItemAt(j),
waitDuration,
captureRequest->mOutputSurfaces[streamId]);//跳转到下面代码
...
}
frameworks/av/services/camera/libcameraservice/device3/Camera3Stream.cpp
status_t Camera3Stream::getBuffer(camera3_stream_buffer *buffer,
nsecs_t waitBufferTimeout,
const std::vector<size_t>& surface_ids) {
...
// Wait for new buffer returned back if we are running into the limit.
if (getHandoutOutputBufferCountLocked() == camera3_stream::max_buffers) {//判断是不是等于max,也就是8
ALOGV("%s: Already dequeued max output buffers (%d), wait for next returned one.",
__FUNCTION__, camera3_stream::max_buffers);
nsecs_t waitStart = systemTime(SYSTEM_TIME_MONOTONIC);
if (waitBufferTimeout < kWaitForBufferDuration) {
waitBufferTimeout = kWaitForBufferDuration;
}
res = mOutputBufferReturnedSignal.waitRelative(mLock, waitBufferTimeout);//休眠等待唤醒。
nsecs_t waitEnd = systemTime(SYSTEM_TIME_MONOTONIC);
mBufferLimitLatency.add(waitStart, waitEnd);
if (res != OK) {
if (res == TIMED_OUT) {
ALOGE("%s: wait for output buffer return timed out after %lldms (max_buffers %d)",
__FUNCTION__, waitBufferTimeout / 1000000LL,
camera3_stream::max_buffers);
}
return res;
}
}
}
status_t Camera3Stream::returnBuffer(const camera3_stream_buffer &buffer,
nsecs_t timestamp, bool timestampIncreasing,
const std::vector<size_t>& surface_ids, uint64_t frameNumber) {
...
// Even if returning the buffer failed, we still want to signal whoever is waiting for the
// buffer to be returned.
mOutputBufferReturnedSignal.signal();//唤醒getbuffer中的休眠
return res;
}
也就是说就算SurfaceTexture的buffer再多,必须等一个request空闲出来,才能继续向cameraprovider发起请求,这一切有就都说的通了。
九、总结
用一个比喻来总结整个问题的过程
你:代表Camera360 App
一叠空杯:代表一个SurfaceTexture,一个杯子代表一个buffer
一个服务员:代表CameraServer
一个饮料机:代表CameraProvider
8个餐盘:一个餐盘代表CameraServer的一个request,为什么是8个,因为代码设置是8个,这个数值可以改成。
服务员的生产者流程,首先看有没有空餐盘,如果有空餐盘就向你要一个空杯子,然后拿着餐盘和空杯子去倒饮料,也就是代表我们正常的向camera hal请求一帧画面,杯子饮料倒满了,就放到桌子上,服务员每秒最多可以倒30杯饮料,代表手机最大的预览输出帧率是30帧,没有空餐盘就休息。
你的消费流程,服务员向你要空杯子,你就给他空杯子,然后你只能挑台面上时间最早的饮料,然后先把空餐盘还给服务员,然后拿起杯子,喝饮料,喝饮料也就代表app把一帧画面显示倒屏幕上,喝完以后空杯子留着,继续用
假设你喝饮料的速度大于等于每秒30杯,这样子整个环节服务员不需要用到所有8个餐盘,最多也就用2个餐盘,你呢最多也就用2个空杯,你和服务员就可以很顺畅的流水协作起来,甚至你会等服务员出饮料,你喝的饮料永远都是新鲜的
但是假如你喝饮料的速度小于每秒30杯,这样子慢慢的桌子上就会堆积饮料,直到8个餐盘和8个杯子都倒满饮料,而且你还喝着第9个杯子的饮料,服务员一看没有空盘子了,就开始休息了,当你喝完第9个杯子的饮料,你拿起最早的那个第一个餐盘,把餐盘给服务员,开始喝第1个杯子的饮料,服务员一看有餐盘了就问你要了你刚喝完的第9个杯子,然后去倒饮料了,倒完又得等你去喝第2个杯子时候,归还的餐盘,周而复始,你只能喝最老的那杯饮料了,你中间永远隔着7杯饮料
最坑爹的就是这个服务员喜欢的工作流程就是一开始的时候不断向你要空杯子,非得等到他的餐盘用完了,才能允许你开始喝第一杯,所以假如你喝的比服务员出饮料速度慢,也就是慢于每秒30杯,你就永远只能喝不新鲜的饮料,而且这个不新鲜的时长由你喝的速度决定,你永远只能喝8乘n毫秒之前打的饮料,n代表你喝一杯饮料的时间。
总结假设
camera app处理一帧的时间是t毫秒
camera hal提供了i个request
camera hal的出帧频率是每秒n帧
如果1000/t<n,最后app会达到的一个预览延迟的时间T约等于(i-1)t+t,也就是it,为什么要加t,因为一帧图像显示到屏幕上也需要t的时间
解决延迟的办法有三个方向
减小t,治本
减少i ,治标不治本,仅仅是减少延迟的时间
减小n,牺牲了录像的出帧的帧率
尾巴
其实Trace只是一个辅助工具,展现的是一段时间内代码的调用的流程,要学会看trace,首先你要了解Android系统,当你了解Android系统中跨进程,跨线程的机制,UI绘制机制,Input事件机制,你看Trace才能在各种线程进程之前自由的穿梭。
不是你没有掌握看Trace的技巧,而是你还没有彻底了解Android系统。
当然当你彻底了解Android系统之后,如何看Trace,还是需要掌握一些技巧的,推荐以下教程:
https://www.androidperformance.com/2020/02/14/Android-Systrace-SurfaceFlinger/
通过分析这个问题,我对bufferqueue的生产者消费者模型,还有fence机制有了更加深入的理解,给大家推荐一些这方面写的比较好的博客。
https://www.jianshu.com/p/dca7c4d9495c
http://tangzm.com/blog/?p=167
https://blog.csdn.net/w401229755/article/details/39228535
https://www.cnblogs.com/brucemengbm/p/6881925.html