1. 从零到一:为什么需要一个独立的人脸采集模块?
在移动应用开发里,人脸识别相关的功能越来越常见,从实名认证、门禁打卡到美颜滤镜,背后都离不开一个基础且关键的环节:人脸采集。这个环节做得好不好,直接决定了后续识别、比对、分析的准确性和用户体验。很多开发者,尤其是刚接触这个领域的,可能会选择直接把百度人脸识别的SDK代码和业务逻辑混在一起写。初期看起来很快,但随着需求迭代,比如要换一个采集UI、要支持活体检测、或者要把采集功能复用到另一个App里,你就会发现代码已经盘根错节,改一处而动全身。
所以,把“人脸采集”这个功能独立封装成一个Module(模块),是一个从项目初期就应该考虑的最佳实践。这不仅仅是代码结构上的“整洁”,更是一种工程思维的体现。一个设计良好的采集Module,对外提供清晰、稳定的接口;对内封装了所有与百度SDK交互的复杂细节、权限处理、相机生命周期管理以及异常处理。业务方只需要关心“何时启动采集”和“如何处理采集结果”,而不必陷入“相机预览为什么黑屏”、“SDK初始化失败怎么回退”这些技术泥潭。
基于热词中频繁出现的
ModuleNotFoundError
、
android studio
配置问题,以及
adb shell
等调试相关词汇,可以看出很多开发者在模块化、环境配置和真机调试上踩过不少坑。这篇文章,我就以集成百度人脸采集SDK为例,手把手带你构建一个高可用、易复用的人脸采集Module,并重点讲解那些官方文档可能一笔带过,但实际开发中一定会遇到的“坑”。
2. 战前准备:环境、依赖与项目结构规划
在动手写代码之前,充分的准备工作能避免一半以上的后续问题。这里的环境准备,远不止安装Android Studio那么简单。
2.1 核心依赖引入与配置避坑
首先,你需要去百度AI开放平台注册并创建一个人脸识别应用,获取
AppID
、
API Key
和
Secret Key
。这是使用任何百度AI服务的前提。
接下来,在Module的
build.gradle
文件中引入依赖。百度人脸Android SDK通常提供两种集成方式:aar包本地依赖和Maven仓库依赖。我强烈推荐使用后者,因为版本管理更方便。
// 在 module 的 build.gradle 的 dependencies 中添加
dependencies {
// 百度人脸识别 SDK 核心库
implementation 'com.baidu.aip:face-android-sdk:最新版本号'
// 可能需要的图片处理库,例如 Glide 用于结果预览
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
// 支持 AndroidX 的权限请求库,简化运行时权限处理
implementation 'com.permissionx.guolindev:permissionx:1.7.1'
}
关键点1:版本号与兼容性
这里的“最新版本号”需要你去百度AI开放平台的文档中心确认。一个常见的坑是,百度SDK的某个版本可能只支持到某个特定的
compileSdkVersion
或
targetSdkVersion
。如果你的主项目SDK版本很高,而百度SDK版本较旧,可能会在编译或运行时出现诡异的兼容性问题。我的经验是,在创建这个Module时,就将其
compileSdkVersion
和
targetSdkVersion
设置成与主项目一致,避免后续联调时的冲突。
关键点2:NDK配置(如果SDK包含so库)
如果百度人脸SDK包含了
.so
动态库(用于加速算法运算),你需要在Module的
build.gradle
中配置NDK过滤,确保只打包你需要的ABI架构,以减小APK体积。
android {
defaultConfig {
ndk {
// 根据实际情况选择,'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
}
2.2 模块化项目结构设计
我们的目标是一个干净的、职责单一的Module。我建议的包结构如下:
com.yourcompany.facecapture
├── api
│ ├── FaceCaptureApi.kt // 对外暴露的接口类
│ └── model
│ ├── CaptureConfig.kt // 采集配置(图片质量、是否活体等)
│ └── CaptureResult.kt // 采集结果(成功/失败,图片数据等)
├── internal // 内部实现,对外不可见
│ ├── baidu // 封装百度SDK的具体操作
│ │ ├── BaiduFaceManager.kt // 单例,管理SDK初始化、认证
│ │ └── FaceDetectorWrapper.kt // 封装检测、采集逻辑
│ ├── camera // 相机相关
│ │ ├── CameraController.kt // 管理相机打开、预览、参数设置
│ │ └── preview // 相机预览View相关
│ ├── ui // 采集界面Activity/Fragment及自定义View
│ │ ├── FaceCaptureActivity.kt
│ │ └── FaceOverlayView.kt // 绘制人脸框、提示语的View
│ └── util
│ ├── PermissionUtil.kt
│ └── ImageUtil.kt // 图片压缩、旋转、格式转换工具
└── FaceCaptureModule.kt // Module的入口,提供初始化方法
为什么这么设计?
api
包下的类构成了Module对外的“契约”,任何外部调用都只依赖这个包。
internal
下的所有细节都被隐藏,未来即使我们把百度SDK换成腾讯云或阿里云,只要保持
api
不变,主项目代码就无需任何修改。这是一种面向接口而非实现的编程思想,对于模块的长期维护至关重要。
3. 核心引擎:百度SDK的初始化与封装
这是整个Module最核心、也最容易出错的部分。百度SDK的初始化必须在合适的时机、且全局只进行一次。
3.1 安全且健壮的初始化流程
我见过很多代码把初始化直接放在
Application
或者
Activity
的
onCreate
里,这很危险。正确的做法是在Module内部提供一个静态的初始化方法,并处理好可能的异常。
// BaiduFaceManager.kt
object BaiduFaceManager {
private const val APP_ID = “你的APP_ID”
private const val API_KEY = “你的API_KEY”
private const val SECRET_KEY = “你的SECRET_KEY”
private var isInitialized = false
private lateinit var faceSDK: FaceSDK // 假设这是百度SDK的主类
@Synchronized
fun init(context: Context, listener: InitListener) {
if (isInitialized) {
listener.onSuccess()
return
}
// 在子线程进行初始化,避免阻塞UI
GlobalScope.launch(Dispatchers.IO) {
try {
// 1. 设置License信息
FaceSDKManager.getInstance().setLicense(context, APP_ID, API_KEY, SECRET_KEY)
// 2. 初始化SDK引擎
val initCode = FaceSDKManager.getInstance().init(context)
withContext(Dispatchers.Main) {
if (initCode == 0) {
isInitialized = true
faceSDK = FaceSDKManager.getInstance().faceDetector
listener.onSuccess()
} else {
listener.onFailed(“初始化失败,错误码:$initCode”)
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
listener.onFailed(“初始化异常:${e.message}”)
}
}
}
}
fun getFaceDetector(): FaceSDK {
if (!isInitialized) {
throw IllegalStateException(“FaceSDK 未初始化,请先调用 init()”)
}
return faceSDK
}
interface InitListener {
fun onSuccess()
fun onFailed(errorMsg: String)
}
}
经验之谈1:网络权限与离线初始化
百度SDK的初始化通常需要一次网络请求来验证License。务必确保在调用
init
之前,应用已经获得了
INTERNET
权限。更稳妥的做法是,在初始化回调失败时,检查错误信息是否包含网络相关的提示,并引导用户检查网络。有些SDK版本支持离线License文件,如果对启动速度要求极高,可以研究此方案。
经验之谈2:生命周期与释放
虽然我们的
BaiduFaceManager
是单例,但SDK内部可能持有相机、内存等资源。需要在合适的时机(例如,所有使用人脸功能的界面都已销毁)调用SDK的释放方法。这通常在Module内部提供一个
release()
方法,并在主App的某个统一生命周期回调中(非必须)调用。
3.2 人脸检测与采集逻辑的封装
初始化成功后,我们就可以使用SDK进行人脸检测了。这里的关键是将百度SDK复杂的回调接口,封装成更易于使用的形式。
// FaceDetectorWrapper.kt
class FaceDetectorWrapper(private val context: Context) {
private val faceDetector: FaceSDK by lazy {
BaiduFaceManager.getFaceDetector()
}
// 检测一帧图像(NV21格式字节数据)
fun detectFrame(
nv21Data: ByteArray,
width: Int,
height: Int,
rotation: Int, // 相机传感器方向
callback: DetectCallback
) {
// 百度SDK检测通常需要将图像转为Bitmap或特定格式
val detectParams = FaceDetectParams().apply {
// 设置图像参数,例如是否检测活体、人脸角度等
this.isLiveness = true
}
// 注意:此处的 detect 方法可能是同步或异步,需查阅最新SDK文档
val faceList = faceDetector.detect(nv21Data, width, height, rotation, detectParams)
if (faceList != null && faceList.isNotEmpty()) {
val faceInfo = faceList[0] // 通常取最大或第一个脸
// 判断人脸质量:是否清晰、是否正对镜头、是否活体等
if (isFaceQualified(faceInfo)) {
callback.onFaceDetected(faceInfo, nv21Data, width, height)
} else {
callback.onFaceUnqualified(faceInfo.livenessScore, “人脸不清晰或未正对镜头”)
}
} else {
callback.onNoFaceDetected()
}
}
private fun isFaceQualified(faceInfo: FaceInfo): Boolean {
// 这里根据 faceInfo 中的属性进行判断,例如:
// faceInfo.blurness 模糊度
// faceInfo.occlusion 遮挡信息
// faceInfo.pitch/yaw/roll 头部姿态角
// faceInfo.livenessScore 活体分数
// 需要根据业务需求设定阈值
return (faceInfo.blurness < 0.5f
&& faceInfo.occlusion.leftEye == 0f // 假设0为无遮挡
&& abs(faceInfo.yaw) < 15f // 偏航角小于15度
&& faceInfo.livenessScore > 0.8f)
}
interface DetectCallback {
fun onFaceDetected(faceInfo: FaceInfo, nv21Data: ByteArray, width: Int, height: Int)
fun onFaceUnqualified(score: Float, reason: String)
fun onNoFaceDetected()
}
}
核心细节:图像格式与旋转处理
这是最大的坑之一。相机回调的预览数据通常是
NV21
(YUV420SP) 格式,而百度SDK的检测接口可能要求
NV21
、
RGB
或
Bitmap
。务必查阅你所用版本的SDK文档,确认输入格式。如果格式不对,检测结果会完全错误或直接崩溃。
另一个致命点是图像旋转。手机相机传感器方向是固定的(通常是横屏),而App的界面可能是竖屏。你需要根据
CameraCharacteristics.SENSOR_ORIENTATION
和当前
Display
的旋转角度,计算出正确的
rotation
参数传递给检测接口。这个参数传错,会导致检测到的人脸框位置完全错乱。我通常会在
CameraController
中统一计算这个值,并随着每一帧数据传递给检测器。
4. 构建采集界面:相机控制与用户体验
有了检测引擎,我们需要一个界面来展示相机预览、绘制人脸框并引导用户。
4.1 使用CameraX简化相机操作
在
android.hardware.Camera
API被弃用后,
Camera2
API又过于复杂。Google推出的
CameraX
库是当前的最佳选择,它提供了生命周期感知、设备兼容性更好。
首先在Module的
build.gradle
中添加依赖:
dependencies {
def camerax_version = “1.3.0”
implementation “androidx.camera:camera-core:${camerax_version}”
implementation “androidx.camera:camera-camera2:${camerax_version}”
implementation “androidx.camera:camera-lifecycle:${camerax_version}”
implementation “androidx.camera:camera-view:${camerax_version}”
}
然后,在
CameraController
中配置和使用:
class CameraController(private val lifecycleOwner: LifecycleOwner,
private val previewView: PreviewView,
private val analysisExecutor: Executor) {
private var camera: Camera? = null
private var imageAnalysis: ImageAnalysis? = null
fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(previewView.context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// 配置预览
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
// 配置图像分析(用于人脸检测)
imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 480)) // 设置分析分辨率,平衡性能与精度
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 只处理最新帧
.build()
.also {
it.setAnalyzer(analysisExecutor) { imageProxy ->
// 在这里将 ImageProxy 转换为 NV21 数据,并调用 FaceDetectorWrapper
processImage(imageProxy)
}
}
// 绑定到生命周期
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
}, ContextCompat.getMainExecutor(previewView.context))
}
private fun processImage(imageProxy: ImageProxy) {
// 将 ImageProxy 的 YUV_420_888 格式转换为 NV21
val nv21Data = ImageUtil.yuv420888ToNv21(imageProxy)
val rotation = imageProxy.imageInfo.rotationDegrees
// 调用封装好的检测器
faceDetectorWrapper.detectFrame(nv21Data, imageProxy.width, imageProxy.height, rotation, object: DetectCallback {
override fun onFaceDetected(faceInfo: FaceInfo, nv21Data: ByteArray, width: Int, height: Int) {
// 更新UI,绘制人脸框
// 如果人脸质量合格且持续一定时间,则触发采集
triggerCapture(nv21Data, width, height, faceInfo)
}
// ... 其他回调处理
})
imageProxy.close() // 非常重要!必须关闭以释放图像缓冲区
}
fun stopCamera() {
imageAnalysis?.clearAnalyzer()
// CameraX 的生命周期绑定会自动处理释放
}
}
避坑指南:图像分析器的性能与阻塞
ImageAnalysis.Analyzer
默认运行在后台线程,但如果你在
processImage
中的操作太耗时(比如复杂的图像转换或同步的检测调用),会导致帧率下降甚至分析管道阻塞。务必:
- 将图像转换等操作优化到极致。
-
确保
faceDetectorWrapper.detectFrame本身是高效的,或者其内部使用了异步处理。 -
使用
STRATEGY_KEEP_ONLY_LATEST策略,丢弃处理不过来的旧帧,保证实时性。 -
在
onFaceDetected等回调中需要更新UI时,记得切换到主线程。
4.2 设计友好的采集引导UI
采集界面 (
FaceCaptureActivity
) 不仅仅是展示相机预览。它需要引导用户完成动作(如眨眼、摇头),并给出清晰的反馈。
-
人脸框与提示层 (
FaceOverlayView) :这是一个自定义View,覆盖在PreviewView之上。在onFaceDetected回调中,将人脸框的坐标(通常是归一化坐标,需要转换为屏幕坐标)传递给这个View进行绘制。同时,根据检测结果(“请正对屏幕”、“光线太暗”、“请缓慢眨眼”)更新提示文字。 -
状态机管理
:一个完整的采集流程可能包含多个状态:
IDLE(准备)、DETECTING(检测中)、QUALIFIED(人脸合格)、CAPTURING(采集中)、SUCCESS/FAILED。使用一个简单的状态机来管理UI变化和逻辑流转,会让代码清晰很多。 - 采集触发与防抖 :不要在人脸合格的第一帧就立即采集。通常设置一个短暂的延迟(比如500ms),在此期间人脸持续合格,再执行实际的采集动作(如保存高质量图片、调用注册接口)。这可以避免因用户微小移动导致的模糊照片。
5. 对外接口设计与模块集成
Module内部实现得再完美,如果对外接口设计得不好,调用方也会用得很痛苦。
5.1 定义清晰的API契约
我们在
api
包下定义的接口,应该尽可能简单、明确。
// CaptureConfig.kt
data class CaptureConfig(
val needLiveness: Boolean = true, // 是否需要活体检测
val imageQuality: Int = 90, // 最终保存JPEG的质量
val timeoutSeconds: Long = 30, // 超时时间
val instructionText: String? = null // 自定义引导文字
)
// CaptureResult.kt
sealed class CaptureResult {
data class Success(
val faceImagePath: String, // 人脸图片本地路径
val faceToken: String? // 百度云返回的FaceToken,可选
) : CaptureResult()
data class Error(
val errorCode: Int,
val errorMessage: String
) : CaptureResult()
object Canceled : CaptureResult() // 用户手动取消
object Timeout : CaptureResult()
}
// FaceCaptureApi.kt
interface FaceCaptureApi {
/**
* 启动人脸采集Activity
* @param context 上下文
* @param config 采集配置
* @param requestCode 用于onActivityResult的回调码(如果使用Activity)
*/
fun startCapture(context: Context, config: CaptureConfig, requestCode: Int)
/**
* 处理Activity结果
*/
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?): CaptureResult?
/**
* 异步初始化SDK(可选,也可在App启动时初始化)
*/
fun initialize(appContext: Context, listener: BaiduFaceManager.InitListener)
}
5.2 提供多种集成方式
为了满足不同场景,我们可以提供两种集成模式:
-
Activity跳转模式
:这是最常用的。Module提供一个
FaceCaptureActivity,主App通过startActivityForResult启动它,并在onActivityResult中通过FaceCaptureApi.handleActivityResult解析结果。这种方式隔离性最好。 -
Fragment嵌入模式
:如果主App希望将采集界面嵌入到自己的某个页面中,我们可以提供一个
FaceCaptureFragment。主App将其添加到自己的布局中,并通过回调接口接收结果。
我们需要实现一个简单的
FaceCaptureModule
对象作为门面(Facade):
// FaceCaptureModule.kt
object FaceCaptureModule : FaceCaptureApi {
private const val CAPTURE_REQUEST_CODE = 0x1001 // 定义一个模块内部请求码
override fun startCapture(context: Context, config: CaptureConfig, requestCode: Int) {
val intent = Intent(context, FaceCaptureActivity::class.java).apply {
putExtra(“CONFIG”, config)
}
if (context is Activity) {
context.startActivityForResult(intent, requestCode)
} else {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
// 非Activity上下文启动,结果需要通过其他方式(如广播)回调,这里简化处理
}
}
override fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?): CaptureResult? {
if (requestCode != CAPTURE_REQUEST_CODE) return null
return when (resultCode) {
Activity.RESULT_OK -> {
val path = data?.getStringExtra(“IMAGE_PATH”)
val token = data?.getStringExtra(“FACE_TOKEN”)
if (path != null) CaptureResult.Success(path, token) else CaptureResult.Error(-1, “数据异常”)
}
Activity.RESULT_CANCELED -> CaptureResult.Canceled
FaceCaptureActivity.RESULT_TIMEOUT -> CaptureResult.Timeout
else -> CaptureResult.Error(data?.getIntExtra(“ERROR_CODE”, -1) ?: -1,
data?.getStringExtra(“ERROR_MSG”) ?: “未知错误”)
}
}
override fun initialize(appContext: Context, listener: BaiduFaceManager.InitListener) {
BaiduFaceManager.init(appContext, listener)
}
}
5.3 在主项目中集成Module
主项目集成这个Module就非常简单了。
-
项目设置
:在项目的
settings.gradle中引入你的Module。include ‘:app’, ‘:facecapture’ -
添加依赖
:在主App的
build.gradle中:dependencies { implementation project(‘:facecapture’) } -
调用采集
:在需要人脸采集的地方,比如一个按钮的点击事件里:
// 初始化(建议在Application中做一次) FaceCaptureModule.initialize(applicationContext, object : BaiduFaceManager.InitListener { override fun onSuccess() { Log.d(“Face”, “SDK初始化成功”) } override fun onFailed(errorMsg: String) { Log.e(“Face”, “SDK初始化失败: $errorMsg”) } }) // 启动采集 val config = CaptureConfig(needLiveness = true, instructionText = “请正对屏幕”) FaceCaptureModule.startCapture(this@MainActivity, config, CAPTURE_REQUEST_CODE) -
处理结果
:在Activity的
onActivityResult中:override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val result = FaceCaptureModule.handleActivityResult(requestCode, resultCode, data) when (result) { is CaptureResult.Success -> { // 拿到人脸图片路径 result.faceImagePath,可以上传或显示 Glide.with(this).load(File(result.faceImagePath)).into(imageView) } is CaptureResult.Error -> { Toast.makeText(this, “采集失败:${result.errorMessage}”, Toast.LENGTH_SHORT).show() } CaptureResult.Canceled -> { /* 用户取消 */ } CaptureResult.Timeout -> { /* 超时 */ } else -> {} } }
6. 调试、优化与常见问题排查
模块开发完成后,真正的挑战才刚刚开始:让它稳定运行在各种千奇百怪的设备和环境中。
6.1 利用ADB与Log进行高效调试
热词中出现了
adb shell sh /storage/...
,这提示了真机调试和脚本化操作的重要性。
-
分层日志
:在Module内部建立清晰的日志系统。使用不同的TAG,例如
Face/Camera、Face/Detect、Face/SDK。通过BuildConfig.DEBUG来控制日志在Release版本中不输出。 -
ADB命令辅助
:
-
adb logcat -s Face:*:只过滤你Module的日志,信息更清晰。 -
adb shell dumpsys media.camera:查看相机服务状态,有时相机打不开可以借此排查。 -
对于权限问题,可以手动通过
adb shell pm grant <package_name> android.permission.CAMERA来授权,加速测试。
-
- 关键信息本地保存 :在调试阶段,可以将检测到的人脸关键点坐标、质量分数、每一帧的图片(在SD卡临时目录)保存下来,便于在电脑上分析问题。
6.2 性能与兼容性优化
-
内存管理
:图像数据(
ByteArray,Bitmap)是内存消耗大户。确保在ImageAnalysis.Analyzer中处理完ImageProxy后立即调用imageProxy.close()。保存的图片也要及时压缩和清理。 - 发热与耗电 :持续进行人脸检测是CPU密集型操作。可以考虑降低检测频率(例如,每3帧检测一次),或者在人脸进入稳定状态后,降低检测的精度或频率。
-
低端机适配
:在
CaptureConfig中提供可配置的“性能模式”选项。在低端机上,使用更低的分析分辨率(如480x640),关闭一些非必要的检测特性(如详细的地标点检测)。 -
屏幕旋转与生命周期
:这是Android相机开发的老大难问题。使用
CameraX可以省去很多麻烦,因为它自身是生命周期感知的。但要处理好Activity重建(如屏幕旋转)时,如何保存和恢复采集状态。可以考虑使用ViewModel来保存非UI的关键状态。
6.3 高频问题排查清单
根据热词和常见经验,这里列出一个问题排查清单:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 预览黑屏 |
1. 相机权限未授予。
2. CameraProvider绑定失败。 3. PreviewView未正确设置SurfaceProvider。 4. 设备不支持所选摄像头。 |
1. 检查Logcat中是否有权限拒绝日志。
2. 检查
cameraProvider.bindToLifecycle
是否成功,监听其异常。
3. 确保在
onResume
等生命周期中启动相机。
4. 尝试切换前置/后置摄像头。 |
| 检测不到人脸 |
1. 图像格式或旋转参数错误。
2. SDK未初始化或初始化失败。 3. 人脸不在检测区域内或光线太暗。 4. 检测器参数(如最小人脸)设置不当。 |
1.
保存一帧原始NV21数据
,用电脑工具查看是否正确。
2. 确认
BaiduFaceManager.init
回调成功。
3. 打开调试UI,查看是否有预览画面,检查环境光。 4. 查阅SDK文档,调整
FaceDetectParams
。
|
| 活体检测失败率高 |
1. 动作指令不清晰或用户配合度低。
2. 活体阈值设置过高/过低。 3. 屏幕反光或佩戴眼镜影响。 |
1. 优化UI引导,让用户明白需要做什么动作。
2. 根据后台统计,在
isFaceQualified
中调整
livenessScore
阈值。
3. 增加“请勿佩戴反光眼镜”、“请调整角度避免反光”等提示。 |
| 集成后App崩溃 |
1. 多Dex问题(方法数超限)。
2. Native库(.so)冲突或缺失。 3. 资源冲突(同名资源文件)。 |
1. 在主App中启用
multiDexEnabled true
。
2. 检查
abiFilters
配置,确保与主App和其他库兼容。使用
adb shell ls /data/app/.../lib
查看安装后的库。
3. 检查Module的
resourcePrefix
避免资源名冲突。
|
| 在部分机型上闪退 |
1. 相机API兼容性问题(特别是Camera2在旧设备上)。
2. 特定厂商的系统定制导致的生命周期问题。 |
1.
使用CameraX,它内部做了大量兼容性处理
。
2. 尝试在
onPause
中立即释放相机,在
onResume
中重新初始化。收集该机型的Logcat崩溃日志重点分析。
|
构建一个独立的人脸采集Module,看似增加了前期的工作量,但它带来的好处是长远的:清晰的职责边界、高效的团队协作、便捷的功能复用以及稳定的系统表现。当你下次需要为一个新的App添加人脸登录功能时,你只需要简单地
implementation project(‘:facecapture’)
,然后几行代码调用即可,那种感觉会告诉你,之前的架构设计是值得的。在实际开发中,这个Module还会根据业务需求不断演进,比如增加红外活体、3D结构光支持,或者与自研算法切换,但只要你守住了“对外接口稳定”和“内部高内聚”这两条底线,这些演进都会变得可控而平滑。


被折叠的 条评论
为什么被折叠?



