408 lines
9.5 KiB
Markdown
408 lines
9.5 KiB
Markdown
# 语音测评闪退问题排查与解决方案
|
||
|
||
## 🔍 **问题描述**
|
||
|
||
App运行语音测评时经常闪退,尤其是在导入数据时更容易发生。
|
||
|
||
---
|
||
|
||
## 📊 **问题分析**
|
||
|
||
### **可能原因1:内存不足**
|
||
- **语音模型文件较大**
|
||
- vosk-model-small-cn-0.22.zip 约50-100MB
|
||
- 首次解压需要大量内存
|
||
- 解压时间约30秒
|
||
|
||
- **导入数据同时运行**
|
||
- 如果正在导入大量学员数据
|
||
- 同时运行语音识别
|
||
- 内存可能不足导致崩溃
|
||
|
||
### **可能原因2:页面切换时回调未停止**
|
||
- **回调函数继续执行**
|
||
- 页面卸载后,语音识别回调可能继续执行
|
||
- 访问已销毁的组件实例导致崩溃
|
||
|
||
- **状态检查不严格**
|
||
- 只检查 `isRecording` 状态
|
||
- 没有检查页面是否已卸载
|
||
|
||
### **可能原因3:数据导入错误影响系统**
|
||
- **班级不存在**
|
||
- 导入的学员数据包含不存在的班级
|
||
- 后端返回错误,前端处理不当
|
||
|
||
- **数据格式错误**
|
||
- Excel格式不符合要求
|
||
- 必填字段为空
|
||
|
||
### **可能原因4:模型初始化失败**
|
||
- **模型文件损坏或缺失**
|
||
- **路径解析错误**
|
||
- **权限问题**
|
||
|
||
---
|
||
|
||
## ✅ **解决方案**
|
||
|
||
### **方案1:优化页面生命周期(已修复)**
|
||
|
||
#### **添加页面卸载标记**
|
||
```javascript
|
||
data() {
|
||
return {
|
||
// ... 其他数据
|
||
pageUnloaded: false // 页面卸载标记
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **在onUnload中设置标记**
|
||
```javascript
|
||
onUnload() {
|
||
console.log('[Speech] 页面卸载,清理资源')
|
||
// 标记页面已卸载,防止回调继续执行
|
||
this.pageUnloaded = true
|
||
|
||
// 停止语音识别
|
||
if (this.isRecording) {
|
||
this.isRecording = false
|
||
try {
|
||
if (typeof stopSpeechVoice === 'function') {
|
||
stopSpeechVoice()
|
||
}
|
||
} catch(e) {
|
||
console.error('[Speech] 停止识别时出错:', e)
|
||
}
|
||
}
|
||
|
||
// 清理定时器
|
||
this.stopAutoScroll()
|
||
if (this.scrollTimer) {
|
||
clearInterval(this.scrollTimer)
|
||
this.scrollTimer = null
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **添加onHide生命周期**
|
||
```javascript
|
||
onHide() {
|
||
console.log('[Speech] 页面隐藏,暂停识别')
|
||
// 页面切换到后台时停止识别
|
||
if (this.isRecording) {
|
||
this.handleStop()
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **在回调中检查页面状态**
|
||
```javascript
|
||
startSpeechVoice((res) => {
|
||
// 检查页面是否已卸载
|
||
if (self.pageUnloaded) {
|
||
console.log('[Speech] 页面已卸载,忽略回调')
|
||
return
|
||
}
|
||
|
||
// 检查是否仍在录音状态
|
||
if (!self.isRecording) {
|
||
console.log('[Speech] 已停止录音,忽略回调')
|
||
return
|
||
}
|
||
|
||
// ... 处理识别结果
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### **方案2:修复数据导入(已修复)**
|
||
|
||
#### **班级验证**
|
||
- ✅ 在导入前验证班级是否存在
|
||
- ✅ 班级不存在时,记录失败原因
|
||
- ✅ 不中断整个导入流程
|
||
|
||
#### **错误处理**
|
||
- ✅ 导入失败的记录单独显示
|
||
- ✅ 提供详细的失败原因
|
||
- ✅ 失败详情支持滚动查看
|
||
|
||
#### **生成测试数据**
|
||
- ✅ 使用 `generate_test_data.py` 生成符合要求的测试数据
|
||
- ✅ 所有字段自动标准化为系统选项
|
||
- ✅ 班级自动映射到系统实际班级
|
||
|
||
---
|
||
|
||
### **方案3:优化语音模型加载**
|
||
|
||
#### **延迟加载**
|
||
```javascript
|
||
onLoad(options) {
|
||
// 先加载内容列表
|
||
if (options.contentId) {
|
||
this.loadContentById(options.contentId)
|
||
} else {
|
||
this.loadContentList()
|
||
}
|
||
|
||
// 延迟1秒后再初始化模型
|
||
setTimeout(() => {
|
||
// #ifdef APP-PLUS
|
||
this.initSpeechModel()
|
||
// #endif
|
||
}, 1000)
|
||
}
|
||
```
|
||
|
||
#### **检查内存**
|
||
```javascript
|
||
initSpeechModel() {
|
||
// 检查可用内存(如果API支持)
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
const memInfo = plus.device.getInfo()
|
||
console.log('[Speech] 设备内存信息:', memInfo)
|
||
} catch(e) {
|
||
console.log('[Speech] 无法获取内存信息')
|
||
}
|
||
// #endif
|
||
|
||
// ... 继续初始化
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### **方案4:添加错误捕获**
|
||
|
||
#### **全局错误处理**
|
||
```javascript
|
||
// App.vue 或 main.js
|
||
uni.onError((error) => {
|
||
console.error('[Global] 全局错误:', error)
|
||
// 可以记录到后端日志
|
||
// 显示友好的错误提示
|
||
uni.showToast({
|
||
title: '应用出错,请重启',
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
})
|
||
```
|
||
|
||
#### **Try-Catch包裹关键代码**
|
||
```javascript
|
||
async evaluateScore() {
|
||
if (!this.recognizedText || !this.selectedContent) {
|
||
uni.showToast({ title: '请先完成语音识别', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
this.isEvaluating = true
|
||
try {
|
||
const result = await evaluateSpeechRecognition(
|
||
this.selectedContent.content,
|
||
this.recognizedText
|
||
)
|
||
|
||
if (result.code === 200 && result.data) {
|
||
this.scoreResult = result.data
|
||
await this.saveScoreToBackend(result.data)
|
||
uni.showToast({ title: '评分完成', icon: 'success', duration: 2000 })
|
||
} else {
|
||
throw new Error(result.msg || '评分失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('评分失败', error)
|
||
uni.showToast({
|
||
title: error.message || '评分失败,请重试',
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
} finally {
|
||
this.isEvaluating = false
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 **测试步骤**
|
||
|
||
### **1. 测试页面切换**
|
||
1. 打开语音测评页面
|
||
2. 开始语音识别
|
||
3. 立即返回首页
|
||
4. 观察是否崩溃
|
||
|
||
**预期结果:** ✅ 不崩溃,识别自动停止
|
||
|
||
### **2. 测试数据导入**
|
||
1. 准备100条测试数据
|
||
2. 在语音测评页面
|
||
3. 同时执行数据导入
|
||
4. 观察是否崩溃
|
||
|
||
**预期结果:** ✅ 不崩溃,两个操作互不影响
|
||
|
||
### **3. 测试模型加载**
|
||
1. 清除模型缓存
|
||
2. 重新打开语音测评
|
||
3. 等待模型加载
|
||
4. 观察是否崩溃
|
||
|
||
**预期结果:** ✅ 不崩溃,正常加载
|
||
|
||
### **4. 测试长时间识别**
|
||
1. 开始语音识别
|
||
2. 持续朗读3分钟
|
||
3. 观察是否崩溃
|
||
|
||
**预期结果:** ✅ 不崩溃,正常识别
|
||
|
||
---
|
||
|
||
## 📋 **使用建议**
|
||
|
||
### **避免同时操作**
|
||
- ❌ 不要在导入数据时使用语音测评
|
||
- ❌ 不要在语音识别时导入大量数据
|
||
- ✅ 先完成一个操作,再进行另一个
|
||
|
||
### **分批导入数据**
|
||
- ✅ 每次导入不超过50条
|
||
- ✅ 等待上一批导入完成再导入下一批
|
||
- ✅ 避免一次性导入过多数据
|
||
|
||
### **定期清理缓存**
|
||
- ✅ 在设置中清理应用缓存
|
||
- ✅ 清理语音模型缓存后重新加载
|
||
- ✅ 卸载重装应用(如果频繁崩溃)
|
||
|
||
### **检查设备**
|
||
- ✅ 确保设备有足够的可用内存(至少500MB)
|
||
- ✅ 关闭其他占用内存的应用
|
||
- ✅ 重启设备
|
||
|
||
---
|
||
|
||
## 🔧 **调试方法**
|
||
|
||
### **1. 查看控制台日志**
|
||
```bash
|
||
# HBuilderX中查看真机运行日志
|
||
# 搜索关键字:[Speech]、Error、crash
|
||
```
|
||
|
||
### **2. 查看后端日志**
|
||
```bash
|
||
# 查看后端日志,是否有错误
|
||
tail -f logs/app.log | grep ERROR
|
||
```
|
||
|
||
### **3. 使用测试数据**
|
||
```bash
|
||
# 生成测试数据
|
||
python generate_test_data.py
|
||
|
||
# 导入测试数据,观察是否正常
|
||
```
|
||
|
||
### **4. 逐步排查**
|
||
1. **不使用语音测评** - 只导入数据
|
||
- 如果正常:语音测评有问题
|
||
- 如果崩溃:数据导入有问题
|
||
|
||
2. **不导入数据** - 只使用语音测评
|
||
- 如果正常:数据导入影响了语音测评
|
||
- 如果崩溃:语音测评本身有问题
|
||
|
||
3. **清除数据** - 卸载重装应用
|
||
- 如果正常:缓存数据有问题
|
||
- 如果崩溃:代码或资源文件有问题
|
||
|
||
---
|
||
|
||
## 💡 **优化建议**
|
||
|
||
### **1. 按需加载模型**
|
||
- 不在onLoad中自动初始化
|
||
- 用户点击"开始说话"时才加载
|
||
- 显示"模型加载中"提示
|
||
|
||
### **2. 使用更小的模型**
|
||
- 当前使用 vosk-model-small-cn-0.22
|
||
- 可以使用更小的模型(如果存在)
|
||
- 或者使用在线API(需要网络)
|
||
|
||
### **3. 优化数据导入**
|
||
- 使用分批导入(每批20-30条)
|
||
- 显示导入进度
|
||
- 支持后台导入
|
||
|
||
### **4. 添加异常恢复**
|
||
- 崩溃后自动保存状态
|
||
- 重新打开时恢复上次状态
|
||
- 提供"恢复上次会话"功能
|
||
|
||
---
|
||
|
||
## ✅ **验收标准**
|
||
|
||
- [ ] 页面快速切换不崩溃
|
||
- [ ] 导入数据时不崩溃
|
||
- [ ] 长时间语音识别不崩溃
|
||
- [ ] 模型加载失败有友好提示
|
||
- [ ] 数据导入失败有详细错误信息
|
||
- [ ] 控制台无严重错误日志
|
||
- [ ] 内存占用在合理范围内
|
||
|
||
---
|
||
|
||
## 📝 **修改文件清单**
|
||
|
||
### **已修改**
|
||
1. ✅ `fronted_uniapp/pages/speech/speech.vue`
|
||
- 添加 `pageUnloaded` 标记
|
||
- 添加 `onHide` 生命周期
|
||
- 回调中检查页面状态
|
||
|
||
2. ✅ `Study-Vue-redis/ry-study-ui/src/views/study/classUser/index.vue`
|
||
- 导入失败对话框添加滚动条
|
||
|
||
3. ✅ `Study-Vue-redis/ry-study-admin/.../StudyClassUserController.java`
|
||
- 移除导入前的班级验证
|
||
|
||
4. ✅ `Study-Vue-redis/ry-study-system/.../StudyClassUserServiceImpl.java`
|
||
- 添加逐条班级验证
|
||
- 记录失败原因到错误信息
|
||
|
||
### **新增文件**
|
||
1. ✅ `generate_test_data.py` - 测试数据生成脚本
|
||
|
||
---
|
||
|
||
## 🎯 **总结**
|
||
|
||
### **核心问题**
|
||
- 页面卸载后回调继续执行
|
||
- 数据导入和语音识别同时进行
|
||
- 内存不足
|
||
|
||
### **解决方法**
|
||
- 严格的生命周期管理
|
||
- 状态检查
|
||
- 分批操作
|
||
- 错误处理
|
||
|
||
### **使用建议**
|
||
- 避免同时操作
|
||
- 定期清理缓存
|
||
- 使用测试数据验证
|
||
|
||
**现在请测试修复后的版本,观察是否还会闪退!** 🚀
|