公司的一个税务智能咨询 APP《爱连塔可思》,主界面如图1所示。

图1

主界面上的数字人是一个 3D 动画,用 Unity3D 实现,按住对话功能通过讯飞语音识别将用户的语音转成文字。APP 上线后本来没什么问题,但后来又要将主要功能集成到另一家公司的APP《大连税务》上,然后问题就出现了。

首先说一下《爱连塔可思》启动时先显示 logo Activity,再显示数字人 Activity。进入数字人 Activity 时, logo Activity 已经 finish 掉了,此时再按系统返回键就退出进程了,这是正常的逻辑,没什么问题,如图2所示。

图2

因为《大连税务》不需要《爱连塔可思》的 logo Activity,所以我们这边把《爱连塔可思》的工程分成两个模块,app 模块和 taxrobot 模块,logo Activity 在 app 模块中,数字人 Activity 在 taxrobot模块中,这样就可以单独打包 taxrobot 模块,把输出的 aar 文件集成到《大连税务》APP上。《大连税务》是在它的主 Activity 上点击一个图标进入数字人 Activity,按返回键就回到主 Activity。如图3所示。

图3

可是实际效果却是,按返回键后《大连税务》闪退了。通过调查发现,这个闪退其实是在离开数字人 Activity 时调用了 Unity3D 的释放资源的方法,而这个释放资源的方法又调用了 exit 方法退出了整个 APP,也就是说,这并不是闪退,而是 Unity3D 的正常逻辑。那怎么解决呢?一开始想在离开时不调用释放资源的方法,结果发现不释放的话,下次再进入数字人 Activity 时,数字人创建失败了,而且理论上不释放还可能造成内存泄露问题,所以必须释放。思前想后,只有一个办法:双进程,就是将 主 Activity 在一个进程中(以下简称”主进程“),数字人 Activity在另一个进程中(以下简称”数字人进程”),这样在离开数字人 Activity 时,退出的是数字人进程,不会影响到主进程。因为 Android 是支持一个 APP 运行在多个进程中的。所以,修改方案是在 AndroidManifest.xml 中的数字人 Activity 的描述中增加 android:process=".unity",如下所示:

<activity
    android:name="com.xx.taxrobot.TaxRobotActivity"
    android:process=".unity" />

这样,主 Activity 运行在 com.xx.dltax 进程(主进程)中,而 数字人 Activity 运行在 com.xx.dltax.unity 进程(数字人进程)中。但是这么修改又遇到另一个问题,就是在数字人 Activity 中按下“按住对话”后,无论说什么,APP 都没有任何反应。调查发现,是因为讯飞语音识别的相关对象创建失败了,经过大量调试和调查仍然找不到原因,所以只能采用规避的办法。因为在主进程中是可以成功创建讯飞语音识别的,所以修改方案是,在 taxrobot 模块中增加 Application 类,在 Application 类中创建一个 Service,在 Service 类中创建讯飞语音识别并提供相应的对外接口方法。因为 Application 是运行在主进程中的且先于 Activity 运行,这样创建的 Service 也在主进程中,讯飞语音识别当然也是在主进程中。现在又遇到另一个问题,数字人 Activity 的“按住对话”和讯飞语音识别不在同一进程中,怎么进行语音识别?答案就是进程通信。Android 中的进程通信有多种方式,这里采用简单的 Messenger,整个通信过程如图4所示。

图4

以上修改方案的主要代码如下所示:

