【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这个宏定义的代码块注释了。
ini
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对象分配内存:
ini
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库源码:
github.com/mono/mono/b...
改造后函数如下,在数字类型value转换过程中,避免生成string,而是直接将char或者ReadOnlySpan写入到StringBuilder中,这里需要注意,所有的相关的函数都改一遍。
scss
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();
}
改造后源码:
github.com/vovgou/loxo...
- TimeSpanFormat改造前
与NumberFormatter原理相同,在Format过程中尽量避免产生新的字符串,避免字符串拼接。
arduino
internal static String Format(TimeSpan value, String format, IFormatProvider formatProvider)
C#官方源码:
github.com/microsoft/r...
改造后的函数:
csharp
internal static void Format(TimeSpan value, ReadOnlySpan<char> format, IFormatProvider formatProvider, StringBuilder result)
改造后源码:
github.com/vovgou/loxo...
- DateTimeFormat
DateTimeFormat修改相对麻烦,因为DateTimeFormat依赖了很多其他类,而C#官方底层很多代码是Native的或者都是Internal的类、方法、属性等,我无法直接使用,所以我只能将其他类中的函数或者属性剥离出来,拷贝到DateTimeFormat类中,另外还有一些特殊的日期类型,比如希伯来、日本等等类型需要处理。
修改前函数:
arduino
internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi) {
return Format(dateTime, format, dtfi, NullOffset);
}
C#官方源码:
github.com/microsoft/r...
修改后函数:
csharp
internal static void Format(DateTime dateTime, ReadOnlySpan<char> format, StringBuilder result)
{
Format(dateTime, format, DateTimeFormatInfo.GetInstance(null), NullOffset, result);
}
修改后的代码:
github.com/vovgou/loxo...
就此,数字类型、DateTime、TimeSpan这几个类型的格式化改造完毕。
扩展StringBuilder,增加支持泛型参数的AppendFormat<TP1..TPn>函数。
StringBuilder本身是有AppendFormat函数的,但是参数是object[]类型,会导致值类型对象的装箱拆箱,new object[]有堆内存分配。所以我们需要扩展一个支持泛型参数的格式化追加函数AppendFormat<TP1..TPn>(),以避免垃圾回收开销。
ini
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。关于使用示例如下:
csharp
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、高性能、又兼顾了使用的方便性。
以下是使用表达式绑定的例子,即存在反射,又有字符串拼接:
ini
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
csharp
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进行绑定。下面是代码示例。
java
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对象拖放到泛型参数集上)。然后将参数集与视图模型绑定。与第一种方式本质是一样的,都是通过创建一个泛型参数集和视图模型绑定。
ini
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
csharp
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是帧数这个数字类型的格式化参数。
kotlin
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<>()支持的代码:
github.com/vovgou/loxo...
Loxodon.Framework.TextMeshPro插件是针对TextMeshPro控件的自定义和扩展:
github.com/vovgou/loxo...
这是侑虎科技第1519篇文章,感谢作者Loxodon Studio供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)
作者主页:www.zhihu.com/people/coco...
再次感谢Loxodon Studio的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)