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