3

I need to load a file in my application and since it is big ( around 250MB) I need to perform this loading off the main thread. What is more, because assets on Android are not stored in a regular directory, but a jar file, I need to use WWW or UnityWebRequest class.

I ended up with helper method like that:

public static byte[] ReadAllBytes(string filePath)
{

    if (Application.platform == RuntimePlatform.Android)
    {
        var reader = new WWW(filePath);
        while (!reader.isDone) { }

        return reader.bytes;
    }
    else
    {
        return File.ReadAllBytes(filePath);
    }
}

The problem is I cannot use it on background thread - Unity won't allow me to create WWW object there. How can I create a method like this, which will read those bytes on current thread?

Michał Powłoka
  • 1,424
  • 1
  • 13
  • 31

3 Answers3

1

just put your while loop inside a CoRoutine and while your request is not done to a yield return. when it is done call a method where you want to use your data:

IEnumerator MyMethod()
{
    var reader = new WWW(filePath);
    while (!reader.isDone) 
    { 
        yield return; // <- use endofFrame or Wait For ore something else if u want
    }
    LoadingDoneDoData(reader.bytes);
}

void LoadingDoneDoData(bytes[] data)
{
    // your Code here
}
Vampirasu
  • 202
  • 1
  • 12
  • Hey, I know how to make it in a coroutine. The point is I would like to have a simple method that just loads it now. Just like the one above, except mine won't work on a background thread. – Michał Powłoka Jan 17 '19 at 12:17
  • Then you use a different thread than the main you have to sync them. Or what do you mean with background thread and load it now? – Vampirasu Jan 17 '19 at 13:31
  • I gave up and decided to load it partly in a coroutine. I did some kind of mix of coroutine and async operations. Gona post it in a sec. – Michał Powłoka Jan 18 '19 at 09:51
1

I think you can use something like

public static async void ReadAllBytes(string filePath, Action<byte[]> successCallback)
{
    byte[] result;
    using (FileStream stream = File.Open(filePath, FileMode.Open))
    {
        result = new byte[stream.Length];
        await stream.ReadAsync(result, 0, (int)stream.Length);
    }

    // Now pass the byte[] to the callback
    successCallback.Invoke();
}

(Source)

Than I guess you can use it like

TheClass.ReadAllBytes(
    "a/file/path/",

    // What shall be done as soon as you have the byte[]
    (bytes) =>
    {
        // What you want to use the bytes for
    }
);

I'm no multi-threading expert but here and also here you can find more examples and how to's for async - await with Unity3d.


Alternatively also the new Unity Jobsystem might be interesting for you.

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Hi, I ended up with something similar, but I am afraid your solution won't work. It is not possible to read a file from streaming assets on Android with raw File operations because it is stored in a jar file... – Michał Powłoka Jan 18 '19 at 09:49
  • @MichałPowłoka oh I didn't see you want to read it from StreamingAssets. You should be able to read the bytes in anyway ... I don't know what you want to do with it exactly but you will need to unpack etc the content yourself. – derHugo Jan 18 '19 at 10:15
  • I know... Unity docs recommend using in such a case WWW class or UnityWebRequest. My problem was I am not allowed to create instance of these on any thread that is not the main thread, other way exception is thrown. – Michał Powłoka Jan 18 '19 at 10:18
  • @MichałPowłoka But why can't you use the `FileStream` for that? it returns you the raw bytes just like the `WWW` would do ... but with the difference that `FileStream` can be used async / threaded – derHugo Jan 18 '19 at 10:21
  • Didn't try FileStream tbh. I did try File.ReadAllBytes. I will check it. – Michał Powłoka Jan 18 '19 at 10:23
  • Tbh I cannot check it right now, I would have to figure out too many things :/ – Michał Powłoka Jan 18 '19 at 10:30
  • @MichałPowłoka the difference is that `File` doesn't contain any async method for reading bytes while the `FileStream` has them – derHugo Jan 18 '19 at 11:10
  • I know, but the problem with using System.IO, in general, is that they fail to find a file, regardless used thread. They cannot open a file, because it is stored in .jar archive. – Michał Powłoka Jan 18 '19 at 11:12
  • Yes I got that but how does using the `WWW` or `UnityWebRequest` change that? You anyway will get the `byte[]` that was what your example code was about. – derHugo Jan 18 '19 at 13:25
  • No. Once again, the problem with using File or FileStream is that it cannot handle opening files that are placed inside .jar archive. It won't work, File.Exists() is false. – Michał Powłoka Jan 18 '19 at 13:39
0

I ended up with the following solution. I did not find a way to load raw file main thread, but I managed to load it in Coroutine and perform further (also heavy) operations on this file on a background Thread. I made a static method like this:

public static void ReadAllBytesInCoroutine(MonoBehaviour context, string filePath, Action<ReadBytesInCoroutineResult> onComplete)
{
    context.StartCoroutine(ReadFileBytesAndTakeAction(filePath, onComplete));
}

private static IEnumerator ReadFileBytesAndTakeAction(string filePath, Action<ReadBytesInCoroutineResult> followingAction)
{
    WWW reader = null;

    try
    {
        reader = new WWW(filePath);
    }
    catch(Exception exception)
    {
        followingAction.Invoke(ReadBytesInCoroutineResult.Failure(exception));
    }

    while (reader != null && !reader.isDone)
    {
        yield return null;
    }
    followingAction.Invoke(ReadBytesInCoroutineResult.Success(reader.bytes));
}

ReadBytesInCoroutineResult is my simple, custom data class:

public class ReadBytesInCoroutineResult
{
    public readonly bool successful;
    public readonly byte[] data;
    public readonly Exception reason;

    private ReadBytesInCoroutineResult(bool successful, byte[] data, Exception reason)
    {
        this.successful = successful;
        this.data = data;
        this.reason = reason;
    }

    public static ReadBytesInCoroutineResult Success(byte[] data)
    {
        return new ReadBytesInCoroutineResult(true, data, null);
    }

    public static ReadBytesInCoroutineResult Failure(Exception reason)
    {
        return new ReadBytesInCoroutineResult(true, null, reason);
    }

}

This way I have a mechanism to order to load a file in coroutine in any place (as long as it is on the main thread). A file is loaded synchronously, but it is not blocking main thread, because of the coroutine. Later I invoke this function and take acquired bytes on a separate thread, where I perform heavy computing on them.

    ResourcesUtils.ReadAllBytesInCoroutine(monoBehavior, filePath, (bytes) => {

        //here I run an async method which takes bytes as parameter

    });
Michał Powłoka
  • 1,424
  • 1
  • 13
  • 31