[译] 同时使用多的相机流,阿里巴巴安卓面试题答案

// 在我们的样例中,SurfaceView 会自动更新。
// ImageReader 有自己的回调,我们必须监听,以检索帧
// 所以不需要为捕获请求设置回调
session.setRepeatingRequest(combinedRequest.build(), null, null)


如果你正确配置了目标 surfaces,则此代码将仅生成满足 [StreamComfigurationMap.GetOutputMinFrameDuration(int, Size)](
) 和 [StreamComfigurationMap.GetOutputStallDuration(int, Size)](
) 确定的最小 FPS 流。实际表现还会因机型而异,Android 给了我们一些保证,可以根据**输出类型**,**输出大小**和**硬件级别**三个变量来支持特定组合。使用不支持的参数组合可能会以低帧率工作,甚至不能工作,触发其中一个故障回调。[文档](
)非常详细地描述了保证工作的内容,强烈推荐完整阅读,我们在此将介绍基础知识。

### 输出类型

**输出类型**指的是帧编码格式,文档描述中支持的类型有 PRIV、YUV、JEPG 和 RAW。文档很好的解释了它们:

> PRIV 指的是使用了 [StreamConfigurationMap.getOutputSizes(Class)](
) 获取可用尺寸的任何目标,没有直接的应用程序可见格式

> YUV 指的是目标 surface 使用了 [ImageFormat.YUV\_420\_888](
) 编码格式

> JPEG 指的是 [ImageFormat.JPEG](
) 格式

> RAW 指的是 [ImageFormat.RAW\_SENSOR](
) 格式

当选择应用程序的输出类型时,如果目标是使兼容性最大化,推荐使用 [ImageFormat.YUV\_420\_888](
) 做帧分析并使用 [ImageFormat.JPEG](
) 保存图像。对于预览和录像传感器来说,你可能会用一个 `SurfaceView`、`TextureView`、`MediaRecorder`、`MediaCodec` 或者 `RenderScript.Allocation`。在这些情况下,不指定图像格式,出于兼容性目的,它将被计为 [ImageFormat.PRIVATE](
)(不管它的实际格式是什么)。去查看设备支持的格式可以使用如下代码:

val characteristics: CameraCharacteristics = …
val supportedFormats = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).outputFormats


### 输出大小

我们调用 [StreamConfigurationMap.getOutputSizes()](
) 可列出所有可用的**输出大小**,但随着兼容性的发展,我们只需要关心两种:PREVIEW 和 MAXIMUM。我们可以将这种大小视为上限;如果文档中说的 PREVIEW 的大小有效,那么任何比 PREVIEW 尺寸小的都可以,MAXIMUM 同理。这有一个[文档](
)的相关摘录:

> 对于尺寸最大的列,PREVIEW 意味着适配屏幕的最佳尺寸,或 1080p(1920x1080),以较小者为准。RECORD 指的是相机支持的最大分辨率由 [CamcorderProfile](
) 确定。MAXIMUM 还指 StreamConfigurationMap.getOutputSizes(int)中相机设备对该格式或目标的最大输出分辨率。

注意,可用的输出尺寸取决于选择的格式。给定 [CameraCharacteristics](
),我们可以像这样查询可用的输出尺寸:

val characteristics: CameraCharacteristics = …
val outputFormat: Int = … // 比如 ImageFormat.JPEG
val sizes = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
.getOutputSizes(outputFormat)


在相机预览和录像的使用场景中,我们应该使用目标类来确定支持的大小,因为文件格式将由相机框架自身处理:

val characteristics: CameraCharacteristics = …
val targetClass: Class = … // 比如 SurfaceView::class.java
val sizes = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
.getOutputSizes(targetClass)


获取到 MAXIMUM 的尺寸很简单——只需要将输出尺寸排序然后返回最大的:

fun getMaximumOutputSize(
characteristics: CameraCharacteristics, targetClass: Class, format: Int? = null):
Size {
val config = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

// 如果提供图像格式,请使用它来确定支持的大小;否则使用目标类
val allSizes = if (format == null)
    config.getOutputSizes(targetClass) else config.getOutputSizes(format)
return allSizes.sortedWith(compareBy { it.height * it.width }).reversed()[0]

}


获取 PREVIEW 的尺寸就需要动下脑子了。回想一下,PREVIEW 指的是适配屏幕的最佳尺寸,或者 1080p (1920x1080),取较小者。请记住,长宽比可能与屏幕的不匹配,如果我们打算全屏显示,我们需要显示黑边或者裁剪。为了获取到正确的预览尺寸,我们需要对比可用的输出尺寸和显示尺寸,同时考虑到可以旋转显示。在这段代码里,我们还封装了一个辅助类 `SmartSize` 用来横简单的比较尺寸大小:

class SmartSize(width: Int, height: Int) {
var size = Size(width, height)
var long = max(size.width, size.height)
var short = min(size.width, size.height)
}

fun getDisplaySmartSize(context: Context): SmartSize {
val windowManager = context.getSystemService(
Context.WINDOW_SERVICE) as WindowManager
val outPoint = Point()
windowManager.defaultDisplay.getRealSize(outPoint)
return SmartSize(outPoint.x, outPoint.y)
}

