4. 相机权限处理:Android 6.0+ 运行时权限请求
在 Android 6.0(API 23)及更高版本中,系统引入了运行时权限模型。应用不再在安装时一次性授予所有权限,而是需要在应用运行时动态请求用户授权。对于自定义相机应用,必须处理 CAMERA 权限(用于打开相机预览和拍照)以及 WRITE_EXTERNAL_STORAGE 权限(用于将照片保存到设备存储)。本章将系统讲解运行时权限的请求流程、最佳实践及常见问题处理。
4.1 运行时权限模型概述
运行时权限模型将权限分为正常权限和危险权限两类。正常权限由系统自动授予,危险权限则需要运行时动态请求。CAMERA 和 WRITE_EXTERNAL_STORAGE 均属于危险权限。
| 权限名称 | 权限常量 | 所属权限组 | 危险级别 |
|---|---|---|---|
| 相机 | android.permission.CAMERA |
CAMERA |
危险 |
| 写入外部存储 | android.permission.WRITE_EXTERNAL_STORAGE |
STORAGE |
危险 |
运行时权限请求的核心流程包含以下步骤:
- 检查权限状态: 使用
ContextCompat.checkSelfPermission()判断权限是否已被授予。 - 解释请求原因: 如果用户之前拒绝过权限,应使用
shouldShowRequestPermissionRationale()判断是否需要显示解释说明。 - 发起权限请求: 调用
ActivityCompat.requestPermissions()弹出系统权限对话框。 - 处理请求结果: 在
onRequestPermissionsResult()回调中处理用户的授权或拒绝操作。
ContextCompat.checkSelfPermission(context, permission)— 检查权限状态ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)— 判断是否需要显示解释ActivityCompat.requestPermissions(activity, permissions, requestCode)— 发起请求onRequestPermissionsResult(requestCode, permissions, grantResults)— 处理结果
4.2 权限请求流程图
下图展示了完整的权限请求决策流程,帮助理解每个分支的处理逻辑:
4.3 在 AndroidManifest.xml 中声明权限
无论运行时如何请求,首先必须在 AndroidManifest.xml 中声明所需权限。这是权限工作的前提条件。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.customcamera">
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 写入外部存储权限(用于保存照片) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- 读取外部存储权限(Android 10+ 推荐使用) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- 相机硬件特性声明(非必需,但建议添加) -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
...
</manifest>
WRITE_EXTERNAL_STORAGE 权限不再提供真正的写入访问权限,系统推荐使用 Scoped Storage(分区存储)。对于相机应用,建议使用 MediaStore API 保存照片,仅在 Android 9 及以下设备上请求 WRITE_EXTERNAL_STORAGE。上述代码中 android:maxSdkVersion="28" 即为此目的。
4.4 运行时权限请求完整实现
以下代码展示了在 Activity 中请求 CAMERA 和 WRITE_EXTERNAL_STORAGE 权限的完整实现:
public class CameraActivity extends AppCompatActivity {
private static final int REQUEST_CODE_PERMISSIONS = 100;
private static final String[] REQUIRED_PERMISSIONS = {
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
// 检查并请求权限
if (allPermissionsGranted()) {
openCamera();
} else {
requestPermissionsIfNeeded();
}
}
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
private void requestPermissionsIfNeeded() {
List<String> permissionsToRequest = new ArrayList<>();
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(permission);
}
}
if (!permissionsToRequest.isEmpty()) {
ActivityCompat.requestPermissions(this,
permissionsToRequest.toArray(new String[0]),
REQUEST_CODE_PERMISSIONS);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
openCamera();
} else {
// 权限被拒绝,检查是否需要显示解释
boolean shouldShowRationale = false;
for (String permission : REQUIRED_PERMISSIONS) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this, permission)) {
shouldShowRationale = true;
break;
}
}
if (shouldShowRationale) {
// 用户拒绝但未勾选"不再询问",可再次解释并请求
showPermissionExplanationDialog();
} else {
// 用户勾选了"不再询问",引导用户前往设置
showSettingsRedirectDialog();
}
}
}
}
private void openCamera() {
// 初始化相机预览和拍照功能
// 后续章节实现
}
private void showPermissionExplanationDialog() {
new AlertDialog.Builder(this)
.setTitle("需要相机权限")
.setMessage("自定义相机应用需要相机权限来拍照," +
"以及存储权限来保存照片。请授予相关权限。")
.setPositiveButton("重新请求", (dialog, which) -> {
requestPermissionsIfNeeded();
})
.setNegativeButton("取消", null)
.show();
}
private void showSettingsRedirectDialog() {
new AlertDialog.Builder(this)
.setTitle("权限被永久拒绝")
.setMessage("相机或存储权限已被永久拒绝," +
"请在系统设置中手动开启。")
.setPositiveButton("前往设置", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
})
.setNegativeButton("取消", null)
.show();
}
}
4.5 权限请求最佳实践
在实际开发中,遵循以下最佳实践可以显著提升用户体验和权限授予率:
| 实践要点 | 说明 | 推荐做法 |
|---|---|---|
| 时机选择 | 在用户即将使用相机功能时请求,而非应用启动时 | 点击"拍照"按钮后触发权限请求 |
| 解释说明 | 在请求前向用户说明为什么需要该权限 | 使用 Snackbar 或 Dialog 提供简短说明 |
| 拒绝处理 | 区分"拒绝一次"和"永久拒绝"两种场景 | 使用 shouldShowRequestPermissionRationale() 判断 |
| 设置引导 | 权限被永久拒绝时,引导用户前往系统设置 | 使用 Settings.ACTION_APPLICATION_DETAILS_SETTINGS |
| 批量请求 | 将相关权限合并为一次请求,减少对话框次数 | 使用字符串数组一次性请求多个权限 |
| 兼容性 | 确保代码在 Android 6.0 以下设备正常运行 | 使用 ContextCompat 和 ActivityCompat 兼容类 |
4.6 权限状态与用户决策路径
用户与权限对话框的交互会产生三种不同的状态,每种状态对应不同的处理策略:
4.7 常见问题与解决方案
原因: 未在 AndroidManifest.xml 中声明权限,或使用了错误的权限字符串。
解决: 检查 manifest 中是否包含 <uses-permission android:name="android.permission.CAMERA" />,并确认权限常量拼写正确。
原因: 未处理权限被拒绝的情况,直接调用了需要权限的 API。
解决: 在 onRequestPermissionsResult 中检查 grantResults,权限未授予时给出友好提示,避免执行敏感操作。
原因: Android 10 引入了分区存储,WRITE_EXTERNAL_STORAGE 权限不再提供直接文件路径写入能力。
解决: 使用 MediaStore API 保存照片,或使用 FileProvider 生成 content URI。对于 Android 10+,不再需要 WRITE_EXTERNAL_STORAGE 权限。
4.8 本章小结
权限处理是自定义相机应用的基础保障。本章涵盖了以下核心知识点:
- 运行时权限模型: Android 6.0+ 要求危险权限在运行时动态请求。
- 权限声明: 在 AndroidManifest.xml 中声明
CAMERA和WRITE_EXTERNAL_STORAGE权限。 - 请求流程: 检查状态 → 解释原因 → 发起请求 → 处理结果。
- 用户决策处理: 区分授权、拒绝一次、永久拒绝三种场景。
- 最佳实践: 时机选择、解释说明、设置引导、兼容性处理。