讀 MAUI 源代碼 理解可綁定對象和可綁定屬性的存儲機制-當前速遞
2023-05-06 13:12:24 | 來源:清一色財經 |
2023-05-06 13:12:24 | 來源:清一色財經 |
在 WPF 里面,依賴屬性的提出的一部分原因是為了省內存。在 MAUI 里面,我猜測省內存是可綁定對象提出的一個原因。由于一個界面控件,例如按鈕等,有著非常龐大數(shù)量的屬性,假設每個控件里面的所有屬性都是需要獨立的對象不能共用,那么在復雜界面上,將會因為大量的控件的大量屬性占用大量的內存。可綁定對象里面可以實現(xiàn)在屬性沒有被賦值時,將可以使用默認值,而對于大部分控件來說,很多不常用的屬性都是使用默認值
和 UWP 與 WPF 不同的是在 MAUI 里面,使用可綁定對象 BindableObject 替換了依賴對象的概念,我閱讀了 MAUI 的源代碼發(fā)現(xiàn)其實只是命名變更了,里面的機制和設計思想都是差不多的。在 MAUI 里面提供 BindableObject 用來支持可綁定屬性機制和附加屬性機制,本文將告訴大家在 MAUI 里面是如何在可綁定對象里面提供可綁定屬性和附加屬性的存儲的機制。
在 WPF 里面,依賴屬性的提出的一部分原因是為了省內存。在 MAUI 里面,我猜測省內存是可綁定對象提出的一個原因。由于一個界面控件,例如按鈕等,有著非常龐大數(shù)量的屬性,假設每個控件里面的所有屬性都是需要獨立的對象不能共用,那么在復雜界面上,將會因為大量的控件的大量屬性占用大量的內存??山壎▽ο罄锩婵梢詫崿F(xiàn)在屬性沒有被賦值時,將可以使用默認值,而對于大部分控件來說,很多不常用的屬性都是使用默認值即可??山壎▽ο笮枰鉀Q的是讓可綁定屬性可以代替普通的 CLR 屬性,對可綁定屬性進行賦值時,可以值和可綁定對象關聯(lián),從而可以讀取出來。既然名字叫可綁定對象,那自然也要實現(xiàn)綁定的支持,綁定的支持的核心就是通知,需要支持在屬性值變更的時候進行通知。接下來將通過閱讀源代碼了解在 MAUI 里是如何實現(xiàn)。
(相關資料圖)
打開 MAUI 的 BindableObject 的源代碼,可以看到在 BindableObject 里有_properties字段,定義如下:
public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler { readonly Dictionary_properties = new Dictionary (4); }
沒錯,這就是在 MAUI 里面的可綁定對象的存儲核心實現(xiàn)。在 MAUI 的可綁定對象里面通過_properties?字典存放可綁定屬性的值內容,字典的 Key 是 BindableProperty 可綁定屬性,字典的 Value 是 BindablePropertyContext 可綁定屬性上下文,初始化字典默認占用 4 個空間,默認初始化空間是為了優(yōu)化而已,沒有什么特別用途。通過此字典定義可以了解到存儲的核心實現(xiàn)就是將可綁定屬性和對應的值存入到對象的字典里,例如給某個可綁定對象的某個叫 Xxx 的可綁定屬性進行賦值,那將會對_properties字典更新 Xxx 屬性的值內容。
在 MAUI 的實現(xiàn)是,在可綁定對象里面,使用 SetValueCore 方法進行屬性更新賦值,我刪掉了不關鍵的邏輯的代碼如下:
internal void SetValueCore(BindableProperty property, object value, SetValueFlags attributes, SetValuePrivateFlags privateAttributes) { // 獲取或創(chuàng)建可綁定屬性上下文信息 BindablePropertyContext context = GetOrCreateContext(property); SetValueActual(property, context, value, currentlyApplying, attributes, silent); } BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property); internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property, out var result) ? result : null; BindablePropertyContext CreateAndAddContext(BindableProperty property) { var context = new BindablePropertyContext { ... }; _properties.Add(property, context); return context; } void SetValueActual(BindableProperty property, BindablePropertyContext context, object value, bool currentlyApplying, SetValueFlags attributes, bool silent = false) { // 觸發(fā)對象變更前事件 context.Value = value; // 觸發(fā)對象已變更事件 }
可以看到賦值的第一步就是調用 GetOrCreateContext 方法,嘗試去拿到上下文信息,如果拿不到就創(chuàng)建。這里的用到的 BindablePropertyContext 上下文信息是存儲可綁定屬性的關鍵,在 BindablePropertyContext 里面存放了很多字段,定義如下:
public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler { internal class BindablePropertyContext { public BindableContextAttributes Attributes; public BindingBase Binding; public QueueDelayedSetters; public BindableProperty Property; public object Value; public bool StyleValueSet; public object StyleValue; } }
可以看到 BindablePropertyContext 是一個內部類型,也不對外開放。在 BindablePropertyContext 里面重要的就是Value?字段,表示存儲的實際值內容。其次為了更好的支持綁定,也添加了Binding字段。
在獲取到 BindablePropertyContext 上下文之后,即可進行賦值,賦值是調用 SetValueActual 方法進行賦值,賦值前后分別觸發(fā)事件用來通知。觸發(fā)通知事件最重要的功能是讓綁定可以有刷新的時機。如此即可完成賦值過程。
通知事件是分別觸發(fā)可綁定的對象的通知事件和對應的可綁定屬性的通知事件,如下面代碼:
void SetValueActual(BindableProperty property, BindablePropertyContext context, object value, bool currentlyApplying, SetValueFlags attributes, bool silent = false) { // 觸發(fā)對象變更前事件 property.PropertyChanging?.Invoke(this, original, value); OnPropertyChanging(property.PropertyName); context.Value = value; // 觸發(fā)對象已變更事件 OnPropertyChanged(property.PropertyName); property.PropertyChanged?.Invoke(this, original, value); }
通過以上代碼可以看到,可綁定對象給可綁定屬性賦值的時候,就是先獲取或創(chuàng)建可綁定屬性上下文,將賦值的參數(shù)值給到 可綁定屬性上下文 的 Value 字段。如此完成賦值過程。
由于賦值的參數(shù)值被放入到 可綁定屬性上下文 的 Value 字段,而 可綁定屬性上下文 又放入到_properties?字典里,相當于間接將 賦值的參數(shù)值 放入到_properties字典里。自然在獲取值過程里,也需要從字典里面讀取。在 MAUI 里面讀取可綁定屬性是通過 GetValue 方法實現(xiàn),代碼如下:
public object GetValue(BindableProperty property) { if (property == null) throw new ArgumentNullException(nameof(property)); var context = property.DefaultValueCreator != null ? GetOrCreateContext(property) : GetContext(property); return context == null ? property.DefaultValue : context.Value; }
以上代碼的判斷 BindableProperty 的 DefaultValueCreator 屬性邏輯是 MAUI 特有的邏輯,和 WPF 與 UWP 不相同,咱下文再聊?;氐将@取屬性的方法上,是通過先獲取對象的可綁定上下文信息,如果能獲取到可綁定上下文,證明此可綁定對象的這個可綁定屬性曾經被賦值過,需要用賦值更新的內容。如果拿到的可綁定屬性上下文是空,那就使用可綁定屬性定義的默認值即可。
在 MAUI 里面,通過 BindableProperty 的 DefaultValueCreator 屬性簡化了可綁定屬性的定義,和讓可綁定屬性更加強大。使用 MAUI 的可綁定屬性和可綁定對象對比 WPF 的依賴屬性和依賴對象的實現(xiàn),可以看到 MAUI 的實現(xiàn)實在簡潔很多。在 MAUI 里的 BindableProperty 的 DefaultValueCreator 屬性是一個委托,定義如下:
public sealed class BindableProperty { public delegate object CreateDefaultValueDelegate(BindableObject bindable); internal CreateDefaultValueDelegate DefaultValueCreator { get; } }
可以看到 BindableProperty 的 DefaultValueCreator 屬性的委托是支持給傳入的可綁定對象進行處理,對可綁定對象返回特定的默認值。這里值得說明的是,通過委托是可以特例給可綁定對象不同的默認值的,但不代表著一定是不同的可綁定對象都一定需要不同的默認值對象。這里只是一個委托,讓委托返回相同的對象是完全可以的。這個委托更多的是使用在判斷可綁定對象類型,根據(jù)可綁定類型對象或者狀態(tài),返回不同的默認值?;蛘呤欠祷匾粋€需要運行時動態(tài)計算值,而不是一個可以寫固定在代碼里面的參數(shù)。
例如對于 FontSize 的可綁定屬性的定義里,就采用讓不同的控件返回不同的默認字體大小,定義如下:
public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(double), typeof(IFontElement), 0d, propertyChanged: OnFontSizeChanged, defaultValueCreator: FontSizeDefaultValueCreator); static object FontSizeDefaultValueCreator(BindableObject bindable) => ((IFontElement)bindable).FontSizeDefaultValueCreator();
也就是說對于不同的可綁定對象,獲取到的默認的字體大小是根據(jù)對應的可綁定對象的 FontSizeDefaultValueCreator 方法實現(xiàn)決定,不同的可綁定對象可以有不同的實現(xiàn),從而實現(xiàn)了讓默認值關聯(lián)上具體的可綁定對象類型。這個創(chuàng)新的設計,可以省掉在 WPF 里面的大量默認依賴屬性值重寫的邏輯代碼,省掉了這部分代碼,也可以大量減少的機制,從而減少更多的代碼。
例如 Span 和 Editor 控件對字體大小默認值有不同的實現(xiàn)。
public class Span : GestureElement, IFontElement { double IFontElement.FontSizeDefaultValueCreator() => double.NaN; } public partial class Button : View, IFontElement { double IFontElement.FontSizeDefaultValueCreator() => this.GetDefaultFontSize(); }
同樣,對于某些可綁定屬性來說,需要給每個可綁定對象的對象不同的默認值對象,例如 Grid 里面的 RowDefinitions 屬性。大家都知道,在 Grid 里面的 RowDefinitions 是一個集合,如果集合也是一個共享的默認值,那自然會存在默認值污染。如果默認值是一個空值,那么將會讓 Grid 邏輯里面存在大量的判斷空邏輯,或者需要其他額外的初始化邏輯。在 MAUI 里面,通過 DefaultValueCreator 委托,實現(xiàn)了每個 Grid 對象使用獨立的默認值對象,代碼如下:
public class Grid : Layout, IGridLayout { public static readonly BindableProperty RowDefinitionsProperty = BindableProperty.Create("RowDefinitions", typeof(RowDefinitionCollection), typeof(Grid), null, validateValue: (bindable, value) => value != null, propertyChanged: UpdateSizeChangedHandlers, defaultValueCreator: bindable => { // 每個 Grid 對象使用獨立的,新創(chuàng)建的默認值對象 var rowDef = new RowDefinitionCollection(); rowDef.ItemSizeChanged += ((Grid)bindable).DefinitionsChanged; return rowDef; }); }
在 MAUI 里面除了可綁定屬性之外,還有一個特殊的屬性類型,附加屬性。附加屬性可以定義在任意的類型里面,通過附加屬性,給某個現(xiàn)有的類型附加上屬性。功能上和 WPF 或 UWP 的附加屬性功能是相同的??山壎▽傩院透郊訉傩远际窍嗤?BindableProperty 類型,只是在創(chuàng)建的時候,調用的靜態(tài)創(chuàng)建方法不同而已。對于可綁定屬性來說,調用的是BindableProperty.Create?方法創(chuàng)建。對于附加屬性來說,調用BindableProperty.CreateAttached創(chuàng)建。在 MAUI 里面,通過閱讀代碼,我認為分開兩個方法更多的是為了兼容 WPF 或 UWP 的寫法,沒有非常本質的差別,參數(shù)也差不多,如下面代碼:
internal static BindableProperty Create(string propertyName, Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode, ValidateValueDelegate validateValue, BindingPropertyChangedDelegate propertyChanged, BindingPropertyChangingDelegate propertyChanging, CoerceValueDelegate coerceValue, BindablePropertyBindingChanging bindingChanging, CreateDefaultValueDelegate defaultValueCreator = null) { return new BindableProperty(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValue, propertyChanged, propertyChanging, coerceValue, bindingChanging, defaultValueCreator: defaultValueCreator); } internal static BindableProperty CreateAttached(string propertyName, Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode, ValidateValueDelegate validateValue, BindingPropertyChangedDelegate propertyChanged, BindingPropertyChangingDelegate propertyChanging, CoerceValueDelegate coerceValue, BindablePropertyBindingChanging bindingChanging, bool isReadOnly, CreateDefaultValueDelegate defaultValueCreator = null) { return new BindableProperty(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValue, propertyChanged, propertyChanging, coerceValue, bindingChanging, isReadOnly, defaultValueCreator); }
如此可以看到可綁定屬性和附加屬性從參數(shù)上是似乎相同的。由于附加屬性也是一個可綁定屬性類型,同理可以了解到附加屬性的存儲也和可綁定對象的可綁定屬性的存儲是相同的。如此也能解答一個問題,在 MAUI 的附加屬性,附加到對象上,附加屬性的參數(shù)值是如何跟隨對象的生命周期的問題。由于附加屬性也是一個可綁定屬性,同樣將參數(shù)值存在可綁定對象的_properties?字典里面,在對象會 GC 回收時,自然_properties字段也被回收,那放在字典里面的參數(shù)值也自然被減去引用,當參數(shù)值的沒有被引用時,也就自然被回收。
在 MAUI 里面,可綁定對象基類型的意義就是提供了可綁定屬性的機制,存儲可綁定屬性的方式就是通過_properties字典存放。通過字典存放的內容是被賦值更改的屬性,沒有賦值更改的屬性是沒有被放入到字典里面,獲取在字典里面沒有存放的屬性時,將會通過對應的可綁定屬性獲取到默認值。默認值的獲取有兩個方式,一個是可綁定屬性的固定的默認值屬性,另一個是通過可綁定屬性的默認值創(chuàng)建委托創(chuàng)建默認值。在 MAUI 里的可綁定屬性的默認值創(chuàng)建委托是一個創(chuàng)新,可以寫出讓不同的可綁定對象使用不同的默認值的功能,也可以寫出根據(jù)不同的可綁定對象類型返回不同的默認值,通過委托的方式靈活實現(xiàn)復雜的功能。