Google cameraview开源框架的bug分析

一.问题背景

前段时间排查某个APP无法拍照问题的时候,发现了APP的log中有如下的异常堆栈:

12-25 11:40:41.562 10170 27566 27566 W System.err: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.SortedSet.last()' on a null object reference

12-25 11:40:41.566 10170 27566 27566 W System.err:     at com.google.android.cameraview.Camera2.prepareImageReader(Camera2.java:450)

12-25 11:40:41.566 10170 27566 27566 W System.err:     at com.google.android.cameraview.Camera2.start(Camera2.java:216)

12-25 11:40:41.566 10170 27566 27566 W System.err:     at com.google.android.cameraview.Camera2.setFacing(Camera2.java:250)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at com.google.android.cameraview.CameraView.setFacing(CameraView.java:337)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at com.xxx.xxx.xxx.xxx(XXXActivity.java:220) //隐藏APP名称

12-25 11:40:41.567 10170 27566 27566 W System.err:     at android.os.Handler.handleCallback(Handler.java:873)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at android.os.Handler.dispatchMessage(Handler.java:99)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at android.os.Looper.loop(Looper.java:201)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at android.app.ActivityThread.main(ActivityThread.java:6815)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at java.lang.reflect.Method.invoke(Native Method)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)

12-25 11:40:41.567 10170 27566 27566 W System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)

问题很明显,APP在开启相机预览的时候出现了空指针异常,堆栈顶端使用的是Google开源的cameraview框架,这个就跟普通view一样使用,可以实现预览和拍照。具体使用可以参见github:https://github.com/google/cameraview

关于cameraview的原理,这边简单介绍一下,它是通过获取系统相机支持的previewSize和pictureSize列表,然后将两个列表取交集,最终开启预览。

二.问题分析

根据上述堆栈定位到camera2.start()方法:

boolean start() {
    if (!chooseCameraIdByFacing()) {
        return false;
    }
    collectCameraInfo();   //收集相机信息
    prepareImageReader();  //开启相机前准备
    startOpeningCamera();  //打开相机
    return true;
}
 
private void collectCameraInfo() {
    StreamConfigurationMap map = mCameraCharacteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    if (map == null) {
        throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
    }
    mPreviewSizes.clear();
    for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
        int width = size.getWidth();
        int height = size.getHeight();
        if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
            mPreviewSizes.add(new Size(width, height));
        }
    }
    mPictureSizes.clear();
    collectPictureSizes(mPictureSizes, map);
    for (AspectRatio ratio : mPreviewSizes.ratios()) {
        if (!mPictureSizes.ratios().contains(ratio)) {
            mPreviewSizes.remove(ratio);
        }
    }
 
    if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
        mAspectRatio = mPreviewSizes.ratios().iterator().next();
    }
}
 
 
private void prepareImageReader() {
    if (mImageReader != null) {
        mImageReader.close();
    }
    Size largest = mPictureSizes.sizes(mAspectRatio).last();                              //空指针出现的地方
    mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
            ImageFormat.JPEG, /* maxImages */ 2);
    mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
}

如上代码,框架源码首先收集相机信息,然后将previewSize和pictureSize取交集,然后选择previewSize里面的AspectRadio作为纵横比,最后以此作为pictureSize的size。

好的,我们debug看看:

调用前置摄像头的时候,获取的手机的pictureSize只有一组,是6560X4928,框架对其做了除以最大公约数处理,得出比例是205:154。同时,取出的previewSize有5组纵横比,每组纵横比有若干个分辨率尺寸。

然后走到下面这块代码进行取交集:

for (AspectRatio ratio : mPreviewSizes.ratios()) {             //交集筛选
        if (!mPictureSizes.ratios().contains(ratio)) {
            mPreviewSizes.remove(ratio);
        }
    }


if (!mPreviewSizes.ratios().contains(mAspectRatio)) {          //筛选完毕后判断previewSize里面是否有默认的纵横比mAspectRatio,默认是4:3,没有的话就从previewSize里面取
        mAspectRatio = mPreviewSizes.ratios().iterator().next();
    }

图中可以看到,previewSize和pictureSize并没有AspectRadio上的交集,因此执行完上述代码后,mPreviewSize集合里面应该是空的,按理说执行到mPreviewSizes.ratios().iterator().next()就会出问题,不会等到下面函数发生空指针了。

咱们再看看上面的for循环,mPreviewSizes是个ArrayMap映射,mPreviewSizes.ratios()是个keySet列表,对列表遍历的同时还对列表进行remove,想不通Google竟然也能犯如此低级的错误,也就是说mPreviewSizes不会清除干净,于是走到下面代码:

private void prepareImageReader() {
    if (mImageReader != null) {
        mImageReader.close();
    }
    Size largest = mPictureSizes.sizes(mAspectRatio).last();                              //空指针出现的地方
    mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
            ImageFormat.JPEG, /* maxImages */ 2);
    mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
}

mAspectRatio是previewSize里面取出来的,pictureSize里面当然没有啊,于是上面代码就自然空指针咯。

三.问题总结

该问题属于Google cameraview框架问题,具体表现在两个问题上:

1.为何一定要取previewSize和pictureSize的交集进行参数准备?

2.for循环集合迭代删除的低级错误,得亏它用的是ArrayMap,但凡使用HashMap,早就报concurrentModificationException了

上述的github地址中,我看了下,早就有人提过类似的问题并且做了修改,但是最新的代码仍然是老样子,不知道为啥没合入修改。。。。。。

发布了10 篇原创文章 · 获赞 89 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/cbzcbzcbzcbz/article/details/104070410