Android一次完美的跨进程服务共享实践

作者: i校长 | 来源:发表于2020-03-12 20:40 被阅读0次

    背景

    最近需要做这样一个事情,一个服务来完成多款App的录音功能,大致有如下逻辑

    • 服务以lib的形式集成到各个端
    • 当主App存在时,所有其他App都使用主App的录音服务
    • 当主App不存在时,其他App使用自带录音服务
    • 有优先级,优先级高的App有绝对的录音权限,不管其他App是否在录音都要暂停,优先处理高优先级的App请求
    • 支持AudioRecord、MediaRecorder两种录音方案

    为什么要这么设计?

    • Android系统底层对录音有限制,同一时间只支持一个进程使用录音的功能
    • 业务需要,一切事务保证主App的录音功能
    • 为了更好的管理录音状态,以及多App相互通信问题

    架构图设计

    Architecture

    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 源码

    相关文章

      网友评论

        本文标题:Android一次完美的跨进程服务共享实践

        本文链接:https://www.haomeiwen.com/subject/nefijhtx.html