寫更好的 Swift 程式碼:效能優化利器 Lazy

語言: CN / TW / HK

Lazy initialization

在用OC開發iOS的時代,相信你會看到很多懶載入的程式碼:

swift // 以懶載入一個UIImageView控制元件為例 - (UIImageView *)imageView { if (!_imageView) { _imageView = [[UIImageView alloc]init]; } return _imageView; }

懶載入就是在第一次訪問某個屬性時,要判斷這個屬性是否已經被初始化,如果已經初始化則直接返回,若沒有初始化則進行初始化。這樣可以把這個屬性延遲初始化,把它和包含它的物件的初始化分隔開,來達到提升效能的目的。

懶載入的好處:

  • 不必將建立的程式碼全部寫在類似- (void)viewDidLoad 方法中,增加了程式碼的可讀性;
  • 只有到真正需要資源的時候才回去載入,節省了記憶體空間;
  • 當收到記憶體警告是, didReceviewMemoryWarning方法中會釋放資源,如果是懶載入的話,以後再次用到了該屬性,可以再次加載出來。

Swift 也能實現類似的懶載入:

swift private var _imageView: UIImageView? var imageView: UIImageView { get { if _imageView == nil { _imageView = UIImageView() } return _imageView! } set { _imageView = newValue } }

用這種方式,雖然我們的需求,但是程式碼實在太多,太不 Swift style 了。lazy 閃亮登場。

在變數屬性前加lazy關鍵字來指定延遲載入:

swift lazy var imageView: UIImageView = UIImageView()

使用lazy關鍵字,我們用更少程式碼實現了相同的行為。

當然如果我們要給 imageView 設定更多的屬性,我們可以通過 閉包進行初始化

swift lazy var imageView: UIImageView = { let imgView = UIImageView() imageView.contentMode = .scaleAspectFill return imageView }()

在使用lazy修飾屬性時,必須宣告屬性是變數

-w747

對於 lazy 的初始化,我們就講到這裡。但是作為一個性能優化利器,文章豈能止步於此。

Lazy sequences

在 Swift 標準庫中,SequenceTypeCollectionType 協議都有個叫 lazy 的計算屬性,它能給我們返回一個特殊的 LazySequence 或者 LazyCollection

``swift /// Augmentselfwith lazy methods such asmap,filter`, etc. extension Collection { public var lazy: LazyCollection { get } }

func lazy(s: S) -> LazySequence

func lazy(s: S) -> LazyRandomAccessCollection

func lazy(s: S) -> LazyBidirectionalCollection

func lazy(s: S) -> LazyForwardCollection ```

這些型別只能被用在 map,flatMap,filter 這樣的高階函式中,而且是以一種惰性的方式。在某些情況下這麼做也對效能會有不小的幫助,例如,直接使用 map 時:

```swift func increment(x: Int) -> Int { print("訪問:(x)") return x+1 }

let array = Array(0..<10)

print("✅結果:") let incArr = array.map(increment) print(incArr[0], incArr[5])

let incArray = array.lazy.map(increment) print("\n✅使用lazy屬性的結果:") print(incArray[0], incArray[5]) ```

輸出的結果:

```swift ✅結果: 訪問:0 訪問:1 訪問:2 訪問:3 訪問:4 訪問:5 訪問:6 訪問:7 訪問:8 訪問:9 1 6

✅使用lazy屬性的結果: 訪問:0 訪問:5 1 6 ```

上面這段程式碼: * 訪問incArr的值之前,所有的輸出值都被計算出來了!即使我們只讀了[0]和[5]這兩個條目,但其他的條目都被訪問 * 訪問incArray,因為使用了lazy,僅就訪問了[0]和[5]兩條條目,不關心的條目沒有被訪問。

使用lazy後算量明顯降低太多。如果 array 的體量更大,且 increment 更復雜,那麼節省的算量就更明顯了。

惰性計算是函數語言程式設計語言的一個特性,有興趣不防深入瞭解下。

Lazy View

與沒有“Lazy”的HStack/VStack的區別在於按需載入,例如,如果您嘗試在HStackVStack上顯示10000個文字,則記憶體會立即激增,但是LazyHStackLazyVStack 只加載螢幕上顯示文字的記憶體。

當然在 SwiftUI 2 還新增了類似懶載入的View:LazyHGrid/LazyVGrid

注意:

Additionally, be aware that the lazy keyword doesn’t perform any thread synchronization. If multiple threads access a lazy property at the same time before the value has been computed, it’s possible the computation could be performed more than once, along with any side effects the computation may have.

lazy修飾的變數是在第一次訪問的時候初始化的,如果多執行緒訪問這個Lazy變數,將會導致不可預知的結果:

  • 有可能不同執行緒生成不同的lazy變數。
  • 有可能一個執行緒初始化還沒完成,另外一個執行緒就返回了這個變數。