如果app打開相機進行預覽,但是不通過setPreviewCallbackWithBuffer函數來獲取預覽的數據的話,mediaserver佔用的cpu資源會非常的低, 在10%左右。而如果想通過setPreviewCallbackWithBuffer等回調獲取數據的話,佔用的cpu資源就會相當的高了,增幅可達15%左右。
如果想要優化這個cpu的佔用率的話,最簡單直接的是降低分辨率或幀率,但是這麼一來,顯示效果就降低了。那麼有沒有辦法在不降低顯示效果的情況下,來降低這個cpu的佔用率呢?答應是當然有的。
下面我們以mtk 6580 android 5.1爲例來講敘這個過程,當然其他平臺的都是一個道理。在mtk 5.1上,正常的預覽和回調給app用戶的數據,各佔用了一路數據。正常的預覽走的是DisplayClient這一路,我們可以DisplayClient.BufOps.cpp這個文件裏看到它對buff數據的處理流程。 回調給app用戶的走的是PreviewClient這一路,我們可以在PreviewClient.BufOps.cpp這個文件裏看到它對buff數據的處理流程。
其中,PreviewClient這一路對應的線程名字是"CamClient@Preview",它在vendor\mediatek\proprietary\hardware\mtkcam\v1\client\CamClient\PreviewCallback\PreviewClient.Thread.cpp裏的readyToRun()函數中,通過::prctl(PR_SET_NAME,(unsigned long)"CamClient@Preview", 0, 0, 0);來設定了它的名字.
當線程準備好後,在vendor\mediatek\proprietary\hardware\mtkcam\v1\client\CamClient\PreviewCallback\PreviewClient.cpp裏的onStateChanged(),會通"postCommand(Command(Command::eID_WAKEUP));"調用 PreviewClient.Thread.cpp發送一個eID_WAKEUP命令。當PreviewClient::threadLoop()收到這個命令後,會調用到onClientThreadLoop這個函數,然後在這個函數裏開啓一個死循環來循環讀取camera傳過來的buff
while (1)
{
MY_LOGD("PreviewClient.Thread.cpp::onClientThreadLoop 6");
// (.1)
waitAndHandleReturnBuffers(pBufQueue);
// (.2) break if disabled.
// add isProcessorRunning to make sure the former pauseProcessor
// is sucessfully processed.
if ( ! isEnabledState() || ! pBufQueue->isProcessorRunning() )
{
MY_LOGI("Preview client disabled");
while ( ! mImgBufList.empty() )
{
::android_atomic_write(1, &mIsWaitBufBack);
MY_LOGI("mImgBufList.size(%d)",mImgBufList.size());
usleep(30*1000);
if(::android_atomic_release_load(&mIsPrvStarted) == 0)
{
MY_LOGI("stop preview");
break;
}
waitAndHandleReturnBuffers(pBufQueue);
}
break;
}
// (.3) re-prepare all TODO buffers, if possible,
// since some DONE/CANCEL buffers return.
prepareAllTodoBuffers(pBufQueue, pBufMgr);
}
這裏的waitAndHandleReturnBuffers定義在PreviewClient.BufOps.cpp這個文件裏,
bool
PreviewClient::
waitAndHandleReturnBuffers(sp<IImgBufQueue>const& rpBufQueue)
{
bool ret = false;
Vector<ImgBufQueNode> vQueNode;
//
// android::CallStack cs("PreviewClient.BufOps.cpp::waitAndHandleReturnBuffers");
MY_LOGD("PreviewClient.BufOps.cpp::waitAndHandleReturnBuffers start");
//
// (1) deque buffers from processor.
rpBufQueue->dequeProcessor(vQueNode);
if ( vQueNode.empty() ) {
MY_LOGW("vQueNode.empty()");
goto lbExit;
}
//
// (2) handle buffers dequed from processor.
ret = handleReturnBuffers(vQueNode);
lbExit:
//
MY_LOGD_IF((2<=miLogLevel), "- ret(%d)", ret);
MY_LOGD("waitAndHandleReturnBuffers end");
return ret;
}
它收到buff後,會調用handleReturnBuffers去處理
bool
PreviewClient::
handleReturnBuffers(Vector<ImgBufQueNode>const& rvQueNode)
{
......
performPreviewCallback(pListImgBuf, rQueNode.getCookieDE());
......
return true;
}
在這個函數裏,我們只需要關心performPreviewCallback這一個函數,它的定義如下:
void
PreviewClient::
performPreviewCallback(sp<ICameraImgBuf>const& pCameraImgBuf, int32_t const msgType)
{
......
pCamMsgCbInfo->mDataCb(
0 != msgType ? msgType : (int32_t)CAMERA_MSG_PREVIEW_FRAME,
pCameraImgBuf->get_camera_memory(),
pCameraImgBuf->getBufIndex(),
NULL,
pCamMsgCbInfo->mCbCookie
);
......
}
在這裏,將處理好的buff數據,通過datacb返回到了framework,然後再返回到了app層。這裏在將buff傳給datacb的時候,大家注意到了,用到的是共享內存傳輸,這裏貌似沒有地方可以優化。而對buff編碼的那些地方,都是調用的標準的yuv函數,更加沒有啥好優化的。所以我們一步步按着這個回調往上看,看有沒有地方可以優化。
通過一步步的跟進,我們最終跑到了frameworks\av\services\camera\libcameraservice\api1\CameraClient.cpp文件的handlePreviewData函數裏,在這個函數裏,通過sp<IMemoryHeap> heap = mem->getMemory(&offset, &size);將共享內存裏的數據取了出來,然後傳給了copyFrameAndPostCopiedFrame。在copyFrameAndPostCopiedFrame裏處理好後,最終會調用到frameworks\base\core\java\android\hardware\Camera.java裏的postEventFromNative函數裏,應用app層就可以收到buff了。
我們要優化處理的地方,就在這個copyFrameAndPostCopiedFrame函數裏。在這個函數裏有一條語句:memcpy(previewBufferBase, (uint8_t *) heapBase + offset, size);。 當前我手上的設備的分辨率是1280*720的,轉換出來的yuv格式的buff的大小爲1382400,也就是1.3M。
拷貝這麼大的一塊內存,是相當的耗費時間和cpu資源的。我們可以在這條memcpy的前後各加一條打印信息,用來打印時間戳,結果發現拷貝一次花費的時間大概爲25~40毫秒。同時我們可以用adb shell top -m 10 -t打印排名靠前的10條線和信息,會發現返回預覽數據的線程"CamClient@Preview"佔用的cpu資源,將會達到15%左右。
這個線程佔用的cpu資源,主要就是在這裏內存拷貝時消耗的。知道了消耗資源的地方,該如何去優化呢?畢竟memcpy是linux標準的函數,好像沒有太多的優化空間了。
優化空間還是有的,針對memcpy的優化,網上有的說可以改寫memcpy,因爲memcpy一次性拷的是一個字節的大小,現在每一幀的數據大小爲1382400,也就要拷貝1382400這麼多次,這是一個相當恐怖的數字。於是有網友就提出,可以如下改進這個函數:
void * mymemcpy ( void * dst,const void * src,size_t count)
{
void * ret = dst;
while (count--) {
*(uint64_t *)dst = *(uint64_t *)src;
dst = (uint64_t *)dst + 1;
src = (uint64_t *)src + 1;
}
return (ret);
}
這個函數和memcpy的區別就是,它可以一次性拷貝8個字節的數據,理論上速度會提升8倍,原來拷貝一次要40毫秒,用這個函數,理論上只需要5毫秒。然而,理想是美好的,現實是殘酷的。經過本人實測,這樣然並卵,速度並沒有提升,反而每一次會多出幾毫秒,具體的原因還不清楚,有清楚原理的大神可以一起交流下。
現在似乎一下陷入了無解地境地,既然memcpy不能優化,那還有什麼辦法沒?當然有,那就優化memcpy的調用邏輯。注意,不是memcpy的實現邏輯,而是調用邏輯。
我們知道,linux的最小內存頁單位是4k,這個我們可以通過getpagesize()函數來確認,它返回的是4096,也就是4kb。如果我每每次拷貝的大小,小於或大於這個值,那麼每拷貝一個字節,cpu都會去通計算跨頁的地址轉換。這是一個相當費時費資源的操作。當然,如果你不是第一次訪問這塊內存,那麼速度就會快很多,因爲線程池會記下你這塊內存的邏輯地址。如果是第一次訪問,那這個計算的過程,就是必不可少的。
這個原理是,linux分配內存時,每一頁(4096個字節)都是儘可能的分配在相連的物理地址上。但是實際上不可能每一塊內存在物理地址上剛好相連。於是linux經常會把你需要的內存分配在邏輯上相連但物理地址不相連的內存塊上,每一頁的物理地址都會記錄下來。然後你第一次去訪問這塊內存時,如果是在一頁上,那邏輯地址是已經計算好了的,直接使用即可。但是如果是超過了一頁,那麼cpu就要先停下來,去計算這個物理地址對應的邏輯地址。而這個過程,是相當長的。當然,計算好後會保存在TLB地址變換高速緩存當中,第二次去訪問這塊內存時,就不用重新計算了,會相當的快。
我們想要節省cpu資源,就必須從這裏入手。
好了,肉戲來了。原生的CameraClient::copyFrameAndPostCopiedFrame函數裏,拷貝邏輯是這樣的:
memcpy(previewBufferBase, (uint8_t *) heapBase + offset, size);
就這麼一條,將整個buff的數據全部拷過來了。我們爲了節省掉地址計算和跨頁轉換的開銷,將它做了如下改動:
int mPagesize = getpagesize();
int count = size/mPagesize;
int mod = size%mPagesize;
ALOGD("copyFrameAndPostCopiedFrame 1, pagesize=%d, count=%d, mod=%d\n", mPagesize,count, mod);
uint8_t *tempHeapBase = (uint8_t *) heapBase + offset;
for(int i = 0; i < count; ++i)
{
memcpy(previewBufferBase, tempHeapBase, mPagesize);
previewBufferBase = (uint8_t *)previewBufferBase+mPagesize;
tempHeapBase += mPagesize;
}
if(mod != 0)
{
memcpy(previewBufferBase, tempHeapBase, mod);
}
這樣改動後,每次按4K頁大小拷貝,不存在跨頁和新的地址轉換操作,節省了時間開銷。然後在這段代碼的前後加上時間戳打印,發現這樣改動後,拷貝完整個buff,時間由之前的40毫秒,降到了驚人的8毫秒。再用top -m 10 -t打印,發現"CamClient@Preview"佔用的cpu資源,由之前的15%左右,降到了驚人的4%左右。
好了,到這裏爲止,優化mediaserver的工作就做完了。大家有沒有很意外,我們並沒有在編碼方面去嘗試優化,而是採用了最簡單,但是也是最不被大家重視的linux 內存基礎進行了優化。
最後再強調一句,這次採用的優化方法,不僅僅在mediaserver上有作用,而是在所有的大塊的內存拷貝代碼段裏,都有作用。
再補充說一下,如果用戶在camera裏回調了預覽數據,導致mediaserver CPU佔用率過高,引起預覽畫面卡的話,那麼除了上面的治本的方法外,還有一種快速高效的治標的方法,那就是提高預覽回調線程的優先級。
比如在回調線程的開始地方設置線程的優先級:
public void run() {
Process.setThreadPriority(mOSPriority);
........
}