blog

Objective-Cオンデマンド・ディスカード・メッセージの動的ディスパッチ

0.はじめに この記事では、Objective-C のメッセージの動的ディスパッチについて簡単に説明します。 1.メッセージの動的ディスパッチ オブジェクトのisaポインタからクラス構造を見つけ、その...

Jun 4, 2020 · 10 min. read
シェア

この記事では、Objective-Cのメッセージの動的ディスパッチについて簡単に説明します。

メッセージの動的配信

Objective-Cは動的言語であり、静的言語がコンパイル時やリンク時に行うことの多くを実行時に行うため、コードをより柔軟にすることができます。

インスタンス・オブジェクトにメッセージが送信されるとき:

  1. オブジェクトの isa ポインタを通してクラス構造を見つけ、そのクラス構造の代入テーブルでメソッドセレクタを検索します。
  2. セレクタが見つからない場合、 objc_msgSend は親クラスのクラス構造を見つけ、親クラス構造のディストリビューション・テーブルでメソッド・セレクタを探します。
  3. 見つからない場合は、 NSObject クラスにたどり着くまで親クラスを探し続けます。
  4. セレクタが見つかると、関数はテーブル内のメソッドを呼び出し、受信オブジェクトのデータ構造を渡します。

メッセージパッシングの処理を高速化するために、ランタイムシステムは メソッドが使用されるときに、そのメソッドのセレクタとアドレスをキャッシュ します。

各クラスには、継承されたメソッドと、そのクラスで定義されたメソッドのセレクタを含むことができる個別のキャッシュがあります。ディスパッチ・テーブルを検索する際、メッセージ・パッシング・ルーチンはまず受信オブジェクト・クラスのキャッシュをチェックします。メソッド・セレクタがキャッシュにある場合、メッセージ・パッシングは関数呼び出しよりもわずかに遅くなります。アプリケーションがキャッシュを "ウォームアップ "するのに十分な時間実行されると、 送信するメッセージのほとんどすべてがキャッシュされたメソッドを見つけます。アプリケーションが実行されると、キャッシュは新しいメッセージに対応するために動的に成長します。

放棄オンデマンドメッセージの動的配信

時間のかかる問題

消費時間の例

以上、時間のかかる動的ディスパッチについて説明しました。システムはメッセージの受け渡しを高速化するためにキャッシュを行いますが、それでも時間のかかる処理は存在します。ほとんどの場合、この時間のかかる部分を無視することができますが、特定のメソッドを繰り返し呼び出す必要がある場合は、静的コールの使用を検討することができます。

