安卓执法仪录像之进程间共享内存

Posted 「已注销」

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓执法仪录像之进程间共享内存相关的知识,希望对你有一定的参考价值。

背景

之前我在文章里说过,我们的录像模块是跨进程的.为什么要这样设计,是因为录像这个过程是一个长期执行的过程,中间不能有断开.为了保证稳定性,通过进程隔离将录像业务同主业务分离.

通常情况下,如果在录像过程中APP崩溃了,就会导致录像文件损坏,可能前面录的数据就丢了.

但是如果做了进程隔离的话,主进程发生异常崩溃,不影响录像进程.而录像进程功能相对单一简单,不会导致崩溃.当主进程崩溃时,录像进程可以检测到,这时及时停止当前的录像,录像依然可以正常结束,就可以播放.

实现

安卓端实现进程隔离是相对容易的,大体上为:

  • 创建一个Service
  • Manifest里面给这个Service配置process属性,表示该服务将运行在独立进程,其进程ID就是process的值.比如:
    <service
            android:name=".logic.impl.FFMuxerService"
            android:process="com.android.mpu.ff" />
    
  • 主进程启动通过startService启动这个服务,并且通过bindService与服务绑定,这样子进程也就启动了.
  • 主进程通过IPC(binder)与子进程进行数据交互.交互过程主要有:
    • 开始录像
    • 录像采集/编码视频帧传递给录像进程
    • 停止录像.

其跨进程的架构图如下所示:

问题以及解决方案

实际实现过程中又不可避免地带来一些问题,主要有:

  • 跨进程Binder大数据量传输限制(TransactionTooLargeException).安卓内部对于Binder的数据传输做了限制,最多不超过1M,但是媒体帧数据量通常比较大,有可能超过了1M.这样通过Binder传递的话,效率不高,而且还容易出现TransactionTooLargeException异常.
    可通过共享内存来解决这个问题.安卓平台有个MemoryFile可以用来共享内存,相比直接传输媒体帧,主进程可创建一个MemoryFile,并将其fd通过binder传递给子进程,子进程得到fd后,取出内存数据.这样可以有效避免大数据量传输的问题,且效率也更高.
    MemoryFile相关知识点,可以参考这篇文章的介绍:MemoryFile
    主进程侧关键代码片段如下:
       // 把内存写入MemoryFile对象
       mf.writeBytes(buffer, 0, 0, info.size)	
       param.frameType = AVMEDIA_TYPE_VIDEO
       param.size = info.size
       // 取得MemoryFile对应的FileDescriptor
       param.pfd = muxCon.getFD(mf)			
       param.timeStampleUs = info.presentationTimeUs
       ...
       paramArg.writeParcelable(param, 0)
       // 通过binder传递给子进程
       val success = muxCon.binder!!.transact(CODE_WRITE_FRAME, paramArg, replay, 0)
    
    录像进程侧:
    // 从binder里面解析输入参数.
    val writeParam = data?.readParcelable<WriteParam>(WriteParam::class.java.classLoader)!!
    // 创建buffer,从filediscripter里面取出媒体帧.
    var buffer = ByteArray(writeParam.size)
    val ins = FDIO(writeParam.pfd!!.fileDescriptor, writeParam.size)
    val readBytes = ins.readBytes(buffer, 0, 0, writeParam.size)
    
  • 录像进程对主进程的崩溃检测.上面说了录像过程中如果主进程崩溃了, 那录像进程应该及时停止当前的录像,确保在录文件能尽快正常停止以免损坏.所以主进程崩溃后,录像进程需要捕获到这个信息.
    Service提供了onBindonUnBind方法,这两个回调可满足需求.onBind表示服务被其它组件绑定(bind)了,onUnBind表示服务被其它组件释放了.也就是说主进程启动录像进程时,就会bind,主进程停止,录像进程就会onUnbind.
        @Override
        override fun onBind(intent: Intent): IBinder 
    	    // 服务被绑定.啥都不用干.打印个日志
            Timber.i("service bind to:$intent")
            return binder
        
    
        override fun onUnbind(intent: Intent?): Boolean 
       	    // 服务被释放了.及时停止录像
            Timber.i("service unbind to:$intent")
            // 每一路都需要停下来.
            muxers.valueIterator().forEach 
                it.apply 
                    muxer?.close()
                    Timber.i("muxer of transId:$transId,path:$path closed.duration:$(SystemClock.elapsedRealtime() - mStartMillis) / 1000 / 60min")
                
            
            muxers.clear()
            return false
        
    
  • 多路录像的问题,由于Service无法多实例启动,所以当同时录多路的话,没有办法通过多个Service来实现,我们在这个唯一的Service内部,通过主进程传入不通的实例ID来做区分.
    录像进程保留多个录像实例,由一个唯一ID来对应,主进程可传入这个ID.
    override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean 
    	// 先取实例ID.
       val transId = data.readInt()
       return try 
           doTransact(transId, code, data, reply, flags)
        catch (e: Throwable) 
       		// 发生异常,及时停止录像.
       		// muxers保存一系列录像实例,可通过transId取出当前录像实例关闭之.
           muxers[transId]?.muxer?.close()
           muxers.remove(transId)
           Timber.e(e, "muxer of transId:$transId 录像进程异常..")
           false
       
    
    

经过我们测试分进程之后,录像基本不会碰到损坏的情况了. 这个问题得到完美解决.

遗留问题

但是分进程也不是万能的,比如录像过程中暴力强制关机,拆电池,那分进程也是没辙的,这种情况下就是无解,遇到后只能想办法通过后期视频修复工具来进行修复了.

开源计划

2022年2月22日更新 录像库目前已经在github上开源.欢迎大家使用,并提出改进意见.地址:https://github.com/tsinglink/androidrecorder

参考资料

以上是关于安卓执法仪录像之进程间共享内存的主要内容,如果未能解决你的问题,请参考以下文章

安卓执法仪录像之进程间共享内存

执法记录仪录像模块的设计

执法记录仪录像模块的设计

执法记录仪录像模块的设计

安卓执法仪编码器之同步/异步模式

安卓执法仪编码器之同步/异步模式