[ Android实战 ] android query: BufferQueue has been abandoned 问题解决
尊重原创,转载请注明出处!
背景
最近在我们的 Camera 应用上发现一个问题,Camera 应用有一个设置页面,进入设置页面后连续按 back 键退回到预览页面,再退出应用回到桌面,概率性出现应用停止运行的崩溃问题。
经过一天的定位,终于找到了问题的所在并修复了此问题,好久没写实战博客了,因此专门写一篇记录一下。
日志分析
抓了一份崩溃时候的日志,其中关键崩溃日志如下:
--------- beginning of crash
12-07 16:11:03.975 7790 7807 E AndroidRuntime: FATAL EXCEPTION: CameraHandler
12-07 16:11:03.975 7790 7807 E AndroidRuntime: Process: com.xxx.xxx, PID: 7790
12-07 16:11:03.975 7790 7807 E AndroidRuntime: java.lang.reflect.UndeclaredThrowableException
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at $Proxy0.openCamera(Unknown Source)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.presenter.CapturePresenter$5.doInBackground(CapturePresenter.java:162)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.presenter.CapturePresenter$5.doInBackground(CapturePresenter.java:1)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.base.BaseHandler$3.run(BaseHandler.java:40)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:751)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:95)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at android.os.Looper.loop(Looper.java:154)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at android.os.HandlerThread.run(HandlerThread.java:61)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: Caused by: java.lang.reflect.InvocationTargetException
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.common.LogProxy$LogInvocationHandler.invoke(LogProxy.java:48)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at java.lang.reflect.Proxy.invoke(Proxy.java:813)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: ... 8 more
12-07 16:11:03.975 7790 7807 E AndroidRuntime: Caused by: java.lang.RuntimeException: startPreview failed
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at android.hardware.Camera.startPreview(Native Method)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.camera.CameraController$6.execute(CameraController.java:117)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.camera.CameraController.apply(CameraController.java:294)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.platform.xxx.XXXCamera$BackCamera.openCamera(XXXCamera.java:132)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: at com.xxx.xxx.platform.xxx.XXXCamera.openCamera(XXXCamera.java:24)
12-07 16:11:03.975 7790 7807 E AndroidRuntime: ... 11 more
这不简单吗?直接在调用 startPreview 的地方 catch RuntimeException,问题解决,到此结束!
开个玩笑~
捕获异常只是规避问题的手段,用 catch 异常解决问题简单粗暴,但是同时这种方法也会掩盖真正的问题,后续再发现异常时,会更难定位。
不是不能用 catch 捕获异常,但是这种解决问题的方式不应该被滥用!
崩溃的发生,一定是因为应用或系统中存在其他的问题,才导致 startPreview 的时候异常,而这个问题,才是我们真正要定位并修复的。
继续分析日志,从崩溃信息往前看,果然看到了一些异常的地方:
12-07 16:11:03.918 557 8117 D QCamera2HWI: int qcamera::QCamera2HardwareInterface::startPreview(): E
......
12-07 16:11:03.927 557 8117 D QCameraHWI_Mem: void qcamera::QCameraMemory::traceLogAllocStart(size_t, int, const char*) : alloc E count=7 size=0
12-07 16:11:03.927 557 8117 D QCameraHWI_Mem: virtual int qcamera::QCameraGrallocMemory::allocate(uint8_t, size_t) : E
12-07 16:11:03.927 7790 7802 E BufferQueueProducer: [SurfaceTexture-0-7790-1] query: BufferQueue has been abandoned
12-07 16:11:03.927 557 8117 E QCameraHWI_Mem: get_min_undequeued_buffer_count failed: No such device (19)
12-07 16:11:03.927 557 8117 D QCameraHWI_Mem: virtual int qcamera::QCameraGrallocMemory::allocate(uint8_t, size_t) : X
12-07 16:11:03.927 557 8117 D QCameraHWI_Mem: void qcamera::QCameraMemory::traceLogAllocEnd(size_t) : X
12-07 16:11:03.928 557 8117 E QCameraStream: int32_t qcamera::QCameraStream::getBufs(cam_frame_len_offset_t*, uint8_t*, uint8_t**, mm_camera_buf_def_t**, mm_camera_map_unmap_ops_tbl_t*): Failed to allocate stream buffers
......
12-07 16:11:03.959 557 5488 D QCamera2HWI: [KPI Perf] static int qcamera::QCamera2HardwareInterface::start_preview(camera_device*): X
--------- beginning of crash
BufferQueue has been abandoned,嗯,就是这个地方了……
Camera 在生成预览时需要从 BufferQueue 中取出 buffer 进行填充,当 BufferQueue 被释放后,无法分配到 Buffer 填充预览数据,就会导致预览失败。
参考资料
第一反应,百度一下,这该死的本能!
然后,还真让我找到了相关的博客和 * 的问题:
BufferQueue has been abandoned解决方案
SurfaceTexture has been abandoned
这两篇文章中都是同样的问题,因为 surfaceTexture 是在方法中的临时变量,所以会导致方法调用结束后创建的 SurfaceTexture 对象不被持有,从而在 GC 时被当做垃圾回收掉。
因此文章中推荐的做法是将 surfaceTexture 变为类的成员变量,这样就能一直持有 SurfaceTexture 的引用,从而避免被回收而导致 BufferQueue has been abandoned 的报错。
但是这跟我遇到的情况还不一样,我们应用中的 TextureView 是在布局文件中声明,onCreate 时解析 XML 创建的,TextureView 包含成员变量 mSurface(类型为 SurfaceTexture),因此本来就一直持有 SurfaceTexture 的引用。
所以,伸手党不好当啊,博客中的解决方案不能直接使用,还是得继续分析。
解决方案
但是,文章中的思路还是可以参考的:SurfaceTexture 的回收会导致 BufferQueue 被释放,从而导致预览失败。
重新阅读了一下应用源码,发现 Camera 的流程是通过 Handler 操作的,由于是异步操作,时序就没法保证了。
联想到复现问题的步骤,退出设置界面回到预览界面的时候会重新打开预览,而从预览界面退回到桌面的时候,又会走 Activity 的销毁流程。当这两个操作结合到一起的时候,确实可能存在 TextureView 已经移除,但 startPreview 还在执行的情况,而这就会导致问题的发生!
应用的框架和逻辑被同事写得太复杂了= =。我的想法是尽可能小地改动,不影响其他的逻辑,自然而然地就想到了加锁。
因为应用是基于 MVP 模式开发的,Activity 在 onDestroy 中会调用 detach 解绑 view,在 Presenter 类中,绑定/解绑 view,以及操作 Camera 的地方加上锁,确保这些操作同步就行了。
@Override
public void attach(ICaptureView view) {
synchronized (mPreviewLock) {
this.mView = view;
}
}
@Override
public void detach() {
synchronized (mPreviewLock) {
this.mView = null;
}
}
@Override
public void openCamera(final int cameraId, final boolean flashOn) {
mCameraHandler.post(new BaseHandler.OnCallback<ICamera>() {
@Override
public void doInBackground(ICamera camera) {
synchronized (mPreviewLock) {
camera.openCamera(request);
}
}
});
}
作为对比,修改之前,手动测个几次十几次就能出现一次崩溃现象;修改之后,手动复测了一百多次,未复现问题~
源码分析
问题解决了,还是得花点时间研究下这块的流程。知其然知其所以然,搞清楚这块的逻辑,知道为啥会报错,无论对后续的开发还是问题定位都会有很大的帮助!
日志中的 BufferQueue has been abandoned,是在 BufferQueueProducer 中打印的,很容易找到报错的地方:
/frameworks/native/libs/gui/BufferQueueProducer.cpp
int BufferQueueProducer::query(int what, int *outValue) {
......
if (mCore->mIsAbandoned) {
BQ_LOGE("query: BufferQueue has been abandoned");
return NO_INIT;
}
......
}
这里判断了 mIsAbandoned,当 mIsAbandoned 为 true 时,会报错。因此,我们需要查看 mIsAbandoned 到底是在什么时候被置为 true 的。源码中搜索一下,定位到 BufferQueueConsumer:
/frameworks/native/libs/gui/BufferQueueConsumer.cpp
status_t BufferQueueConsumer::disconnect() {
......
mCore->mIsAbandoned = true;
......
return NO_ERROR;
}
这里还有一个知识点,BufferQueueProducer 和 BufferQueueConsumer 是在 BufferQueue 中成对创建的,使用的是同一个 BufferQueueCore,因此在 BufferQueueConsumer 将 mCore->mIsAbandoned 置为 true 时,也会对 BufferQueueProducer 中的逻辑造成影响:
/frameworks/native/libs/gui/BufferQueue.cpp
void BufferQueue::createBufferQueue(sp<IGraphicBufferProducer>* outProducer,
sp<IGraphicBufferConsumer>* outConsumer,
const sp<IGraphicBufferAlloc>& allocator) {
sp<BufferQueueCore> core(new BufferQueueCore(allocator));
sp<IGraphicBufferProducer> producer(new BufferQueueProducer(core));
sp<IGraphicBufferConsumer> consumer(new BufferQueueConsumer(core));
*outProducer = producer;
*outConsumer = consumer;
}
再继续往下看,涉及代码太多了,就不放了,简单画了个调用流程图,可以参考一下:
即 Activity 销毁时,系统会先通过 surfaceflinger 移除相应的 layer,并释放相应的 BufferQueue。
再简单画下上面资料中的流程图,即 BufferQueueConsumer 的所有强引用都被释放后,也是走相同的流程,释放相应的 BufferQueue。
总结
都看到这了,还不给我点个赞吗???
Camera 在预览过程中,必须持有 Surface 的强引用,因此需要注意:
1、不能将 SurfaceTexture 作为临时变量,这样在方法执行结束后,由于没有持有对 Surface 的强引用,系统可能会释放 BufferQueue,导致预览出错。
2、需要注意 Activity 和 Camera 操作的时序问题,如果在预览过程中退出应用,必须在 Surface 移除前先停止预览,否则也会导致预览出错。