- (void)viewDidLoad {
 [super viewDidLoad];
 [self msgSend];
 [self imp];
}
- (void)msgSend {
 CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
 NSString *key = @"DemoIntegerKey";
 for (int i = 0; i < ; i++) {
 [[NSUserDefaults standardUserDefaults] setInteger:i forKey:key];
 }
 NSLog(@"%d", (int)[[NSUserDefaults standardUserDefaults] integerForKey:key]);
 CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
 NSLog(@"%f", end - start);
}
- (void)imp {
 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
 SEL sel = @selector(setInteger:forKey:);
 void(*imp)(id, SEL, NSInteger, NSString *) = (void(*)(id, SEL, NSInteger, NSString *))[userDefaults methodForSelector:sel];
 CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
 NSString *key = @"DemoIntegerKey";
 for (int i = 0; i < ; i++) {
 imp(userDefaults, sel, i, key);
 }
 NSLog(@"%d", (int)[[NSUserDefaults standardUserDefaults] integerForKey:key]);
 CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
 NSLog(@"%f", end - start);
}
//  
 .00 CodeDemo[-: CodeDemo[.0-: CodeDemo[-: CodeDemo[.

出力から、動的なディスパッチから静的な呼び出しへの切り替えは論理的に問題なく、10wの呼び出しは0.8秒の利得をもたらすことがわかります。

テスト情報:

  • Xcode 11.6

  • iPhone 11 Pro

  • MacBook Pro

    • プロセッサー 2 GHz デュアルコア Intel Core i5
    • 8 GB 1867 MHz LPDDR3

そういえば、 +load コールについて一言。

loadは アプリケーションの起動時に呼ばれ、+initializeは クラスが初めて使用される時に呼ばれる」という記事を読んだことがあるかもしれません。

不思議に思う人もいるかもしれませんが、クラスを最初に使う のは +loadを 呼び出すときではないでしょうか?

実際、この記述は不正確で、次のようにすべきです 。 メソッドのルックアップや転送処理中に、初めてクラスにメッセージを送ると、 次のコードに示すように +initialize がトリガーされます

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
 bool initialize, bool cache, bool resolver)
{
 ...
 // 現在のクラスを最初に呼び出すときは、initialiseコードを実行する。
 if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
 // クラスを初期化し、メモリ空間を開放する
 cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
 // runtimeLock may have been dropped but is now locked again
 }
 ...
 return imp;
}

一方、 +loadは スタティック・コールを使用する直接的な方法です。

typedef void(*load_method_t)(id, SEL);
static void call_class_loads(void)
{
 int i;
 
 // Detach current loadable list.
 struct loadable_class *classes = loadable_classes;
 int used = loadable_classes_used;
 loadable_classes = nil;
 loadable_classes_allocated = 0;
 loadable_classes_used = 0;
 
 // Call all +loads for the detached list.
 // ローダブルをトラバースする_class他にヒントがあれば、コメント欄で遠慮なくシェアしてほしい。+loadIMPを使用して、直接
 for (i = 0; i < used; i++) {
 Class cls = classes[i].cls;
 load_method_t load_method = (load_method_t)classes[i].method;
 if (!cls) continue; 
 if (PrintLoading) {
 _objc_inform("LOAD: +[%s load]
", cls->nameForLogging());
 }
 (*load_method)(cls, @selector(load)); // impダイレクト・コール
 }
 
 // Destroy the detached list.
 if (classes) free(classes);
}

注目してください:

load メソッドは、関数のメモリアドレスを直接使って呼び出されます。

つまり、クラス間、親クラス間、カテゴリー間での+loadメソッドの呼び出しは相互に排他的です

サブクラスは親クラスの +load メソッドを積極的に呼び出すことはありません。クラスとカテゴリの両方が +load を 実装している場合は、両方の +load メソッドが別々に呼び出されます。

バイナリサイズ

バイナリサイズの変更例

NSUserDefaults 関連のコードを例にしてみましょう。プロジェクト内の多くの軽量な永続化は、以下のコードのような NSUserDefaults を 使用して行われます:

[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:@"XXXKey"];

しかし、このコードが増えると、バイナリコードのサイズに影響を与えます。例えば、以下の6行の セット コードを20回繰り返し、合計120回コールして、パッケージサイズをチェックします。

@implementation ViewController
- (void)viewDidLoad {
 [super viewDidLoad];
 [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:@"CodeDemoInteger0Key"];
 [[NSUserDefaults standardUserDefaults] setInteger:1 forKey:@"CodeDemoInteger1Key"];
 [[NSUserDefaults standardUserDefaults] setInteger:2 forKey:@"CodeDemoInteger2Key"];
 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"CodeDemoBool0Key"];
 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"CodeDemoBool1Key"];
 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"CodeDemoBool2Key"];
 ... x 20
}
@end

この時点でMachOファイルは 67KB であることがわかります。

NSUserDefaults standardUserDefaults]を取り出して 、もう一度見てみてください:

@implementation ViewController
- (void)viewDidLoad {
 [super viewDidLoad];
 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
 [userDefaults setInteger:0 forKey:@"CodeDemoInteger0Key"];
 [userDefaults setInteger:1 forKey:@"CodeDemoInteger1Key"];
 [userDefaults setInteger:2 forKey:@"CodeDemoInteger2Key"];
 [userDefaults setBool:YES forKey:@"CodeDemoBool0Key"];
 [userDefaults setBool:YES forKey:@"CodeDemoBool1Key"];
 [userDefaults setBool:YES forKey:@"CodeDemoBool2Key"];
 ... x 20
}

この時点でのMachOファイルは 59KBで-=67-59=8 KBの 節約になっていることがわかります。

テスト情報:

  • Debug包

  • Xcode 11.6

  • iPhone 11 Pro

  • MacBook Pro

    • プロセッサー 2 GHz デュアルコア Intel Core i5
    • 8 GB 1867 MHz LPDDR3

バイナリー・サイズのばらつきの原因

// main.m
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
 [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:@"CodeDemoInteger0Key"];
 return 1;
}

