如果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); ........
}