背景

最近需要做这样一个事情,一个服务来完成多款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 源码