List of Texture2D in ScriptableObject is leaking Memory

How do you properly store a Texture2D in a ScriptableObject? From what I see it’s not behaving correctly and leaking memory.

Quick example:

ScriptableObject looks like this:

[Serializable]
public class MyData: ScriptableObject
{
    public List<Texture2D> images;
   ...
    public void Reset()
    {
        images = new List<Texture2D>();
    }
}

In order to persist the SO this is required:

UnityEditor.AssetDatabase.AddObjectToAsset(myData.images, myData);
UnityEditor.AssetDatabase.SaveAssets();

However on every Reset the texture gets reset, but the image remains in the ScriptableObject from what I can see in the object itself. I’m aware I can and have to remove the asset from the object in the Reset() method, but that doesn’t work all the time. Every now and then an image remains.

Example: If I change code and hit save and go back to the unity editor, then sometimes the SO is reset, but the Texture2D remains and is in fact selectable in the ScriptableObject in the project view.

What is the correct way to deal with Texture2D in a ScriptableObject? And how can I check for leaks in the ScriptableObject and clean them up? I mean, in the project view the images are obviously there, but not referencable anymore.

I’m confused because I see:

  • public List<Texture2D> images;
  • image = new List<Texture2D>();
  • myData.image

Are we talking about image or images ? It matters.

In any case, there’s actually an important order-of-operations thing here.

You MUST have called the asset database and added the texture, THEN assign it to the fields in the ScriptableObject.

I know it seems weird, but the act of adding an asset to the asset database fundamentally changes how it gets assigned to all fields after that, enabling it to be properly serialized.

Not sure if that’s related to the leaking, but in any case, you are certainly assigning it before adding it for the simple reason you are adding the field from the already assigned field, so that will always have issues.

Steps:

  • create the asset and get a reference
  • do NOT add it to any public field
  • add it to the asset database
  • assign that same exact reference to public field now

I thought I’d create a minimal verifyable example:

The scriptableobject:

using System.Collections.Generic;
using UnityEngine;

namespace Testing
{
    [CreateAssetMenu(fileName = "Data", menuName = "Test/Create Object", order = 1)]
    public class MyData : ScriptableObject
    {
        public List<Texture2D> textures;

        public void Reset()
        {
            textures = new List<Texture2D>();
        }
    }
}

The editor for it:

using UnityEngine;
using UnityEditor;

namespace Testing
{
    [CustomEditor(typeof(MyData))]
    public class MyDataTestEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            DrawDefaultInspector();

            MyData myData = target as MyData;

            if (GUILayout.Button("Add Texture"))
            {
                Texture2D texture = CreateTexture();

                UnityEditor.AssetDatabase.AddObjectToAsset(texture, myData);
                UnityEditor.AssetDatabase.SaveAssets();

                myData.textures.Add(texture);
            }

            if (GUILayout.Button("Reset (leaks)"))
            {
                myData.Reset();
            }

            if (GUILayout.Button("Delete/Reset"))
            {
                foreach( Texture2D texture in myData.textures)
                {
                    UnityEditor.AssetDatabase.RemoveObjectFromAsset(texture);
                    UnityEditor.AssetDatabase.SaveAssets();
                }

                myData.Reset();

            }

            serializedObject.ApplyModifiedProperties();
        }

        private Texture2D CreateTexture()
        {
            Color color = Random.ColorHSV();

            Texture2D texture = new Texture2D(1, 1);
            texture.SetPixel(0, 0, color);
            texture.Apply();

            return texture;
        }
    }
}

Simply removing the reference doesn’t work, the binary data of the texture remain in the scriptableobject.

By the way, is there some way to iterate through it? I mean, I can see they are there in the project.

Here’s a video about the effect:

https://www.youtube.com/watch?v=WUjhrBERWMk

I mean, I should put that texture deletion code into the Reset method. However I’m not sure I got all cases, eg when you enter play mode, have recompilation after you edited a C# file and what not. I mean, the way this works it’s bound to “leak”, question is how to do this properly.

Thanks for the help!

Good question… perhaps with one of those on before serialization / on after serialization type callbacks??

If those Texture2Ds still exist, you actually CAN get them en masse with FindObjectsOfType(), or so I believe. I think that sorta grabs ALL of them, but it would be up to you to see which ones you cared about.

One other note: I always seemed to have difficulties with attaching assets together in Unity, eg, the way you’re making a texture and adding it to your SO. It always seemed to be better to write the texture to disk (either as a Unity asset, which makes it HUGE in disk size), or as a PNG, importing it, then attaching the reference that way. Just something to consider…

Yeah, the PNG solution seems to be the safest choice. At least in that case you see if something’s left. In the current state you only see that the scriptableobject keeps on growing in size and the whole system is getting slower and slower.