Android16 开机后立即打开相机闪退问题分析与解决总结

Android16 开机后立即打开相机闪退问题分析与解决总结

文章目录

一、前言

Android16 的AML项目有个相机问题这里简单记录下。

开机后立即点击桌面原生相机图标,相机有概率直接闪退(弹应用程序错误对话框),

退回桌面。

开机若干秒后再打开则正常。

基本信息:

  • 项目:SY-A311Y3-AN16-EDLA-IFPD(Android 16 / EDLA / A311Y3)
  • 相机包名 :com.android.camera2(崩溃模块 VideoModule)
  • 摄像头 :USB / UVC 外接 ------ 50M USB Cam TV2858A,由 Amlogic EmulatedCamera_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、日志证据与已排除项

确定性证据:

  1. 崩溃栈本身(2 次,完全一致);

  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
  3. 成功/失败的时间窗口差异:失败 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、小结

  1. 直接崩溃点 :VideoModule.isCameraFrontFacing() 对 getCharacteristics() 返回值未判空,直接 .isFacingFront() 抛 NPE;
  2. 根因是开机时序竞态 :requestCamera() 先派发异步 open、后抓 CameraDeviceInfo 快照。开机瞬间 UVC 摄像头还没枚举完,快照是空的;而 open 在工作线程执行时 HAL 已就绪 → 成功。相机已可用这个事实永远不会被同步回快照;
  3. 为什么是概率性:纯时序竞态,取快照的瞬间 HAL 是否已枚举完 UVC 设备。开机越早点击越容易命中(T+32s 必崩,T+56s 正常);
  4. 为什么崩了两次 :第一次 NPE 在 onCameraAvailable() 末尾,被 CameraActivity 的 catch (RuntimeException) 吞掉,mHardwareSpec 没赋上值;紧接着 onChangeCamera() → getHardwareSpec() 第二次踩同一个坑,这次没有 try-catch,进程被杀;
  5. 修复两层 :根因------open 成功后刷新过期快照(CameraController.onCameraOpened());兜底------所有 getCharacteristics() 调用点判空降级(6 处);
  6. 历史补丁的教训 :modify by skg 把 AIOOBE 改成返回 null,只是把崩溃点从 portability 层推到了调用方,问题从必崩且栈清楚变成了概率崩且栈在别处。补丁改返回值语义时,必须同步检查所有调用点;
  7. 外接 UVC 摄像头的开机就绪时间要当成秒级不确定量。内置 CSI 摄像头在 HAL 启动时就枚举好了,而 UVC 要等 USB 枚举 + uvcvideo 绑定 + HAL 注册;IFPD/EDLA 这类外接摄像头机型,所有开机即用场景都要按此窗口期做防护;
  8. 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、验证步骤

主用例(原问题):

  1. 确认外接 UVC 摄像头(50M USB Cam TV2858A)已插在 usb 1-1.2.1;
  2. 冷启动设备,开机动画刚结束、桌面刚出来就立刻点击原生相机;
  3. 反复 20 次以上(本问题是概率性的,必须多次回归);
  4. 预期:不再闪退;最坏情况是预览稍晚出图,但应用存活。

日志验证:

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 成功即刷新快照,所有调用点判空兜底,开机即用不再闪退。

有问题欢迎评论区交流。

相关推荐
用户2018792831671 小时前
从点击图标到首帧"上菜":一篇吃透 Android 启动优化原理
android
niucloud-admin1 小时前
NIUCLOUD LITE AI开发文档--命名规范
开发语言·php
WL学习笔记1 小时前
模板初阶(C++)
开发语言·c++·后端·模板初阶
SunnyDays10111 小时前
使用 Java 高效管理 Excel 批注:添加、读取、修改和删除
java·开发语言·excel
ydjs678899-bot2 小时前
为什么大型monorepos变慢了Rush.js + Sparo对此做了什么
开发语言·javascript·ecmascript
坊钰2 小时前
【LangGraph框架入门级】12. 人机交互:interrupt 中断与 Command 恢复
开发语言·数据库·python·人机交互·ai编程·langgraph
开开心心就好2 小时前
打印机卡住打不了?一键清理卡死打印队列
服务器·开发语言·算法·pdf·ocr·idea·visual studio code