WPF的依赖属性是怎么节约内存的?
WPF升级了CLR的属性系统,加入了依赖属性和附加属性。依赖属性的使用有很多好处,其中有两点是我认为最为亮眼的:
1)节省内存的开销;
2)属性值可以通过Binding依赖于其它对象上,这就使得我的数据源一变动全部依赖于此数据源的依赖属性全部进行更新。
第二点开发过WPF或者SilverLight应用程序都能无比畅快地感受它带来的好处,而在节省内存这个亮点上我们也行未能深刻地感受它带来的心理上的爽快,本人试着简单地说明依赖属性到底是怎么样为我们节省内存的。
我们先来看看传统的CLR属性,先来定义个人的类Person,简单点,只包含了Name和Coutry属性,Country默认是China。
public class Person
{
public Person()
{
Country = "China";
}
public string Name { get; set; }
public string Country { get; set; }
}
如果我们现在需要造10000个中国人,太简单了,在一个循环里实例化10000个Person就行了。如果你根本不关心程序占用内存的消耗你当然不会心痛,否则你就会喊坑爹了,因为这10000个中国人的Country都是China,但内存必须为每个China开辟空间来存放,这实在暴殄天物啊!好吧,到这里你应该知道依赖属性靠节省内存这个亮点都可以闪亮登场了,虽然我们最爽的还是前面说的第一点的一变全变的功能。
谈依赖属性DependencyProperty就绕不过要谈谈依赖对象DependencyObject,这还得从Dependency提供的两个方法说起,GetValue和SetValue。
public class DependencyObject :DispatcherObject
{
public object GetValue(DependencyProperty dp)
{
}
public void SetValue(DependencyProperty dp,object value)
{
}
}
原来DependencyProperty本身不提供获取和设置依赖属性值的操作,而是由DependencyObject来负责。DependencyObject是使用GetValue和SetValue方法通过DependencyProperty实例实现对属性值的读取和保存。注意这里所说的读取和保存的属性值其实就是DependencyProperty对应的属性值,也就是SetValue方法第二个参数object类型的value,它和DependencyProperty实例对象本身是不同的东西。如果还不能很好理解,没关系,继续往下看就明白了,我们下面就升级下Person的Country属性,使之成为真正的依赖属性。
public class Person : DependencyObject
{
/// <summary>
/// 依赖属性
/// </summary>
public static readonly DependencyProperty CountryProperty = DependencyProperty.Register("Country", typeof(string), typeof(Person), new PropertyMetadata("China"), new ValidateValueCallback(CountryValidateValueCallback));
/// <summary>
/// 依赖属性的CLR属性包装器
/// </summary>
public string Country
{
get { return (string)GetValue(CountryProperty); }
set { SetValue(CountryProperty, value); }
}
/// <summary>
/// 属性值验证
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static bool CountryValidateValueCallback(object value)
{
string country = (string)value;
if (country.Equals("Japan")) return false;
return true;
}
/// <summary>
/// Name是一个CLR属性
/// </summary>
public string Name { get; set; }
}
上面创建的一个依赖属性CountryProperty是用到DependencyProperty.Register最完全的参数的重载方法,关于这个方法里面各个参数的详情不是本文讨论的重点,如果你需要详细了解,可以阅读Clingingboy写的关于依赖属性的文章。DependencyProperty.Register的一个参数”Country”用来指明哪个CLR属性作为这个依赖属性的包装器,另外还需要使用这个它的HashCode,这点后面会讲到。第二个参数“typeof(string)”指明此依赖属性存储的的什么类型的值。第三个参数“typeof(Person)”用来指明此依赖属性宿主是什么类型,也需要使用它的HashCode。第四个参数“new PropertyMetadata("China")”可以指定依赖属性读取值的默认值,这里默认是“China”,第五个参数“ new ValidateValueCallback(CountryValidateValueCallback)”,它指定验证值的方法,好吧,我们这个验证值的方法就把Japan先排除吧,凡是Country赋值为Japan就会抛出异常。
这里定义的CountryProperty是一个static,我们知道静态对象里面的值都是一变全变的,也就是我在一处修改了静态对象,所有引用这个静态对象的地方都会改变,这说明CountryProperty不适合来保存value这个属性值,否则假如我实例化10000个Person,这10000个Person都共用一个CountryProperty,到底用来保存哪个Person对象的Country呢?
上面实例化一个DependencyProperty没有使用new关键字,而是使用DependencyProperty.Register这个静态方法,这个静态方法大有乾坤,下面我们重点分析下这个静态方法。
我们先看看DependencyProperty类中DependencyProperty.Register的源码,这里为了阅读方便将干扰我们阅读的部分代码去掉了:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback)
{
RegisterParameterValidation(name, propertyType, ownerType);
DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
return property;
}
RegisterParameterValidation(name, propertyType, ownerType)方法验证第一个、第二个和第三个参数是否为null,任何一个为null都将抛出异常,说明这三个参数在调用Register方法是必传的。
DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback),这行告诉我们内部又使用了RegisterCommon方法来实例化DependencyProperty对象。我们在来看看RegisterCommon方法的关键源码,同样只保留了关键的源码:
private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
{
FromNameKey key = new FromNameKey(name, ownerType);
lock (Synchronized)
{
if (PropertyFromName.Contains(key))
{
throw new ArgumentException(SR.Get(SRID.PropertyAlreadyRegistered, name, ownerType.Name));
}
}
// Establish default metadata for all types, if none is provided
if (defaultMetadata == null)
{
defaultMetadata = AutoGeneratePropertyMetadata(propertyType, validateValueCallback, name, ownerType);
}
// Create property
DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
// Map owner type to this property
// Build key
lock (Synchronized)
{
PropertyFromName[key] = dp;
}
return dp;
}
if (defaultMetadata == null)
{
defaultMetadata = AutoGeneratePropertyMetadata(propertyType, validateValueCallback, name, ownerType);
}
从这段代码中我们可以知道DependencyProperty.Register的第四个参数传为null的时候,会创建一个默认的元数据。
PropertyFromName[key] = dp;这句代码告诉我们创建出来的DependencyProperty对象是保持在名为PropertyFromName的全局的Hashtable中的。
private static Hashtable PropertyFromName = new Hashtable();
那么这个key是怎么来的呢,回到源码的第一句: FromNameKey key = new FromNameKey(name, ownerType),可以知道PropertyFromName中的key对象类型是FromNameKey,我们知道判断一个Hashtable中元素的key是否相同判断的是key的HashCode是否相同,所以我们下一步自然是需要看看FromNameKey的GetHashCode返回的什么:
private class FromNameKey
{
public FromNameKey(string name, Type ownerType)
{
_name = name;
_ownerType = ownerType;
_hashCode = _name.GetHashCode() ^ _ownerType.GetHashCode();
}
public void UpdateNameKey(Type ownerType)
{
_ownerType = ownerType;
_hashCode = _name.GetHashCode() ^ _ownerType.GetHashCode();
}
public override int GetHashCode()
{
return _hashCode;
}
}
现在一目了然了,我们前面提到了DependencyProperty.Register的第一个参数“Country”和第三个参数typeof(Person)需要用到它们的HachCode,这里的FromNameKey的HashCode值就是第一个参数的HashCode和第三个参数typeof(Person)的HashCode进行^运算得到的。从而我们可以推知,一个类型的同一个Dependency在全局的PropertyFromName里面只会保存一个实例对象(因为Hashtable的键值对里面不允许存在相同的键)。
通过以上分析,我们可以总结下DependencyProperty.Register的作用:
1)将一个DependencyProperty对象存储在一个全局的Hashtable中(PropertyFromName),而这个Hashtable存储的对象的Key由依赖属性对象对应的CLR属性包装器名称和依赖属性对象寄存的类型DHashCode决定。
2)DependencyProperty对象保存了一个属性元数据的默认值;
3)返回一个DependencyProperty对象实例。
DependencyProperty对象只是保存了一个默认值,那么我们调用DependencyObject的SetValue(DependencyProperty dp,object value)方法保存属性值的时候,这里面的第二个参数value到底保存到哪里去了呢?这就需要来阅读下DependencyObject类的源码了,阅读源码我们发现DependencyObject类有一个私有数组变量:
private EffectiveValueEntry[] _effectiveValues;
当我们调用DependencyObject的GetValue(DependencyProperty dp)方法的时候,会根据DependencyProperty 对象的GlobalIndex属性判断EffectiveValueEntry[] 是否包含这个依赖属性对象的属性值,如果没有就返回依赖属性的默认值。理解这点很关键,这是依赖属性节省内存的关键之一,因为一个类里面的依赖属性对象的静态的,也就是所有的实例化对象都是公用这个依赖属性对象,回到我们开篇的话题,当你实例化10000个Person的时候,如果没有调用Person的SetValue方法,那么读取这10000个Person的Country属性时,都是从一个依赖属性对象的CountryProperty的默认值读取的,这个时候内存中只用一个地址存放“China”就可以了,大大节省了内存的开销。
当我们调用DependencyObject的SetValue(DependencyProperty dp,object value)方法时,就会将这个值保存在EffectiveValueEntry[]某个EffectiveValueEntry类型的元素上。
在WPF中大部分UI控件都有很长的继承体系,一个控件通过继承而来的属性就有一箩筐了。另外将依赖属性定义在控件的父类里面,那么这个父类所有的子类对象都会共享依赖属性对象,可见依赖属性的使用大大降低了控件对内存的消耗。
希望这篇文章对于你理解WPF中的依赖属性节省内存的机制有所帮助。笔者水平有限,如果说的不对,请高手斧正。
后记:成文后,我想起了string类型在.NET中采用享元模式,也就是说string a = "China"和string b = “China”两个变量a和b其实指向同一个地址的。在本文中的Person类中CountryProperty存储的数据类型是一个string,因为.NET对string的特殊优化,所以定义一个存储string类型DependencyProperty来说明节省内存不太合适,假如我创建10000个Person对象而言,那么采用依赖属性的优势就是节省了存放指向“China”地址的引用的地址——这么说来还是有节省内存的优势,只不过如果用其他类型如List<Int>类型来写这个文章更合适些了。
参考资料: