龙空技术网

Unity UGUI开发,0GC更新视图

侑虎科技 39

前言:

当前你们对“unity 集合视图”大概比较珍视,姐妹们都想要知道一些“unity 集合视图”的相关知识。那么小编也在网上搜集了一些对于“unity 集合视图””的相关知识,希望大家能喜欢,各位老铁们一起来了解一下吧!

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!

前段时间在优化Unity游戏项目,发现在战斗场景中,UI需要更新大量内容,比如血量、伤害、各种技能效果等等,由于战斗比较激烈,一直在高频更新UI视图,通过UWA深度分析发现字符拼接产生的垃圾收集也不少。于是就想优化一下,分析了一下产生GC的原因,大概有下面几个方面。

UI文字显示更新时字符串的拼接产生的GC。数字类型转为字符串类型分配的GC(比如血量变化都必须由数字转为文字再显示)。值类型在转文字时的装箱拆箱(比如使用String.format拼接字符串,都存在这个问题)。

我们游戏UI文字显示都是使用TMP控件做的,看了下TMP的源码,TMP_Text控件是支持通过char[]或者StringBuilder更新的,这样就完全可以绕过String,直接通过StringBuilder或者char[]去更新UI,而不必转为字符串了。

下面是TMP_Text.cs中的源码,为了测试0GC效果,我将文件中SetText()函数和StringBuilderToIntArray()函数中UNITY_EDITOR这个宏定义的代码块注释了。

public void SetText(StringBuilder text){      m_inputSource = TextInputSources.SetCharArray;      //#if UNITY_EDITOR      //// Set the text in the Text Input Box in the Unity Editor only.      //m_text = text.ToString();      //#endif      StringBuilderToIntArray(text, ref m_TextParsingBuffer);      m_isInputParsingRequired = true;      m_havePropertiesChanged = true;      m_isCalculateSizeRequired = true;      SetVerticesDirty();      SetLayoutDirty();}

有了方案,下面就只需要解决前面提到的3个问题即可。

第一个问题,所有字符串拼接都使用StringBuilder即可,StringBuilder可以完全多次复用,Unity的UI刷新都在主线程,也不存在线程安全问题,全局使用一个StringBuilder。

第二个问题,数字类型转字符串,数字由0-9和小数点这几个固定字符组成,数字类型转字符串改为数字类型转char[]即可,char[]也全局复用,将数字转为char[],然后写入到StringBuilder中。

第三个问题,数字在String.format或者StringBuilder.AppendFormat时会转为Object对象,这存在装箱拆箱问题。这就需要实现一个支持泛型参数的格式化追加函数。比如:StringBuilder.AppendFormat<TP1,TP2,TP3... TPn>()

所以重点在于解决第二和第三个问题,我阅读了C#官方有关StringBuilder.AppendFormat()的代码,需要在格式化同时还避免装箱拆箱,避免GC的类型主要是基本数字类型、DateTime类型、TimeSpan类型,其他的你要乐意可以支持一下Unity的Vector2-4,别的也就没有了。中间的具体过程我不多说,最终任务就3个,数字转字符串是通过NumberFormatter.NumberToString()函数实现,需要在这个基础上改造为无GC的方式。DateTime和TimeSpan的格式化由DateTimeFormat.cs和TimeSpanFormat.cs类实现,同样需要改造。

上源码:

NumberFormatter

mono/mcs/class/corlib/System/NumberFormatter.cs at main · mono/mono · GitHub

改造前原函数如下,会将数字类型value直接转为string类型,必须在堆上为string对象分配内存:

public static string NumberToString (string format, uint value, IFormatProvider fp){    NumberFormatter inst = GetInstance (fp);    inst.Init (format, value, Int32DefPrecision);    string res = inst.IntegerToString (format, fp);    inst.Release();    return res;}

Mono库源码:

mono/mcs/class/corlib/System/NumberFormatter.cs at main · mono/mono · GitHub

改造后函数如下,在数字类型value转换过程中,避免生成string,而是直接将char或者ReadOnlySpan写入到StringBuilder中,这里需要注意,所有的相关的函数都改一遍。

public static void NumberToString(ReadOnlySpan<char> format, uint value, IFormatProvider fp, StringBuilder result){    NumberFormatter inst = GetInstance(fp);    inst.Init(format, value, Int32DefPrecision);    inst.IntegerToString(format, fp, result);    inst.Release();}

改造后源码:

loxodon-framework/Loxodon.Framework.TextFormatting/Assets/LoxodonFramework/TextFormatting/Runtime/Framework/TextFormatting/NumberFormatter.cs at master · vovgou/loxodon-framework · GitHub

TimeSpanFormat改造前

与NumberFormatter原理相同,在Format过程中尽量避免产生新的字符串,避免字符串拼接。

internal static String Format(TimeSpan value, String format, IFormatProvider formatProvider)

C#官方源码:

referencesource/mscorlib/system/globalization/timespanformat.cs at master · microsoft/referencesource · GitHub

改造后的函数:

internal static void Format(TimeSpan value, ReadOnlySpan<char> format, IFormatProvider formatProvider, StringBuilder result)

改造后源码:

loxodon-framework/Loxodon.Framework.TextFormatting/Assets/LoxodonFramework/TextFormatting/Runtime/Framework/TextFormatting/TimeSpanFormat.cs at master · vovgou/loxodon-framework · GitHub

DateTimeFormat

DateTimeFormat修改相对麻烦,因为DateTimeFormat依赖了很多其他类,而C#官方底层很多代码是Native的或者都是Internal的类、方法、属性等,我无法直接使用,所以我只能将其他类中的函数或者属性剥离出来,拷贝到DateTimeFormat类中,另外还有一些特殊的日期类型,比如希伯来、日本等等类型需要处理。

修改前函数:

internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi) {    return Format(dateTime, format, dtfi, NullOffset);}

