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

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

背景


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

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

为什么要这么设计?


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

架构图设计


image.png

App层

包含公司所有需要集成录音服务的端,这里不需要解释

Manager层

该层负责Service层的管理,包括: 服务的绑定,解绑,注册回调,开启录音,停止录音,检查录音状态,检查服务运行状态等

Service层

核心逻辑层,通过AIDL的实现,来满足跨进程通信,并提供实际的录音功能。

目录一览


image.png

看代码目录的分配,并结合架构图,我们来从底层往上层实现一套逻辑

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实现的录音服务,对跨进程的数据处理有了更加深刻的认知,这里面有几个比较难处理的就是录音的状态维护,还有就是优先级的维护,能把这两点整明白其实也很好处理。不扯了,有问题留言区交流。


目录
相关文章
|
14天前
|
安全 数据处理 Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第26天】 在面对现代Android开发时,性能优化和流畅的用户体验成为了开发者们追求的目标。Kotlin作为一种现代化的编程语言,通过其协程特性为Android应用带来了前所未有的并发处理能力。本文将深入探讨如何利用Kotlin协程提升Android应用的响应性和效率,同时保持代码的简洁性。我们将从协程的基础概念出发,逐步揭示如何在实际应用中运用这些强大的工具,以及它们如何改善应用架构和用户交互体验。
|
18天前
|
Java API 调度
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
Android系统 自定义开机广播,禁止后台服务,运行手动安装应用接收开机广播
40 0
|
18天前
|
Android开发
Android 11 添加Service服务SELinux问题
Android 11 添加Service服务SELinux问题
40 1
|
10天前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第30天】在移动开发领域,随着用户需求的不断增长和设备性能的持续提升,实现流畅且高效的用户体验已成为开发者的首要任务。针对Android平台,Kotlin协程作为一种新兴的异步编程解决方案,以其轻量级线程管理和简洁的代码逻辑受到广泛关注。本文将深入探讨Kotlin协程的概念、优势以及在实际Android应用中的运用,通过实例演示如何利用协程提升应用性能和响应能力,为开发者提供一条构建更高效Android应用的实践路径。
|
1天前
|
缓存 Java Android开发
安卓应用性能优化实践
【5月更文挑战第9天】在移动开发领域,应用的性能是决定用户体验的关键因素之一。特别是对于安卓平台,由于设备的多样性,性能优化变得尤为重要。本文将深入探讨针对安卓应用的性能优化策略,从内存管理、多线程处理到布局优化等方面提供实用的技术建议和最佳实践,帮助开发者提升应用的流畅度与响应速度。
|
2天前
|
Shell Linux
【Linux】进程实践项目(更新中) — 自主shell编写
前几篇文章,我们学习进程的相关知识:进程概念,进程替换,进程控制。熟悉了进程到底是个什么事情,接下来我们来做一个实践,来运用我们所学的相关知识。这个项目就是手搓一个shell模块,模拟实现Xshell中的命令行输入。
7 1
|
4天前
|
移动开发 Java Android开发
Android应用开发:Kotlin语言的优势与实践
【5月更文挑战第7天】 在移动开发的世界中,Android平台的Kotlin语言以其高效、简洁的语法和强大的功能吸引了众多开发者。本文将深入探讨Kotlin语言的核心优势,并通过实际案例展示如何在Android应用开发中有效地运用这一现代编程语言。我们将从语言特性出发,逐步分析其在提升开发效率、改善代码质量以及增强应用性能方面的具体表现,为读者提供一个全面而细致的Kotlin应用开发指南。
|
6天前
|
存储 前端开发 Java
Android应用开发中的MVP架构模式实践
【5月更文挑战第5天】随着移动应用开发的复杂性增加,传统的MVC(Model-View-Controller)架构在应对大型项目时显得笨重且不灵活。本文将探讨一种更适应现代Android应用开发的架构模式——MVP(Model-View-Presenter),并展示如何在Android项目中实现该模式以提升代码的可维护性和可测试性。通过对比分析MVP与传统MVC的差异,以及提供一个实际案例,读者将能深入了解MVP的优势和实施步骤。
|
10天前
|
移动开发 调度 Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第30天】 在移动开发领域,性能优化与流畅的用户体验始终是开发者追求的目标。随着Kotlin语言在Android开发中的普及,其提供的协程特性成为了解决异步编程问题的有力工具。本文将通过深入分析Kotlin协程的原理与实践,展示如何在Android应用中利用协程提升响应速度和处理效率,同时保证代码的简洁性和可维护性。我们将从基本概念出发,逐步深入到协程的高级使用场景,帮助开发者构建更加高效的Android应用。
|
10天前
|
安全 网络安全 Android开发
云端防御策略:融合云服务与网络安全的未来构建高效的Android应用:从内存优化到电池寿命
【4月更文挑战第30天】 随着企业加速向云计算环境转移,数据和服务的云端托管成为常态。本文探讨了在动态且复杂的云服务场景下,如何构建和实施有效的网络安全措施来保障信息资产的安全。我们将分析云计算中存在的安全挑战,并展示通过多层次、多维度的安全框架来提升整体防护能力的方法。重点关注包括数据加密、身份认证、访问控制以及威胁检测与响应等关键技术的实践应用,旨在为读者提供一种结合最新技术进展的网络安全策略视角。 【4月更文挑战第30天】 在竞争激烈的移动市场中,Android应用的性能和资源管理已成为区分优秀与平庸的关键因素。本文深入探讨了提升Android应用效率的多个方面,包括内存优化策略、电池

相关实验场景

更多