ハイライト
2020 年の新しい iOS および macOS Objective-C ランタイムは、クラスの読み取りデータと書き込みデータを分割し、相対メソッド リストに切り替え、arm64 タグ付きポインター形式を調整することにより、システム レベルのメモリ使用量を削減します。同時に、アプリはパブリック ランタイムと Foundation API を介してオブジェクト情報を読み取ることのみが必要です。
主要内容
ほとんどの開発者は、Objective-C ランタイム ヘッダー ファイルを毎日開くわけではありません。問題は、ほぼすべての iOS および macOS アプリがその上に構築されていることです。Objective-C クラスはそれを使用し、Swift クラスは同じクラス インフラストラクチャを共有します。 Apple は、このセッションの冒頭で非常に簡単に説明しました。これは、新しい API の移行方法を教えるための講義ではありません。理想的には、コードを変更しない場合、アプリはシステム ランタイムの短縮による恩恵を受けることができます。 (00:11)
本当のリスクは、別の種類のコードから発生します。デバッグ ツール、パフォーマンス ツール、レガシー ライブラリ、またはランタイムの内部構造を直接読み取るサードパーティの依存関係です。これらのフィールドの場所が過去に安定していたと思われたからといって、それが API であるとは限りません。 2020 ランタイムでは、これらの内部レイアウトが移動されました。クラスの読み取りおよび書き込みデータが分割され、メソッド リストが 64 ビット ポインターから相対オフセットに変更され、arm64 タグ付きポインターのビット レイアウトも変更されました。ビットや構造を直接読み取るコードはクラッシュしたり、さらに悪いことにユーザー データを誤って処理したりする可能性があります。 (00:32、21:24)
Appleが与えた道は明らかだ。外側は通過する必要があるため、ランタイムは内側で変化し続ける可能性があります。class_getName、method_getImplementation、isKindOfClass:、CFGetTypeIDこのタイプのパブリック API は情報を読み取ります。その結果、システムはより多くのメモリをクリーン メモリとして保持できるため、iOS デバイスのダーティ メモリのコストが削減され、アプリがより多くの利用可能なメモリ領域を取得できるようになります。 (07:01、22:27)
詳細
クラスデータ: めったに使用されない読み取りおよび書き込みフィールドを移動します。
(01:16) ランタイムのクラス オブジェクトは、メタクラス、スーパークラス、メソッド キャッシュなど、最も頻繁にアクセスされる情報を保存します。より多くの静的情報が配置されます。class_ro_t内部、roクラス名、メソッド、プロトコル、インスタンス変数などの読み取り専用を示します。クラスが初めて使用されるとき、ランタイムはまた、class_rw_t、実行時に生成される保存データは変更される可能性があります。
主な違いは、クリーンなメモリとダーティなメモリの違いです。class_ro_t一度読み取られたデータは変更されず、システムがメモリを必要とするときに破棄してディスクから再ロードできます。class_rw_tプロセスの実行中に変更されます。 iOS にはスワップがないため、ダーティ メモリのコストが高くなります。 Apple は実際のデバイスでそれを測定しました。システム内でclass_rw_tこれは約 30 MB を消費しますが、実際にメソッドを変更するクラスは約 10% だけであり、Swift の分解された名前フィールドはすべての Swift クラスで必要なわけではありません。したがって、ランタイムは、めったに使用されないフィールドを拡張レコードに移動します。約 90% のクラスではデータのこの部分を割り当てる必要がなく、システム レベルで約 14 MB が節約されます。
heap Mail | egrep 'class_rw|COUNT'
キーポイント:
heap Mailメールプロセスのヒープメモリ割り当てを表示します。 -egrep 'class_rw|COUNT'だけ保管してくださいclass_rw_t関連する行と統計ヘッダー。- セッションデモでは、Mail は約 9,000 を使用します
class_rw_t、900 をわずかに超えるものだけが詳細な情報を必要とします。 - このコマンドはアプリ コード パスではなく、Mac 上のランタイム データ構造の変更によるメモリへの影響を検証するのに適しています。 (05:37)
クラスを内部フィールドで判断しないでください。クラス名、親クラス、メソッド リストにはすべてパブリック ランタイム API があります。 (07:35)
class_getName
class_getSuperclass
class_copyMethodList
キーポイント:
class_getNameクラス名を読んで依存関係を回避するclass_ro_t特定のレイアウト。 -class_getSuperclass親クラスを読んで、クラス オブジェクト内のフィールドの順序が変更されていないと想定しないようにします。 -class_copyMethodListランタイムが新旧の内部形式に対して処理するメソッドのリストを取得します。
メソッドリスト: 64 ビットポインタを 32 ビット相対オフセットに変更する
(08:04) Objective-C メソッドの各エントリには、セレクター、型エンコーディング、実装の 3 つのフィールドが含まれています。古い形式では、各フィールドは 64 ビット システムのポインターであり、メソッド エントリは 24 バイトを占めます。メソッド実装とメタデータは同じバイナリ イメージ内にあり、64 ビット アドレス空間全体を指す必要がないため、2020 ツールチェーンはこれらのフィールドをバイナリ内の 32 ビット相対オフセットに変換できます。
これには 3 つの影響があります。まず、メソッドのエントリ サイズが半分になります。 Apple は、一般的な iPhone でシステムレベルのメソッド データを約 80 MB 測定していますが、新しい形式では約 40 MB が節約されます。第 2 に、相対オフセットでは、ロード後にダイナミック リンカーが絶対アドレスを修正する必要がなく、実際の読み取り専用メモリに保持できます。 3 番目に、スウィズリングは元のメソッド リストを書き換えることはなくなりますが、グローバル テーブルを介してメソッドを置き換えられた実装にマップし、1 回のスウィズルでメソッド リストのページ全体が汚れることを回避します。 (10:40、11:30)
Xcode は、最小のデプロイメント ターゲットに基づいて、対応する形式を生成します。ターゲット システムが十分に新しい場合、ツールチェーンは相対メソッド リストを自動的に生成します。古いシステムのサポートが依然として必要な場合、Xcode は古い形式を生成し続けます。このリスクは、ビルド製品の展開ターゲットが実際に実行されているシステムよりも新しい場合に発生します。古いランタイムは 2 つの 32 ビット フィールドを 64 ビット ポインターとして読み取り、最終的にはメソッド情報を読み取るときにクラッシュします。 (12:23、13:31)
同じ 3 つのフィールドをパブリック API で読み取ることができます。 (14:38)
method_getName
method_getTypeEncoding
method_getImplementation
キーポイント:
method_getName戻りメソッドセレクター。 -method_getTypeEncodingイントロスペクションとメッセージ転送で使用する戻りパラメーターと戻り値のエンコーディング。 -method_getImplementationメソッド実装エントリを返します。呼び出し元は、メソッド リストが古いポインター形式であるか、新しい相対オフセット形式であるかを知る必要はありません。
タグ付きポインター: arm64 形式が変更され、型チェック API
(14:53) タグ付きポインターは、実際のオブジェクト ポインターの 0 に固定された空きビットを使用して、小さなオブジェクトをポインター値に直接エンコードします。NSNumber、NSDateなどのタイプを使用して、小さな値にヒープ オブジェクトを割り当てることを回避します。また、セッションは、プロセスの開始時にこれらの値がランダムな値で難読化されることを思い出させます。メモリ内のペイロードを直接表示しようとすると、難読化の結果が表示されます。
Intel は下位ビットを使用してタグ付きポインターをマークします。 arm64 の古い形式では最上位ビットが使用されます。objc_msgSend通常のポインタ、タグ付きポインタ、nil共通の道。 2020 年の arm64 フォーマットでは、引き続き最上位ビットのタグが保持されますが、タグ番号は下位 3 ビットに移動されます。拡張タグは、ARM Top Byte Ignore で無視できる上位領域に配置されます。このようにして、拡張タグ付きポインタのペイロードは、バイナリの定数データを指す通常のポインタを収容できるため、本来はダーティ メモリに保存する必要があるデータが削減されます。 (19:03、20:08)
古いコードがビット マスクを使用してタグ付きポインターを独自に決定する場合、iOS 14 以降は間違った答えが得られる可能性があります。オブジェクトと CF 型は両方とも標準 API に従う必要があります。 (21:52)
if ([obj isKindOfClass:[NSString class]]) {
// a string
}
NSUInteger length = [obj length];
if (CFGetTypeID(obj) == CFStringGetTypeID()) {
// a string
}
CFIndex length = CFStringGetLength(obj);
キーポイント:
isKindOfClass:タグ付きポインターと実際のオブジェクト ポインターの違いは Foundation に処理させます。 -[obj length]右NSString正常に動作するために、呼び出し元はペイロードを読み取る必要はありません。 -CFGetTypeIDそしてCFStringGetTypeIDCore Foundation層の種類判定エントリです。 -CFStringGetLengthCF API を介した長さの読み取りも、タグ付きポインター ビット レイアウトに依存しません。
重要ポイント
実行時の依存関係の監査を実行する
何をすべきか: Objective-C ランタイムの内部構造、手書きのタグ付きポインター ビット マスクに直接アクセスするプロジェクト内のすべてのコードとサードパーティの依存関係をリストし、メソッド リストのレイアウトを想定します。
実行する価値がある理由: セッションでは、2020 年にこれらの内部レイアウトが変更されたと繰り返し述べています。古いコードは、ユーザーがシステムをアップグレードした後にクラッシュしたり、オブジェクトの種類を誤って判断したりする可能性があります。
開始方法: まずは検索してくださいclass_rw_t、method list、tagged pointer, ビット操作の種類を判断し、読み込んだクラスやメソッドのコードを次のように置き換えます。class_getName、class_copyMethodList、method_getImplementation。
デプロイメントターゲットチェックをバイナリ製品に追加
対処方法: CI 内のアプリ、フレームワーク、外部バイナリの最小デプロイメント ターゲットをチェックして、製品で実際にサポートされている最小システム バージョンを超えていないことを確認します。
実行する価値がある理由: 相対メソッド リストは、新しい形式を理解するために新しいランタイムに依存します。新しい形式で構築されたライブラリを古いシステム上で実行すると、2 つの 32 ビット フィールドが誤ったポインタに変換されます。
開始方法: ビルド設定、サードパーティ フレームワークの最小システム バージョン、および実際のテスト マトリックスを同じチェック スクリプトに入れ、サポートされている最も古いシステムで起動テストと動的コール スモーク テストを実行します。
ダーティ メモリ比較プロセスを構築する
やるべきこと: Xcode、SDK、またはデプロイメント ターゲットをアップグレードした後、修正されたアプリと修正されたシナリオを使用して、ランタイム関連のダーティ メモリを比較します。
実行する価値がある理由: セッションの使用heap実証済みclass_rw_t分割後の収益。チームは同じ方法を使用して、アプリがシステム最適化の恩恵を受けるかどうかを確認することもできます。
開始方法: 安定した動作パスのセットを準備し、収集しますheap <AppName> | egrep 'class_rw|COUNT'出力し、結果をパフォーマンス レコードに書き込み、以前のビルドと比較します。
安全なオブジェクト タイプを判断する層をカプセル化します。
何をすべきか: 履歴コード内の Foundation/CF タイプの判断を少数のヘルパーに集中させ、それらを内部でのみ呼び出すようにします。isKindOfClass:、CFGetTypeIDおよび対応するタイプの API。
実行する価値がある理由: タグ付きポインター形式は、ランタイム管理者のみが安全に解釈できる詳細です。集中カプセル化後は、コードレビュー中に新しいビットレベルの判断が本体に入るのを防ぐのが簡単になります。
開始方法: 最初にカバーしますNSString、NSNumber、NSDateまた、一般的な CF タイプでは、タグ付きポインターの可能な入力に対する単体テストを追加し、古いビット マスク ブランチを削除します。
関連セッション
- Swift 用の Objective-C フレームワークを洗練する — Swift で Objective-C API を正しく自然に呼び出す方法の続きです。
- Swift の新機能 — Swift 2020 のランタイム パフォーマンスと言語ライブラリの更新を補足します。
- Swift でポインターを安全に管理する — Swift の安全でないポインター、生のポインター、バインドされたメモリの正しい境界について説明します。
- Unsafe Swift — 安全でない操作が必要な場合と、未定義の動作を回避する方法を言語安全モデルから説明します。
コメント
GitHub Issues · utterances