[Autofac套件介紹-4] 深入討論Autofac 生命週期

本文探討Autofac生命週期管理、記憶體管理基本觀念,最後透過Autofac一起享受依賴反轉所帶來的便利。

前言

之前有過慘痛的經驗,以為使用Autofac就不用在關心物件記憶體的釋放,結果API不過才幾百人使用,開了16G記憶體才只能勉強撐一天,雖然每天晚上固定回收還是可正常使用,但小弟自認是一個要邁向專業道路的工程師,發現問題,自然不可能輕易放過。每個問題都是增加經驗值的好機會!

Autofac既然身為一個老牌的DI Framework,不太可能會沒注意到記憶體的控管,所以,真相只有一個,就是對Autofac架構不夠了解,以致忽略了一些眉眉角角。

 

記憶體管理基本觀念

記憶體用完就要釋放,是很基本的道理,但卻不一定每個人都有做。但不做有三種情境,1.不知道要做 2.不會做 3.不爽做。除了第3種情境的人是少數之外,大部分應該都是前2種。

在寫這篇文章之前,我也一直很困擾,不知道何時需要釋放記憶體。IDisposable介面大家都懂,但卻不是每個物件都有實作此介面。後來才發現下面的重點,特別用紅色樣式來加深印象。

記憶體釋放分兩種類型,一種是Managed Resources,一種是UnManaged Resources。前者可靠GC (Garbage Collection,C#資源回收車)協助管理;後者須自行管理(呼叫Dispose方法是其中一種方式)。

有了上面基本觀念後,那下一個問題,該如何區分Managed、UnManaged Resources?

  1. Managed : 包含在.NET sandbox內的任何東西,基本上.Net Framework的類別都是屬於Managed的類型。
  2. UnManaged : 反之就是在.NET sandbox外的任何東西,如透過Win32 API 方法回傳給你的內容。

以上是精準的定義,老實講很難理解。另一方面,在實務上,我們幾乎不會直接操作UnManaged Resources,即使有用到,也大都會被.NET Framework封裝起來,而有封裝UnManaged Resources的類別,一般都會實作IDisposiable介面,所以我們只需要呼叫Dispose,便能釋放這些資源。

因此,也可以這樣理解,有實作IDisposiable的物件,基本上就是屬於UnManaged Resouces,必須透過呼叫Dispose釋放資源,GC無法協助回收;而沒有實作IDisposiabe的物件,屬於Managed Resouces,當沒有任何參考指向該物件個體時,GC會自動回收。

 

Autofac有雷?

先來看一個範例幫助了解,以下是沒有Autofac的寫法,需要使用"new"關鍵字。

while (true)
    var resource = new MyResource(); // 物件會自動被GC釋放

如果改成Autofac寫法則是

using (var container = builder.Build())
{
    while (true)
        var resource = container.Resolve<IResource>(); // 最終造成記憶體耗盡
}

由上面兩個範例可以發現,Autofac成功的讓我們程式的操作相依於介面(IResource),而非直接相依於實作(Resource),但好像又挖了一個坑給我們跳,讓人匪夷所思。難道是直接呼叫Resolve產生Resource物件個體才造成這個問題的嗎?那假如改為由Autofac自動注入,是否就正常了呢?直接看下一個範例。

interface IService { }

class MyComponent : IService
{
    // 透過建構子由Autofac自動注入相依性
    public MyComponent(IResource resource) { … }
}
while (true)
    //照樣爆炸
    var s = container.Resolve<IService>();

由此可發現,問題並不是在這裡,而是要掌握住下面的一個觀念。

Autofac會追蹤所有由它建立的Disposiable物件,不管你是何種方法要求的(如建構子注入、自行Resolve、etc...)。且由Autofac來控管這些物件的生與死,GC無法介入。

 

Autofac為何要如此設計?

最近,也有小摸專案管理的書,其中有一個觀念就講到,想要做出偉大的軟體,第一個是要探討在沒有你的軟體之前,人們是如何工作的,而有了你這個軟體之後,人們會如何使用,並改變行既有的行為,進而獲得你希望他們得到的利益。

所以,我們回頭看看,在沒有Autofac之前,我們是如何來管理記憶體的呢?不外乎兩種方式

  1. C# 的Using
  2. 呼叫Dispose方法

但很快就會遇到,有些情境並不能符合需求,下面有3個例子可參考。

1. 共用的資源

多個物件共用資源,但我們很難知道何時此項資源已無人使用可以釋放。

2. 巢狀物件的資源管理將會牽一髮動全身

比如A物件擁有B物件,一開始假設A、B都未握有UnManaged的資源,所以兩者都無需特別呼叫Dispose。但假設B物件修改為須管理UnManaged的資源,代表只要使用到A的物件,也必須告訴A何時該Dispose。巢狀物件愈大串,影響將會愈大。

3. 介面定義兩難

假設有一個ICache的介面,原本有一個實作是MemoryCache,且未握有UnManaged的資源,因此ICache無須實作IDisposiable;但假設後續又多了一個實作是FileCache,握有UnManaged的資源,導致ICache必須宣告成IDisposiable。但這對MemoryCache來講,需實作Dispose方法是不make sence的。(因為MemoryCache並未握有UnManaged的資源)

綜合以上的痛點,Autofac的設計便是為了幫助我們跳脫這些問題,下個段落將會說明實作Autofac生命週期的幾種方式。

 

Autofac管理生命週期的方法

1. 區域Scope方式實作

假設我們很明確知道物件需存活的範圍,可以使用類似Using的做法,建立起區域的Scope,等到括號結束,Autofac會協助釋放建立的資源。

while (true)
{
    //建立一個生命周期的範圍(scope)
    using (var lifetimeScope = container.BeginLifetimeScope())
    {
        var resource = lifetimeScope.Resolve<IMyResource>();
    }
    //括號結束會釋放掉資源
}

scope也可由注入的方式得到,可參考下面範例。

ILifetimeScope _lifetimeScope;

public BMW(ILifetimeScope lifetimeScope)
{
    _lifetimeScope = lifetimeScope;
}

2. 建立特定Scope內共用的資源

假設我們希望在特定條件下,共用同一個物件個體,比如說有一個底層物件提供基本功能,而A、B物件在建構子內都需注入此物件,就可利用此方式共用。可看下列範例,關鍵字是"InstancePerMatchingLifetimeScope"。

var builder = new ContainerBuilder();

//註冊車子
builder.RegisterType<BMW>().As<ICar>();

//註冊駕駛人,跟上面不同的是,假如SCope 對應到CarDriver 將共用同一個實體
builder.RegisterType<HankDriver>().As<IDriver>().InstancePerMatchingLifetimeScope("carDriver");

var container = builder.Build();

//宣告Key為carDriver的Scope
using (var scope = container.BeginLifetimeScope("carDriver"))
{
    var bmw1 = scope.Resolve<ICar>();
    var bmw2 = scope.Resolve<ICar>();

    //比較Driver 和 Car
    var sameDriver = bmw1.GetDriver().Equals(bmw2.GetDriver());
    var sameCar = bmw1.Equals(bmw2); 

    //Car是不同物件  Driver是同一個
    Console.WriteLine(string.Format("The car is the same? {0}", sameCar));
    Console.WriteLine(string.Format("The driver is the same? {0}", sameDriver) );

    Console.ReadLine();
}

輸出結果如下所示

3. Owned Instances

前面介紹的方式,是透過Scope來管理生命週期,難免還是受限於要知道Scope的範圍,Autofac提供另一個方式,把生命週期封裝到一個Owned類別內,讓我們可以透過單一物件,便同時掌握生命週期的管理和Service的使用,用完之後呼叫Dispose即可,就不受限於要事先告知Scope範圍,可參考下面範例。

//宣告同時握有生命週期 以及Service(ICar)的物件
var ownedService = container.Resolve<Owned<ICar>>();

//Value = BMW 所以可呼叫BMW的Drive方法
ownedService.Value.Drive();
//用完之後 Dispose 大幅增加彈性 只要握有ownedService物件的參考 可以在任何時候Dispose
ownedService.Dispose();

4. 結合Func Factory

最後一種情境,假設A物件初始化需要注入某個底層物件,但A物件並不是所有方法都需要用到這個底層物件,且這個底層物件初始化的成本較高,就可以考慮利用工廠的方式,注入工廠給A物件,在A物件需要此底層物件時再透過工廠將此底層物件初始化出來。

Func<Owned<IDriver>> _resourceFactory;

//注入生產Driver的工廠 並封裝到Owned內 
public BMW(Func<Owned<IDriver>> resourceFactory)
{
    this._resourceFactory = resourceFactory;
}

public IDriver GetDriver()
{  
    //需要時再透工廠取得物件
    return this._resourceFactory.Invoke().Value;
}

 

 

結語

Autofac負責主宰物件的生與死,但如何有效地告訴Autofac如何管理這些物件,是我們的責任。要能讓Autofac有效地做記憶體管理,一個先決條件就是,要了解自己的軟體架構設計,各層之間的相依關係,才能妥善發揮Autofac的能力。

了解這些之後,我們就能透過Autofac將依賴反轉,享受它所帶來的美好一切吧。