簡體   English   中英

async/await, Task 和 [weak self]

[英]async/await, Task and [weak self]

好的,我們都知道在 Swift 中的傳統並發中,如果您在 class 中執行(例如)a.network 請求,並且在完成該請求時您引用屬於該 class 的 function,您必須通過[weak self]中,像這樣:

func performRequest() {
   apiClient.performRequest { [weak self] result in
      self?.handleResult(result)
   }
}

這是為了阻止我們在閉包中強烈捕獲self並導致不必要的保留/無意中引用已經退出 memory 的其他實體。

在異步/等待中怎么樣? 我在網上看到相互矛盾的事情,所以我將向社區發布兩個示例,看看您對這兩個示例有何看法:

class AsyncClass {
   func function1() async {
      let result = await performNetworkRequestAsync()
      self.printSomething()
   }

   func function2() {
      Task { [weak self] in
         let result = await performNetworkRequestAsync()
         self?.printSomething()         
      }
   }

   func function3() {
      apiClient.performRequest { [weak self] result in
         self?.printSomething()
      }
   }

   func printSomething() {
      print("Something")
   }
}

function3很簡單——老式的並發意味着使用[weak self] function2我認為是對的,因為我們仍然在閉包中捕獲東西,所以我們應該使用[weak self] function1這是由 Swift 處理的,還是我應該在這里做一些特別的事情?

最重要的是,將[weak self]捕獲列表與Task對象一起使用通常沒有什么意義。 請改用取消模式。


一些詳細的考慮:

  1. 不需要弱捕獲列表。

    你說:

    在 Swift 的傳統並發中,如果您在 class 中執行(例如)a.network 請求,並且在該請求完成時您引用屬於該 class 的 function,您必須通過[weak self]

    這不是真的。 是的,使用[weak self]捕獲列表可能是謹慎的或可取的,但這不是必需的。 你“必須”對self使用weak引用的唯一時間是存在持久的強引用循環時。

    對於編寫良好的異步模式(其中被調用的例程一旦完成就釋放閉包),不存在持久的強引用循環風險。 不需要[weak self]

  2. N. 盡管如此,弱捕獲列表還是很有用的。

    在這些傳統的 escaping 閉包模式中使用[weak self]仍然有用。 具體來說,在沒有對selfweak引用的情況下,閉包將保持對self的強引用,直到異步過程完成。

    一個常見的例子是當你發起一個網絡請求來顯示場景中的一些信息時。 如果您在某些異步網絡請求正在進行時關閉場景,則沒有必要將視圖 controller 保留在 memory 中,等待僅更新早已消失的關聯視圖的網絡請求。

    不用說,對selfweak引用實際上只是解決方案的一部分。 如果保留self等待異步調用的結果沒有意義,那么讓異步調用繼續進行通常也沒有意義。 例如,我們可以將對selfweak引用與取消掛起的異步進程的deinit結合起來。

  3. 弱捕獲列表在 Swift 並發中用處不大。

    考慮function2的這種排列:

     func function2() { Task { [weak self] in let result = await apiClient.performNetworkRequestAsync() self?.printSomething() } }

    這看起來不應該在performNetworkRequestAsync進行時保留對self的強引用。 但是對屬性apiClient的引用將引入強引用,而不會出現任何警告或錯誤消息。 例如,在下面,我讓AsyncClass在紅色路標處掉出 scope,但是盡管有[weak self]捕獲列表,它直到異步過程完成后才被釋放:

    在此處輸入圖像描述

    在這種情況下, [weak self]捕獲列表的作用很小。 請記住,在 Swift 並發中,幕后發生了很多事情(例如,“暫停點”之后的代碼是“延續”等)。 它與簡單的 GCD 調度不同。 請參閱Swift 並發:幕后

    但是,如果您也將所有屬性引用設為weak ,那么它將按預期工作:

     func function2() { Task { [weak self] in let result = await self?.apiClient.performNetworkRequestAsync() self?.printSomething() } }

    希望未來的編譯器版本會警告我們這種隱藏的對self的強引用。

  4. 使任務可取消。

    與其擔心是否應該使用對selfweak引用,不如考慮簡單地支持取消:

     var task: Task<Void, Never>? func function2() { task = Task { let result = await apiClient.performNetworkRequestAsync() printSomething() task = nil } }

    然后,

     @IBAction func didTapDismiss(_ sender: Any) { task?.cancel() dismiss(animated: true) }

    現在,顯然,假設您的任務支持取消。 大多數 Apple async API 都可以。 (但是,如果您編寫了自己的withUnsafeContinuation樣式的實現,那么您將需要定期檢查Task.isCancelled或將您的調用包裝在withTaskCancellationHandler或其他類似機制中以添加取消支持。但這超出了這個問題的 scope。)

如果您正在 class 中執行(例如)a.network 請求,並且在完成該請求時您引用了屬於該 class 的 function,您必須像這樣傳遞 [weak self]

這不是真的。 當您在 Swift 中創建閉包時,閉包引用或“關閉”的變量默認保留,以確保這些對象在調用閉包時有效使用。 這包括self ,當self在閉包內部被引用時。

您想要避免的典型保留周期需要兩件事:

  1. 閉包保留self ,並且
  2. self保留關閉返回

如果self強烈地堅持閉包,並且閉包強烈堅持self ,就會發生保留循環——默認情況下,ARC 規則沒有進一步的干預,object 都不能被釋放(因為有東西保留了它),所以 memory 永遠不會被釋放釋放。

有兩種方法可以打破這個循環:

  1. 當你完成調用閉包時,明確地斷開閉包和self之間的鏈接,例如,如果self.action是一個引用self的閉包,則在調用后將nil分配給self.action ,例如

    self.action = { /* Strongly retaining `self`. */ self.doSomething() // Explicitly break up the cycle. self.action = nil }

    這通常不適用,因為它使self.action一次性完成,並且在調用self.action()之前您還有一個保留周期。 或者,

  2. 有一個 object保留另一個。 通常,這是通過決定哪個 object 是父子關系中另一個的所有者來完成的,並且通常, self最終強烈保留閉包,而閉包通過weak self弱引用self ,以避免保留它

無論self是什么以及閉包做什么,這些規則都是正確的:是否.network 調用、animation 回調等。

使用您的原始代碼,如果apiClientself的成員,您實際上只有一個保留周期,並且在 .network 請求期間保持關閉:

func performRequest() {
   apiClient.performRequest { [weak self] result in
      self?.handleResult(result)
   }
}

如果閉包實際上是在別處分派的(例如, apiClient不直接保留閉包),那么您實際上不需要[weak self] ,因為從來沒有一個循環開始!

規則與 Swift 並發和Task完全相同

  1. 您傳遞給Task以初始化它的閉包默認保留它引用的對象(除非您使用[weak...]
  2. Task在任務持續期間(即,在執行時)保持關閉狀態
  3. 如果self在執行期間保留Task ,您將有一個保留周期

function2()的情況下, Task被啟動並異步分派,但self不保留生成的Task object,這意味着不需要[weak self] 相反,如果function2()存儲了創建的Task那么您將有一個潛在的保留周期,您需要將其分解:

class AsyncClass {
    var runningTask: Task?

    func function4() {
        // We retain `runningTask` by default.
        runningTask = Task {
            // Oops, the closure retains `self`!
            self.printSomething()
        }
    }
}

如果您需要保留任務(例如,這樣您就可以cancel它),您將希望避免讓任務保留selfTask { [weak self]... } )。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM