前言
Camera360應用錄像預覽在我們的設備上存在滯后的問題。
具體現(xiàn)象在你快速攝像頭角度的時候,預覽畫面不能及時更新到當前攝像頭拍攝的角度的畫面,
或者你拍你自己的手,快速握拳展開,預覽畫面需要延遲一些時間才能顯示展開的手
一、程序員的直覺
線索
一:原生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創(chuàng)建的SurfaceView,然后SurfaceFlinger可以直接顯示Camera的buffer。
優(yōu)點:預覽延遲少,因為整個預覽幀到顯示屏幕的過程,其實是跳過APP。
缺點:無法進一步對預覽的效果進行加工處理
2.2架構B

CameraServer直接輸出buffer給Camera APP創(chuàng)建的SurfaceTexture,APP再通過OpenGL等手段加工,然后在將加工后的buffer通過SurfaceView交給了SurfaceFlinger,SurfaceFlinger顯示Camera的buffer。
優(yōu)點:應用可以對預覽的效果進行加工處理,例如美顏效果。
缺點:一旦加工超時,就會導致預覽幀無法及時的顯示到屏幕上,導致預覽延遲。
進一步的直覺
看完上面的架構描述,加上上面的兩個線索,再考考你的直覺,Camera360采用的是架構A還是架構B。
答案呼之欲出,Camera360采用的是架構B,因為架構B的缺點和線索完全吻合,而且大概率是Camera360的加工超時導致的這個問題。
以上所有推測是在我沒有看Trace和代碼之前完成,我比較喜歡提前推測問題的可能性,因為順著推測更有利于分析Trace和代碼。
三、一幀預覽buffer到屏幕顯示
接下來我們通過Trace來分析一幀預覽buffer到屏幕顯示,看看導致這個問題的終極元兇是誰。
3.1 CameraProvider->CameraServer->Camera360

箭頭1:CameraProvider(HAL) 回調CallBack,然后通過Binder調用告訴CameraServer一幀已經(jīng)準備好。
箭頭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,但是發(fā)現(xiàn)queuebuffer的時候被同一個bufferqueue的上一幀的GPU繪制卡主了,卡了接近40ms,直到上一幀完成渲染才完成了queuebuffer。
為什么會卡,我們后面分析,先把流程走完 ?
第四步:
GPU開始工作,完成渲染,消耗了接近60ms。
從第3步到第4步,這一幀完全完成GPU繪制就浪費了100ms以上,這還不算加工時間,還有Camera回調到APP的時間,最后SurfaceView顯示到屏幕的時間,真正攝像頭旋轉到拍攝到第一幀到顯示到屏幕就遠遠大于100ms。
四、新增知識點
通過這個Trace我發(fā)現(xiàn)兩個之前沒有掌握的知識點,請看下圖。

4.1 Trace分析
知識點1
queuebuffer沒有完成,SurfaceView的buffer數(shù)量就會增加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的方法執(zhí)行完了,也不代表這幀準備好了,需要等到這幀的GPU渲染完成。
這里都出現(xiàn)了一個相同的詞,等待GPU渲染完成,GPU渲染完成就是通過Fence機制通知的。Fence機制,這里暫時不展開說了。
五、不對勁的點
問題還遠沒有結束,Camera360預覽滯后的真實體驗遠大于我前面分析的100ms極限,滯后的感覺至少有500ms以上,繼續(xù)跟蹤。
六、顯示的那一幀是什么時候拍的?
回過頭來看這個架構圖,大家會發(fā)現(xiàn),其實我只是分析到了從CameraServer的buffer到SurfaceFlinger顯示的流程,但是我沒有跟蹤這個buffer創(chuàng)建的時間點。
也就是從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幀
發(fā)現(xiàn)問題穩(wěn)定后,看下面的trace從buffer給hal,再到hal回傳buffer,中間正好有8個prepareHalRequests

8.1 SurfaceTexture的buffer size
通過搜索SurfaceTexture-0-11745-1: 0~SurfaceTexture-0-11745-1: 9發(fā)現(xiàn)SurfaceTexture的數(shù)量是10,我一開始是以為SurfaceTexture的上限,導致了buffer的堆積,但是我搜了一下源碼SurfaceTexture的buffer的上限是64,而且10和8也沒有太大關系,排除了這個可能性。
8.2 MAX_INFLIGHT_REQUESTS
cameraserver中MAX_INFLIGHT_REQUESTS是8,正好和trace吻合。
//我們平臺上是8,不同的平臺可能會設置不同的數(shù)值
#define MAX_INFLIGHT_REQUESTS 8
看到prepareHalRequests的過程中調用getBuffer的時候會去判斷requests的數(shù)量是不是等于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空閑出來,才能繼續(xù)向cameraprovider發(fā)起請求,這一切有就都說的通了。
九、總結
用一個比喻來總結整個問題的過程
你:代表Camera360 App
一疊空杯:代表一個SurfaceTexture,一個杯子代表一個buffer
一個服務員:代表CameraServer
一個飲料機:代表CameraProvider
8個餐盤:一個餐盤代表CameraServer的一個request,為什么是8個,因為代碼設置是8個,這個數(shù)值可以改成。
服務員的生產(chǎn)者流程,首先看有沒有空餐盤,如果有空餐盤就向你要一個空杯子,然后拿著餐盤和空杯子去倒飲料,也就是代表我們正常的向camera hal請求一幀畫面,杯子飲料倒?jié)M了,就放到桌子上,服務員每秒最多可以倒30杯飲料,代表手機最大的預覽輸出幀率是30幀,沒有空餐盤就休息。
你的消費流程,服務員向你要空杯子,你就給他空杯子,然后你只能挑臺面上時間最早的飲料,然后先把空餐盤還給服務員,然后拿起杯子,喝飲料,喝飲料也就代表app把一幀畫面顯示倒屏幕上,喝完以后空杯子留著,繼續(xù)用
假設你喝飲料的速度大于等于每秒30杯,這樣子整個環(huán)節(jié)服務員不需要用到所有8個餐盤,最多也就用2個餐盤,你呢最多也就用2個空杯,你和服務員就可以很順暢的流水協(xié)作起來,甚至你會等服務員出飲料,你喝的飲料永遠都是新鮮的
但是假如你喝飲料的速度小于每秒30杯,這樣子慢慢的桌子上就會堆積飲料,直到8個餐盤和8個杯子都倒?jié)M飲料,而且你還喝著第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只是一個輔助工具,展現(xiàn)的是一段時間內代碼的調用的流程,要學會看trace,首先你要了解Android系統(tǒng),當你了解Android系統(tǒng)中跨進程,跨線程的機制,UI繪制機制,Input事件機制,你看Trace才能在各種線程進程之前自由的穿梭。
不是你沒有掌握看Trace的技巧,而是你還沒有徹底了解Android系統(tǒng)。
當然當你徹底了解Android系統(tǒng)之后,如何看Trace,還是需要掌握一些技巧的,推薦以下教程:
https://www.androidperformance.com/2020/02/14/Android-Systrace-SurfaceFlinger/
通過分析這個問題,我對bufferqueue的生產(chǎn)者消費者模型,還有fence機制有了更加深入的理解,給大家推薦一些這方面寫的比較好的博客。
http://www.itdecent.cn/p/dca7c4d9495c
http://tangzm.com/blog/?p=167
https://blog.csdn.net/w401229755/article/details/39228535
https://www.cnblogs.com/brucemengbm/p/6881925.html