通過這篇文章,不僅可以了解到Attribute的工作原理,還可以了解到GetcustomeAttribute是的內部執行流程。最后,你會看到,使用緩存機制可以極大的優化反射Attribute的性能。
本文結構:
1.為什么在對象上標記Attribute性能很慢。
2.編譯器如何編譯帶有Attribute標記的類型
3.定義解析器,在運行時獲取並解析對象上的Attribute
4.GetCustomeAttributes方法的工作原理
5.優化Attribute
6.Attribute性能優化完整代碼
7.總結
參考資料:
關於Attribute的緩存思想請參見后面的鏈接,當然這篇文章里還介紹了依賴注入以及ORM中的一些優化,鏈接如下:http://www.codeproject.com/Articles/503527/Reflection-optimization-techniques
關於CustomeAttribute的介紹請參見ECMA-CLI文檔第二部分Partition_II_Metadata的第22章第10節,但我再后面還是給出了這個介紹。
1.為什么在對象上標記Attribute性能很慢。
首先請看如下代碼:
[TableAttribute("student")] class StudentModel { public string Id{get;set;} } class TableAttribute : Attribute { string name; public string Name { get { return name; } } public TableAttribute(string name) { this.name = name; } }
性能損耗1:每創建一個StudentModel對象,都要利用反射創建一個TableAttribute對象。
性能損耗2:上一步驟中創建的所有TableAttribute都是完全相同的,值都是”student”。換句話說,如果創建了n 個StudentModel,那么就會具有n個完全一樣的TableAttribute。
2.編譯器如何編譯帶有Attribute標記的類型
編譯器在編譯上述代碼時,發現StudentModel有一個TableAttribute特性,就會在最終的程序集中生成一個customeAttribute元數據表,這個表有三個字段:parent,type,value。其中parent指向StudentModel類,type指向TableAttribute的構造函數,value值是要傳遞給TableAttribute的參數,這個里的參數就是”student”。如下圖:
3.定義解析器,在運行時獲取並解析對象上的Attribute
如果對象上僅僅是標記了Attribute,那么是不會有什么性能損失的,因為我們並沒有使用它。
每當我們自定義一個Attribute時,都要同時創建一個用於解析這個Attribute的解析器。這個解析器需要做兩件事:第一獲取對象上標記的Attribute,其次根據Attribute中的屬性值來執行相應的動作。代碼如下:
class AttributeInterpreter { public static void Interprete() { TableAttribute[] attributes = typeof(StudentModel).GetCustomAttributes(typeof(TableAttribute), true) as TableAttribute[]; foreach(var eachAttribute in attributes) { string tableName = eachAttribute.TableName; //根據tableName訪問數據庫中的對應表,然后讀取數據並創建StudentModel對象。 } } }
這里,首先獲取StudentModel的Type,然后調用Type的GetCustomeAttributes方法,並傳遞了typeof(TableAttribute)參數。最后GetCustomeAttributes方法返回StudentModel上標記的TableAttribue對象。
附注:ECMA-CLI中的CustomeAttribute詳細說明:

2 2 . 1 0 C us to mAt tr i b ut e : 0 x0 C The CustomAttribute table has the following columns: ? Parent (an index into any metadata table, except the CustomAttribute table itself; more precisely, a HasCustomAttribute (§24.2.6) coded index) ? Type (an index into the MethodDef or MemberRef table; more precisely, a CustomAttributeType (§24.2.6) coded index) ? Value (an index into the Blob heap) Partition II 119 The CustomAttribute table stores data that can be used to instantiate a Custom Attribute (more precisely, an object of the specified Custom Attribute class) at runtime. The column called Type is slightly misleading—it actually indexes a constructor method—the owner of that constructor method is the Type of the Custom Attribute. A row in the CustomAttribute table for a parent is created by the .custom attribute, which gives the value of the Type column and optionally that of the Value column (§21).
4.GetCustomeAttributes方法的工作原理
在運行時,當調用GetCustomeAttributes時,CLR會遍歷customeAttribute元數據表,查找parent=StudentModle和type=TableAttribute的CustomeAttribute元數據表。找到之后,根據type字段找到TableAttribute,然后創建他的實例,調用構造函數,並為構造函數傳遞參數“student”。
5.優化Attribute
優化的第一步就是利用緩存機制來存儲已經創建好的TableAttribute,當需要對象時直接從緩存里取,這樣只有在第一次創建StudentModel時,才會反射創建TableAttribute對象。如下
private static Dictionary<Type, TableInfo> cache = new Dictionary<Type, TableInfo>(); public StudentModel() { Type studentModelType = this.GetType(); Type tableAttType=typeof(TableAttribute); if(!cache.TryGetValue(tableAttType, out tableINfo)) { TableAttribute[] tableAttributes = studentModelType.GetCustomAttributes(typeof(TableAttribute), true) as TableAttribute[]; if(tableAttributes == null) return; TableAttribute tableAttribute = tableAttributes[0]; cache.Add(tableAttType, new TableInfo(tableAttribute.TableName)); }
使用緩存需要考慮鎖的問題。當幾個線程同時修改緩存時,必須保證在同一時刻只有一個線程修改,這里使用雙鎖機制來進行線程同步。如下:
public StudentModel() { Type studentModelType = this.GetType(); Type tableAttType=typeof(TableAttribute); TableInfo tableInfo; if(!cache.TryGetValue(tableAttType, out tableInfo)) { lock(this) { if(!cache.TryGetValue(tableAttType, out tableInfo)) { TableAttribute[] tableAttributes = studentModelType.GetCustomAttributes(typeof(TableAttribute), true) as TableAttribute[]; if(tableAttributes == null) return; TableAttribute tableAttribute = tableAttributes[0]; cache.Add(tableAttType, new TableInfo(tableAttribute.TableName)); } } } }
優化的結果:只在第一次創建對象時反射Attribute,減少了內存的使用。
6.Attribute性能優化完整代碼

[Table("student")] class StudentModel { public string Id{get;set;} //定義緩存,存儲已經創建好的TableAttribute對象。 private static Dictionary<Type, TableInfo> cache = new Dictionary<Type, TableInfo>(); public StudentModel() { Type studentModelType = this.GetType(); Type tableAttType=typeof(TableAttribute); TableInfo tableInfo; if(!cache.TryGetValue(tableAttType, out tableInfo)) { lock(this) { if(!cache.TryGetValue(tableAttType, out tableInfo)) { TableAttribute[] tableAttributes = studentModelType.GetCustomAttributes(typeof(TableAttribute), true) as TableAttribute[]; if(tableAttributes == null) return; TableAttribute tableAttribute = tableAttributes[0]; cache.Add(tableAttType, new TableInfo(tableAttribute.TableName)); } } } } } //AlloMultiple為true,表明這個特性不能被重復標記,原因很簡單,一個實體對象不能映射到兩個表。 [AttributeUsage(AttributeTargets.Class,AllowMultiple=false)] class TableAttribute : Attribute { string tableName; public string TableName { get { return tableName; } } public TableAttribute(string name) { this.tableName = name; } }
7.總結
在使用Attribute特性時,編譯器會在最終的程序集中生成一個CustomeAttribute元數據表,這個表實際上是一個映射表,其中的type字段和parent字段將特性和對象相關聯。
僅僅定義個Attribute是沒有任何實際作用的,我們需要定義一個解析器用於獲取對象上的Attribute並根據其屬性值采取相應的動作。
在運行的時候,想要獲取對象上的某特性時,需要調用Type的GetCustomeAttributes方法。這個方法在內部使用了反射來獲取特性並實例化特性。
由於反射很浪費性能,並且對於類型的每一個對象,與其關聯的Attribute都是相同的,所以可以利用字典只保存其中的一個。由於反射很浪費性能,所以可以考慮將創建好的Attribute緩存起來。
如果這個緩存需要被多個線程修改,需要使用鎖來同步,這里為了提供性能,使用了雙鎖機制和Monitor。