第 5 章:创建你的第一个 Camera2 项目
欢迎来到 Camera2 教程系列的动手实践部分。在前几章中,你学习了智能手机相机硬件以及 Camera2 API 的理论基础。现在,是时候卷起袖子编写真正的代码了。到本章结束时,你将拥有一个可以运行的 Android 项目,它成功初始化了 Camera2 API 并访问了 CameraManager 服务——这是在枚举相机、打开设备或显示预览之前至关重要的第一步。
如果你想查看我们将在此系列中构建的所有内容的生产级示例,请查看 GitHub 和 Google Play 上的 Android Camera Parameters 应用。它演示了高级 Camera2 用法,包括完整的 CameraCharacteristics 枚举、手动拍摄控制和多摄像头支持。
为什么要从项目设置开始?
在编写任何一行 Camera2 代码之前,必须正确配置你的应用程序。Camera2 是一个低级、性能敏感的 API,在设置上走捷径会导致神秘的崩溃、ANR(应用无响应)或画面永远无法送达。正确设置 Camera2 项目的三大支柱是:
- 权限 — Android 框架在安装时(清单文件)和运行时(用户同意)都对相机访问进行了限制。
- 线程架构 — Camera2 回调绝不能阻塞主线程;我们需要一个专用的后台线程。
- 视图配置 — 如果你打算使用 TextureView 进行预览(推荐方法),则必须启用硬件加速。
让我们系统地解决每一个问题。
第 1 步:创建一个新的 Android Studio 项目
启动 Android Studio 并创建一个新项目。对于本教程系列,我们建议:
- 模板:Empty Activity(最简单的起点)
- 语言:Kotlin(现代 Android 开发的标准;本系列中的所有示例均使用 Kotlin)
- 最低 SDK:API 21 (Lollipop) — 这是原生支持 Camera2 的第一个 SDK 级别。如果你需要通过 OTG 支持外部 USB 相机,请针对 API 23 或更高版本。如果你需要为保存照片提供分区存储支持(第 9 章),则 API 29+ 是相关的,但我们会在那里处理向后兼容性。
- 构建配置语言:Kotlin DSL 或 Groovy — 均可;我们的示例将与构建系统无关。
项目生成后,打开模块级的 build.gradle(或 build.gradle.kts)文件。默认的 Empty Activity 模板包含了你大部分所需的依赖项,但请确认至少包含:
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
// Camera2 是 Android 框架的一部分,因此基础 API 不需要额外的依赖项。
// androidx.camera.camera2 仅用于 CameraX 互操作。
}
你不需要添加任何外部 Camera2 依赖项。整个 android.hardware.camera2 包都是 Android 框架的一部分。Jetpack CameraX 库是建立在 Camera2 之上的独立高级抽象;在本教程中,我们直接使用原生 Camera2 API。
第 2 步:在 AndroidManifest.xml 中声明权限
每个相机应用程序都必须在 AndroidManifest.xml 中声明 CAMERA 权限。这会告知 Google Play 商店你的应用使用了相机硬件,并在 Android 6.0 (API 23) 及更高版本上启用运行时权限对话框。
打开 app/src/main/AndroidManifest.xml 并添加以下元素作为根 <manifest> 标签的子项(不要放在 <application> 内部):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- ✅ 相机权限声明 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 可选的特性声明(用于 Google Play 过滤) -->
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application
android:allowBackup="true"
...>
<activity
android:name=".MainActivity"
android:exported="true"
android:hardwareAccelerated="true">
...
</activity>
</application>
</manifest>
让我们分解一下重要部分:
<uses-permission android:name="android.permission.CAMERA" />
这是核心权限。没有它,任何对相机服务的调用都将抛出 SecurityException。在 API 22 及更低版本上,用户在安装时授予此权限;在 API 23+ 上,你还必须在运行时请求它(稍后介绍)。
<uses-feature android:name="android.hardware.camera" android:required="true" />
此声明告知 Google Play 将你的应用过滤到至少拥有一个摄像头的设备上。如果你的应用在没有摄像头的情况下也能运行(例如,带有可选拍摄功能的图库应用),请设置 android:required="false"。如果你根本不声明此项,Google Play 会假设不需要摄像头,这可能会导致你的应用被安装在没有摄像头的设备上。
Activity 上的 android:hardwareAccelerated="true"
这对于 TextureView 预览渲染至关重要。TextureView 使用 GPU 合成管线来高效显示相机画面。如果在 Activity 或 Application 级别未启用硬件加速,TextureView 将静默失败,无法渲染或显示黑屏。现代 Android 的默认值是对整个应用为 true,但在承载 TextureView 的任何 Activity 上显式声明它是一个好习惯。
第 3 步:运行时权限请求
在 Android 6.0 (Marshmallow, API 23) 及更高版本中,在清单中声明权限只是完成了一半工作。你还必须使用 Activity Compat 库在运行时显式请求用户授权。标准模式是:
- 使用
ContextCompat.checkSelfPermission检查权限是否已授予。 - 如果已授予,继续进行相机初始化。
- 如果未授予,调用
ActivityCompat.requestPermissions显示系统对话框。 - 在
onRequestPermissionsResult中处理结果。
这是 MainActivity.kt 中完整的权限流程:
package com.example.camera2tutorial
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (allPermissionsGranted()) {
initializeCamera()
} else {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
initializeCamera()
} else {
Toast.makeText(
this,
"需要相机权限才能使用此应用。",
Toast.LENGTH_LONG
).show()
finish()
}
}
}
private fun initializeCamera() {
// TODO: 我们将在下面的章节中实现此方法。
// 这里是设置 CameraManager 的地方。
// 目前,仅记录成功日志。
android.util.Log.d(TAG, "权限已授予。准备初始化相机。")
}
companion object {
private const val TAG = "Camera2Tutorial"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}
为什么 allPermissionsGranted() 使用数组模式
即使我们现在只需要 CAMERA,定义一个 REQUIRED_PERMISSIONS 数组可以轻松地在以后添加其他权限(例如用于旧式照片保存的 WRITE_EXTERNAL_STORAGE 或用于视频的 RECORD_AUDIO)。all { ... } 函数在继续之前检查数组中的每一个权限是否都已授予。
第 4 步:后台线程 (HandlerThread)
这是初学者 Camera2 代码中最常被忽略的细节,它会导致随机、难以重现的错误。让我们了解为什么 Camera2 需要后台线程,然后正确实现它。
为什么 Camera2 绝对不能在主线程运行
Android 主(UI)线程负责:
- 以 60-120 FPS 绘制 UI
- 处理用户触摸事件
- 分发生命周期回调
- 默认运行所有 Activity/Fragment 代码
Camera2 API 同步交付几个关键回调:
CameraDevice.StateCallback— 当相机打开、断开连接或报错时CameraCaptureSession.StateCallback— 当捕获会话配置完成时CameraCaptureSession.CaptureCallback— 针对每一帧(每秒高达 60+ 次!)
如果这些回调在主线程运行,会发生两件灾难性的事情:
- 掉帧和卡顿:如果处理一个回调耗时哪怕 10ms,一个 60FPS 的帧就会被跳过,用户会看到卡顿。
- 死锁和 ANR:一些 Camera2 方法(如
close())是同步的并等待回调。如果回调必须在调用close()的同一个线程上运行,就会产生死锁。
解决方案是使用带有独立 Looper 的专用后台线程,通过 HandlerThread 实现。
正确实现 HandlerThread
后台线程的生命周期必须与相机操作的生命周期相匹配。我们在 Activity 启动/恢复时启动线程,并在 Activity 停止/暂停时退出线程。
package com.example.camera2tutorial
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraManager
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
// --- 后台线程组件 ---
private lateinit var backgroundThread: HandlerThread
private lateinit var backgroundHandler: Handler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (allPermissionsGranted()) {
initializeCamera()
} else {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
}
override fun onResume() {
super.onResume()
startBackgroundThread()
// 如果在应用处于后台时授予了权限,则重新初始化
if (allPermissionsGranted() && this::cameraManager.isInitialized) {
// (cameraManager 在下面声明)
}
}
override fun onPause() {
stopBackgroundThread()
super.onPause()
}
private fun startBackgroundThread() {
backgroundThread = HandlerThread("Camera2Background").apply {
start()
}
backgroundHandler = Handler(backgroundThread.looper)
Log.d(TAG, "后台线程已启动: ${backgroundThread.name}")
}
private fun stopBackgroundThread() {
backgroundThread.quitSafely()
try {
backgroundThread.join(1000) // 最多等待 1 秒完成清理
Log.d(TAG, "后台线程已正常停止")
} catch (e: InterruptedException) {
Log.e(TAG, "连接后台线程时被中断", e)
}
}
// --- CameraManager 初始化 ---
private lateinit var cameraManager: CameraManager
private fun initializeCamera() {
cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIdList = cameraManager.cameraIdList
Log.d(TAG, "成功访问 CameraManager。找到 ${cameraIdList.size} 个摄像头。")
cameraIdList.forEachIndexed { index, cameraId ->
Log.d(TAG, "摄像头 $index: ID = $cameraId")
}
Toast.makeText(
this,
"CameraManager 已初始化!找到 ${cameraIdList.size} 个摄像头。",
Toast.LENGTH_LONG
).show()
}
// --- 权限处理(与之前相同) ---
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
initializeCamera()
} else {
Toast.makeText(
this,
"需要相机权限才能使用此应用。",
Toast.LENGTH_LONG
).show()
finish()
}
}
}
companion object {
private const val TAG = "Camera2Tutorial"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}
关键线程模式详解
-
onResume()中的startBackgroundThread():每次 Activity 进入前台时,我们都会创建一个全新的HandlerThread,启动它,并创建一个绑定到该线程Looper的Handler。这个 Handler 将被传递给所有接受回调的 Camera2 方法(openCamera、createCaptureSession等)。 -
onPause()中的stopBackgroundThread():在 Activity 进入后台之前,我们在线程上调用quitSafely()。这告诉 Looper 在当前消息处理完后停止处理新消息(与quit()不同,后者会丢弃挂起的消息)。然后我们调用join(1000)以阻塞主线程最多一秒钟,同时等待后台线程完成清理。这可以防止资源泄漏。 -
为什么使用
HandlerThread而不是CoroutineDispatcher? Camera2 比 Kotlin 协程早几年出现,其回调系统从根本上是基于 Handler/Looper 的。虽然你可以使用Dispatchers.Default.asExecutor()或在suspendCoroutine中包装回调以获得更高级别的代码,但底层的 Camera2 API 仍然需要一个 Looper 线程来处理回调。直接使用HandlerThread是官方 Android 示例中记录的标准规范做法。
第 5 步:完整的初始化流程(合并)
现在让我们看看应用程序启动时必须发生的全套事件序列。顺序至关重要:权限 → 线程 → CameraManager。如果你颠倒任何步骤,代码将会崩溃或表现不一致。
上面的流程图说明了为什么每个步骤都存在:
- 权限网关:整个相机子系统都受到保护;在用户表示同意之前,我们无法继续。
- 线程先于 CameraManager:虽然
getSystemService()本身是线程安全的,但我们希望在执行任何回调驱动的 Camera2 操作(从下一章开始)之前,后台线程已经运行。 - CameraManager → cameraIdList:调用
cameraIdList是验证 CameraManager 是否正常工作的成本最低的方法。如果此调用成功且未抛出异常,则说明你的清单声明、运行时权限和服务绑定都是正确的。
综合:运行并验证
此时,你拥有了一个完整的、可运行的 Camera2 项目,它具备:
- 使用正确的 SDK 目标创建 Android 项目。
- 在清单中声明 CAMERA 权限。
- 在运行时请求权限,并处理接受和拒绝路径。
- 在
onResume中启动专用 HandlerThread,并在onPause中正常停止。 - 获取
CAMERA_SERVICE系统服务并强制转换为CameraManager。 - 调用
cameraIdList并记录相机的数量和 ID。
运行后你应该看到的内容
- 首次启动时,Android 会显示权限对话框:"允许 Camera2Tutorial 拍摄照片和录制视频吗?"
- 点击允许。
- 出现一个 Toast:"CameraManager 已初始化!找到 X 个摄像头。"
- 在 Logcat 中(通过
Camera2Tutorial过滤),你应该看到如下条目:D/Camera2Tutorial: 后台线程已启动: Camera2BackgroundD/Camera2Tutorial: 成功访问 CameraManager。找到 4 个摄像头。D/Camera2Tutorial: 摄像头 0: ID = 0D/Camera2Tutorial: 摄像头 1: ID = 1D/Camera2Tutorial: 摄像头 2: ID = 2D/Camera2Tutorial: 摄像头 3: ID = 3 - 当你按下主屏幕键或离开应用时,Logcat 显示:
D/Camera2Tutorial: 后台线程已正常停止
如果你看到这些日志,恭喜你!你已经成功建立了 Camera2 应用程序的基础。目前还没有相机预览——那将在第 8 章中介绍——但底层的"管道"是正确的。如果你收到 SecurityException,请仔细检查你是否接受了权限对话框。如果 cameraIdList 返回空数组,则设备可能没有摄像头(在手机上不太可能)或者权限被拒绝。
常见设置错误排查
SecurityException: Lacking privileges to access camera service
这意味着运行时权限未获得授权。检查:
- 你是否在清单中添加了
<uses-permission android:name="android.permission.CAMERA" />。 - 你是否使用了正确的请求代码调用
ActivityCompat.requestPermissions。 - 用户是否在对话框中点击了允许。
- 如果你在物理设备上测试,请转到 设置 → 应用 → 你的应用 → 权限,确保相机已启用。
backgroundHandler 出现 NullPointerException
如果你在 startBackgroundThread() 运行之前尝试使用 backgroundHandler,就会发生这种情况。确保所有接受 Handler 的 Camera2 操作仅在调用 onResume 且线程正在运行之后执行。在我们的代码中,initializeCamera() 是从 onCreate 调用的,但它仅同步使用 CameraManager;需要 backgroundHandler 的回调将在后续章节中添加,并正确地通过 onResume 进行控制。
后续章节中 TextureView 显示黑屏
如果你跳过前面的步骤现在就添加 TextureView,请确保在清单中的 Activity 上设置了 android:hardwareAccelerated="true"。此外,还要确保 TextureView 已附加到视图层级并在 XML 布局中可见。
小结
在本章中,你构建了一个 Android Camera2 应用程序的完整脚手架。你学习了:
- 项目结构:如何使用 Empty Activity 模板创建新的 Android Studio 项目,针对 API 21+,使用 Kotlin,并验证无需额外的 Camera2 依赖项。
- 清单配置:
CAMERA权限声明、用于 Google Play 过滤的uses-feature标签,以及 Activity 上用于 TextureView 渲染的hardwareAccelerated="true"。 - 运行时权限:使用
ContextCompat.checkSelfPermission和ActivityCompat.requestPermissions进行完整的检查 → 请求 → 结果循环,并处理允许和拒绝路径。 - 后台线程:为什么 Camera2 回调绝不能在主线程运行,以及如何实现一个受生命周期管理的
HandlerThread+Handler对,在onResume中使用startBackgroundThread(),并在onPause中使用quitSafely()+join()调用stopBackgroundThread()。 - CameraManager 初始化:获取
CAMERA_SERVICE系统服务,强制转换为CameraManager,调用cameraIdList验证服务是否正常工作,并记录发现的相机 ID。
本章中的代码是后续所有内容的基础。Android Camera Parameters 应用(GitHub,Google Play)正是使用了这些模式——为不同的工作负载使用多个 HandlerThread,仔细的权限检查,以及稳健的生命周期管理。
下一章
现在 CameraManager 已成功初始化,我们有了一个相机 ID 列表,下一步是查询每个摄像头的性能。在第 6 章:发现相机中,你将:
- 学习相机 ID 字符串代表什么(以及为什么永远不应硬编码关于它们的假设)。
- 使用
LENS_FACING区分前置、后置和外部 (USB OTG) 摄像头。 - 查询每个摄像头的硬件级别 (
INFO_SUPPORTED_HARDWARE_LEVEL),以确定其为 LEGACY、LIMITED、FULL 还是 LEVEL_3。 - 迭代设备上的每个摄像头,并使用
CameraCharacteristics记录其属性。
到第 6 章结束时,你将拥有一个可运行的相机枚举实用程序,它可以从设备中提取真实的 Camera2 元数据——这已经是你可以用来比较不同手机相机硬件的工具了!