【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类实现,同样需要改造。
上源码:
改造前原函数如下,会将数字类型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库源码:
https://github.com/mono/mono/blob/main/mcs/class/corlib/System/NumberFormatter.cs
改造后函数如下,在数字类型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(); }
- TimeSpanFormat改造前
与NumberFormatter原理相同,在Format过程中尽量避免产生新的字符串,避免字符串拼接。
internal static String Format(TimeSpan value, String format, IFormatProvider formatProvider)
改造后的函数:
internal static void Format(TimeSpan value, ReadOnlySpan<char> format, IFormatProvider formatProvider, StringBuilder result)
- DateTimeFormat
DateTimeFormat修改相对麻烦,因为DateTimeFormat依赖了很多其他类,而C#官方底层很多代码是Native的或者都是Internal的类、方法、属性等,我无法直接使用,所以我只能将其他类中的函数或者属性剥离出来,拷贝到DateTimeFormat类中,另外还有一些特殊的日期类型,比如希伯来、日本等等类型需要处理。
修改前函数:
internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi) { return Format(dateTime, format, dtfi, NullOffset); }
修改后函数:
internal static void Format(DateTime dateTime, ReadOnlySpan<char> format, StringBuilder result) { Format(dateTime, format, DateTimeFormatInfo.GetInstance(null), NullOffset, result); }
就此,数字类型、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 namespace public 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<>()支持的代码:
https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework.TextFormatting
Loxodon.Framework.TextMeshPro插件是针对TextMeshPro控件的自定义和扩展:
https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework.TextMeshPro
这是侑虎科技第1519篇文章,感谢作者Loxodon Studio供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)
作者主页:https://www.zhihu.com/people/cocowolf
再次感谢Loxodon Studio的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)
标签:ch,0GC,format,StringBuilder,builder,pos,视图,Unity,public From: https://www.cnblogs.com/uwatech/p/17929994.html