背景
最近需要做这样一个事情,一个服务来完成多款 App 的录音功能,大致有如下逻辑
- 服务以 lib 的形式集成到各个端
- 当主 App 存在时,所有其他 App 都使用主 App 的录音服务
- 当主 App 不存在时,其他 App 使用自带录音服务
- 有优先级,优先级高的 App 有绝对的录音权限,不管其他 App 是否在录音都要暂停,优先处理高优先级的 App 请求
- 支持 AudioRecord、MediaRecorder 两种录音方案
为什么要这么设计?
- Android 系统底层对录音有限制,同一时间只支持一个进程使用录音的功能
- 业务需要,一切事务保证主 App 的录音功能
- 为了更好的管理录音状态,以及多 App 相互通信问题
架构图设计
App 层
包含公司所有需要集成录音服务的端,这里不需要解释
Manager 层
该层负责 Service 层的管理,包括:
服务的绑定,解绑,注册回调,开启录音,停止录音,检查录音状态,检查服务运行状态等
Service 层
核心逻辑层,通过 AIDL 的实现,来满足跨进程通信,并提供实际的录音功能。
目录一览
看代码目录的分配,并结合架构图,我们来从底层往上层实现一套逻辑
IRecorder 接口定义
public interface IRecorder {String startRecording(RecorderConfig recorderConfig);
void stopRecording();
RecorderState state();
boolean isRecording();}
IRecorder 接口实现
class JLMediaRecorder : IRecorder {
private var mMediaRecorder: MediaRecorder? = null
private var mState = RecorderState.IDLE
@Synchronized
override fun startRecording(recorderConfig: RecorderConfig): String {
try {mMediaRecorder = MediaRecorder()
mMediaRecorder?.setAudioSource(recorderConfig.audioSource)
when (recorderConfig.recorderOutFormat) {
RecorderOutFormat.MPEG_4 -> {mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
mMediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
}
RecorderOutFormat.AMR_WB -> {mMediaRecorder?.setOutputFormat(MediaRecorder.OutputFormat.AMR_WB)
mMediaRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB)
}
else -> {mMediaRecorder?.reset()
mMediaRecorder?.release()
mMediaRecorder = null
return "MediaRecorder 不支持 AudioFormat.PCM"
}
}
} catch (e: IllegalStateException) {mMediaRecorder?.reset()
mMediaRecorder?.release()
mMediaRecorder = null
return "Error initializing media recorder 初始化失败";
}
return try {
val file = recorderConfig.recorderFile
file.parentFile.mkdirs()
file.createNewFile()
val outputPath: String = file.absolutePath
mMediaRecorder?.setOutputFile(outputPath)
mMediaRecorder?.prepare()
mMediaRecorder?.start()
mState = RecorderState.RECORDING
""
} catch (e: Exception) {mMediaRecorder?.reset()
mMediaRecorder?.release()
mMediaRecorder = null
recorderConfig.recorderFile.delete()
e.toString()}
}
override fun isRecording(): Boolean {return mState == RecorderState.RECORDING}
@Synchronized
override fun stopRecording() {
try {if (mState == RecorderState.RECORDING) {mMediaRecorder?.stop()
mMediaRecorder?.reset()
mMediaRecorder?.release()}
} catch (e: java.lang.IllegalStateException) {e.printStackTrace()
}
mMediaRecorder = null
mState = RecorderState.IDLE
}
override fun state(): RecorderState {return mState}
}
这里需要注意的就是加 @Synchronized 因为多进程同时调用的时候会出现状态错乱问题,需要加上才安全。
AIDL 接口定义
interface IRecorderService {void startRecording(in RecorderConfig recorderConfig);
void stopRecording(in RecorderConfig recorderConfig);
boolean isRecording(in RecorderConfig recorderConfig);
RecorderResult getActiveRecording();
void registerCallback(IRecorderCallBack callBack);
void unregisterCallback(IRecorderCallBack callBack);
}
注意点:
自定义参数需要实现 Parcelable 接口
需要回调的话也是 AIDL 接口定义
AIDL 接口回调定义
interface IRecorderCallBack {void onStart(in RecorderResult result);
void onStop(in RecorderResult result);
void onException(String error,in RecorderResult result);
}
RecorderService 实现
接下来就是功能的核心,跨进程的服务
class RecorderService : Service() {
private var iRecorder: IRecorder? = null
private var currentRecorderResult: RecorderResult = RecorderResult()
private var currentWeight: Int = -1
private val remoteCallbackList: RemoteCallbackList<IRecorderCallBack> = RemoteCallbackList()
private val mBinder: IRecorderService.Stub = object : IRecorderService.Stub() {override fun startRecording(recorderConfig: RecorderConfig) {startRecordingInternal(recorderConfig)
}
override fun stopRecording(recorderConfig: RecorderConfig) {if (recorderConfig.recorderId == currentRecorderResult.recorderId)
stopRecordingInternal()
else {
notifyCallBack {
it.onException(
"Cannot stop the current recording because the recorderId is not the same as the current recording",
currentRecorderResult
)
}
}
}
override fun getActiveRecording(): RecorderResult? {return currentRecorderResult}
override fun isRecording(recorderConfig: RecorderConfig?): Boolean {return if (recorderConfig?.recorderId == currentRecorderResult.recorderId)
iRecorder?.isRecording ?: false
else false
}
override fun registerCallback(callBack: IRecorderCallBack) {remoteCallbackList.register(callBack)
}
override fun unregisterCallback(callBack: IRecorderCallBack) {remoteCallbackList.unregister(callBack)
}
}
override fun onBind(intent: Intent?): IBinder? {return mBinder}
@Synchronized
private fun startRecordingInternal(recorderConfig: RecorderConfig) {
val willStartRecorderResult =
RecorderResultBuilder.aRecorderResult().withRecorderFile(recorderConfig.recorderFile)
.withRecorderId(recorderConfig.recorderId).build()
if (ContextCompat.checkSelfPermission(
this@RecorderService,
android.Manifest.permission.RECORD_AUDIO
)
!= PackageManager.PERMISSION_GRANTED
) {logD("Record audio permission not granted, can't record")
notifyCallBack {
it.onException(
"Record audio permission not granted, can't record",
willStartRecorderResult
)
}
return
}
if (ContextCompat.checkSelfPermission(
this@RecorderService,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
!= PackageManager.PERMISSION_GRANTED
) {logD("External storage permission not granted, can't save recorded")
notifyCallBack {
it.onException(
"External storage permission not granted, can't save recorded",
willStartRecorderResult
)
}
return
}
if (isRecording()) {
val weight = recorderConfig.weight
if (weight < currentWeight) {logD("Recording with weight greater than in recording")
notifyCallBack {
it.onException(
"Recording with weight greater than in recording",
willStartRecorderResult
)
}
return
}
if (weight > currentWeight) {
// 只要权重大于当前权重,立即停止当前。stopRecordingInternal()}
if (weight == currentWeight) {if (recorderConfig.recorderId == currentRecorderResult.recorderId) {
notifyCallBack {
it.onException(
"The same recording cannot be started repeatedly",
willStartRecorderResult
)
}
return
} else {stopRecordingInternal()
}
}
startRecorder(recorderConfig, willStartRecorderResult)
} else {startRecorder(recorderConfig, willStartRecorderResult)
}
}
private fun startRecorder(
recorderConfig: RecorderConfig,
willStartRecorderResult: RecorderResult
) {logD("startRecording result ${willStartRecorderResult.toString()}")
iRecorder = when (recorderConfig.recorderOutFormat) {
RecorderOutFormat.MPEG_4, RecorderOutFormat.AMR_WB -> {JLMediaRecorder()
}
RecorderOutFormat.PCM -> {JLAudioRecorder()
}
}
val result = iRecorder?.startRecording(recorderConfig)
if (!result.isNullOrEmpty()) {logD("startRecording result $result")
notifyCallBack {it.onException(result, willStartRecorderResult)
}
} else {
currentWeight = recorderConfig.weight
notifyCallBack {it.onStart(willStartRecorderResult)
}
currentRecorderResult = willStartRecorderResult
}
}
private fun isRecording(): Boolean {return iRecorder?.isRecording ?: false}
@Synchronized
private fun stopRecordingInternal() {logD("stopRecordingInternal")
iRecorder?.stopRecording()
currentWeight = -1
iRecorder = null
MediaScannerConnection.scanFile(
this,
arrayOf(currentRecorderResult.recorderFile?.absolutePath),
null,
null
)
notifyCallBack {it.onStop(currentRecorderResult)
}
}
private fun notifyCallBack(done: (IRecorderCallBack) -> Unit) {val size = remoteCallbackList.beginBroadcast()
logD("recorded notifyCallBack size $size")
(0 until size).forEach {done(remoteCallbackList.getBroadcastItem(it))
}
remoteCallbackList.finishBroadcast()}
}
这里需要注意的几点:
因为是跨进程服务,启动录音的时候有可能是多个 app 在同一时间启动,还有可能在一个 App 录音的同时,另一个 App 调用停止的功能,所以这里维护好当前 currentRecorderResult 对象的维护,还有一个 currentWeight 字段也很重要,这个字段主要是维护优先级的问题,只要有比当前优先级高的指令,就按新的指令操作录音服务。
notifyCallBack 在合适时候调用 AIDL 回调,通知 App 做相应的操作。
RecorderManager 实现
step 1
服务注册,这里按主 App 的包名来启动,所有 App 都是以这种方式启动
fun initialize(context: Context?, serviceConnectState: ((Boolean) -> Unit)? = null) {
mApplicationContext = context?.applicationContext
if (!isServiceConnected) {
this.mServiceConnectState = serviceConnectState
val serviceIntent = Intent()
serviceIntent.`package` = "com.julive.recorder"
serviceIntent.action = "com.julive.audio.service"
val isCanBind = mApplicationContext?.bindService(
serviceIntent,
mConnection,
Context.BIND_AUTO_CREATE
) ?: false
if (!isCanBind) {logE("isCanBind:$isCanBind")
this.mServiceConnectState?.invoke(false)
bindSelfService()}
}
}
isCanBind 是 false 的情况,就是未发现主 App 的情况,这个时候就需要启动自己的服务
private fun bindSelfService() {val serviceIntent = Intent(mApplicationContext, RecorderService::class.java)
val isSelfBind =
mApplicationContext?.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)
logE("isSelfBind:$isSelfBind")
}
step 2
连接成功后
private val mConnection: ServiceConnection = object : ServiceConnection {override fun onServiceConnected(name: ComponentName, service: IBinder) {mRecorderService = IRecorderService.Stub.asInterface(service)
mRecorderService?.asBinder()?.linkToDeath(deathRecipient, 0)
isServiceConnected = true
mServiceConnectState?.invoke(true)
}
override fun onServiceDisconnected(name: ComponentName) {
isServiceConnected = false
mRecorderService = null
logE("onServiceDisconnected:name=$name")
}
}
接下来就可以用 mRecorderService 来操作 AIDL 接口,最终调用 RecorderService 的实现
// 启动
fun startRecording(recorderConfig: RecorderConfig?) {if (recorderConfig != null)
mRecorderService?.startRecording(recorderConfig)
}
// 暂停
fun stopRecording(recorderConfig: RecorderConfig?) {if (recorderConfig != null)
mRecorderService?.stopRecording(recorderConfig)
}
// 是否录音中
fun isRecording(recorderConfig: RecorderConfig?): Boolean {return mRecorderService?.isRecording(recorderConfig) ?: false
}
这样一套完成的跨进程通信就完成了,代码注释很少,经过这个流程的代码展示,应该能明白整体的调用流程。如果有不明白的,欢迎留言区哦。
总结
通过这两天,对这个 AIDL 实现的录音服务,对跨进程的数据处理有了更加深刻的认知,这里面有几个比较难处理的就是录音的状态维护,还有就是优先级的维护,能把这两点整明白其实也很好处理。不扯了,有问题留言区交流。
欢迎交流:
git 源码