SwiftProgrammingiOS Developer

Swiftのゼロ化弱参照を実装するためのサイドテーブルメカニズムを、弱い関係を持たないオブジェクトに対してメモリオーバーヘッドを課さずに特徴付けてください。

Hintsage AIアシスタントで面接を突破

質問への回答。

Objective-Cは、弱参照のために手動のretain/releaseサイクルと直接ポインタに依存しており、これにはランタイムスウィズリングまたはグローバルハッシュテーブルが必要でした。これにより、オブジェクトアクセスごとに著しいパフォーマンスペナルティが発生しました。AppleSwiftを設計する際には、ゼロ化弱参照をサポートする自動メモリ管理モデルが必要でした。これは、参照されているオブジェクトが解放されると自動的にnilになるもので、決して弱い参照に遭遇しない大多数のオブジェクトに負担をかけないようにする必要があります。この必要性から、要求時にのみ弱参照メタデータを外部化するサイドテーブルアーキテクチャが開発されました。

中心となる問題は、メモリ効率と安全性のバランスを取ることでした。すべてのオブジェクトヘッダーが弱参照トラッキングのためにインラインストレージ(弱いポインタのリンクリストやインライン弱カウントなど)を含んでいると、すべてのクラスインスタンスのメモリフットプリントが大幅に増加し、強参照のみを使用するパフォーマンスクリティカルなコードにペナルティを課すことになります。逆に、オブジェクトアドレスでキー付けされたグローバルハッシュテーブルに弱参照を保存すると、オブジェクトが解放される際に同期ボトルネックや複雑な再請求ロジックが発生します。課題は、弱い参照を持たないオブジェクトにコストをゼロで課し、最後の強い参照が消えたときにスレッドセーフな原子的ゼロ化を保証するメカニズムを作成することでした。

Swiftは、各クラスインスタンスのヘッダーにnull許容ポインタを持つ別のヒープ割り当てされたサイドテーブル構造体を含むサイドテーブルシステムを採用しています。このサイドテーブルは、弱参照カウントとオブジェクトへのバックポインタを格納します。実際には弱参照はオブジェクトではなく、このサイドテーブルを指し示します。強参照カウントがゼロに達すると、ランタイムはサイドテーブル内でオブジェクトポインタを原子的にnilにし、次回のアクセス時にすべての既存の弱参照がnilを観察しますが、オブジェクトのメモリは弱参照カウントがゼロに達するまで確保されたままです。その時点で、サイドテーブルとオブジェクトメモリの両方が再請求されます。

現実の状況

ソーシャルメディアアプリケーションの高解像度画像パイプラインを開発していると想像してみてください。ここで、ViewControllerインスタンスがユーザーのアバターをダウンロードして表示します。冗長なネットワーク要求を防ぐために、ダウンロードされたUIImageオブジェクトへの参照を保存するImageCacheシングルトンを実装します。これにより、同じアバターを表示する複数のビューコントローラーが基盤となるメモリバッファを共有できます。

考慮したアプローチの1つは、任意の追い出しポリシーを持つNSCacheに強参照を保存することでした。これにより即座のアクセスと型安全が保証されましたが、キャッシュがすべての画像を無期限に保持するため、深刻なメモリリークが発生し、最終的にはメモリ警告やシステムのメモリ監視によるアプリ終了を引き起こしました。利点には単純さと迅速なアクセスが含まれますが、無制限のメモリ増加という欠点があったため、実運用には不適でした。

別の考慮されたアプローチは、ビューコントローラーが解放時にキャッシュに通知して特定のエントリを削除できるようにする手動オブザーバーパターンの実装でした。この理論ではリークを防ぐことができましたが、ビュー層とキャッシング層間の脆弱な強い結合を生じさせ、急速なナビゲーションの遷移中にレースコンディションを処理するために広範なボイラープレートが必要となり、通知メッセージが逃されたり遅れて配信されるとクラッシュのリスクがありました。

選択された解決策は、キャッシュ実装内でSwiftのネイティブ弱参照を利用しました:

class ImageCache { private var cache: [URL: WeakBox<UIImage>] = [:] func image(for url: URL) -> UIImage? { return cache[url]?.value } func setImage(_ image: UIImage, for url: URL) { cache[url] = WeakBox(value: image) } } final class WeakBox<T: AnyObject> { weak var value: T? init(value: T) { self.value = value } }

キャッシュ辞書の値をWeakBoxラッパーを用いて弱として宣言することで、ImageCacheは画像がまだメモリ内に存在するかを確認でき、アクティブにそのアバターを表示しているビューコントローラーがない場合には自動的に再請求を行うことができました。これにより、メモリリークと手動の帳簿管理オーバーヘッドが排除され、迅速なフィードのスクロール中にピークメモリ使用量が40%削減され、システムのメモリ監視によりアプリ終了を防ぎました。

候補者が見逃すことが多い点

なぜ弱参照へのアクセスは強参照へのアクセスよりも遅くなることがあり、このパフォーマンス差が測定可能になる特定の条件は何ですか?

弱参照へのアクセスには、オブジェクトヘッダーに格納されたサイドテーブルポインタのデリファレンス、そのサイドテーブルからオブジェクトポインタを原子的にロードし、ゼロ化されているかを確認する必要があります。オーバーヘッドは最小限ですが(通常は単一の追加の間接参照)、大規模なコレクション(数千のアイテム)を反復処理する際に、すべての要素が閉じたループ内で弱参照を介してアクセスされると、測定可能になります。一方、強参照は原子的な保証なしに単一のポインタチェースのみを必要とします。

実装レベルでの無所有参照と弱参照の違いは何であり、オブジェクトが解放された後に無所有参照にアクセスしようとすると、nilを返すのではなくランタイムクラッシュを引き起こすのはなぜですか?

弱参照はゼロ化を可能にするためにサイドテーブルを利用するのに対し、無所有参照(デフォルトの安全モードでは)もサイドテーブルを参照しますが、無所有参照が存在する限りオブジェクトが割り当てられていると仮定します。そのため、オブジェクトが解放されるとクラッシュが発生します。これは、サイドテーブルエントリが破壊済みとしてマークされますが、nilにはされません。候補者は、安全でない無所有参照がサイドテーブルを全くバイパスし、解放後にアクセスするとメモリが破損するダングリングCポインタのように振る舞うことをしばしば見逃します。一方、安全な無所有参照は少なくともサイドテーブルの解放ビットを介して決定論的にトラップされます。

なぜオブジェクトインスタンスのメモリは、deinitが完了し、すべての強参照が失われた後でもヒープに残り続け、実際にこのメモリはいつ解放されるのですか?

そのメモリは、サイドテーブルが弱参照カウントを保持しているため持続します。オブジェクトヘッダーとその関連ストレージは、弱カウントがゼロに達するまで再請求できません。これにより、弱参照が再利用されたメモリを指すことは決してありません。最後の弱参照が破棄され(弱カウントがゼロに減少)、その時点でランタイムがサイドテーブルとオブジェクトのメモリ領域の両方を解放するまで、これは開発者には見えないプロセスですが、使いまわし後の不正使用の脆弱性を防ぐために重要です。