背景
最近需要做这样一个事情,一个服务来完成多款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实现的录音服务,对跨进程的数据处理有了更加深刻的认知,这里面有几个比较难处理的就是录音的状态维护,还有就是优先级的维护,能把这两点整明白其实也很好处理。不扯了,有问题留言区交流。