fun getPreviewOutputSize(
context: Context, characteristics: CameraCharacteristics, targetClass: Class,
format: Int? = null): Size {

// 比较哪个更小:屏幕尺寸还是 1080p
val hdSize = SmartSize(1080, 720)
val screenSize = getDisplaySmartSize(context)
val hdScreen = screenSize.long >= hdSize.long || screenSize.short >= hdSize.short
val maxSize = if (hdScreen) screenSize else hdSize

// 如果提供图像格式,请使用它来确定支持的大小;否则使用目标类
val config = characteristics.get(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val allSizes = if (format == null)
    config.getOutputSizes(targetClass) else config.getOutputSizes(format)

// 获取可用尺寸并按面积从最大到最小排序
val validSizes = allSizes
        .sortedWith(compareBy { it.height * it.width })
        .map { SmartSize(it.width, it.height) }.reversed()

// 然后,获得小于或等于最大尺寸的最大输出尺寸
return validSizes.filter {
    it.long <= maxSize.long && it.short <= maxSize.short }[0].size

}


### 硬件层次

要决定运行时可用能力,相机应用需要的最重要的信息是支持的**硬件级别**。再一次,我们可以从此[文档](
)学习:

> 支持的硬件级别是摄像机设备功能的上层描述,汇总出多种功能到一个字段中。每一等级相比前一等级都新增了一些功能,并且始终是上一级别的超集。等级的顺序是 LEGACY < LIMITED < FULL < LEVEL\_3。

使用 [CameraCharacteristics](
) 对象,我们可以使用单个语句检索硬件级别:

val characteristics: CameraCharacteristics = …

// 硬件级别将是其中之一:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)


### 把所有部分拼合起来

一旦我们了解了输出类型、输出尺寸和硬件级别,我们就可以确定哪些视频流组合是有效的。举个例子,有一个具有 [LEGACY](
) 硬件级别的 `CameraDevice` 支持的配置的快照.照来自 [createCaptureSession](
) 方法的文档:

![](https://user-gold-cdn.xitu.io/2018/12/12/167a03cf3adb2ede?imageView2/0/w/1280/h/960/ignore-error/1)

因为 LEGACY 是可能性最低的硬件等级,我们可以从一个表中推断出每一个支持 Camera2 的设备(API 21 及以上)可以使用正确的配置输出最多三个并发流——这非常酷!然而,可能在很多机器上无法实现最大可用吞吐量,因为你的代码可能会产生很大性能开销,引发性能约束,例如内存、CPU 甚至是发热。

现在我们已经掌握了在框架的支持下使用两个并发流的所需知识,我们可以更深入了解目标输出缓冲区的配置。例如,如果我们的目标是具有 [LEGACY](
) 硬件级别的设备,我们可以设置两个目标输出表面:一个使用 [ImageFormat.PRIVATE](
) 另一个使用 [ImageFormat.YUV\_420\_888](
)。只要我们使用 PREVIEW 的尺寸,这应该是上表所支持的组合。使用上面定义的方法,获取相机 ID 所需的预览尺寸非常简单:

val characteristics: CameraCharacteristics = …
val context = this as Context // 假设我们在一个 Activity 中

val surfaceViewSize = getPreviewOutputSize(
context, characteristics, SurfaceView::class.java)
val imageReaderSize = getPreviewOutputSize(
context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888)


We must wait until `SurfaceView` is ready using the provided callbacks, like this:

val surfaceView = findViewById(…)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// 我们不需要具体的图片格式,他会被视为 RRIV
// 现在 Surface 已经就绪,我们可以用它作为 CameraSession 的输出目标
}

})


我们甚至可以调用 [SurfaceHolder.setFixedSize()](
) 强制 `SurfaceView` 适配输出流的大小,但在 UI 方面更好的做法是采取类似于 [GitHub 上 HDR 取景器](
) 中 [FixedAspectSurfaceView](
) 的方法,这样可以同时在宽高比和可用空间上使用绝对大小,同时可在 Activity 改变时自动调整。

使用所需格式从 `ImageReader` 中设置另一个表面更加容易,因为无需等待回调:

val frameBufferCount = 3 // 只是一个例子,取决于你对 ImageReade 的使用
val imageReader = ImageReader.newInstance(

Android核心知识点

面试成功其实是必然的,因为我做足了充分的准备工作,包括刷题啊,看一些Android核心的知识点,看一些面试的博客吸取大家面试的一些经验。

下面这份PDF是我翻阅了差不多3个月左右一些Android大博主的博客从他们那里取其精华去其糟泊所整理出来的一些Android的核心知识点,全部都是精华中的精华,我能面试到现在2-2资深开发人员跟我整理的这本Android核心知识点有密不可分的关系,在这里本着共赢的心态分享给各位朋友。

[译] 同时使用多的相机流,阿里巴巴安卓面试题答案

不管是Android基础还是Java基础以及常见的数据结构,这些是无原则地必须要熟练掌握的,尤其是非计算机专业的同学,面试官一上来肯定是问你基础,要是基础表现不好很容易被扣上基础不扎实的帽子,常见的就那些,只要你平时认真思考过基本上面试是没太大问题的。

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2021BAT 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。

节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

础,要是基础表现不好很容易被扣上基础不扎实的帽子,常见的就那些,只要你平时认真思考过基本上面试是没太大问题的。

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2021BAT 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。

节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》

上一篇:Java 8 Collectors.collectingAndThen()


下一篇:◆◆0PM01-创建信息类型(infotype)教程