Android一次完美的跨程式服務共享實踐

i校長發表於2020-03-12

背景

最近需要做這樣一個事情,一個服務來完成多款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 原始碼

相關文章