class SpeechUtil: Service(), Callback, RecognizerListener {
    companion object {
        const val KEY_ASR_TEXT = "asrText"                  // 键:语音识别的文字
        const val KEY_MESSENGER = "messenger"               // 键:信使
        const val MSG_GET_ASR_RESULT = 1000                 // 得到语音识别结果消息
        const val MSG_START_LISTENING = 1001                // 开始语音识别消息
        const val MSG_STOP_LISTENING = 1002                 // 停止语音识别消息
        private lateinit var sClientMessenger: Messenger    // 客户端信使
        private lateinit var sServerMessenger: Messenger    // 服务器信使

        /**
         * 初始化。
         * @param context   设备环境
         * @param callback  回调
         */
        fun init(context: Context, callback: Callback?) {
            val intent = Intent(context, SpeechUtil::class.java)
            if (callback != null) {
                val handler = Handler(Looper.getMainLooper(), callback)
                val messenger = Messenger(handler)
                intent.putExtra(KEY_MESSENGER, messenger)
                val connection = object: ServiceConnection {
                    override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
                        sServerMessenger = Messenger(iBinder)
                    }

                    override fun onServiceDisconnected(componentName: ComponentName) {
                    }
                }
                try {
                    Thread.sleep(1000)
                } catch (e: Exception) {
                    e.printStackTrace()
                }
                context.bindService(intent, connection, 0)
            } else {
                context.startService(intent)
            }
        }

        /**
         * 开始语音识别。
         */
        fun startListening() {
            val message = Message.obtain(null, MSG_START_LISTENING)
            try {
                sServerMessenger.send(message)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        /**
         * 停止语音识别。
         */
        fun stopListening() {
            val message = Message.obtain(null, MSG_STOP_LISTENING)
            try {
                sServerMessenger.send(message)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    private lateinit var mSpeechRecognizer: SpeechRecognizer    // 语音识别者

    /**
     * 处理消息。
     * @param msg   消息
     * @return 消息在处被处理则返回 true,否则返回 false
     */
    override fun handleMessage(msg: Message): Boolean {
        if (msg.what == MSG_START_LISTENING) {
            mSpeechRecognizer.startListening(this)
        } else if (msg.what == MSG_STOP_LISTENING) {
            mSpeechRecognizer.stopListening()
        }
        return false
    }

    /**
     * 绑定事件的响应方法。
     * @param intent    意图
     * @return 绑定者
     */
    override fun onBind(intent: Intent): IBinder? {
        sClientMessenger = intent.getParcelableExtra<Messenger>(KEY_MESSENGER)!!
        val handler = Handler(Looper.getMainLooper(), this)
        return Messenger(handler).binder
    }

    /**
     * 创建事件的响应方法。
     */
    override fun onCreate() {
        // 创建语音识别者
        mSpeechRecognizer = SpeechRecognizer.createRecognizer(this, InitListener { i: Int ->
            mSpeechRecognizer.setParameter(SpeechConstant.PARAMS, null)
            mSpeechRecognizer.setParameter(SpeechConstant.RESULT_TYPE, "json")
            mSpeechRecognizer.setParameter(SpeechConstant.ACCENT, "mandarin")
            mSpeechRecognizer.setParameter(SpeechConstant.VAD_BOS, "4000")
            mSpeechRecognizer.setParameter(SpeechConstant.AUDIO_FORMAT, "wav")
            mSpeechRecognizer.setParameter(SpeechConstant.LANGUAGE, "zh_CN")
        })
    }

    /**
     * 语音识别结束事件的响应方法。
     */
    override fun onEndOfSpeech() {
        mSpeechRecognizer.stopListening()
    }

    /**
     * 获取到语音识别结果的响应方法。
     * @param result    语音识别结果
     * @param b         ?
     */
    override fun onResult(result: RecognizerResult, b: Boolean) {
        mSpeechRecognizer.stopListening()
        val message = Message.obtain()
        message.what = MSG_GET_ASR_RESULT
        val data = Bundle()
        data.putString(KEY_ASR_TEXT, result.resultString)
        message.data = data
        try {
            sClientMessenger.send(message)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

总结一下,主要有3点:

1.Unity3D的 Activity 退出时,整个 APP 的进程也会退出,所以要返回上一个 Activity 就得采用双进程。

2.在 Unity3D 的 进程里创建讯飞语音识别会失败,原因不明,所以只能在主进程里创建。

3.主进程和 Unity3D 进程之间通过 Messenger 通信。

全文完。

更多推荐