HoloLens2添加TextWriter后按钮粘滞卡顿问题求助
问题:HoloLens2测验应用中CSV写入导致按钮交互粘滞
我使用Unity 2019.4.20f1(因特殊要求必须使用该版本)开发面向HoloLens2的测验类应用,应用会随机展示预池化题目,用户点击选项按钮作答。但添加TextWriter记录题目、答案、耗时到CSV的功能后,HoloLens2上的按钮出现「粘滞」现象(手部网格触碰按钮后短暂停留),尝试过协程但仅延迟卡顿时机,且题切换时间有限不能过度延迟,求解决办法。
现有代码:
using System.Collections; using System.Collections.Generic; using System.Diagnostics; using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.UI; using System.IO; using System; public class QuizUI : MonoBehaviour { [SerializeField] private QuizManager quizManager; [SerializeField] private GameObject startButton; [SerializeField] private GameObject imageHolder; [SerializeField] private Image questionImage; [SerializeField] private GameObject questionModel; [SerializeField] private GameObject optionHolder; [SerializeField] private List<GameObject> options; [SerializeField] private GameObject endInfo; private Question question; private bool answered = false; private GameObject clone; public Stopwatch stopwatch; public bool val; string filename = ""; private string clickedBtn; private void Awake() { imageHolder.gameObject.SetActive(false); optionHolder.gameObject.SetActive(false); endInfo.gameObject.SetActive(false); filename = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/PurdueLogfile.csv"; } private void OnEnable() { var interactable = startButton.GetComponent<Interactable>(); interactable.OnClick.AddListener(() => Prepare()); } private void OnDisable() { var interactable = startButton.GetComponent<Interactable>(); interactable.OnClick.RemoveListener(() => Prepare()); } private void Prepare() { startButton.gameObject.SetActive(false); imageHolder.gameObject.SetActive(true); optionHolder.gameObject.SetActive(true); //add the listner to all the buttons for (int i = 0; i < options.Count; i++) { GameObject localBtn = options[i]; var interactable = options[i].GetComponent<Interactable>(); interactable.OnClick.AddListener(() => OnClick(localBtn)); } //log start time TextWriter tw = new StreamWriter(filename, true); tw.WriteLine("[" + System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff") + "]" + "," + "Start"); tw.Close(); quizManager.StartGame(); } public void SetQuestion(Question question) { //set the question this.question = question; //check for questionType switch (question.questionType) { case QuestionType.IMAGE: questionImage.transform.parent.gameObject.SetActive(true); questionImage.transform.gameObject.SetActive(true); //activate image holder questionModel.gameObject.SetActive(false); questionImage.sprite = question.questionImg; //set the image sprite break; case QuestionType.MODEL: questionImage.transform.parent.gameObject.SetActive(true); questionImage.transform.gameObject.SetActive(false); questionModel.gameObject.SetActive(true); clone = Instantiate(question.questionMdl, new Vector3(0, -0.15f, 0.8f), Quaternion.Euler (0f, -90f, 0f)) as GameObject; clone.transform.SetParent(questionModel.transform); break; } //assign options to respective option buttons for (int i = 0; i < options.Count; i++) { options[i].transform.GetChild(0).GetComponentInChildren<TextMesh>().text = question.options[i]; } answered = false; //stopwatch to start stopwatch = new Stopwatch(); stopwatch.Start(); } public void OnClick(GameObject btn) { if (!answered) { answered = true; bool val = quizManager.Answer(btn.name); UnityEngine.Debug.Log(btn.name); UnityEngine.Debug.Log(val); clickedBtn = btn.name; //stopwatch to stop stopwatch.Stop(); UnityEngine.Debug.Log(stopwatch.Elapsed); //log answers TextWriter tw = new StreamWriter(filename, true); tw.WriteLine("[" + System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff") + "]" + "," + quizManager.selectedQuestion.questionID + "," + quizManager.selectedQuestion.questionType + "," + stopwatch.ElapsedMilliseconds + "," + val + "," + clickedBtn); tw.Close(); //destroy 3D models DestroyImmediate(clone, true); } } public void EndGame() { imageHolder.gameObject.SetActive(false); optionHolder.gameObject.SetActive(false); endInfo.gameObject.SetActive(true); } }
解决方案
核心问题:主线程IO阻塞
当前代码在主线程中直接执行文件写入操作,HoloLens2的存储IO操作相对耗时,会阻塞UI交互线程,导致按钮反馈延迟、手部网格粘滞。协程如果只是延迟执行但仍在主线程,无法根本解决问题。
具体改进方案
1. 使用后台线程处理文件写入
创建线程安全的日志队列,把需要写入的日志内容先存入队列,再启动后台线程异步处理写入,完全不占用主线程资源。
修改代码步骤:
- 添加线程安全队列和后台线程相关字段
- 重构日志写入逻辑,将日志内容入队,后台线程循环处理队列
using System.Collections.Concurrent; using System.Threading; public class QuizUI : MonoBehaviour { // 新增:线程安全的日志队列 private ConcurrentQueue<string> logQueue = new ConcurrentQueue<string>(); private Thread logThread; private bool isLoggingThreadRunning = false; private void Awake() { imageHolder.gameObject.SetActive(false); optionHolder.gameObject.SetActive(false); endInfo.gameObject.SetActive(false); filename = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/PurdueLogfile.csv"; // 启动后台日志线程 StartLogThread(); } private void StartLogThread() { isLoggingThreadRunning = true; logThread = new Thread(ProcessLogQueue) { IsBackground = true, // 后台线程随程序退出自动终止 Priority = ThreadPriority.BelowNormal // 降低线程优先级,避免抢占UI线程资源 }; logThread.Start(); } private void ProcessLogQueue() { while (isLoggingThreadRunning) { if (logQueue.TryDequeue(out string logLine)) { try { // 使用File.AppendAllText简化写入,自动处理打开/关闭流 File.AppendAllText(filename, logLine + Environment.NewLine); } catch (Exception e) { UnityEngine.Debug.LogError($"日志写入失败:{e.Message}"); } } else { // 队列为空时短暂休眠,减少CPU占用 Thread.Sleep(10); } } } private void OnDestroy() { // 终止日志线程前,先处理完队列中剩余的日志 isLoggingThreadRunning = false; if (logThread != null && logThread.IsAlive) { logThread.Join(1000); // 等待最多1秒,确保剩余日志写入完成 } } // 替换原有的直接写入逻辑,改为入队 private void Log(string content) { logQueue.Enqueue(content); } // 修改Prepare中的日志调用 private void Prepare() { // ...其他原有代码 // 替换原TextWriter写入逻辑 Log($"[{System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff")}],Start"); quizManager.StartGame(); } // 修改OnClick中的日志调用 public void OnClick(GameObject btn) { if (!answered) { answered = true; bool val = quizManager.Answer(btn.name); UnityEngine.Debug.Log(btn.name); UnityEngine.Debug.Log(val); clickedBtn = btn.name; stopwatch.Stop(); UnityEngine.Debug.Log(stopwatch.Elapsed); // 替换原TextWriter写入逻辑 Log($"[{System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff")}],{quizManager.selectedQuestion.questionID},{quizManager.selectedQuestion.questionType},{stopwatch.ElapsedMilliseconds},{val},{clickedBtn}"); Destroy(clone, true); // 替换DestroyImmediate为异步Destroy } } }
2. 额外优化点
- 替换
DestroyImmediate为Destroy:DestroyImmediate会在主线程立即执行销毁操作,可能阻塞帧渲染,改用异步的Destroy更合适。 - 预加载模型:如果题目模型较多,提前预池化模型对象,避免在
SetQuestion中临时Instantiate导致的卡顿。
3. 验证和调试
- 部署到HoloLens2后,观察按钮交互是否恢复流畅
- 检查CSV文件是否正常生成,日志内容是否完整
内容的提问来源于stack exchange,提问作者PolarBear22
相关产品推荐
相关产品推荐

