Async image loader with two caching layers for Unity. It supports loading images from web or local paths and provides memory and disk caching to optimize performance. The package includes features for automatic image setting, cancellation handling, error handling, and lifecycle management.
Wait for image get loaded then set:
image.sprite = await ImageLoader.LoadSprite(imageURL);
Don't wait, use callback to set loaded image later:
ImageLoader.LoadSprite(imageURL).ThenSet(image).Forget();
Use callback to set image and still wait for the completion:
await ImageLoader.LoadSprite(imageURL).ThenSet(image);
- ✔️ Async loading from Web or Local
ImageLoader.LoadSprite(imageURL);
- ✔️ Memory and Disk caching - tries to load from memory first, then from disk
- ✔️ Dedicated thread for disk operations
- ✔️ Supports loading of
Texture2D
andSprite
- ✔️ Avoids loading same image multiple times simultaneously, a new load task waits for completion of existed task
- ✔️ Uses
UnityWebRequest
to load data which works smooth across all platforms includingWebGL
- ✔️ Cache supported on at
WebGL
. Memory cache works, Disk cache isn't allowed by the platform - ✔️ Set into Image
ImageLoader.LoadSprite(imageURL).ThenSet(image);
- ✔️ Set into RawImage
ImageLoader.LoadSprite(imageURL).ThenSet(rawImage);
- ✔️ Set into Material
ImageLoader.LoadSprite(imageURL).ThenSet("_MainTex", material);
- ✔️ Set into SpriteRenderer
ImageLoader.LoadSprite(imageURL).ThenSet(spriteRenderer);
- ✔️ Set into anything
- ✔️ Cancellation
ImageLoader.LoadSprite(imageURL).Cancel();
- ✔️ Cancellation callback
ImageLoader.LoadSprite(imageURL).Cancelled(() => ...);
- ✔️ Error callback
ImageLoader.LoadSprite(imageURL).Failed(exception => ...);
- ✔️ Debug level for logging
ImageLoader.settings.debugLevel = DebugLevel.Error;
- ✔️ Debug level per each task
ImageLoader.LoadSprite(imageURL).SetLogLevel(DebugLevel.Trace);
- Unity Image Loader
- Usage
- Other
- Install OpenUPM-CLI
- Open command line in Unity project folder
- Run the command
openupm add extensions.unity.imageloader
In the main thread somewhere at the start of the project need to call ImageLoader.Init();
once to initialize static properties in the right thread. It is required to make in the main thread. Then you can use ImageLoader
from any thread and at any time.
ImageLoader.Init(); // just once from the main thread
ImageLoader.LoadSprite
returns IFuture<Sprite>
. This instance provides all range of callbacks and API to modify it. Understanding IFuture<T>
.
ImageLoader.LoadSprite(imageURL) // loading process started
// ┌──────────────────────────┬────────────────────────────────────────────────────────────────────────┐
// │ Loading lifecycle events │ │
// └──────────────────────────┘ │
.LoadedFromMemoryCache(sprite => Debug.Log("Loaded from memory cache")) // on loaded from memory cache │
.LoadingFromDiskCache (() => Debug.Log("Loading from disk cache")) // on loading from disk cache │
.LoadedFromDiskCache (sprite => Debug.Log("Loaded from disk cache")) // on loaded from disk cache │
.LoadingFromSource (() => Debug.Log("Loading from source")) // on loading from source │
.LoadedFromSource (sprite => Debug.Log("Loaded from source")) // on loaded from source │
// ────────────────────────────────────────────────────────────────────────────────────────────────────┘
// ┌───────────────────────────┬──────────────────────────────────────────┐
// │ Negative lifecycle events │ │
// └───────────────────────────┘ │
.Canceled(() => Debug.Log("Canceled")) // on canceled │
.Failed(exception => Debug.LogException(exception)) // on failed to load │
// ───────────────────────────────────────────────────────────────────────┘
// ┌──────────────────────────────────────┬──────────────────────────────┐
// │ Successfully loaded lifecycle events │ │
// └──────────────────────────────────────┘ │
.Then(sprite => Debug.Log("Loaded")) // on loaded │
.ThenSet(image) // on loaded set sprite into image │
// ──────────────────────────────────────────────────────────────────────┘
// ┌──────────────────────┬──────────────────────────────────────────────────────────────────────────┐
// │ The end of lifecycle │ │
// └──────────────────────┘ │
.Completed(isLoaded => Debug.Log($"Completed, isLoaded={isLoaded}")) // on completed │
// // [loaded, failed or canceled] │
// ──────────────────────────────────────────────────────────────────────────────────────────────────┘
.Forget(); // removes the compilation warning, does nothing else
// Load a sprite from the web and cache it for faster loading next time
image.sprite = await ImageLoader.LoadSprite(imageURL);
// Load a sprite from the web and set it directly to the Image component
await ImageLoader.LoadSprite(imageURL).ThenSet(image);
// Load a Texture2D from the web and cache it for faster loading next time
material.mainTexture = await ImageLoader.LoadTexture(imageURL);
// Load a Texture2D from the web and set it directly to the Material
await ImageLoader.LoadTexture(imageURL).ThenSet(material);
ImageLoader.LoadSprite(imageURL).ThenSet(image1, image2).Forget();
ImageLoader.LoadSprite(imageURL) // Attempt to load a sprite
.ThenSet(image) // If successful, set the sprite to the Image component
.Failed(exception => Debug.LogException(exception)) // If an error occurs, log the exception
.Forget(); // Forget the task to avoid compilation warning
ImageLoader.LoadSprite(imageURL) // Attempt to load a sprite
.ThenSet(image) // If successful, set the sprite to the Image component
.Then(sprite => image.gameObject.SetActive(true)) // If successful, activate the GameObject
.Failed(exception => image.gameObject.SetActive(false)) // If an error occurs, deactivate the GameObject
.Forget(); // Forget the task to avoid compilation warning
// Load image and wait
await ImageLoader.LoadSprite(imageURL);
// Load image, set image and wait
await ImageLoader.LoadSprite(imageURL).ThenSet(image);
// Skip waiting for completion.
// To do that we can simply remove 'await' from the start.
// To avoid compilation warning need to add '.Forget()'.
ImageLoader.LoadSprite(imageURL).ThenSet(image).Forget();
Cancellation is helpful if target image consumer doesn't exist anymore. For example the Image
was destroyed because another level had been loaded. The is not much sense to continue to load the image. It would safe some network traffic, CPU resources, and RAM. IFuture<T>
provides wide range of options to cancel the ongoing loading process.
ImageLoader.LoadSprite(imageURL)
.ThenSet(image)
.CancelOnEnable(this) // cancel on OnEnable event of current MonoBehaviour
.CancelOnDisable(this) // cancel on OnDisable event of current MonoBehaviour
.CancelOnDestroy(this); // cancel on OnDestroy event of current MonoBehaviour
var future = ImageLoader.LoadSprite(imageURL).ThenSet(image);
future.Cancel();
var cancellationTokenSource = new CancellationTokenSource();
// loading with attached cancellation token
ImageLoader.LoadSprite(imageURL, cancellationToken: cancellationTokenSource.Token)
.ThenSet(image)
.Forget();
cancellationTokenSource.Cancel(); // canceling
var cancellationTokenSource = new CancellationTokenSource();
ImageLoader.LoadSprite(imageURL)
.ThenSet(image)
.Register(cancellationTokenSource.Token) // registering cancellation token
.Forget();
cancellationTokenSource.Cancel(); // canceling
using (var future = ImageLoader.LoadSprite(imageURL).ThenSet(image))
{
// future would be canceled and disposed outside of the brackets
}
ImageLoader.LoadSprite(imageURL) // load sprite
.ThenSet(image) // if success set sprite into image
.CancelOnDestroy(this) // cancel OnDestroy event of current gameObject
.Forget();
ImageLoader.LoadSprite(imageURL) // load sprite
.ThenSet(image) // if success set sprite into image
.Failed(exception => Debug.LogException(exception)) // if fail print exception
.CancelOnDestroy(this) // cancel OnDestroy event of current gameObject
.Forget();
ImageLoader.LoadSprite(imageURL) // load sprite
.ThenSet(image) // if success set sprite into image
.Then(sprite => image.gameObject.SetActive(true)) // if success activate gameObject
.Failed(exception => image.gameObject.SetActive(false)) // if fail deactivate gameObject
.Canceled(() => Debug.Log("ImageLoading canceled")) // if cancelled
.CancelOnDisable(this) // cancel OnDisable event of current gameObject
.Forget();
Timeout triggers IFuture<T>
cancellation.
Set global timeout in the settings:
ImageLoader.settings.timeout = TimeSpan.FromSeconds(30);
Set timeout for a specific loading request (IFuture<T>
):
ImageLoader.LoadSprite(imageURL) // load sprite
.ThenSet(image) // if success set sprite into image
.Timeout(TimeSpan.FromSeconds(10)) // set timeout duration 10 seconds
.Forget();
Cache system based on the two layers. The first layer is Memory cache, second is Disk cache. Each layer could be enabled or disabled. Could be used without caching at all. By default both layers are enabled. WebGL
doesn't support Disk cache, because it doesn't have access to disk.
ImageLoader.settings.useMemoryCache = true;
default value istrue
ImageLoader.settings.useDiskCache = true;
default value istrue
By default it uses Application.persistentDataPath + "/ImageLoader"
ImageLoader.settings.diskSaveLocation = Application.persistentDataPath + "/myCustomFolder";
It overrides global ImageLoader.settings
ImageLoader.LoadSprite(url)
.SetUseDiskCache(false)
.SetUseMemoryCache(true);
// Override Memory cache for specific image
ImageLoader.SaveToMemoryCache(url, sprite);
// Take from Memory cache for specific image if exists
ImageLoader.LoadSpriteFromMemoryCache(url);
// Check if any cache contains specific image
ImageLoader.CacheContains(url);
// Check if Memory cache contains specific image
ImageLoader.MemoryCacheContains(url);
// Check if Disk cache contains specific image
ImageLoader.DiskCacheContains(url);
// Clear memory Memory and Disk cache for all images
ImageLoader.ClearCacheAll();
// Clear only Memory and Disk cache for specific image
ImageLoader.ClearCache(url);
// Clear only Memory cache for all images
ImageLoader.ClearMemoryCacheAll();
// Clear only Memory cache for specific image
ImageLoader.ClearMemoryCache(url);
// Clear only Disk cache for all images
ImageLoader.ClearDiskCacheAll();
// Clear only Disk cache for specific image
ImageLoader.ClearDiskCache(url);
Memory cache could be cleared automatically if to use Reference<T>
and the heavy Texture2D
memory would be released as well. Read more at Texture Memory Management.
Texture2D objects consume a lot of memory. Ignoring it may impact performance or even trigger OutOfMemory
crash by operation system. To avoid it, let's dig deeper into tools the package provides. We worry less about Disk cache, because it doesn't impact game performance directly. Let's focus on the Memory cache.
It is simple, just executing this line of code would release memory of a single Texture2D in the case if no other Reference
pointing on it exists. Before doing that please make sure that no Unity component is using the texture.
ImageLoader.ClearMemoryCache(url);
Under the hood it calls UnityEngine.Object.DestroyImmediate(texture)
.
⚠️ ReleasingTexture2D
from memory while any Unity's component uses it may trigger native app crash or even Unity Editor crash
ImageLoader can manager memory releasing of loaded textures. To use it need to call ImageLoader.LoadSpriteRef
instead of ImageLoader.LoadSprite
. It returns Reference<Sprite>
object which contains Sprite
and Url
. When Reference<Sprite>
object is not needed anymore, call reference.Dispose()
method to release memory, or just don't save the reference on it. It is IDisposable
and it will be disposed by Garbage Collector. Each new instance of Reference<Sprite>
increments reference counter of the texture. When the last reference is disposed, the texture memory releases. Also, if any reference is alive, calling ImageLoader.ClearMemoryCache
or ImageLoader.ClearCache
would have zero effect for only referenced textures. It prints warning messages about it.
// Load sprite image and get reference to it
var reference = await ImageLoader.LoadSpriteRef(imageURL);
// Take from Memory cache reference for specific image if exists
var reference = ImageLoader.LoadSpriteRefFromMemoryCache(url);
// Dispose `reference` when you don't need the texture anymore
reference.Dispose();
// You may also nullify the reference to let Garbage Collector at some point to Dispose it for you
reference = null;
⚠️ ReleasingTexture2D
from memory while any Unity's component uses it may trigger native app crash or even Unity Editor crash. Please pay enough attention to manageReference<T>
instances in a proper way. Or do not use them.
Reference<T>.ThenSet
has a unique feature to attach the reference to the target consumer if consumer is UnityEngine.Component
. The reference would be disposed as only the consumer gets destroyed.
ImageLoader.LoadSpriteRef(imageURL) // load sprite using Reference
.ThenSet(image) // if success set sprite into image, also creates binding to `image`
.Forget();
It automatically dispose the reference as only this.gameObject
gets OnDestroy
callback.
ImageLoader.LoadSpriteRef(imageURL) // load sprite using Reference
.Then(reference => reference.DisposeOnDestroy(this))
.Then(reference =>
{
var sprite = reference.Value;
// use sprite
})
.Forget();
var count = ImageLoader.GetReferenceCount(imageURL); // get count of references
The IFuture<T>
interface represents an asynchronous operation that will eventually produce a result of type T
. It provides a range of methods and properties to handle the lifecycle of the asynchronous operation, including loading, success, failure, and cancellation events.
Id
: Unique identifier for the future.Url
: URL associated with the future.IsCancelled
: Indicates if the operation has been cancelled.IsLoaded
: Indicates if the operation has successfully loaded the result.IsCompleted
: Indicates if the operation has completed (either successfully or with an error).IsInProgress
: Indicates if the operation is currently in progress.Status
: Current status of the future.CancellationToken
: Token used to cancel the operation.Value
: The result of the operation.LogLevel
: The logging level for the operation.
Then(Action<T> onCompleted)
: Registers a callback to be executed when the operation successfully completes and produces a result.Failed(Action<Exception> action)
: Registers a callback to be executed if the operation fails with an exception.Completed(Action<bool> action)
: Registers a callback to be executed when the operation completes, regardless of success or failure. The boolean parameter indicates whether the operation was successful.Canceled(Action action)
: Registers a callback to be executed if the operation is canceled.SetUseDiskCache(bool value = true)
: Configures whether the operation should use disk caching.SetUseMemoryCache(bool value = true)
: Configures whether the operation should use memory caching.SetLogLevel(DebugLevel value)
: Sets the logging level for the operation.Cancel()
: Cancels the operation if it is still in progress.Forget()
: Ignores the result of the operation, useful for avoiding compilation warnings about unawaited tasks.AsUniTask()
: Converts theIFuture<T>
instance to aUniTask<T>
.AsTask()
: Converts theIFuture<T>
instance to aTask<T>
.AsReference(DebugLevel logLevel = DebugLevel.Trace)
: Converts theIFuture<T>
instance to aFuture<Reference<T>>
instance.GetAwaiter()
: Returns an awaiter for theIFuture<T>
instance, allowing it to be awaited using theawait
keyword.PassEvents(IFutureInternal<T> to, bool passCancelled = true)
: Passes events to another future.PassEvents<T2>(IFutureInternal<T2> to, Func<T, T2> convert, bool passCancelled = true)
: Passes events to another future with conversion.Register(CancellationToken cancellationToken)
: Registers a new cancellation token to cancel the future with it.Timeout(TimeSpan duration)
: Sets a timeout duration for the future. If the duration is reached, it fails the future with a related exception.
ImageLoader.LoadSprite(imageURL) // Start loading the sprite
.Then(sprite => Debug.Log("Loaded")) // On successful load
.Failed(exception => Debug.LogException(exception)) // On failure
.Completed(isLoaded => Debug.Log($"Completed, isLoaded={isLoaded}")) // On completion
.Canceled(() => Debug.Log("Canceled")) // On cancellation
.Forget(); // Avoid compilation warnings
The Reference<T>
class is used to manage the lifecycle of loaded resources, such as Texture2D
or Sprite
, in a memory-efficient manner. It helps to automatically release memory when the resource is no longer needed, preventing memory leaks and optimizing performance.
Value
: The actual resource (e.g.,Sprite
orTexture2D
) that is being referenced.Url
: The URL associated with the resource.IsDisposed
: Indicates whether the reference has been disposed.
Dispose()
: Disposes the reference, releasing the associated resource from memory. This should be called when the resource is no longer needed.DisposeOnDestroy(Component component)
: Automatically disposes the reference when the specifiedComponent
is destroyed. This is useful for ensuring that resources are released when the associated GameObject is destroyed.DisposeOnDisable(Component component)
: Automatically disposes the reference when the specifiedComponent
is disabled.DisposeOnEnable(Component component)
: Automatically disposes the reference when the specifiedComponent
is enabled.
// Load a sprite using Reference
var reference = await ImageLoader.LoadSpriteRef(imageURL);
// Use the sprite
var sprite = reference.Value;
// Dispose the reference when the sprite is no longer needed
reference.Dispose();
// Alternatively, automatically dispose the reference when the GameObject is destroyed
ImageLoader.LoadSpriteRef(imageURL)
.Then(reference => reference.DisposeOnDestroy(this))
.Then(reference =>
{
var sprite = reference.Value;
// use sprite
})
.Forget();
The Future<Reference<T>>
class combines the functionality of IFuture<T>
and Reference<T>
, providing a powerful tool for managing the lifecycle of asynchronous operations that produce resources such as Texture2D
or Sprite
. This combination allows you to handle the loading process and the resource management in a memory-efficient manner.
The Future<Reference<T>>
class is needed to ensure that resources are loaded asynchronously and managed efficiently. By using this class, you can:
- Load Resources Asynchronously: Start loading resources such as
Texture2D
orSprite
without blocking the main thread. - Manage Resource Lifecycle: Automatically release resources when they are no longer needed, preventing memory leaks and optimizing performance.
- Handle Loading Events: Register callbacks for various events such as success, failure, and cancellation during the loading process.
- Bind Resources to Components: Automatically dispose of resources when the associated
UnityEngine.Component
is destroyed, ensuring that resources are released when the GameObject is destroyed.