17、权限与相机:CAMERA权限、相机预览权限、拍照权限分离
相机权限这块,说实话,是Android运行时权限里最容易让人踩坑的地方之一。很多开发者觉得「我要用相机,那就申请一个CAMERA权限呗」,结果到了Android 10、11上,应用直接闪退或者拍照黑屏。
为什么会这样?因为Google把相机相关的权限拆得更细了。我个人习惯把相机权限分成三层来理解:基础相机权限、预览权限、拍照权限。这三者不是包含关系,而是各有各的职责。
17.1 传统CAMERA权限的「大锅饭」问题
在Android 9及之前,你只要在Manifest里声明了CAMERA权限,就能干所有跟相机相关的事——打开相机、预览画面、拍照、录视频。这就像一个大锅饭,什么活都包了。
但问题来了:用户可能只想用相机扫个二维码,并不想让你拍照存图。你申请了CAMERA权限,用户一看「这个应用要访问相机」,心里就犯嘀咕。更麻烦的是,有些应用其实只需要预览画面(比如AR滤镜),根本不需要拍照功能,但也不得不申请完整的CAMERA权限。
我在项目中遇到过这样一个案例:一个扫码登录的功能,用户授权后,我们只是打开相机预览,识别二维码,然后关闭。但有些用户投诉说「你们偷偷拍照」。其实我们根本没拍,但权限声明里写的是CAMERA,用户不理解这个区别。
17.2 Android 10+ 的权限细化策略
到了Android 10(API 29),Google引入了更细粒度的相机权限控制。虽然表面上还是CAMERA权限,但系统内部开始区分「预览」和「拍照」两种行为。
说白了,就是系统会监控你的Camera对象是怎么用的:
- 如果你只调了
setPreviewDisplay()或startPreview(),系统认为你在「预览」 - 如果你调了
takePicture(),系统认为你在「拍照」 - 如果你同时做了,那就是「完整相机使用」
嗯,这里要注意:系统并不会在代码层面强制你分开申请权限,但Google Play的政策会要求你说明为什么需要拍照功能。如果你的应用只需要预览,却在Manifest里声明了CAMERA权限,审核可能会被拒。
takePicture(),系统会直接抛出SecurityException。这不是开玩笑的,我曾经有个同事在后台服务里偷偷拍照做分析,结果上线后崩溃率飙升。
17.3 实际开发中的权限分离方案
那我们在代码里怎么实现这种分离呢?我的做法是:按功能模块申请权限,而不是一刀切。
举个例子,如果你的应用有扫码功能和拍照功能,应该这样设计:
// 扫码模块 - 只需要预览权限
public class QRScannerActivity extends AppCompatActivity {
private static final int PERMISSION_REQUEST_CAMERA_PREVIEW = 100;
private void checkPreviewPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 这里只申请CAMERA权限,但功能上只做预览
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
PERMISSION_REQUEST_CAMERA_PREVIEW);
} else {
startPreviewOnly();
}
}
private void startPreviewOnly() {
// 只调用预览相关API
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
// 绝不调用 takePicture()
}
}
// 拍照模块 - 需要完整相机权限
public class CameraCaptureActivity extends AppCompatActivity {
private static final int PERMISSION_REQUEST_CAMERA_FULL = 200;
private void checkFullCameraPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 同样申请CAMERA权限,但功能上会拍照
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
PERMISSION_REQUEST_CAMERA_FULL);
} else {
startCameraWithCapture();
}
}
}
你可能会问:「这不都是申请同一个CAMERA权限吗?有什么区别?」
区别在于用户感知和审核策略。虽然代码里都是CAMERA权限,但你在Google Play的隐私政策里可以分开说明:扫码功能仅用于预览,拍照功能才会保存图片。这样用户和审核人员都能理解你的权限使用意图。
17.4 使用CameraX的权限分离实践
如果你用的是CameraX库(我个人强烈推荐),权限分离会更清晰。CameraX把预览、拍照、分析分成了独立的UseCase:
// 只绑定预览用例 - 不需要拍照权限
val preview = Preview.Builder().build()
val previewView = findViewById<PreviewView>(R.id.previewView)
preview.setSurfaceProvider(previewView.surfaceProvider)
// 绑定拍照用例 - 需要拍照功能
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// 根据功能需求选择性绑定
if (needCapture) {
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
} else {
cameraProvider.bindToLifecycle(this, cameraSelector, preview)
}
这样做的好处是:你的代码结构本身就反映了权限的使用范围。审核人员看你的代码,一眼就能明白哪些模块需要拍照,哪些只需要预览。
android:requiredFeature="android.hardware.camera"和android:requiredFeature="android.hardware.camera.autofocus"。这样没有相机的设备(比如某些平板)根本不会显示你的应用,省得用户下载后发现不能用。
17.5 权限分离的流程图
下面这张图展示了相机权限分离的核心逻辑。我画的时候特意把「预览」和「拍照」两条路径分开,这样你一看就明白:
17.6 避坑指南:我曾经踩过的三个坑
第一个坑: 我曾经在onResume()里申请权限,在onPause()里释放相机。结果用户快速切换应用时,权限对话框还没消失,相机就被释放了,导致空指针。后来我改成在权限回调里才初始化相机。
第二个坑: 有个项目用了第三方的扫码库,那个库在内部偷偷调了takePicture()做图像分析。结果Google Play审核说我们「未经用户同意拍照」。我查了半天才发现是第三方库的问题。所以我现在用第三方库之前,一定会反编译看看它到底调了什么API。
第三个坑: Android 11上,如果你在bindToLifecycle时绑定了ImageCapture,但用户只授权了预览权限(理论上不可能,但有些定制ROM会这样),系统会静默失败,不抛异常也不回调。我排查了整整两天才发现是权限问题。解决方案是在拍照前主动检查权限状态。
📌 核心总结:
- CAMERA权限在代码层面是同一个,但在功能设计和隐私政策上要分开
- 预览和拍照是两种不同的使用场景,不要混为一谈
- 使用CameraX的UseCase分离,能让你的代码结构更清晰
- 审核时,Google Play更关注你实际做了什么,而不是你声明了什么
好了,关于相机权限的分离就讲到这里。记住一句话:权限申请是手段,不是目的。你真正要做的,是让用户明白你为什么需要这个权限,以及你拿这个权限到底干了什么。