--- title: 'Flutter Singleton' tags: Flutter disqus: hackmd --- <style> .red { color: red; } .blue { color: blue; } </style> <font size="6">Flutter Singleton</font> **目錄:** [TOC] ## 單線程與多線程 1. 單線程(Single-threading): - 單線程是指在一個時間點上,程式只執行一個指令序列。也就是說,程式是按照順序執行的,每個任務必須等待前一個任務完成後才能執行。 - 單線程模型簡單,易於理解和調試,因為不需要擔心競態條件和同步問題。 - 缺點是效率較低,特別是當需要處理大量計算或需要等待外部資源時,會出現程式阻塞的情況 2. 多線程(Multithreading): - 多線程是指在一個程式中同時執行多個獨立的指令序列,每個指令序列稱為一個線程(Thread)。 - 多線程使得程式可以同時執行多個任務,提高了系統的效率和反應速度。 - 線程之間可以共享資源,但這也可能導致競態條件(Race Condition)和同步問題(Synchronization Issues)。 - 多線程需要謹慎的設計和管理,以確保線程之間的協調和資源共享是安全的。 :::warning 單線程語言模型:Dart, JavaScript 多線程語言模型:Swift, Java ::: ## 管理Singleton 單線程模型的語言實現單例模式時,我們本身已經可以不用再去考慮線程安全的問題了,因為他不會在同一時間引用Singleton。 至於多線程,就要看語言特性了 1. Java(<span class="red">需要管理</span>) 特性: - 競態條件:競態條件是指多個線程同時嘗試訪問和共享資源(單實例實例)時可能導致的問題。如果不使用鎖,多個線程可以同時通過單實例模式的檢查,導致多個實例化,破壞了單個實例的定義。 - 懶加載(Lazy Initialization):在很多情況下,單例模式需要在第一次訪問時才創建實例,以減少啟動時間和資源佔用。懶加載的方式可能會導致多個線程在同一時間嘗試創建實例。使用鎖可以確保只有一個線程創建實例。 - 並發訪問:在多線程應用程序中,多個線程可能會同時訪問單個實例實例。如果沒有適當的同步,可能會導致數據損壞、內存洩漏和狀態不一致。 解決方式: - 原子性操作:鎖(例如synchronized關鍵字或ReentrantLock類)提供了原子性操作的機制,保證同一時間只有一個線程可以執行關鍵代碼塊,從而避免了並發問題。 2. Swift(<span class="red">不需要管理</span>) 特性: - 線程安全的延遲加載:在 Swift 中,let關鍵字用於定義常量,常量是線程安全的。因此,你可以使用let來創建延遲加載的單例,而不用擔心線程安全問題。Swift 保證了常量的初始化只會發生一次,即使在多線程環境下也是如此。這是因為 Swift 在運行時會自動執行懶加載,並在初始化過程中加鎖以確保線程安全。 ## Flutter(able) 的單例模式 一般來說,要在代碼中使用單例模式,結構上會有下面這些約定俗成的要求: - 單例類(Singleton)中包含一個引用自身類的靜態屬性實例(instance),且能自行創建這個實例。 - 該實例只能通過靜態方法getInstance()訪問。 - 類構造函數通常沒有參數,且被標記為私有,確保不能從類外部實例化該類。 ```dart= class Singleton { static Singleton _instance; // 私有的命名构造函数 Singleton._internal(); static Singleton getInstance() { if (_instance == null) { _instance = Singleton._internal(); } return _instance; } } ``` 同時,在實現單例模式時,也需要考慮如下幾點,以防在使用過程中出現問題: - 否需要懶加載,即類實例只在第一次需要時創建。 - 是否線程安全,在Java、C++ 等多線程語言中需要考慮到多線程的並發問題。<span class="red">由於Dart 是單線程模型的語言,所有的代碼通常都運行在同一個isolate 中,因此不需要考慮線程安全的問題。</span> - 在某些情況下,單例模式會被認為是一種反模式,因為它違反了SOLID 原則中的單一責任原則,單例類自己控制了自己的創建和生命週期,且單例模式一般沒有接口,擴展困難。 - 單例模式的使用會影響到代碼的可測試性。如果單例類依賴比較重的外部資源,比如DB,我們在寫單元測試的時候,希望能通過mock 的方式將它替換掉。而單例類這種硬編碼式的使用方式,導致無法實現mock 替換。 ## Dart 化 如上文所說的,Dart 語言作為單線程模型的語言,實現單例模式時,我們本身已經可以不用再去考慮線程安全的問題了。Dart 的很多其他特性也依然可以幫助到我們實現更加Dart 化的單例。 使用getter操作符,可以打破單例模式中既定的,一定要寫一個getInstance()靜態方法的規則,簡化我們必須要寫的模版化代碼,如下的get instance: ```dart= class Singleton { static Singleton _instance; static get instance { if (_instance == null) { _instance = Singleton._internal(); } return _instance; } Singleton._internal(); } ``` Dart 的getter 的使用方式與普通方法大致相同,只是調用者不再需要使用括號,這樣,我們在使用時就可以直接使用如下方式拿到這個單例對象: ```dart= final singleton = Singleton.instance; ``` 而Dart 中特有的工廠構造函數(factory constructor)也原生具備了不必每次都去創建新的類實例的特性,將這個特性利用起來,我們就可以寫出更優雅的Dart(able) 單例模式了,如下: ```dart= class Singleton { static Singleton _instance; Singleton._internal(); // 工廠構造函數 factory Singleton() { if (_instance == null) { _instance = Singleton._internal(); } return _instance; } } ``` 這裡我們不再使用getter操作符額外提供一個函數,而是將單例對象的生成交給工廠構造函數,此時,工廠構造函數僅在第一次需要時創建,並之後每次返回相同的_instance實例。這時,我們就可以像下面這樣使用普通構造函數的方式獲取到單例了: ```dart= final singleton = Singleton(); ``` 最簡化的版本 ```dart= class Singleton { Singleton._internal(); factory Singleton() => _instance; static final Singleton _instance = Singleton._internal(); } ``` ## Flutter 化 說到工廠構造函數/空安全操作符等Dart 語法上的特性,Flutter 應用中的例子已經屢見不鮮了,但光看單例模式的定義,我們還必須聯想到Flutter 中另一個非常重要的widget,那就是InheritedWidget。 如果你已經是一個Flutter 小能手,或者已經看過《Flutter 開發之旅從南到北》和之前的文章的話,一定已經對他的作用有了清晰的認識了。 InheritedWidget 狀態可遺傳的特性可以幫助我們很方便的實現父子組件之間的數據傳遞,同時,它也可以作為狀態管理中的數據倉庫,作為整個應用的數據狀態統一保存的地方。 ![](https://hackmd.io/_uploads/r1aDTHBkT.png) 上面代碼中,我們通過繼承InheritedWidget 就實現了自己的可遺傳組件_InheritedStateContainer,其中的data變量表示全局狀態數據,在這裡就可以被認為是整個應用的一個單例對象。 _InheritedStateContainer還接受child參數作為它的子組件,child表示的所以子組件們就都能夠以某種方式得到data這個單一的全局數據了。 約定俗成地,Flutter 源碼經常會提供一些of方法(類比getInstance())作為幫助我們拿到全局數據的輔助函數。 以Flutter 中典型的Theme 對象為例。我們通常會在應用的根組件MaterialApp中創建ThemeData對像作為應用統一的主題樣式對象: ```dart= MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); ``` 在其他任意的組件中,我們可以使用Theme.of(context)拿到該對象了,且這個對象全局唯一。如下所示,我們可以將該 對ThemeData像中的primaryColor應用在Text中: ```dart= // 使用全局文本样式 Text( 'Flutter', style: TextStyle(color: Theme.of(context).primaryColor), ) ```