C#官方源码:

referencesource/mscorlib/system/globalization/datetimeformat.cs at master · microsoft/referencesource · GitHub

修改后函数:

internal static void Format(DateTime dateTime, ReadOnlySpan<char> format, StringBuilder result){    Format(dateTime, format, DateTimeFormatInfo.GetInstance(null), NullOffset, result);}

修改后的代码:

loxodon-framework/Loxodon.Framework.TextFormatting/Assets/LoxodonFramework/TextFormatting/Runtime/Framework/TextFormatting/DateTimeFormat.cs at master · vovgou/loxodon-framework · GitHub

就此,数字类型、DateTime、TimeSpan这几个类型的格式化改造完毕。

扩展StringBuilder,增加支持泛型参数的AppendFormat<TP1..TPn>函数。

StringBuilder本身是有AppendFormat函数的,但是参数是object[]类型,会导致值类型对象的装箱拆箱,new object[]有堆内存分配。所以我们需要扩展一个支持泛型参数的格式化追加函数AppendFormat<TP1..TPn>(),以避免垃圾回收开销。

public static class StringBuilderExtensions    {        private const int FORMAT_SPAN_SIZE = 128;        private static readonly object EMPTY = new object();        [ThreadStatic]        private static StringBuilder result = new StringBuilder(128);        public static StringBuilder AppendFormat<T>(this StringBuilder builder, string format, T[] values)        {            return AppendFormat(builder, format, values, GetFormatter<T>());        }        public static StringBuilder AppendFormat<T>(this StringBuilder builder, string format, T value)        {            return AppendFormat(builder, format, value, GetFormatter<T>());        }        public static StringBuilder AppendFormat<T0, T1>(this StringBuilder builder, string format, T0 t0, T1 t1)        {            return AppendFormat(builder, format, 2, t0, t1, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2)        {            return AppendFormat(builder, format, 3, t0, t1, t2, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3)        {            return AppendFormat(builder, format, 4, t0, t1, t2, t3, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3, T4>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4)        {            return AppendFormat(builder, format, 5, t0, t1, t2, t3, t4, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3, T4, T5>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5)        {            return AppendFormat(builder, format, 6, t0, t1, t2, t3, t4, t5, EMPTY, EMPTY, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3, T4, T5, T6>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)        {            return AppendFormat(builder, format, 7, t0, t1, t2, t3, t4, t5, t6, EMPTY, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3, T4, T5, T6, T7>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7)        {            return AppendFormat(builder, format, 8, t0, t1, t2, t3, t4, t5, t6, t7, EMPTY, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3, T4, T5, T6, T7, T8>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8)        {            return AppendFormat(builder, format, 9, t0, t1, t2, t3, t4, t5, t6, t7, t8, EMPTY);        }        public static StringBuilder AppendFormat<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(this StringBuilder builder, string format, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9)        {            return AppendFormat(builder, format, 10, t0, t1, t2, t3, t4, t5, t6, t7, t8, t9);        }        private static StringBuilder AppendFormat<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>(StringBuilder builder, string format, int paramCount, T0 t0, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9)        {            if (format == null)                throw new ArgumentNullException("format");            int pos = 0;            int len = format.Length;            char ch = '\x0';            Span<char> formatSpan = stackalloc char[FORMAT_SPAN_SIZE];            int formatIndex = 0;            while (true)            {                while (pos < len)                {                    ch = format[pos];                    pos++;                    if (ch == '}')                    {                        if (pos < len && format[pos] == '}') // Treat as escape character for }}                            pos++;                        else                            FormatError();                    }                    if (ch == '{')                    {                        if (pos < len && format[pos] == '{') // Treat as escape character for {{                            pos++;                        else                        {                            pos--;                            break;                        }                    }                    builder.Append(ch);                }                if (pos == len)                    break;                pos++;                if (pos == len || (ch = format[pos]) < '0' || ch > '9')                    FormatError();                int index = 0;                do                {                    index = index * 10 + ch - '0';                    pos++;                    if (pos == len)                        FormatError();                    ch = format[pos];                } while (ch >= '0' && ch <= '9' && index < 1000000);                if (index >= paramCount)                    throw new FormatException("The index of the format is out of range.");                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                bool leftJustify = false;                int width = 0;                if (ch == ',')                {                    pos++;                    while (pos < len && format[pos] == ' ')                        pos++;                    if (pos == len)                        FormatError();                    ch = format[pos];                    if (ch == '-')                    {                        leftJustify = true;                        pos++;                        if (pos == len)                            FormatError();                        ch = format[pos];                    }                    if (ch < '0' || ch > '9')                        FormatError();                    do                    {                        width = width * 10 + ch - '0';                        pos++;                        if (pos == len)                            FormatError();                        ch = format[pos];                    } while (ch >= '0' && ch <= '9' && width < 1000000);                }                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                formatIndex = 0;                if (ch == ':')                {                    pos++;                    while (true)                    {                        if (pos == len)                            FormatError();                        ch = format[pos];                        if (!IsValidFormatChar(ch))                            break;                        formatSpan[formatIndex++] = ch;                        pos++;                                          }                }                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                if (ch != '}')                    FormatError();                pos++;                ReadOnlySpan<char> fmt = formatSpan.Slice(0, formatIndex);                switch (index)                {                    case 0:                        Format(fmt, t0, result.Clear());                        break;                    case 1:                        Format(fmt, t1, result.Clear());                        break;                    case 2:                        Format(fmt, t2, result.Clear());                        break;                    case 3:                        Format(fmt, t3, result.Clear());                        break;                    case 4:                        Format(fmt, t4, result.Clear());                        break;                    case 5:                        Format(fmt, t5, result.Clear());                        break;                    case 6:                        Format(fmt, t6, result.Clear());                        break;                    case 7:                        Format(fmt, t7, result.Clear());                        break;                    case 8:                        Format(fmt, t8, result.Clear());                        break;                    case 9:                        Format(fmt, t9, result.Clear());                        break;                    default:                        throw new NotSupportedException();                }                int pad = width - result.Length;                if (!leftJustify && pad > 0)                    builder.Append(' ', pad);                AppendStringBuilder(builder, result);                result.Clear();                if (leftJustify && pad > 0)                    builder.Append(' ', pad);            }            return builder;        }        private static StringBuilder AppendFormat<T>(StringBuilder builder, string format, T value, IFormatter formatter)        {            if (format == null)                throw new ArgumentNullException("format");            int pos = 0;            int len = format.Length;            char ch = '\x0';            Span<char> formatSpan = stackalloc char[FORMAT_SPAN_SIZE];            int formatIndex = 0;            while (true)            {                while (pos < len)                {                    ch = format[pos];                    pos++;                    if (ch == '}')                    {                        if (pos < len && format[pos] == '}') // Treat as escape character for }}                            pos++;                        else                            FormatError();                    }                    if (ch == '{')                    {                        if (pos < len && format[pos] == '{') // Treat as escape character for {{                            pos++;                        else                        {                            pos--;                            break;                        }                    }                    builder.Append(ch);                }                if (pos == len)                    break;                pos++;                if (pos == len || (ch = format[pos]) < '0' || ch > '9')                    FormatError();                int index = 0;                do                {                    index = index * 10 + ch - '0';                    pos++;                    if (pos == len)                        FormatError();                    ch = format[pos];                } while (ch >= '0' && ch <= '9' && index < 1000000);                if (index >= 1)                    throw new FormatException("The index of the format is out of range.");                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                bool leftJustify = false;                int width = 0;                if (ch == ',')                {                    pos++;                    while (pos < len && format[pos] == ' ')                        pos++;                    if (pos == len)                        FormatError();                    ch = format[pos];                    if (ch == '-')                    {                        leftJustify = true;                        pos++;                        if (pos == len)                            FormatError();                        ch = format[pos];                    }                    if (ch < '0' || ch > '9')                        FormatError();                    do                    {                        width = width * 10 + ch - '0';                        pos++;                        if (pos == len)                            FormatError();                        ch = format[pos];                    } while (ch >= '0' && ch <= '9' && width < 1000000);                }                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                //object arg = args[index];                formatIndex = 0;                if (ch == ':')                {                    pos++;                    while (true)                    {                        if (pos == len)                            FormatError();                        ch = format[pos];                        if (!IsValidFormatChar(ch))                            break;                        formatSpan[formatIndex++] = ch;                        pos++;                                           }                }                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                if (ch != '}')                    FormatError();                pos++;                ReadOnlySpan<char> fmt = formatSpan.Slice(0, formatIndex);                Format(fmt, value, formatter, result.Clear());                int pad = width - result.Length;                if (!leftJustify && pad > 0)                    builder.Append(' ', pad);                AppendStringBuilder(builder, result);                result.Clear();                if (leftJustify && pad > 0)                    builder.Append(' ', pad);            }            return builder;        }        private static StringBuilder AppendFormat<T>(StringBuilder builder, string format, T[] values, IFormatter formatter)        {            if (format == null)                throw new ArgumentNullException("format");            int pos = 0;            int len = format.Length;            char ch = '\x0';            Span<char> formatSpan = stackalloc char[FORMAT_SPAN_SIZE];            int formatIndex = 0;            while (true)            {                while (pos < len)                {                    ch = format[pos];                    pos++;                    if (ch == '}')                    {                        if (pos < len && format[pos] == '}') // Treat as escape character for }}                            pos++;                        else                            FormatError();                    }                    if (ch == '{')                    {                        if (pos < len && format[pos] == '{') // Treat as escape character for {{                            pos++;                        else                        {                            pos--;                            break;                        }                    }                    builder.Append(ch);                }                if (pos == len)                    break;                pos++;                if (pos == len || (ch = format[pos]) < '0' || ch > '9')                    FormatError();                int index = 0;                do                {                    index = index * 10 + ch - '0';                    pos++;                    if (pos == len)                        FormatError();                    ch = format[pos];                } while (ch >= '0' && ch <= '9' && index < 1000000);                if (index >= values.Length)                    throw new FormatException("The index of the format is out of range.");                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                bool leftJustify = false;                int width = 0;                if (ch == ',')                {                    pos++;                    while (pos < len && format[pos] == ' ')                        pos++;                    if (pos == len)                        FormatError();                    ch = format[pos];                    if (ch == '-')                    {                        leftJustify = true;                        pos++;                        if (pos == len)                            FormatError();                        ch = format[pos];                    }                    if (ch < '0' || ch > '9')                        FormatError();                    do                    {                        width = width * 10 + ch - '0';                        pos++;                        if (pos == len)                            FormatError();                        ch = format[pos];                    } while (ch >= '0' && ch <= '9' && width < 1000000);                }                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                T value = values[index];                formatIndex = 0;                if (ch == ':')                {                    pos++;                    while (true)                    {                        if (pos == len)                            FormatError();                        ch = format[pos];                        if (!IsValidFormatChar(ch))                            break;                        formatSpan[formatIndex++] = ch;                        pos++;                                          }                }                while (pos < len && (ch = format[pos]) == ' ')                    pos++;                if (ch != '}')                    FormatError();                pos++;                ReadOnlySpan<char> fmt = formatSpan.Slice(0, formatIndex);                Format(fmt, value, formatter, result.Clear());                int pad = width - result.Length;                if (!leftJustify && pad > 0)                    builder.Append(' ', pad);                AppendStringBuilder(builder, result);                result.Clear();                if (leftJustify && pad > 0)                    builder.Append(' ', pad);            }            return builder;        }        private static bool IsValidFormatChar(char ch)        {            if (ch == 123 || ch == 125)//{ }                 return false;            if ((ch >= 32 && ch <= 122) || ch == 124)                return true;            return false;        }        private static void Format<T>(ReadOnlySpan<char> format, T value, IFormatter formatter, StringBuilder builder)        {            if (formatter is IFormatter<T> genericFormatter)                genericFormatter.Format(format, value, builder);            else                formatter.Format(format, value, builder);        }        private static void Format<T>(ReadOnlySpan<char> format, T value, StringBuilder builder)        {            IFormatter formatter = GetFormatter<T>();            if (formatter is IFormatter<T> genericFormatter)                genericFormatter.Format(format, value, builder);            else                formatter.Format(format, value, builder);        }        private static StringBuilder AppendStringBuilder(StringBuilder builder, StringBuilder value)        {            int len = value.Length;            for (int i = 0; i < len; i++)            {                builder.Append(value[i]);            }            return builder;        }        private static void FormatError()        {            throw new FormatException("Invalid Format");        }    }

到目前为止已经支持了一个支持字符串格式化,且完全0GC的StringBuilder。关于使用示例如下:

using System;using System.Text;using UnityEngine;using Loxodon.Framework.TextFormatting;//make sure to first import the required namespacepublic class Example : MonoBehaviour{    StringBuilder builder = new StringBuilder();    void Update()    {        builder.Clear();        builder.AppendFormat<DateTime,int>("Now:{0:yyyy-MM-dd HH:mm:ss} Frame:{0:D6}", DateTime.Now,Time.frameCount);        builder.AppendFormat<float>("{0:f2}", Time.realtimeSinceStartup);           }    }

自定义TextMeshPro控件

既然花了大量时间做了一个0GC的StringBuilder,那么也就不在乎再多花点时间去扩展TextMeshPro控件了。我们项目中,前端同事经常会使用表达式绑定去更新UI视图,比如战斗中的各种事件提示:伤害100、吸血50、游戏时间倒计时等等,都是字符串和数字的拼接,使用表达式绑定虽然方便,但是使用是有成本的,在IL2CPP编译下不支持JIT,表达式解析需要依赖反射,性能并不好。所以我干脆写了一个支持格式化功能的文本控件FormattableTextMeshProUGUI和一个文本模版控件TemplateTextMeshProUGUI,这样即确保了0GC、高性能、又兼顾了使用的方便性。

以下是使用表达式绑定的例子,即存在反射,又有字符串拼接:

bindingSet.Bind(health).For(v => v.text).ToExpression(vm => string.Format("血量{0}",vm.Hero.Health));bindingSet.Bind(damage).For(v => v.text).ToExpression(vm => string.Format("伤害{0}",vm.Ability.Damage));
FormattableTextMeshProUGUI
  public class FormattableTextMeshProUGUI : TextMeshProUGUI    {        internal static StringBuilder BUFFER = new StringBuilder();        [SerializeField]        protected string m_Format = "{0}";        [SerializeField]        protected int m_ParameterCount = 1;        private Parameters m_Parameters;        public string Format        {            get { return this.m_Format; }            set { this.m_Format = value; }        }        public int ParameterCount        {            get { return this.m_ParameterCount; }            set { this.m_ParameterCount = value; }        }        protected override void OnEnable()        {            base.OnEnable();            Initialize();        }        public override void SetAllDirty()        {            base.SetAllDirty();            Initialize();        }        protected virtual void Initialize()        {            SetText(BUFFER.Clear().Append(m_Format));        }        public ArrayParameters<T> AsArray<T>()        {            if (m_Parameters == null)                m_Parameters = new ArrayParameters<T>(this, this.ParameterCount);            if (m_Parameters is ArrayParameters<T> parameters)                return parameters;            throw new NotSupportedException($"The current parameter type has been set to \"{m_Parameters.GetType()}\" and cannot be converted to other types.");        }        public GenericParameters<P1> AsParameters<P1>()        {            if (m_Parameters == null)                m_Parameters = new GenericParameters<P1>() { Text = this };            if (m_Parameters is GenericParameters<P1> parameters)                return parameters;            throw new NotSupportedException($"The current parameter type has been set to \"{m_Parameters.GetType()}\" and cannot be converted to other types.");        }        public GenericParameters<P1, P2> AsParameters<P1, P2>()        {            if (m_Parameters == null)                m_Parameters = new GenericParameters<P1, P2>() { Text = this };            if (m_Parameters is GenericParameters<P1, P2> parameters)                return parameters;            throw new NotSupportedException($"The current parameter type has been set to \"{m_Parameters.GetType()}\" and cannot be converted to other types.");        }        public GenericParameters<P1, P2, P3> AsParameters<P1, P2, P3>()        {            if (m_Parameters == null)                m_Parameters = new GenericParameters<P1, P2, P3>() { Text = this };            if (m_Parameters is GenericParameters<P1, P2, P3> parameters)                return parameters;            throw new NotSupportedException($"The current parameter type has been set to \"{m_Parameters.GetType()}\" and cannot be converted to other types.");        }        public GenericParameters<P1, P2, P3, P4> AsParameters<P1, P2, P3, P4>()        {            if (m_Parameters == null)                m_Parameters = new GenericParameters<P1, P2, P3, P4>() { Text = this };            if (m_Parameters is GenericParameters<P1, P2, P3, P4> parameters)                return parameters;            throw new NotSupportedException($"The current parameter type has been set to \"{m_Parameters.GetType()}\" and cannot be converted to other types.");        }    }

FormattableTextMeshProUGUI控件的AsParameters<>()函数可以转为一个泛型参数集,支持1-4个不同参数,也可以通过AsArray()创建一个泛型数组,通过泛型参数集或者泛型数组和ViewModel进行绑定。下面是代码示例。

public class FormattableTextMeshProUGUIExample : MonoBehaviour{    public FormattableTextMeshProUGUI paramBinding1;    private ExampleViewModel viewModel;    private void Start()    {        ApplicationContext context = Context.GetApplicationContext();        IServiceContainer container = context.GetContainer();        BindingServiceBundle bundle = new BindingServiceBundle(context.GetContainer());        bundle.Start();        BindingSet<FormattableTextMeshProUGUIExample, ExampleViewModel> bindingSet = this.CreateBindingSet<FormattableTextMeshProUGUIExample, ExampleViewModel>();        //Create a parameter collection using AsParameters<P1, P2, ...>(). It supports 1-4 parameters         //without the need for value type boxing/unboxing or string concatenation, ensuring a GC-free         //experience. For testing the 0GC effect on a mobile device, if testing in Unity Editor, please         //modify the source code of the TextMeshPro plugin by removing any code related to         //StringBuilder.ToString() in the functions TMP_Text.SetText and TMP_Text.StringBuilderToIntArray.        //format:The format follows the same formatting parameters as string.Format(), for example: DateTime - Example1, {0:yyyy-MM-dd HH:mm:ss}, FrameCount: {1}        bindingSet.Bind(paramBinding1.AsParameters<DateTime, int>()).For(v => v.Parameter1).To(vm => vm.Time);        bindingSet.Bind(paramBinding1.AsParameters<DateTime, int>()).For(v => v.Parameter2).To(vm => vm.FrameCount);                    bindingSet.Build();        this.viewModel = new ExampleViewModel();        this.viewModel.Time = DateTime.Now;        this.viewModel.FrameCount = 1;        this.SetDataContext(this.viewModel);    }}

除了上面的使用方法外,还支持另外一种使用方式,在脚本FormattableTextMeshProUGUIExample中定义一个类型为GenericParameters<DateTime,int>的参数集变量,在UnityEditor中将FormattableTextMeshProUGUI拖放到下图脚本的属性paramBinding1上(我扩展了编辑器,支持将FormattableTextMeshProUGUI对象拖放到泛型参数集上)。然后将参数集与视图模型绑定。与第一种方式本质是一样的,都是通过创建一个泛型参数集和视图模型绑定。

public class FormattableTextMeshProUGUIExample : MonoBehaviour{    public GenericParameters<DateTime,int> paramBinding1;//参数绑定示例1,支持1-4个不同参数    private ExampleViewModel viewModel;    private void Start()    {        ApplicationContext context = Context.GetApplicationContext();        IServiceContainer container = context.GetContainer();        BindingServiceBundle bundle = new BindingServiceBundle(context.GetContainer());        bundle.Start();        BindingSet<FormattableTextMeshProUGUIExample , ExampleViewModel> bindingSet = this.CreateBindingSet<FormattableTextExample, ExampleViewModel>();        //使用AsParameters<P1,P2,...>() 函数创建一个参数集合,然后绑定,支持1-4个参数,没有值对象的装箱拆箱,没有字符串拼接,降低GC,使用TMP文本可以完全无GC        //format:格式与string.Format()的格式化参数相同如:DateTime:Example1,{0:yyyy-MM-dd HH:mm:ss}, FrameCount:{1}        bindingSet.Bind(paramBinding1).For(v => v.Parameter1).To(vm => vm.Time);        bindingSet.Bind(paramBinding1).For(v => v.Parameter2).To(vm => vm.FrameCount);        bindingSet.Build();        this.viewModel = new ExampleViewModel();        this.viewModel.Time = DateTime.Now;        this.viewModel.FrameCount = 1;        this.SetDataContext(this.viewModel);    }}

从以上这两个示例可以看出,值类型的参数都采用了泛型类型,不会有装箱拆箱操作,同时因为文本控件内部使用的是StringBuilder.AppendFormat<>()函数,而且一直在复用StringBuilder,这都避免了内存分配,所以整个UI的更新可以实现完全0GC的效果。

TemplateTextMeshProUGUI