上記のコードをClangで書き換えて、ターミナルに入力してください:

$ xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc -fobjc-arc -mios-version-min=8.0.0 -fobjc-runtime=ios-8.0.0 main.m

出来上がった main.cpp ファイルには次のようなコードがあります:

// main.cpp
int main(int argc, char * argv[]) {
 ((void (*)(id, SEL, NSInteger, NSString * _Nonnull __strong))(void *)objc_msgSend)((id)((NSUserDefaults * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSUserDefaults"), sel_registerName("standardUserDefaults")), sel_registerName("setInteger:forKey:"), (NSInteger)0, (NSString *)&__NSConstantStringImpl__var_folders_n5_gxnsjpjltxf49yqr0000gp_T_ViewController_bc73cd_mi_0);
 return 1;
}

シンプルに:

// main.cpp
int main(int argc, char * argv[]) {
 objc_msgSend(objc_msgSend(objc_getClass("NSUserDefaults"), sel_registerName("standardUserDefaults")), sel_registerName("setInteger:forKey:"), (NSInteger)0, (NSString *)&__NSConstantStringImpl__var_folders_n5_gxnsjpjltxf49yqr0000gp_T_ViewController_df801d_mi_0);
 return 1;
}

少し最適化すれば:

// main.m
static NSString *Integer0Key = @"CodeDemoInteger0Key";
int main(int argc, char * argv[]) {
 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
 [userDefaults setInteger:0 forKey:Integer0Key];
 [userDefaults setInteger:0 forKey:Integer0Key];
 return 1;
}
// 簡易メイン.cpp
static NSString *Integer0Key = (NSString *)&__NSConstantStringImpl__var_folders_n5_gxnsjpjltxf49yqr0000gp_T_ViewController_caa7f7_mi_0;
int main(int argc, char * argv[]) {
 NSUserDefaults *userDefaults = objc_msgSend(objc_getClass("NSUserDefaults"), sel_registerName("standardUserDefaults"));
 objc_msgSend(userDefaults, sel_registerName("setInteger:forKey:"), (NSInteger)0, (NSString *)Integer0Key);
 objc_msgSend(userDefaults, sel_registerName("setInteger:forKey:"), (NSInteger)0, (NSString *)Integer0Key);
 return 1;
}

objc_msgSend の戻り値のローカル変数を取り出して、それを呼び出すことで、重複コードを大幅に削減できることがわかります。元の要件に従って 60 回の [[NSUserDefaults standardUserDefaults]setInteger:forKey:]; 呼び出しがある場合、この時点では +=60+1=61 個の objc_msgSend しかないため、多くのコードを節約できます。

2つのMachOファイルを比較すると、 viewDidLoad メソッドのサイズが 0xB02Fから 0x901Cに 減少していることもわかります。

6759=8//=kb/1024/8=1kb

コードの最適化は、以下の2つのケースで考えることができます:

  • メソッドが何度も何度も呼び出される場合

    • IMPを直接取得し、メッセージ送信を使わずに直接呼び出します。
  • コード内で長い呼び出しが繰り返され、その動的性質を利用する必要がない場合

      • 同じ関数内であれば、最初に最長共通部分を取得し、それを個別に呼び出すことができます。

        User *user = self.listData.firstObject.user;
        NSInteger ID = user.ID;
        NSString *name = user.name;
        
      • 複数の関数の中にある場合は、この値を する ギルティ・プレジャーズ 書くことを検討してください。

        - (User *)firstUser {
         return self.listData.firstObject.user;
        }
        
      • センター メソッドや ゲッターメソッドを 使う必要がない場合は、メンバー変数を使って直接アクセスします。

        _name = @"Name";
        NSString *name = _name;
        

これらの方法は、個々には限られたリフトしかもたらさないかもしれませんが、大規模なプロジェクトでは、時間をかけて大きなリフトを生み出すことができます。

Read next

JSコードをすっきり翻訳する - 素早いベストプラクティス

コードが動くかどうかだけでなく、コードの書き方そのものの質も気にするのであれば、コードをすっきりさせることを追求していると言ってもいいでしょう。プロの開発者は、機械のためだけでなく、将来の自分のため、そして「他の人」のためにコードを書くことになります。 以上の議論から、コードをきれいに書く方法は、次のようになります。

Jun 3, 2020 · 8 min read