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

43次阅读

共计 8942 个字符,预计需要花费 23 分钟才能阅读完成。

背景

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

正文完
 0