Android16 开机后立即打开相机闪退问题分析与解决总结
文章目录
- [Android16 开机后立即打开相机闪退问题分析与解决总结](#Android16 开机后立即打开相机闪退问题分析与解决总结)
一、前言
Android16 的AML项目有个相机问题这里简单记录下。
开机后立即点击桌面原生相机图标,相机有概率直接闪退(弹应用程序错误对话框),
退回桌面。
开机若干秒后再打开则正常。
基本信息:
- 项目:SY-A311Y3-AN16-EDLA-IFPD(Android 16 / EDLA / A311Y3)
- 相机包名 :
com.android.camera2(崩溃模块VideoModule) - 摄像头 :USB / UVC 外接 ------
50M USB Cam TV2858A,由 AmlogicEmulatedCamera_FakeCamera3(camhal)承载
这个和系统代码、系统初始化、硬件设备初始化也有关,其他Android系统不一样会遇到。
但是也是原生Camera2的缺陷,其他方案也可能存在,所以记录下,仅供参考。
二、分析解决
日志分析,日志里这一次开机复现了连续 2 次闪退,第 3 次打开成功:
| 时刻(设备时间) | 距开机 | 事件 |
|---|---|---|
| 163356.4 | T+0.14s | 内核启动,uvcvideo 注册 |
| 163359.3 | T+3.53s | Found UVC 1.00 device 50M USB Cam TV2858A |
| 163414~163420 | T+18s~T+24s | LOCKED_BOOT_COMPLETED 用户解锁完成 |
| 163425.8 | T+30s | 用户点击相机图标,CameraLauncher 启动 |
| 163428.0 | T+32s | 闪退 #1(NPE) |
| 163432.4 | T+36s | 闪退 #2(NPE) |
| ~16351x | T+75s+ | 第 3 次打开成功,正常出图 |
对比同一份日志里其它几次开机:成功打开相机的时刻分别是 T+56s、T+62s,
都明显晚于本次失败的 T+32s。时间窗口差异是本问题的核心线索。
一般开机都要半分钟左右,所以刚开机点击是有概率崩溃;
有时候开机亮屏慢一些,打开相机已经过了一分钟了,所以是正常的。
这里把问题现象、根因分析和修改方案做个总结,方便大家快速上手。
1、结论先行
| 问题 | 结论 |
|---|---|
| 直接崩溃点 | VideoModule.isCameraFrontFacing() 对 getCharacteristics(mCameraId) 的返回值未做判空,直接 .isFacingFront() |
| 为什么会是 null | CameraController.mInfo(CameraDeviceInfo 快照)是在打开相机的那一刻同步抓取的。开机瞬间 cameraserver / camhal 还没枚举完 UVC 摄像头,Camera.getNumberOfCameras() 返回 0,快照里一个设备都没有 |
| 为什么相机已经打开成功了还会 null | requestCamera() 里先派发异步 open、后才刷新 mInfo 。open 在工作线程真正执行时 HAL 已就绪 → 成功;而主线程早一步抓到的 mInfo 仍是空快照,且此后永不刷新 |
| 为什么是概率性 | 纯时序竞态:取快照的瞬间 HAL 是否已枚举完 UVC 设备。开机越早点击越容易命中(T+32s 必崩,T+56s 正常) |
为什么崩在 getHardwareSpec 而不是 onCameraAvailable |
onCameraAvailable() 末尾同样调了 isCameraFrontFacing(),第一次 NPE 被 CameraActivity 的 catch (RuntimeException) 吞掉了;紧接着 onChangeCamera() → getHardwareSpec() 第二次踩同一个坑,这次没有 try-catch,进程被杀 |
| SELinux 报错是不是元凶 | 不是。avc denied 全日志出现 147 次,成功打开相机的那几次同样有,不是本问题的区分因素(但确实是一个独立待修问题) |
2、崩溃栈
两次闪退栈完全一致:
FATAL EXCEPTION main
Process com.android.camera2, PID 3357
java.lang.NullPointerException Attempt to invoke virtual method
'boolean com.android.ex.camera2.portability.CameraDeviceInfo$Characteristics.isFacingFront()'
on a null object reference
at com.android.camera.VideoModule.isCameraFrontFacing(VideoModule.java:444)
at com.android.camera.VideoModule.getHardwareSpec(VideoModule.java:550)
at com.android.camera.app.CameraAppUI.onChangeCamera(CameraAppUI.java:1256)
at com.android.camera.CameraActivity.onCameraOpened(CameraActivity.java:583)
at com.android.camera.app.CameraController.onCameraOpened(CameraController.java:178)
3、代码走查(完整因果链)
代码都在 release/packages/apps/Camera2 目录下。
空快照的产生:AndroidCameraAgentImpl.java
java
public static AndroidCameraDeviceInfo create() {
try {
numberOfCameras = Camera.getNumberOfCameras(); // ← 开机瞬间可能为 0
cameraInfos = new Camera.CameraInfo[numberOfCameras];
for (int i = 0; i < numberOfCameras; i++) { ... }
} catch (RuntimeException ex) {
return null; // ← 也可能整个返回 null
}
}
并且 getCharacteristics() 有一处历史补丁 modify by skg,把数组越界改成了返回 null:
java
public Characteristics getCharacteristics(int cameraId) {
/* modify by skg start */ // ← 历史补丁:原本会抛 ArrayIndexOutOfBounds
if (mCameraInfos == null || cameraId < 0 || cameraId >= mCameraInfos.length) {
return null; // 改成返回 null 后,异常类型从 AIOOBE 变成了 NPE
}
/* modify by skg end */
}
注:这个补丁只是把数组越界换成了空指针,把崩溃点从 portability 层推到了调用方,并没有真正解决问题。
竞态的根源:CameraController.requestCamera()
java
if (mCameraProxy == null) {
checkAndOpenCamera(cameraManager, id, mCallbackHandler, this); // ① 先派发异步 open
}
mInfo = cameraManager.getCameraDeviceInfo(); // ② 后抓快照(主线程)
时序:
主线程 ①requestCamera → 派发 OPEN_CAMERA ─────────────┐
│
主线程 ②mInfo = getCameraDeviceInfo() │ HAL 尚未枚举完 → 空快照 ❌
▼
工作线程 真正执行 open(HAL 已就绪)→ 成功 ✅
│
主线程 ③onCameraOpened → getCharacteristics() 读①②的空快照 → null → NPE 💥
mInfo 仅在构造函数和 requestCamera() 末尾赋值,onCameraOpened() 里不再刷新,
所以相机已经可用这个事实永远不会被同步回快照。
第一次 NPE 被吞掉:CameraActivity.onCameraOpened()
java
try {
mCurrentModule.onCameraAvailable(camera); // ← VideoModule 末尾调 isCameraFrontFacing()
} catch (RuntimeException ex) { // 第 1 次 NPE 在这里被吞
Log.e(TAG, "Error connecting to camera", ex);
}
mCameraAppUI.onChangeCamera(); // ← 第 2 次 NPE,无保护,进程死亡
因为第一次 NPE 抛在赋值语句里,mHardwareSpec 保持为 null;而它前面的 mCameraSettings 已经赋值成功。于是 getHardwareSpec() 第二次踩同一个坑:
java
public HardwareSpec getHardwareSpec() {
if (mHardwareSpec == null) { // ← null(上一步抛异常没赋上)
mHardwareSpec = (mCameraSettings != null // ← 非 null(已赋值)
? new HardwareSpecImpl(..., isCameraFrontFacing()) : null); // 💥 再踩一次
}
return mHardwareSpec;
}
两个字段一个 null 一个非 null,恰好是 onCameraAvailable 跑到最后一句才抛异常的指纹。
4、日志证据与已排除项
确定性证据:
-
崩溃栈本身(2 次,完全一致);
-
摄像头是 UVC 外接设备,开机需要走 USB 枚举 + HAL 注册,天然比内置 CSI 摄像头慢:
[ 0.137537] usbcore registered new interface driver uvcvideo [ 3.532788] usb 1-1.2.1 Found UVC 1.00 device 50M USB Cam TV2858A -
成功/失败的时间窗口差异:失败 T+32s、T+36s;成功 T+56s 以及重试后的 T+75s+。
已排除项:
| 疑点 | 排除依据 |
|---|---|
SELinux avc denied 读 vendor_system_share_skg_prop |
全日志 147 次,成功打开相机的会话里同样存在,非区分因素 |
CaptureModule 的同类调用 |
该处代码已被公板注释掉,改走 CameraManager 带判空的实现 |
| 相机被其它应用占用 | 无 CAMERA_DISCONNECTED / disconnect / 占用冲突日志 |
| i2c_read returned -6 刷屏 | 信源芯片(LT8711)读写失败,与相机无关,全程持续存在 |
5、修改方案(已实施)
两层修复:兜底判空 (彻底消除闪退)+ 根因刷新(保证朝向 UI 状态正确)。
修改 1(根因):open 成功后刷新过期快照
CameraController.java → onCameraOpened(),open 成功本身就证明设备存在,因此在回调给任何监听者之前重新抓一次快照:
java
mCameraProxy = camera;
mRequestingCameraId = EMPTY_REQUEST;
/* modify by lwz start, 20261015 */
if (mInfo == null || mInfo.getCharacteristics(camera.getCameraId()) == null) {
Log.w(TAG, "Stale camera device info, refreshing for camera " + camera.getCameraId());
CameraAgent agent = (mUsingNewApi && mCameraAgentNg != null) ? mCameraAgentNg : mCameraAgent;
CameraDeviceInfo refreshed = agent.getCameraDeviceInfo();
if (refreshed != null) {
mInfo = refreshed;
}
}
/* modify by skg end */
if (mCallbackReceiver != null) {
mCallbackReceiver.onCameraOpened(camera);
}
要点:
- 只在快照确实不可用时才刷新,正常路径零额外开销;
- 刷新失败(仍返回 null)则保留原
mInfo,由修改 2 兜底,不会把情况变得更糟。
修改 2(兜底):所有 getCharacteristics() 调用点判空降级
| 文件 | 方法 | 改动 |
|---|---|---|
VideoModule.java |
isCameraFrontFacing() ★崩溃点 |
null → Log.w + 返回 false |
VideoModule.java |
isCameraBackFacing() |
null → Log.w + 返回 false |
VideoModule.java |
initializeRecorder() 取 getSensorOrientation() |
null → Log.w + 按 0 处理 |
PhotoModule.java |
isCameraFrontFacing() |
同 VideoModule(拍照模式同样的隐患) |
PhotoModule.java |
setDisplayOrientation() |
null → Log.w + 朝向按 0 |
settingsCameraFacingSetting.java |
构造函数 | null → Log.w + 回退到后置为 0 约定 |
示例(VideoModule.java):
java
private boolean isCameraFrontFacing() {
/* modify by skg start, 20261015 */
Characteristics info =
mAppController.getCameraProvider().getCharacteristics(mCameraId);
if (info == null) {
Log.w(TAG, "Camera characteristics not available for camera ID " + mCameraId);
return false;
}
return info.isFacingFront();
/* modify by skg end */
}
完整的git修改记录,如下:
diff --git a/apps/Camera2/src/com/android/camera/PhotoModule.java b/apps/Camera2/src/com/android/camera/PhotoModule.java
old mode 100644
new mode 100755
index 7f42aaac7..a62382ffa
--- a/apps/Camera2/src/com/android/camera/PhotoModule.java
+++ b/apps/Camera2/src/com/android/camera/PhotoModule.java
@@ -1485,8 +1485,17 @@ public class PhotoModule
* @return Whether the currently active camera is front-facing.
*/
private boolean isCameraFrontFacing() {
- return mAppController.getCameraProvider().getCharacteristics(mCameraId)
- .isFacingFront();
+ // modify by lwz start, 20261015
+ // See VideoModule.isCameraFrontFacing(): the device info snapshot can
+ // still be empty right after boot while the UVC camera is enumerated.
+ Characteristics info =
+ mAppController.getCameraProvider().getCharacteristics(mCameraId);
+ if (info == null) {
+ Log.w(TAG, "Camera characteristics not available for camera ID: " + mCameraId);
+ return false;
+ }
+ return info.isFacingFront();
+ // modify by lwz end
}
/**
@@ -1726,7 +1735,14 @@ public class PhotoModule
mDisplayRotation = CameraUtil.getDisplayRotation(mActivity);
Characteristics info =
mActivity.getCameraProvider().getCharacteristics(mCameraId);
- mDisplayOrientation = info.getPreviewOrientation(mDisplayRotation);
+ // modify by skg start, 20261015
+ if (info != null) {
+ mDisplayOrientation = info.getPreviewOrientation(mDisplayRotation);
+ } else {
+ mDisplayOrientation = 0;
+ Log.w(TAG, "Camera characteristics not available for camera ID: " + mCameraId);
+ }
+ // modify by skg end
mUI.setDisplayOrientation(mDisplayOrientation);
if (mFocusManager != null) {
mFocusManager.setDisplayOrientation(mDisplayOrientation);
diff --git a/apps/Camera2/src/com/android/camera/VideoModule.java b/apps/Camera2/src/com/android/camera/VideoModule.java
index ca07c144f..af377dea7 100755
--- a/apps/Camera2/src/com/android/camera/VideoModule.java
+++ b/apps/Camera2/src/com/android/camera/VideoModule.java
@@ -440,16 +440,34 @@ public class VideoModule extends CameraModule
* @return Whether the currently active camera is front-facing.
*/
private boolean isCameraFrontFacing() {
- return mAppController.getCameraProvider().getCharacteristics(mCameraId)
- .isFacingFront();
+ // modify by lwz start, 20261015
+ // Boot-time race: the CameraDeviceInfo snapshot may have been taken
+ // before the camera service finished enumerating the USB/UVC camera,
+ // so the characteristics can still be null here even though the device
+ // opened successfully. Degrade instead of throwing NPE.
+ Characteristics info =
+ mAppController.getCameraProvider().getCharacteristics(mCameraId);
+ if (info == null) {
+ Log.w(TAG, "Camera characteristics not available for camera ID: " + mCameraId);
+ return false;
+ }
+ return info.isFacingFront();
+ // modify by lwz end
}
/**
* @return Whether the currently active camera is back-facing.
*/
private boolean isCameraBackFacing() {
- return mAppController.getCameraProvider().getCharacteristics(mCameraId)
- .isFacingBack();
+ // modify by skg start, 20261015
+ Characteristics info =
+ mAppController.getCameraProvider().getCharacteristics(mCameraId);
+ if (info == null) {
+ Log.w(TAG, "Camera characteristics not available for camera ID: " + mCameraId);
+ return false;
+ }
+ return info.isFacingBack();
+ // modify by lwz end
}
/**
@@ -1193,8 +1211,14 @@ public class VideoModule extends CameraModule
// on the size restriction.
}
- int sensorOrientation =
- mActivity.getCameraProvider().getCharacteristics(mCameraId).getSensorOrientation();
+ // modify by skg start, 20261015
+ Characteristics recordInfo =
+ mActivity.getCameraProvider().getCharacteristics(mCameraId);
+ if (recordInfo == null) {
+ Log.w(TAG, "Camera characteristics not available for camera ID: " + mCameraId);
+ }
+ int sensorOrientation = (recordInfo != null ? recordInfo.getSensorOrientation() : 0);
+ // modify by lwz end
int deviceOrientation =
mAppController.getOrientationManager().getDeviceOrientation().getDegrees();
int rotation = CameraUtil.getImageRotation(
diff --git a/apps/Camera2/src/com/android/camera/settings/CameraFacingSetting.java b/apps/Camera2/src/com/android/camera/settings/CameraFacingSetting.java
old mode 100644
new mode 100755
index 8227ad6d7..a6ee78182
--- a/apps/Camera2/src/com/android/camera/settings/CameraFacingSetting.java
+++ b/apps/Camera2/src/com/android/camera/settings/CameraFacingSetting.java
@@ -18,8 +18,10 @@ package com.android.camera.settings;
import com.android.camera.app.AppController;
import com.android.camera.CameraActivity;
+import com.android.camera.debug.Log;
import com.android.camera.one.OneCamera;
import com.android.camera2.R;
+import com.android.ex.camera2.portability.CameraDeviceInfo.Characteristics;
import android.content.res.Resources;
@@ -28,6 +30,8 @@ import android.content.res.Resources;
* SharedPreferences keyed by Keys.KEY_CAMERA_ID.
*/
public class CameraFacingSetting {
+ private static final Log.Tag TAG = new Log.Tag("CameraFacingSetting");
+
private final SettingsManager mSettingsManager;
private final String mSettingScope;
@@ -51,14 +55,24 @@ public class CameraFacingSetting {
mCameraFacingDefaultValue =
Integer.parseInt(resources.getString(R.string.pref_camera_id_default));
- if (appController.getCameraProvider().getCharacteristics(mCameraFacingDefaultValue).
- isFacingFront()) {
+ // modify by lwz start, 20261015
+ // Characteristics can be null right after boot (camera service has not
+ // finished enumerating the USB/UVC camera yet). Fall back to the
+ // back-facing-is-0 convention instead of throwing NPE.
+ Characteristics info =
+ appController.getCameraProvider().getCharacteristics(mCameraFacingDefaultValue);
+ if (info == null) {
+ Log.w(TAG, "Camera characteristics not available for camera ID: "
+ + mCameraFacingDefaultValue);
+ }
+ if (info != null && info.isFacingFront()) {
mCameraFacingFrontValue = 0;
mCameraFacingBackValue = 1;
} else {
mCameraFacingBackValue = 0;
mCameraFacingFrontValue = 1;
}
+ // modify by skg end
}
三、其他
1、小结
- 直接崩溃点 :
VideoModule.isCameraFrontFacing()对getCharacteristics()返回值未判空,直接.isFacingFront()抛 NPE; - 根因是开机时序竞态 :
requestCamera()先派发异步 open、后抓CameraDeviceInfo快照。开机瞬间 UVC 摄像头还没枚举完,快照是空的;而 open 在工作线程执行时 HAL 已就绪 → 成功。相机已可用这个事实永远不会被同步回快照; - 为什么是概率性:纯时序竞态,取快照的瞬间 HAL 是否已枚举完 UVC 设备。开机越早点击越容易命中(T+32s 必崩,T+56s 正常);
- 为什么崩了两次 :第一次 NPE 在
onCameraAvailable()末尾,被CameraActivity的catch (RuntimeException)吞掉,mHardwareSpec没赋上值;紧接着onChangeCamera()→getHardwareSpec()第二次踩同一个坑,这次没有 try-catch,进程被杀; - 修复两层 :根因------open 成功后刷新过期快照(
CameraController.onCameraOpened());兜底------所有getCharacteristics()调用点判空降级(6 处); - 历史补丁的教训 :
modify by skg把 AIOOBE 改成返回 null,只是把崩溃点从 portability 层推到了调用方,问题从必崩且栈清楚变成了概率崩且栈在别处。补丁改返回值语义时,必须同步检查所有调用点; - 外接 UVC 摄像头的开机就绪时间要当成秒级不确定量。内置 CSI 摄像头在 HAL 启动时就枚举好了,而 UVC 要等 USB 枚举 + uvcvideo 绑定 + HAL 注册;IFPD/EDLA 这类外接摄像头机型,所有开机即用场景都要按此窗口期做防护;
- SELinux
avc denied不是元凶:成功会话里同样存在,非区分因素,但是独立待修问题。
2、改动文件清单
全部位于 packages 仓(release/packages,独立 git 仓库):
| 文件 | 改动 |
|---|---|
apps/Camera2/src/com/android/camera/app/CameraController.java |
onCameraOpened() 新增过期快照刷新(根因) |
apps/Camera2/src/com/android/camera/VideoModule.java |
isCameraFrontFacing() / isCameraBackFacing() / initializeRecorder() 判空降级 |
apps/Camera2/src/com/android/camera/PhotoModule.java |
isCameraFrontFacing() / setDisplayOrientation() 判空降级 |
apps/Camera2/src/com/android/camera/settings/CameraFacingSetting.java |
构造函数判空降级;新增 Log / Characteristics import 与 TAG |
3、验证步骤
主用例(原问题):
- 确认外接 UVC 摄像头(
50M USB Cam TV2858A)已插在usb 1-1.2.1; - 冷启动设备,开机动画刚结束、桌面刚出来就立刻点击原生相机;
- 反复 20 次以上(本问题是概率性的,必须多次回归);
- 预期:不再闪退;最坏情况是预览稍晚出图,但应用存活。
日志验证:
bash
# 应当看到快照刷新(证明根因路径被命中并自愈)
adb logcat -s CAM_CameraController | grep "Stale camera device info"
# 兜底判空被命中时(刷新也失败的极端情况)
adb logcat | grep "Camera characteristics not available"
# 不应再出现
adb logcat | grep "isFacingFront.*null object reference"
抓取建议:复现时优先用 adb logcat -b all -v threadtime > cam.log 单独抓,避免与内核 printk 混在一起。
回归用例:
| 用例 | 预期 |
|---|---|
| 开机等待 1 分钟后打开相机 | 正常,且不出现 Stale camera device info(走正常路径) |
| 拍照 / 录像 / 录像中抓拍 | 正常;录像方向正确(initializeRecorder 改动点) |
| 前后置切换(若设备支持多摄) | 朝向、闪光灯按钮状态正确(修改 1 保证 facing 判定准确) |
| 相机设置页进出 | 正常(CameraFacingSetting 改动点) |
| 拔掉 USB 摄像头后打开相机 | 不闪退,由应用自身提示无相机 |
| 切信源(USB-Follow 触发重枚举)后打开相机 | 不闪退 |
构建编译:
bash
source build/envsetup.sh && lunch <你的 target>
m Camera2
Camera2 默认是在product/app目录下。
但是Camera2 apk 编译后可以直接install 安装验证测试,安装后会在date/app目录下保存:
W82B_B:/product/app/Camera2 # ls
Camera2.apk lib oat
W82B_B:/product/app/Camera2 # dumpsys package com.android.camera2 | grep path
overlay paths:
path: /data/app/~~YN-IfNsffHeWzkG6FO3LRg==/com.android.camera2-Ilq_fZvHXbOARODtYsx8jA==/base.apk
W82B_B:/product/app/Camera2 #
以上就是开机后立即打开相机闪退问题的分析与解决总结,整体下来:
问题根因是异步 open + 同步抓快照的时序竞态,开机瞬间 UVC 摄像头还没枚举完,快照是空的,相机打开成功后快照也不刷新。
修复后 open 成功即刷新快照,所有调用点判空兜底,开机即用不再闪退。
有问题欢迎评论区交流。