在使用IoC設計模式時,有一個有點難懂卻不能迴避的問題 -- 如何妥善管理物件生命週期,避免記憶體洩漏(Memory Leak)?

要了解此議題,先大推一篇關於Autofac物件生命週期的經典文章,其中有頗詳細的闡述,這篇筆記只簡短摘要我實際應用的心得,關於完整說明推薦大家參考原文。

問題從何而來?

基本上,純.NET世界的資源(Managed Resource,例如: 儲存.NET物件所用的記憶體)有GC(Garbage Collection)機制把關,它能精準掌握物件是否仍在有效範圍,當物件已不可能再被使用,便會在必要時(例如: 剩餘記憶體不足)回收這些已消滅物件所耗用的記憶體。但程式要運行,多少會涉及一些非.NET所掌管的資源(Unmanged Resource,例如: 網路連線、磁碟機上的檔案... 等等),當.NET物件使用到這些Unmanaged Resource,建議的做法是實作IDispose介面,在Dispose()方式中確實釋放所有動用到的資源,以免.NET物件消失後佔著茅坑不拉屎,阻礙其他Process使用。

一般來說,若物件有實作IDispose,我們可寫成using (var boo = new SomeDisposableClass()) { … },確保範圍結束後一定呼叫Dispose()釋放資源。但using只適用單一Method內部,若物件建立好要交給其他程式應用,便不能任意Dispose(),以免別人要用時已成廢物;但是,若不確實在物件使用完畢後呼叫Dispose(),又會造成資源不當佔用。這是個難題,且沒有什麼神奇解法,設計實務多半靠"建立物件者必須負責善後"原則作為解決方案。

應用Autofac時,物件都是透過Container.Resolve<SomeType>()方式取得,換言之,物件是由Autofac建立,那麼Dispose()也會由Autofac呼叫嗎? 做個實驗吧! 寫個實作IDisposable的類別:

using System;
 
class ResourceMonster : IDisposable
{
    public string Name = "Anonymous";
    public void Test()
    {
        Console.WriteLine("{0}: Hi there.", Name);
    }
    public void Dispose()
    {
        Console.WriteLine("{0}: Disposed.", Name);
    }
}

測試程式如下:

using Autofac;
using System;
 
namespace Lifetime
{
    class Program
    {
        static void Main(string[] args)
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ResourceMonster>();
            IContainer container = builder.Build();
 
            var monster = container.Resolve<ResourceMonster>();
            monster.Test();
            Console.WriteLine("Before IContainer Dispose");
            container.Dispose();
            Console.WriteLine("After IContainer Dispose");
 
            Console.ReadLine();
        }
    }
}

Anonymous: Hi there.
Before IContainer Dispose
Anonymous: Disposed.
After IContainer Dispose

執行結果如上,IContainer有實作IDisposable,當我們呼叫container.Dispose(),container會盡責地善後 -- 呼叫它所建立monster物件的Dispose()方法。

測試程式只用於示範,因此在Main()單一方法內註冊型別、建立IContainer,用完就馬上把IContainer.Dispose(),但這不符合應用實。IContainer建立要註冊所有列管型別,需要耗費資源、時間,不可能每次使用前才建立,用完就丟,下次要用再重建。因此IContainer整個Process多半只會建一份,通常安排在程式(或網站)啟動事件中建立好,並以static屬性方式供整個Process共用。在某個Method呼叫Resolve<T>建立的物件,不可能依賴IContainer.Dispose()善後(因為其他人還要繼續用它建立物件),為此,Autofac提供了ILifetimeScope提供較短的生命週期應用。

運作原理是先透過IContainer.BeginLifetimeScope()建立ILifetimeScope取代IContainer,它具有跟IConatiner幾乎一致的介面,如此我們便可改用ILifetimeScope.Resolve<T>建立物件,而ILifetimeScope算是為特定目所建的獨立容器,在使用完畢後可任意Dispose()而不會影響IContainer。另外,ILifetimeScope還可以透過.BeginLifetimeScope()再建立子ILifetimeScope形成巢狀結構,上層容器Dispose()時會一併呼叫下層容器進行Dispose(),可依不同需求彈性應用。

以下是簡單的ILifetimeScope範例,先透過container.BeginLifetimeScope()建立ILifetimeScope,再改用它來Resolve<ResourceMonster>()取得物件,透過using自動終結scope(using結束時背後會呼叫scope.Dispose()),由執行結果可觀察到兩個ResourceMonster物件也自動被Dispose():

using Autofac;
using System;
 
namespace Lifetime
{
    class Program
    {
        static IContainer container = null;
        static void AutofacConfig()
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ResourceMonster>();
            container = builder.Build();
        }
        static void Test()
        {
            using (var scope = container.BeginLifetimeScope())
            {
                var monster1 = scope.Resolve<ResourceMonster>();
                monster1.Name = "No1";
                monster1.Test();
                var monster2 = scope.Resolve<ResourceMonster>();
                monster2.Name = "No2";
                monster2.Test();
            }
        }
 
        static void Main(string[] args)
        {
            AutofacConfig();
            Test();
            Console.ReadLine();
        }
    }
}

執行結果:

No1: Hi there.
No2: Hi there.
No2: Disposed.
No1: Disposed.

另外,先前介紹過Autofac Singleton,ILifetimeScope也可當作Instance共用的單位,例如: 指定只建一個Instance供全ILifetimeScope共用,做法是在RegisterType<T>時加上InstancePerLifetimeScope(),例如:

        static IContainer container = null;
        static void AutofacConfig()
        {
            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<ResourceMonster>();
            builder.RegisterType<TheNewOne>().InstancePerLifetimeScope();
            container = builder.Build();
        }
 
        static void Test2()
        {
            var scope1 = container.BeginLifetimeScope();
            var scope2 = container.BeginLifetimeScope();
            var one1 = scope1.Resolve<TheNewOne>();
            Console.WriteLine("1->");
            one1.ShowUniqueKey();
            var one2A = scope2.Resolve<TheNewOne>();
            var one2B = scope2.Resolve<TheNewOne>();
            Console.WriteLine("2A->");
            one2A.ShowUniqueKey();
            Console.WriteLine("2B->");
            one2B.ShowUniqueKey();
        }
 
        static void Main(string[] args)
        {
            AutofacConfig();
            Test2();
            Console.ReadLine();
        }

執行結果如下,如預期兩個ILifetimeScope所取得的TheNewOne Unique Key不同,而第二個ILifetimeScope取得的兩個TheNewOne Unique Key相同,證明為同一個Instance。

Constructor Executed
1->
Unique Key=de3fdf54-a67e-4896-98b4-d7ab1314dbe8
Constructor Executed
2A->
Unique Key=7dc4ac10-aafa-4630-8a64-b517cde8fc82
2B->
Unique Key=7dc4ac10-aafa-4630-8a64-b517cde8fc82

最後提一下Owned Instance,指Autofac在Resolve<T>時先產生新的ILifetimeScope,再用它建立物件並傳回ILifetimeScope。寫法為var ownedService = conatiner.Resolve<Owned<SomeType>>(),而ownedService.Value即為所建立的SomeType Instance,而ownedService.Dispose()則可用來結束該ILifetimeScope。

【結論】

在大多數應用情境下,建議改用ILifetimeScope取代IContainer物件Resolve<T>(),並在作業結束後執行ILifetimeScope.Dispose()以落實資源回收,減少記憶體洩漏的風險。

【延伸閱讀】


Comments

Be the first to post a comment

Post a comment