C#/.Net的多播委託到底是啥?徹底剖析下

前言

委託在.Net裏面被託管代碼封裝了之後,看起來似乎有些複雜。但是實際上委託即是函數指針,而多播委託,即是函數指針鏈。本篇來只涉及底層的邏輯,慎入。


概括

1.示例代碼

public delegate void ABC(); //委託寫在類的外面
public class Test
{
  public ABC AAA;
  public void A() {  }
  public void B() {  }
}
static void Main(string[] args)
{
   Test test = new Test();
   test.AAA += new ABC(test.A);
   test.AAA += new ABC(test.B);
   test.AAA(); //test.AAA.Invoke();
}

以上的test.AAA+=的等號後面每放一個函數,就相當於多了一個函數指針。號稱:多播委託。

2.多播原理僞代碼
以上委託可以簡化成以下僞代碼,其它所有多播委託均可依次類推。

int i;// i表示多播委託的次數
if(i==1) //也就是隻test.AAA += new ABC(test.A);然後調用test.AAA()
{
   test.A() //只有一個多播,直接調用這一個函數
}
else // 如果大於一個多播委託,如示例兩個多播
{
   IntPtr FunPtr=test.A()+test.B(); //函數A和函數B形成了一個新的託管地址
   FunPtr();//在新形成的託管地址裏面分別調用函數A和函數B
}

3.內存模型
對象(object)的內存,大致是:

爲了簡潔,實質非常龐大
header+MethodTable+field

委託根據對象來,以示例代碼的test對象爲例,test對象有一個filed也即是委託類型的變量AAA。AAA則是new ABC得來的。new ABC所實例化對象的filed是分別爲函數A,B。那麼他們的內存模型如下所示:

test==header+Mehtodtalbe + AAA(test.AAA(1) or test.AAA(2)+test.AAA(1))
test.AAA(1)==new ABC(test.A):header+Methodtable+函數A(precode)
test.AAA(2)==new ABC(test.B):header+Methodtable+函數B(precode)

特例:當只有一個多播委託(多播僞代碼裏的i==1),類似於以下這種情況:

如果:
static void Main(string[] args)
{
   Test test = new Test();
   test.AAA += new ABC(test.A);//只有一個多播
   test.AAA(); //test.AAA.Invoke();
}
那麼:
test==header+Mehtodtalbe + AAA(test.AAA(1))
test.AAA(1)==new ABC(test.A)(header+Methodtable+函數A(precode,offset:0x18))

內存:
0x000001DB38D552C0  00007ffa3b3654d8 000001db38d55858
這裏的0x000001DB38D552C0即test的MethodTable地址。
000001db38d55858即new ABC(test.A)的MethodTable地址

委託裏面只有一個方法test.A(多播僞代碼裏的i==1),這種情況的話,JIT會直接尋找test.AAA(1)的MethodTable,加上偏移位0x18,也即是函數test.A的函數地址。然後運行。

注意了,因爲對象test只有一個filed:AAA。超過一個以上的多播(多播僞代碼裏的i!=1,也即else邏輯),它的field是一直變化的,比如new ABC(test.A)的時候,它的filed是test.AAA(1)。而new ABC(test.B)的時候,它的field則是test.AAA(2)+test.AAA(1)組合成的託管函數,覆蓋掉前面的。如果有test.AAA(3),那麼後面繼續組合,繼續覆蓋test對象的field。

當它組合之後,形成一個新的地址,CLR會在這個地址的基礎上加上偏移量0x18(同上特例)進行託管函數代碼調用。JIT Compile之後,在裏面分別調用函數test.A,test.B,完成委託的多播。
參照如下代碼:

test.AAA(); //test.AAA.Invoke();
00007FFA3AFF7A27  mov         rcx,qword ptr [rbp+28h]
00007FFA3AFF7A2B  mov         rcx,qword ptr [rcx+8]
00007FFA3AFF7A2F  mov         rax,qword ptr [rbp+28h]
00007FFA3AFF7A33  call        qword ptr [rax+18h]
00007FFA3AFF7A36  nop

4.託管和非託管
依次調用順序,以下函數按照順序在多播委託中調用:
託管:

System.MulticastDelegate:CtorClosed //把對象test對象的field設置爲abc
System.Delegate:Combine //組合成新的委託,也即函數指針鏈,如果只有一個多播,則即那一個函數指針
System.Runtime.CompilerServices.CastHelpers.ChkCastClass //進行類型轉換

非託管:

JIT_WriteBarrier //設置card_table,防止GC標記的時候漏掉

5.原理圖
多播委託原理如下圖所示:
image
單個委託實際上就是調用函數指針,而多個委託,則是通過多播委託組合單個委託形成一個新的託管函數,在這個託管函數裏面進行單個函數一一調用。


結尾

作者:江湖評談
關注公衆號:jianghupt。後臺回覆:dotnet7。獲取一套.Net7 CLR源碼教程。
image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章