Страницы

Поиск по вопросам

среда, 22 января 2020 г.

Странный баг с мусорными строками в Unity

#c_sharp #unity3d #отладка


Появился ооочень странный наполовину плавающий баг с мусорными строками.

Всю архитектуру описывать очень долго, поэтому напишу вкратце, но если будет надо,
обновлю вопрос с детальным описанием.

В кратце так:

создал систему для локализации игры. Есть класс Localization со статическими строковыми
полями только для чтения, и есть файл xml со строками. Данный клас, с помощью стандартного
XmlTextReader и рефлексии, заполняет нужные поля соответствующими значениями.

using System;
using System.Reflection;
using UnityEngine;
using System.Xml;
using System.IO;

public sealed class Localization
{
    const string PATH_TO_LOCALIZATION_FILES = @"TextAssets\Localization\";
    public static SystemLanguage CurrentLanguage { get; private set; }

    #region strings;

    static public readonly string downloading;

    static public readonly string play_game;
    static public readonly string settings;
    static public readonly string squad;

    //тут еще куча static public readonly string полей 

    #endregion;


    /// 
    /// Уставнавливает новый язык локализации, если для него есть определение.
    /// 
    /// Новый язык локализации
    /// Если язык локализации был изменен, возвратится true, иначе - false
    public static bool SetLanguage(SystemLanguage language)
    {
        SystemLanguage newLanguage;
        ReloadStrings(language, out newLanguage);

        var res = true;
        if (newLanguage == CurrentLanguage)
            res = false;
        else
            CurrentLanguage = newLanguage;

        return res;
    }

    /// 
    /// Находит в соответствующем файле локализации нужные строки и устанавливает
их значения соответствующим полям.
    /// Так как не все языки поддерживаются, то загружен будет либо указанный
язык, либо язык по умолчанию (английский)
    /// 
    /// Язык локализации для загрузки
    /// Сюда будет занесено значение загруженого языка
локализации
    static void ReloadStrings(SystemLanguage language, out SystemLanguage newLanguage)
    {
        string languageFileName = string.Empty;

        switch (language)
        {
            case SystemLanguage.Russian:
                languageFileName = "RU";
                newLanguage = SystemLanguage.Russian;
                break;
            default:
                //english
                languageFileName = "EN";
                newLanguage = SystemLanguage.English;
                break;
        }

        TextAsset localisationFile = Resources.Load(PATH_TO_LOCALIZATION_FILES
+ languageFileName);
        using (TextReader txtReader = new StringReader(localisationFile.text))
        {
            using (XmlTextReader reader = XmlReader.Create(txtReader) as XmlTextReader)
            {
                while (reader.ReadToFollowing("string"))
                {
                    reader.MoveToAttribute("name");
                    var name = reader.Value;
                    reader.MoveToContent();
                    var value = reader.ReadElementContentAsString();

                    SetString(name, value);
                }
            }
        }
    }

    /// 
    /// Устанавливает значение строкового ресурса по его имени
    /// 
    /// Имя строкового ресурса
    /// Новое значение строкового ресурса
    static void SetString(string name, string value)
    {
        var field = typeof(Localization).GetField(name, BindingFlags.Static | BindingFlags.Public);

        if (field == null)
            throw new Exception(string.Format("Поля строкового ресурса с именем \"{0}\"
не существует.", name));

        field.SetValue(null, value);
    }

    /// 
    /// Получает значение строкового ресурса по его имени
    /// 
    /// Имя строкового ресурса
    /// Значение строкового ресурса
    public static string GetString(string name)
    {
        var field = typeof(Localization).GetField(name, BindingFlags.Static | BindingFlags.Public);

        if (field == null)
            throw new Exception(string.Format("Поля строкового ресурса с именем \"{0}\"
не существует.", name));

        return field.GetValue(null) as string;
    }
}


Когда будет нужно, обращаемся к нужному полю класса и берем нужное значение. Если
же строку нужно задать не с кода, а с редактора Unity, то передаем нужное имя поля,
а в коде вызываем метод Localozation.GetString(string name).



Теперь самое интересное

При первом запуске уровня все работает нормально. Но при перезапуске уровня случается
вот такая дичь:

Запускаем отладку и видим, что с классом локализации и значениями полей все нормально:



Пропускаем шаг отладки и видим, что, на самом деле, было передано "мусорное значение"
строки.



Причем, это случается не просто при перезапуске уровня, но именно при перезапуске
уровня, если на этот предмет уже кликали перед перезапуском уровня.

Дальше больше

Если вытаскивать значение полей с помощью рефлексии, то все нормально. А если обращаться
к полям напрямую (через имя класса и имя поля), то там оказываются мусорные значения.

using System.Text;
using UnityEngine;

public class TestGarbageStrings : MonoBehaviour
{
    public void DoTestFields()
    {
        StringBuilder builder = new StringBuilder();

        builder.Append("\r\n" + Localization.downloading);

        builder.Append("\r\n" + Localization.play_game);
        builder.Append("\r\n" + Localization.settings);
        builder.Append("\r\n" + Localization.squad);

        //тут еще куча обращений к другим полям

        DialogBox.Instance
            .AddCancelButton("Ясно")
            .SetText(builder.ToString(), true)
            .SetSize(800, 600)
            .Show();
    }

    public void DoTestReflection()
    {
        StringBuilder builder = new StringBuilder();

        var fields = typeof(Localization).GetFields(System.Reflection.BindingFlags.Static
| System.Reflection.BindingFlags.Public);
        foreach (var f in fields)
        {
            builder.Append("\r\n");
            builder.Append(f.GetValue(null));
        }

        DialogBox.Instance
            .AddCancelButton("Ясно")
            .SetText(builder.ToString(), true)
            .SetSize(800, 600)
            .Show();
    }
}


Если обращаться к полям на прямую(мусорные значения):



Если брать значения с помощью рефлексии(правильные значения):



Подскажите пожалуйста, в чем может быть проблема. Хотя бы малейшие предположения.
Возможно, кто то сталкивался с подобным (но такого и врагу не пожелаешь).
    


Ответы

Ответ 1



Модификатор readonly подразумевает, что поле не будет меняться после создания объекта (или после вызова статического конструктора). В связке со static это предположение разрешает JIT-у кэшировать значения в тех местах, где вы обращаетесь к полю. Или вообще кэшировать значения полей где-то глобально после первого обращения к ним. И даже убирать из кода if-ы, проверяющие значения полей - вобщем, JIT считает такие поля константами времени выполнения. При просмотре через отладчик вы видите реальное значение поля. При вытягивании через reflection - тоже. А реально выполняется код, в который JIT вписал константный старый адрес. Потому что вы ему это явно разрешили. Соответствующие оптимизации для static readonly были в основном CLR со времен 3.5, в Core - появились 2 года назад - Treat readonly fields as JIT time constants. В mono (не уверен, на чем сейчас Unity работает) они скорее всего тоже есть. Уберите readonly и все заработает. Скорее всего, т.к. минимального кода для воспроизведения проблемы в вопросе нет.

Комментариев нет:

Отправить комментарий