  public class TemplateTextMeshProUGUI : TextMeshProUGUI    {        [SerializeField]        [TextArea(5, 10)]        private string m_Template;        private object data;        private TextTemplateBinding templateBinding;        protected TextTemplateBinding Binding        {            get            {                if (templateBinding == null)                    templateBinding = new TextTemplateBinding(SetText);                return templateBinding;            }        }        public string Template        {            get { return this.m_Template; }            set            {                if (string.Equals(this.m_Template, value))                    return;                this.m_Template = value;                Binding.Template = this.m_Template;            }        }        public object Data        {            get { return this.data; }            set            {                if (Equals(this.data, value))                    return;                this.data = value;                Binding.Data = this.data;            }        }        protected override void OnEnable()        {            base.OnEnable();            Initialize();        }        public override void SetAllDirty()        {            base.SetAllDirty();            Initialize();        }        protected virtual void Initialize()        {            SetText(BUFFER.Clear().Append(m_Template));        }        protected override void OnDestroy()        {            if (templateBinding != null)            {                templateBinding.Dispose();                templateBinding = null;            }            base.OnDestroy();        }    }

这个控件比格式化文本控件更强大,更好用。支持将一个ViewModel对象或者子对象绑定到TemplateTextMeshProUGUI.Data属性,模版控件内置了路径解析和数据绑定功能,能自动通过文本模板{}中间的VM属性的路径(如:{Hero.AttackDamage})创建绑定代理,自动监听VM属性的改变来更新控件的文本内容,使用时只需要将Data属性和ViewModel绑定即可。

文本模版格式:Frame:{FrameCount:D6},Health:{Hero.Health:D4} AttackDamage:{Hero.AttackDamage} Armor:{Hero.Armor}

其中FrameCount、Hero是绑定到Data的对象的属性。Health、AttackDamage和Armor是Hero对象的属性。FrameCount后面的D6是帧数这个数字类型的格式化参数。

public class FormattableTextMeshProUGUIExample : MonoBehaviour{    public FormattableTextMeshProUGUI paramBinding1;//参数绑定示例1,支持1-4个不同参数    public GenericParameters<DateTime, int> paramBinding2;//参数绑定的另外一种方式,支持1-4个不同参数    public FormattableTextMeshProUGUI arrayBinding;//也可以使用 ArrayParameters<float>    public TemplateTextMeshProUGUI template;//模版绑定    private ExampleViewModel viewModel;    private void Start()    {        ApplicationContext context = Context.GetApplicationContext();        IServiceContainer container = context.GetContainer();        BindingServiceBundle bundle = new BindingServiceBundle(context.GetContainer());        bundle.Start();        BindingSet<FormattableTextMeshProUGUIExample, ExampleViewModel> bindingSet = this.CreateBindingSet<FormattableTextMeshProUGUIExample, ExampleViewModel>();        //使用AsParameters<P1,P2,...>() 函数创建一个参数集合,然后绑定,支持1-4个参数,没有值对象的装箱拆箱,没有字符串拼接,无GC(请在手机上测试,Editor下需要修改TMP_Text.SetText和TMP_Text.StringBuilderToIntArray的源码,关闭调试代码)        //format:格式与string.Format()的格式化参数相同如:DateTime:Example1,{0:yyyy-MM-dd HH:mm:ss}, FrameCount:{1}        bindingSet.Bind(paramBinding1.AsParameters<DateTime, int>()).For(v => v.Parameter1).To(vm => vm.Time);        bindingSet.Bind(paramBinding1.AsParameters<DateTime, int>()).For(v => v.Parameter2).To(vm => vm.FrameCount);        //本质上与上面的例子是相同的,只是另外一种用法        //format:Example2,{0:yyyy-MM-dd HH:mm:ss}, FrameCount:{1}        bindingSet.Bind(paramBinding2).For(v => v.Parameter1).To(vm => vm.Time);        bindingSet.Bind(paramBinding2).For(v => v.Parameter2).To(vm => vm.FrameCount);        //使用AsArray<T>() 获得一个数组然后进行绑定,支持多个类型相同的参数,没有值对象的装箱拆箱,没有字符串拼接,无GC(请在手机上测试,Editor下需要修改TMP_Text.SetText和TMP_Text.StringBuilderToIntArray的源码,关闭调试代码)        //format:MoveSpeed:{0:f4}  AttackSpeed:{1:f2}        bindingSet.Bind(arrayBinding.AsArray<float>()).For(v => v[0]).To(vm => vm.Hero.MoveSpeed);        bindingSet.Bind(arrayBinding.AsArray<float>()).For(v => v[1]).To(vm => vm.Hero.AttackSpeed);        //使用文本模版(TemplateTextMeshProUGUI)绑定,直接将一个对象绑定到模板的Data属性上即可。        //文本模版格式与string.Format类似,仅需要将{0},{1}中的数字,替换为对象属性名即可        //template text:当前时间:{Time:yyyy-MM-dd HH:mm:ss}         bindingSet.Bind(template).For(v => v.Template).To(vm => vm.Template);//模版可以绑定,也可以在编辑器上配置        bindingSet.Bind(template).For(v => v.Data).To(vm => vm);        bindingSet.Build();        this.viewModel = new ExampleViewModel();        this.viewModel.Template = "Template,Frame:{FrameCount:D6},Health:{Hero.Health:D4} AttackDamage:{Hero.AttackDamage} Armor:{Hero.Armor}";        this.viewModel.Time = DateTime.Now;        this.viewModel.TimeSpan = TimeSpan.FromSeconds(0);        this.viewModel.Hero = new Hero();        this.SetDataContext(this.viewModel);    }    void Update()    {        viewModel.Time = DateTime.Now;        viewModel.FrameCount = Time.frameCount;        viewModel.Hero.Health = (Time.frameCount % 1000) / 10;    }}public class ExampleViewModel : ObservableObject{    private DateTime time;    private TimeSpan timeSpan;    private string template;    private int frameCount;    private Hero hero;    public DateTime Time    {        get { return this.time; }        set { this.Set(ref time, value); }    }    public TimeSpan TimeSpan    {        get { return this.timeSpan; }        set { this.Set(ref timeSpan, value); }    }    public int FrameCount    {        get { return this.frameCount; }        set { this.Set(ref frameCount, value); }    }    public string Template    {        get { return this.template; }        set { this.Set(ref template, value); }    }    public Hero Hero    {        get { return this.hero; }        set { this.Set(ref hero, value); }    }}public class Hero : ObservableObject{    private float attackSpeed = 95.5f;    private float moveSpeed = 2.4f;    private int health = 100;    private int attackDamage = 20;    private int armor = 30;    public float AttackSpeed    {        get { return this.attackSpeed; }        set { this.Set(ref attackSpeed, value); }    }    public float MoveSpeed    {        get { return this.moveSpeed; }        set { this.Set(ref moveSpeed, value); }    }    public int Health    {        get { return this.health; }        set { this.Set(ref health, value); }    }    public int AttackDamage    {        get { return this.attackDamage; }        set { this.Set(ref attackDamage, value); }    }    public int Armor    {        get { return this.armor; }        set { this.Set(ref armor, value); }    }}

以上所有代码都已经在我的MVVM框架中开源,可以从我的GitHub仓库中签出试用。

Loxodon.Framework.TextFormatting插件包括所有针对StringBuilder.AppendFormat<>()支持的代码:

loxodon-framework/Loxodon.Framework.TextFormatting at master · vovgou/loxodon-framework · GitHub

Loxodon.Framework.TextMeshPro插件是针对TextMeshPro控件的自定义和扩展:

loxodon-framework/Loxodon.Framework.TextMeshPro at master · vovgou/loxodon-framework · GitHub

这是侑虎科技第1519篇文章,感谢作者Loxodon Studio供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:安全验证 - 知乎

再次感谢Loxodon Studio的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

标签: #unity 集合视图 #unity五个视图