使用されなくなったメモリが適切にクリアされず、スペースを占有し続けると、iOSアプリでメモリリークが発生します。 これは、アプリのパフォーマンスを傷つけることができ、最終的には、アプリが利用可能なメモリが不足すると、クラッシュの原因となります。 メモリリークがどのように発生するかをよりよく理解するには、まずiOSアプリがメモリをどのように管理するかを知ることが重要です。 Swiftクロージャでメモリリークを防ぐ方法を見てみましょう。
自動参照カウント
iOSでは、共有メモリは、各オブジェクトがそれへの参照を持っている他のオブジェクトの数を追跡することによって管理されます。 この参照カウントが0に達すると、オブジェクトへの参照がなくなり、メモリから安全にクリアできます。 名前が示すように、この参照カウントは参照型に対してのみ必要ですが、値型はメモリ管理を必要としません。 参照カウントは、以前はオブジェクトに対してretain
を呼び出して参照カウントを増やし、次にオブジェクトに対してrelease
を呼び出して参照カウントを減 このコードはほぼ完全にボイラープレートであり、書くのが退屈で間違いが起こりやすい。 そのため、ほとんどの厄介なタスクと同様に、コンパイル時に必要なretain呼び出しとrelease呼び出しを行う自動参照カウント(ARC)によって自動化されました。
ARCは主にメモリ管理について心配する必要性を減らしましたが、循環参照があるときはいつでも迅速なメモリリークを作成することができます。 たとえば、apartment
のプロパティを持つPerson
クラスがあり、Apartment
クラスにはPerson
という名前のプロパティがあるとしますtenant
:
class Person { var name: String var age: Int init(name: String, age: Int) {…} var apartment: Apartment?}class Apartment { let unit: String init(unit: String) {…} var tenant: Person?}let person = Person(name: "Bob", age: 30)let unit4A = Apartment(unit: "4A")person.apartment = unit4Aunit4A.tenant = person
新しいPerson
とApartment
を作成し、それらをお互いのプロパティに割り当てると、それらは循環的にお互いを参照しています。 循環参照は、2つ以上のオブジェクトでも発生する可能性があります。 この状況では、Person
とApartment
の両方が他のオブジェクトへの参照を保持しているため、この場合、どちらも参照カウント0になり、他のオブジェクトがいずれか これは保持サイクルと呼ばれ、メモリリークの原因となります。
関連:実際のSwiftyになる3つの機能
ARCが保持サイクルを扱う方法は、異なるタイプの参照を持つことです:強い、弱い、および所有されていません。 強い参照は、すでに説明したようなものであり、これらの参照はオブジェクトの参照数を増やします。 一方、弱い参照は、まだオブジェクトへの参照を与えている間、その参照カウントを増加させません。 したがって、Apartment
のtenant
プロパティをApartment
代わりに弱い参照にすると、保持サイクルが中断されます:
class Apartment { let unit: String init(unit: String) {…} weak var tenant: Person?}
今、私たちのPerson
オブジェクトはまだそのApartment
をメモリに保持していますが、その逆はもはや真実ではありません。 したがって、Person
への最後の強い参照がなくなったとき、その参照カウントは0に低下し、その参照カウントも0に低下するApartment
を解放し、両方ともメモリか
Swiftでは、オブジェクトをメモリに保持する責任を負わない場合、オブジェクトがメモリを変更したり離れたりしないことを保証できないため、弱参照は省略可能なvar
でなければなりません。 これは、参照の第三のタイプが遊びに来る場所です。 非所有参照は弱参照に似ていますが、オプションではないlet
にすることができますが、オブジェクトがnil
であってはならないことが確実な場合にのみ使 Force unwrapped optionalsのように、あなたはコンパイラに”心配しないでください、私はこれを得ました。 信用してください。.”しかし、弱参照のように、所有されていない参照はオブジェクトをメモリに保持するために何もしておらず、メモリを離れてアクセスしようとすると、 再び、ちょうど力のようにoptionalsをアンラップしました。
保持サイクルは、2つのオブジェクトが互いに指していると見やすいですが、Swiftのクロージャが関係しているときは見にくく、これがほとんどの保持サイクルが起こるのを見てきました。
クロージャでの保持サイクルの回避
クロージャはSwiftの参照型であり、クラスと同じように簡単に保持サイクルを引き起こす可能性があることを覚えておくことが重要です。 クロージャを後で実行するには、クロージャを実行するために必要な変数を保持する必要があります。 クラスと同様に、クロージャはデフォルトで参照をstrongとしてキャプチャします。 クロージャを持つretainサイクルは、次のようになります:
class SomeObject { var aClosure = { self.doSomething() } ...}
関連:コールバックにはいつブロックとクロージャまたはデリゲートを使用する必要がありますか?
この場合、SomeObject
クラスはaClosure
への強い参照を持ち、aClosure
はself
(SomeObject
インスタンス)も強くキャプチャしました。 これは、プログラマがself
を認識せずに誤ってキャプチャするのを防ぐために、Swiftがクロージャ内でself.
を明示的に追加する理由であり、したがって保持サイクルを引き起こす可能性が最も高い。
クロージャキャプチャ変数をweakまたはunownedとして持つには、特定の変数をキャプチャする方法についてクロージャ命令を与えることができます:
class SomeObject { var aClosure = { in self.doSomething() delegate?.doSomethingElse() } ...}
クロージャ構文の詳細はこちら。
チュートリアルや例で見たほとんどのクロージャの例は、self
をunownedとしてキャプチャし、self
をweakとしてキャプチャするとオプションになるため、self
をunownedと呼び、作業するのがより不便になる可能性があるためです。 しかし、以前に学んだように、self
がもはやメモリ内にない場合、クラッシュが発生するため、これは本質的に安全ではありません。 これは、すべてのオプション変数を安全にアンラップする作業をしたくないため、強制的にアンラップすることとあまり変わりません。 あなたの閉鎖がある限りself
が周りにあることを確信できない限り、代わりに弱いものを捕獲しようとするべきです。 クロージャ内に非オプションのselfが必要な場合は、if let
またはguard let
を使用して、クロージャ内に強力で非オプションのself
を取得することを検討してください。 Swiftクロージャ内でこの新しい強力な参照を作成したため、この参照はクロージャの最後に解放されるため、保持サイクルを作成することはありません:
var aClosure = { in if let strongSelf = self { doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf) }}
またはさらに良い:
var aClosure = { in guard let strongSelf = self else { return } doSomethingWithNonOptional(strongSelf) doSomethingElseNonOptional(strongSelf)}
自己を強く捕捉する
クロージャでself
を弱く捕捉するのは良い習慣ですが、必ずしも必要ではありません。 self
によって保持されていないクロージャは、保持サイクルを引き起こすことなく強く捕捉することができます。 これのいくつかの一般的な例は次のとおりです。
GCDでDispatchQueue
を操作する
DispatchQueue.main.async { self.doSomething() // Not a retain cycle}
との作業UIView.animate(withDuration:)
UIView.animate(withDuration: 1) { self.doSomething() // Not a retain cycle}
最初のものはself
がDispatchQueue.main
シングルトンを保持しないため、保持サイクルではありません。 第二の例では、UIView.animate(withDuration:)
はクラスメソッドであり、self
も保持には関与していません。
このような状況でself
を強くキャプチャしても、保持サイクルは発生しませんが、それはあなたが望むものではないかもしれません。 たとえば、GCDに戻る:
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { self.doSomething()}
このクロージャはさらに60秒間実行されず、実行されるまでself
が保持されます。 これはあなたが望む動作かもしれませんが、この時間中にself
がメモリを離れることができるようにしたい場合は、それを弱くキャプチャし、self
がまだ:
DispatchQueue.main.asyncAfter(deadline: .now() + 60) { in self?.doSomething()}
self
を強くキャプチャする必要がない別の興味深い場所は、lazy
変数であり、クロージャではありません。:
lazy var fullName = { return self.firstName + " " + self.lastName }()
しかし、lazy
変数がクロージャである場合、selfを弱くキャプチャする必要があります。 これの良い例は、Swift Programming Language guideから来ています:
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { in if let text = self.text { return "<(self.name)>(text)</(self.name)>" } else { return "<(self.name) />" } }}
これは、asHTML
クロージャはHTMLElement
が存在する限り存在しなければならないが、もはや存在しないため、所有されていない参照を使用するのが妥当なインスタンスの良い例でもあります。
TL;DR
Swiftでクロージャを扱うときは、変数、特にself
をキャプチャする方法に注意してください。self
が何らかの方法でクロージャを保持している場合は、弱くキャプ オプションのself
で作業したくないという理由だけでなく、クロージャが実行されるたびにメモリ内にあることを確認できる場合にのみ、変数を非所有 これにより、Swiftクロージャでのメモリリークを防ぎ、アプリのパフォーマンスを向上させることができます。