🔥面试官的小抄 面试进阶一网打尽,可能是东半球最好的面试资料
了解更多 加uestc_xsf(备注掘金进群) 技术交流、获取学习资料
Camera 演进简介
最近在项目中遇到 Camera相关的场景,之前对这块不是很了解,趁机补了一下盲区。Android Camera 相关也是生态碎片化较为严重的一块,Android FrameWorkt提供Camera API来实现拍照与屏幕录制的能力,目前Android有三类API
- Camera (为了便于区分 下面简称 Camera1)
此类是用于控制设备相机的旧版API,在Android5.0以下使用,现已Deprecated
Android 5.0以上升级的方案,控制设备相机的API,并且开放出硬件支持级别的厂商定制
(谷歌开放出官方库CameraView 帮助解决相机兼容性问题,也有其他一些三方库)
JetPack中引入,基与Cmaera2 API封装,简化了开发流程,并增加生命周期控制
\
那么 Camera和Camera2如何使用?Camera2优秀在哪里?官方开发的的CameraView和CameraX又是为了解决什么问题,下面来一个个了解下。
Camera1和Camera2实践
相机开发核心流程如下
- 检查权限
- 检测设备摄像头,打开相机
- 创建预览帧 显示实时画面 (一般是通过 SurfaceView、TextureView进行实时预览),每个相机有支持预览的尺寸比如4:3或者16:9、11:9等,比如定制或者横竖屏场景 需要计算合适的预览尺寸
- 设置相机参数,进行拍照监听
- 拍照 保存图片或者操作原始数据
- 释放相机资源
其中步骤3在Camera1和Camera2中稍有不同,Camera1拍照前必须先开启预览,而Camera2流程做了解耦,可以无需预览直接拍照
Camera1
权限声明
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
在Android 6.0以上需要动态申请权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
打开相机
支持传入id
Camera.open() //默认后置摄像头
Camera.open(int id)
创建预览
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
...
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
...
startPreview();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}
});
...
private void startPreview() {
try {
//设置实时预览
mCamera.setPreviewDisplay(mSurfaceHolder);
//Orientation
setCameraDisplayOrientation();
//开始预览
mCamera.startPreview();
……
} catch (Exception e) {
e.printStackTrace();
}
}
在设置预览时候可以通过 setPreviewCallback( Camera.PreviewCallback
)监听预览数据的数据
设置相机参数
Camera.Parameters
//获取Parameters对象
mParameters = camera?.parameters
设置相机相关参数内容非常丰富,这里介绍几个常用的
- setFocusMode 设置对焦模式
- setPreviewSize 设置预览图片大小 (相机支持不同的预览尺寸,比如横竖屏需要计算预览尺寸d可以看下谷歌的CameraView中的CmaeraView1#chooseOptimalSize处理)
- setPreviewFormat 设置预览格式 默认返回NV21
- setPictureSize 设置保存图片的大小
- setPictureFormat 设置保存图片的格式
- setDisplayOrientation 设置相机预览画面旋转的角度,degree取值 0,90,180,270,这里详细可以参考 oritation 或者 Android相机开发中尺寸和方向问题
- setPreviewDisplay 设置实时预览 SurfaceHolder
- setPreviewCallback 监听相机预览数据回调
拍照
mCamera.takePicture
private void takePicture() {
if (null != mCamera) {
mCamera.takePicture(new Camera.ShutterCallback() {
@Override
public void onShutter() {
//按下快门后的回调
}
}, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
//回调没压缩的 base data
}
}, new Camera.PictureCallback() {
@Override
public void onPictureTaken(final byte[] data, Camera camera) {
mCamera.startPreview();
//save data
}
});
}
}
释放相机资源
mCamera.stopPreview
mCamera.release
private void releaseCamera() {
if (null != mCamera) {
mCamera.stopPreview();
mCamera.stopFaceDetection();
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
}
Camera2
从 Android 5.0 开始,Google 引入了一套全新的相机框架 Camera2(android.hardware.camera2)并且废弃了旧的相机框架 Camera1(android.hardware.Camera) ,相较于Camera1,Camera2架构上发生了变化,主要是将相机设备模拟成一个管道,按照顺序处理每一帧的请求并返回给调用方,API上的使用难度,本节先介绍下Camera2在核心流程上的使用
\
由于Camera2架构设计成了管道,在拍照流程中细分出了通过3个类来协同
- CaptureRequest
相机捕获图像的设置请求,包含传感器,镜头,闪光灯等
- CaptureRequest.Builder
CaptureRequest的构造器,使用Builder模式,设置更加方便
- CameraCaptureSession
请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道。一个CameraDevice一次只能开启一个CameraCaptureSession。
源端是相机,另一端是 Target,Target可以是Preview,也可以是ImageReader。
相机过程中处理数据也做了一些优化,抽象出了
- ImageReader
用于从相机打开的通道中读取需要的格式的原始图像数据,可以设置多个ImageReader。
- CameraCharacteristics
主要用于获取相机信息,内部携带大量的信息信息
- CameraDevice
相机设备类,和Camera1中的Camera同级
- CmaeraManager
相机系统服务,用于管理和连接相机设备
\
拍摄流程重新抽象
- 创建一个用于从Pipeline获取图片的CaptureRequest
- 修改CaptureRequest的配置
- 创建两个不同尺寸的Surface用于接收图片数据,并且将他们添加到CaptureRequest中
- 发送配置好的CaptureRequest 到Pieline中等待结果
一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束
获取相机服务
CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
根据相机ID获取相机信息 CameraCharateristics
private boolean chooseCameraIdByFacing() {
try {
int internalFacing = INTERNAL_FACINGS.get(mFacing);
final String[] ids = mCameraManager.getCameraIdList();
……
for (String id : ids) {
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
Integer level = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (level == null ||
level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue;
}
Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
if (internal == null) {
throw new NullPointerException("Unexpected state: LENS_FACING null");
}
if (internal == internalFacing) {
mCameraId = id;
mCameraCharacteristics = characteristics;
return true;
}
}
// Not found
……
// The operation can reach here when the only camera device is an external one.
// We treat it as facing back.
mFacing = Constants.FACING_BACK;
return true;
} catch (CameraAccessException e) {
throw new RuntimeException("Failed to get a list of camera devices", e);
}
}
初始化ImageReader
ImageReader是获取图像数据的重要途径,通过它可以获取到不同格式的图像数据,例如JPEG、YUV、RAW等。通过ImageReader.newInstance(int width, int height, int format, int maxImages)创建ImageReader对象,有4个参数:
- width:图像数据的宽度
- height:图像数据的高度
- format:图像数据的格式,例如ImageFormat.JPEG,ImageFormat.YUV_420_888等
- maxImages:最大Image个数,Image对象池的大小,指定了能从ImageReader获取Image对象的最大值,过多获取缓冲区可能导致OOM,所以最好按照最少的需要去设置这个值
ImageReader其他相关的方法和回调:
- ImageReader.OnImageAvailableListener:有新图像数据的回调
- acquireLatestImage():从ImageReader的队列里面,获取最新的Image,删除旧的,如果没有可用的Image,返回null
- acquireNextImage():获取下一个最新的可用Image,没有则返回null
- close():释放与此ImageReader关联的所有资源
- getSurface():获取为当前ImageReader生成Image的Surface
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);
}
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
try (Image image = reader.acquireNextImage()) {
Image.Plane[] planes = image.getPlanes();
if (planes.length > 0) {
ByteBuffer buffer = planes[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
mCallback.onPictureTaken(data);
}
}
}
};
打开相机设备
cameraManager.openCamera(@NonNull String cameraId,@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)的三个参数:
- cameraId:摄像头的唯一标识
- callback:设备连接状态变化的回调
- handler:回调执行的Handler对象,传入null则使用当前的主线程Handler
mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);
private final CameraDevice.StateCallback mCameraDeviceCallback
= new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
//表示相机打开成功,可以真正开始使用相机,创建Capture会话
mCamera = camera;
mCallback.onCameraOpened();
//创建Capture会话
startCaptureSession();
}
@Override
public void onClosed(@NonNull CameraDevice camera) {
//调用Camera.close()后的回调方法
mCallback.onCameraClosed();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
//当相机断开连接时回调该方法,需要进行释放相机的操作
mCamera = null;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
//当相机打开失败时,需要进行释放相机的操作
Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");
mCamera = null;
}
};
创建 CaptureRequest及其target配置
Camera2是通过管道链接 request+target建立会话,首先我们得通过CaptureRequest.Builder配置好
使用 TEMPLATE_STILL_CAPTURE 模板创建一个用于拍照的 CaptureRequest.Builder 对象,并且添加拍照的 Surface 和预览的 Surface 到其中:
captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)
通过CameraDevice.createCaptureRequest()创建CaptureRequest.Builder对象,传入一个templateType参数,templateType用于指定使用何种模板创建CaptureRequest.Builder对象,templateType的取值:
- TEMPLATE_PREVIEW:预览模式
- TEMPLATE_STILL_CAPTURE:拍照模式
- TEMPLATE_RECORD:视频录制模式
- TEMPLATE_VIDEO_SNAPSHOT:视频截图模式
- TEMPLATE_MANUAL:手动配置参数模式
除了模式的配置,CaptureRequest还可以配置很多其他信息,例如图像格式、图像分辨率、传感器控制、闪光灯控制、3A(自动对焦-AF、自动曝光-AE和自动白平衡-AWB)控制等。在createCaptureSession的回调中可以进行设置
// Auto focus should be continuous for camera preview.
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
setAutoFlash(mPreviewRequestBuilder);
// Finally, we start displaying the camera preview.
mPreviewRequest = mPreviewRequestBuilder.build();
拍照和预览
在Camera2中拍照和预览是解耦开的,有了CaptureRequest之后,需要借助CaptureSession(Capture会话)来描述
mCameraDevice.createCaptureSession()创建Capture会话,它接受了三个参数:
- outputs:用于接受图像数据的surface集合
- callback:用于监听 Session 状态的CameraCaptureSession.StateCallback对象
- handler:用于执行CameraCaptureSession.StateCallback的Handler对象,传入null则使用当前的主线程Handler
\
拍照
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler);
该方法也有三个参数,和mCaptureSession.setRepeatingRequest一样:
- request:CaptureRequest对象
- listener:监听Capture 状态的回调
- handler:用于执行CameraCaptureSession.CaptureCallback的Handler对象,传入null则使用当前的主线程Handler
这里设置了mCaptureCallback:
PictureCaptureCallback mCaptureCallback = new PictureCaptureCallback() {
@Override
public void onPrecaptureRequired() {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
setState(STATE_PRECAPTURE);
try {
mCaptureSession.capture(mPreviewRequestBuilder.build(), this, null);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to run precapture sequence.", e);
}
}
@Override
public void onReady() {
captureStillPicture();
}
};
\
预览
Camera2中,通过连续重复的Capture实现预览功能,每次Capture会把预览画面显示到对应的Surface上。连续重复的Capture操作通过mCaptureSession.setRepeatingRequest(mPreviewRequest,mCaptureCallback, mBackgroundHandler)实现,该方法有三个参数:
- request:CaptureRequest对象
- listener:监听Capture 状态的回调
- handler:用于执行CameraCaptureSession.CaptureCallback的Handler对象,传入null则使用当前的主线程Handler
停止预览使用mCaptureSession.stopRepeating()方法。
\
释放相机资源
先后对CaptureSession,CameraDevice,ImageReader进行close操作,释放资源
void stop() {
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;
}
if (mCamera != null) {
mCamera.close();
mCamera = null;
}
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
}
\
Cmaera2做了哪些优化
架构升级
参考 android 官网 source.android.com/devices/cam…
架构上,Camera2 的 API 模型被设计成一个 Pipeline(管道),它按顺序处理每一帧的请求并返回请求结果给客户端。下面这张来自官方的图展示了 Pipeline 的工作流程
\
在CaptureRequest中设置不同的Surface用于接收不同的图片数据,最后从不同的Surface中获取到图片数据和包含拍照相关信息的CaptureResult
假设我们想要同时拍摄两张不同尺寸的图片,并且在拍摄的过程中闪光灯必须亮起来。整个拍摄流程如下:
- 创建一个用于从 Pipeline 获取图片的 CaptureRequest。
- 修改 CaptureRequest 的闪光灯配置,让闪光灯在拍照过程中亮起来。
- 创建两个不同尺寸的 Surface 用于接收图片数据,并且将它们添加到 CaptureRequest 中。
- 发送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照结果。
一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。
其中Caputure有以下三种工作模式
- 单次模式 One-shot
指的是一次的Capture操作,例如设置闪光灯、对焦模式、拍一张照片,多个单次模式的Capture会进入队列按照顺序执行
- 多次模式 Burst
指的是连续多次执行指定的Capture操作,该模式执行期间不允许插入其他Capture操作,如连续拍摄100张照片,在这100张照片拍摄期间任何新的capture都会等待
- 重复模式 Repeating
指的是不断重复执行指定的Capture操作,当有其他模式的Capture提交会暂停改模式转而执行其他模式的Capture
\
Supported Hardware Level
相机功能的强大与否和硬件息息相关,不同厂商对 Camera2 的支持程度也不同,所以 Camera2 定义了一个叫做 Supported Hardware Level 的重要概念,其作用是将不同设备上的 Camera2 根据功能的支持情况划分成多个不同级别以便开发者能够大概了解当前设备上 Camera2 的支持情况。截止到 Android P 为止,从低到高一共有 LEGACY、LIMITED、FULL 和 LEVEL_3 四个级别:
- LEGACY:向后兼容的级别,处于该级别的设备意味着它只支持 Camera1 的功能,不具备任何 Camera2 高级特性。
- LIMITED:除了支持 Camera1 的基础功能之外,还支持部分 Camera2 高级特性的级别。
- FULL:支持所有 Camera2 的高级特性。
- LEVEL_3:新增更多 Camera2 高级特性,例如 YUV 数据的后处理等
\
新特性
- 支持在开启相机前检查相机信息
- 在不开启预览情况下拍照
- 一次拍摄多张不同格式和尺寸的图片
- 控制曝光时间
- 连拍
\
简化开发谷歌做的努力
其他优秀三方库
CameraView
主要是为了解决不同版本Camer使用兼容性问题
根据官方的说明:
API Level | Camera API | Preview View |
---|---|---|
9-13 | Camera1 | SurfaceView |
14-20 | Camera1 | TextureView |
21-23 | Camera2 | TextureView |
24 | Camera2 | SurfaceView |
- Camera 区分:Android5.0(21)以下使用 Camera1,以上使用 Camera2
- Preview View:Android6.0(23)以上使用SurfaceView(SurfaceView在Android7.0上增加了新特性(平移、旋转等)),这里应该是 Android7.0以上(>23)使用SurfaceView,其他都使用TextureView,最新的源码sdk最低版本要求14。
类图如下
通过桥接模式+ 适配器模式,抽象出相机操作的抽象类CameraViewImpl和预览抽象类PreviewImpl,业务侧只操作接口
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
……
// Internal setup
// 1.创建预览视图
final PreviewImpl preview = createPreviewImpl(context);
mCallbacks = new CallbackBridge();
// 2.根据 Android SDK 版本选择不同的 Camera
if (Build.VERSION.SDK_INT < 21) {
mImpl = new Camera1(mCallbacks, preview);
} else if (Build.VERSION.SDK_INT < 23) {
mImpl = new Camera2(mCallbacks, preview, context);
} else {
mImpl = new Camera2Api23(mCallbacks, preview, context);
}
……
// 设置相机 ID,如前置或者后置
setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
// 设置预览界面的比例,如 4:3 或者 16:9
String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio);
if (aspectRatio != null) {
setAspectRatio(AspectRatio.parse(aspectRatio));
} else {
setAspectRatio(Constants.DEFAULT_ASPECT_RATIO);
}
// 设置对焦方式
setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
// 设置闪光灯
setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
a.recycle();
// Display orientation detector
// 初始化显示设备(主要指手机屏幕)的旋转监听,主要用来设置相机的旋转方向
mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
@Override
public void onDisplayOrientationChanged(int displayOrientation) {
mImpl.setDisplayOrientation(displayOrientation);
}
};
}
createPreViewImpl实现
private PreviewImpl createPreviewImpl(Context context) {
PreviewImpl preview;
if (Build.VERSION.SDK_INT >= 23) {
preview = new SurfaceViewPreview(context, this);
} else {
preview = new TextureViewPreview(context, this);
}
return preview;
}
使用起来也比较简单
<com.google.android.cameraview.CameraView
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:keepScreenOn="true"
android:adjustViewBounds="true"
app:autoFocus="true"
app:aspectRatio="4:3"
app:facing="back"
app:flash="auto"/>
CameraX
CameraX 是一个 Jetpack 支持库,目的是简化Camera的开发工作,它是基于Camera2 API的基础,向后兼容至 Android 5.0(API 级别 21)。
它有以下几个特性:
- 易用性,只需要几行代码就可以实现预览和拍照
- 保持设备的一致性,在不同相机设备上,对宽高比、屏幕方向、旋转、预览大小和高分辨率图片大小,做到都可以正常使用
- 相机特性的扩展,增加人像、HDR、夜间模式和美颜等功能
- 具备生命周期的管理
具体使用可以参考官方网址使用文档