本文探討Autofac生命週期管理、記憶體管理基本觀念,最後透過Autofac一起享受依賴反轉所帶來的便利。
前言
之前有過慘痛的經驗,以為使用Autofac就不用在關心物件記憶體的釋放,結果API不過才幾百人使用,開了16G記憶體才只能勉強撐一天,雖然每天晚上固定回收還是可正常使用,但小弟自認是一個要邁向專業道路的工程師,發現問題,自然不可能輕易放過。每個問題都是增加經驗值的好機會!
Autofac既然身為一個老牌的DI Framework,不太可能會沒注意到記憶體的控管,所以,真相只有一個,就是對Autofac架構不夠了解,以致忽略了一些眉眉角角。
記憶體管理基本觀念
記憶體用完就要釋放,是很基本的道理,但卻不一定每個人都有做。但不做有三種情境,1.不知道要做 2.不會做 3.不爽做。除了第3種情境的人是少數之外,大部分應該都是前2種。
在寫這篇文章之前,我也一直很困擾,不知道何時需要釋放記憶體。IDisposable介面大家都懂,但卻不是每個物件都有實作此介面。後來才發現下面的重點,特別用紅色樣式來加深印象。
有了上面基本觀念後,那下一個問題,該如何區分Managed、UnManaged Resources?
- Managed : 包含在.NET sandbox內的任何東西,基本上.Net Framework的類別都是屬於Managed的類型。
- 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為何要如此設計?
最近,也有小摸專案管理的書,其中有一個觀念就講到,想要做出偉大的軟體,第一個是要探討在沒有你的軟體之前,人們是如何工作的,而有了你這個軟體之後,人們會如何使用,並改變行既有的行為,進而獲得你希望他們得到的利益。
所以,我們回頭看看,在沒有Autofac之前,我們是如何來管理記憶體的呢?不外乎兩種方式
- C# 的Using
- 呼叫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將依賴反轉,享受它所帶來的美好一切吧。