【问题标题】:Coroutine is blocking main thread on save image协程在保存图像时阻塞了主线程
【发布时间】:2017-11-09 15:32:39
【问题描述】:

我有一个协程,它截取屏幕截图并保存到文件系统。但是当协程运行时,游戏会暂时冻结,event onFinishedScreenShot 会过早触发。

我认为协程是“在后台线程上”。

有没有办法调整我的代码在不阻塞主线程的情况下保存屏幕截图并让event onFinishedScreenShot 触发 AFTER 写入文件完成?或者有没有更好的方法来处理保存游戏的屏幕截图?

MySingleton.cs

public event Action<string> onFinishedScreenShot;

public IEnumerator TakeScreenshot(string filename) {

    Texture2D areaImage = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);

    yield return new WaitForEndOfFrame();

    areaImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0, false);
    areaImage.Apply();

    byte[] bytes = areaImage.EncodeToPNG();
    File.WriteAllBytes(filename, bytes);

    if (onFinishedScreenShot != null) {
        onFinishedScreenShot(filename);
    }
}

AnotherClass.cs

private void Start() {
    LoadTexture(someFileName);
}


public void SomeMethodInAnotherClass() {
        StartCoroutine(MySingleton.TakeScreenshot(someFileName));
}


public void LoadTexture(string filename) {

    if (!filename.Equals("NOT SET") ) {

        byte[] bytes = File.ReadAllBytes(GetSaveFilename(filename));

        Texture2D texture = new Texture2D(500, 500, TextureFormat.DXT5, false);
        texture.filterMode = FilterMode.Trilinear;
        texture.LoadImage(bytes);

        RectTransform rt = locationSnapShot.rectTransform;

        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, rt.rect.width, rt.rect.height), new Vector2(0.5f, 0.0f), 16.0f);
        locationSnapShot.sprite = sprite;

        Debug.Log("Removing event listener.");
        MySingleton.onFinishedScreenShot -= LoadTexture;
    } else {
        Debug.Log("No screen shot. Listening for a change.");
        MySingleton.onFinishedScreenShot += LoadTexture;
    }
}

【问题讨论】:

    标签: c# unity3d coroutine system.io.file


    【解决方案1】:

    Unity 协程和“线程”是两个完全不同的特性。协程是一种管理何时代码执行的方式,但与线程无关。把两者想象成这样:

    协程:“执行 X,等待 Y 完成,然后执行 Z。”

    线程:“同时执行 X 和 Y,然后执行 Z。”

    如果你想打开自己的线程,C# 可以通过System.Threading 来实现,但你需要小心;几乎 100% 的 Unity API(来自UnityEngine 命名空间的任何内容)都会在它们在主线程上运行时断言。您需要获取原始字节数据,打开一个新线程,然后写入文件:

    ...
    byte[] bytes = areaImage.EncodeToPNG();
    
    // Start a new thread here
    File.WriteAllBytes(filename, bytes);
    
    if (onFinishedScreenShot != null) {
        onFinishedScreenShot(filename);
    }
    

    关于事件触发的最后一个问题:File.WriteAllBytes 是一个同步调用,因此您的事件绝对会在文件被写入之后触发(除非您从未列出的其他任何地方触发该事件在问题中)。

    【讨论】:

    • 你又一次帮助我理解了!谢谢! @Foggzie
    • @user-44651 总是乐于提供帮助。
    【解决方案2】:

    可能是因为您的脚本中有循环引用。

    MySingleton.onFinishedScreenShot += LoadTexture;
    

    您正在处理函数中注册处理程序。 尝试在 Start() 函数中执行此操作。 这可以解决问题。

    【讨论】:

      【解决方案3】:

      这与协程无关。使用ThreadPool.QueueUserWorkItem 在另一个线程中调用File.WriteAllBytes 函数。首先,从this 帖子中获取UnityThread 类,这样可以更轻松地从另一个Thread 调用主Thread 上的函数。以下是您的代码应该是什么样子。

      public event Action<string> onFinishedScreenShot;
      
      //Data to pass to the Thread function
      public class SaveData
      {
          public string fileName;
          public byte[] imgByteArray;
      }
      
      void Awake()
      {
          //Enable the Thread callback API
          UnityThread.initUnityThread();
      }
      
      public IEnumerator TakeScreenshot(string filename)
      {
      
          Texture2D areaImage = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
      
          yield return new WaitForEndOfFrame();
      
          areaImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0, false);
          areaImage.Apply();
      
          SaveData saveData = new SaveData();
          saveData.fileName = filename;
          saveData.imgByteArray = areaImage.EncodeToPNG();
      
          //Save on a new Thread
          WriteAllBytes(saveData);
      }
      
      void WriteAllBytes(SaveData saveData)
      {
          ThreadPool.QueueUserWorkItem(new WaitCallback(saveFile));
      }
      
      private void saveFile(object state)
      {
          SaveData saveData = (SaveData)state;
          File.WriteAllBytes(saveData.fileName, saveData.imgByteArray);
      
          //Invoke the event on the MainThread
          UnityThread.executeInUpdate(() =>
          {
              if (onFinishedScreenShot != null)
              {
                  onFinishedScreenShot(filename);
              }
          });
      }
      

      【讨论】:

      • 我目前遇到了同样的问题,它实际上不是来自文件 IO,而是来自 areaImage.EncodeToPNG 行,这可能需要一段时间。不幸的是,它使用了 UnityEngine,正如Foggzie 所说,如果 Unity 不添加这个,就没有办法使这个异步
      • @derHugo 有一种方法可以在另一个线程中使用本机 C++ 插件。
      猜你喜欢
      • 1970-01-01
      • 2020-05-21
      • 2017-12-24
      • 2019-09-01
      • 1970-01-01
      • 2014-12-20
      • 1970-01-01
      • 2018-02-16
      • 1970-01-01
      相关资源
      最近更新 更多