blog

iOSの基礎 - メソッド検索プロセスの分析(落とし穴あり)

iOSレイヤーシリーズへようこそ!\nこの記事の概要\nこの記事では、いくつかのケースを送信するメソッドの下部にあるメソッドの性質を分析し、プロセスを見つけるためのメソッドは、cache_tと組み合わ...

Oct 29, 2020 · 13 min. read
シェア

基礎となるiOSシリーズへようこそ!

この記事の概要

この記事では、主にいくつかのケースを送信するメソッドの下部にあるメソッドの性質を分析し、メソッドのルックアップ処理、cache_tと組み合わせて、メッセージ送信プロセスのよりマクロ的な理解。

なぜサブクラスはNSObjectのオブジェクトメソッドを実装するためにクラスメソッドを呼び出せるのですか?

メソッド検索プロセスを深く理解していないと、行き詰まってしまうかもしれません。ここでは、メソッド検索プロセスを分析します。

runtime

前回の記事「iOSボトムキャッシュ_tプロセス解析」では、cache_tがメソッドをキャッシュすること、そのメソッドが何であるか、メソッドを呼び出すと実際に何が行われるかについて説明しました。これらはすべてランタイムと密接に関係しています。

a.runtime

ocにランタイム機能があることは周知の事実ですが、ocの最下層はcやc++のような静的言語にコンパイルされており、ランタイムはありません。現時点では、iOSはocにランタイム機能を提供するために使用されるc、c++、アセンブリで書かれたapiのセットをカプセル化しています。

b.runtime

ランタイムには2つのバージョンがあります。

  • legacy

  • modern

基礎となるソースコードでは、__OBJC2__と__OBJC2__を使用しています!OBJC2__と__OBJC2__で区別しています。現在では一般的に __OBJC2__ が使用されているので、レガシーバージョンは基本的に無視してかまいません。

c.runtimeの呼び出し型は

ランタイムコールには次の3種類しかありません。

  • Objective - C Code

  • NSObject

  • runtime api

メソッドの性質

メソッドは実際にはclass_rw_tにひっそりとあるコードの断片で、技術的にはメソッドの本質が呼び出されるべき場所です。

最初にCJPersonクラスを作成し、初期化してメソッドを呼び出し、その下にカスタム関数を同時に呼び出します。

void play(){
 NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
 @autoreleasepool {
 CJPerson *person = [CJPerson alloc];
 [person work];
 
 play();
 }
 return 0;
}

クラスの性質を調べる際には、clangコンパイルが使われます。

clang -rewrite-objc main.m -o main.cpp

main.cppを開き、まっすぐ最後まで来てください。

int main(int argc, const char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
 CJPerson *person = ((CJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJPerson"), sel_registerName("alloc"));
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("work"));
 play();
 }
 return 0;
}

整理整頓と強い回転の除去

int main(int argc, const char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
 CJPerson *person = objc_msgSend(objc_getClass("CJPerson"), sel_registerName("alloc"));
 objc_msgSend(person, sel_registerName("work"));
 play();
 }
 return 0;
}

ご覧のように、メソッドを呼び出すと objc_msgSend を介してメッセージが送信されますが、 play() 関数を呼び出すとメッセージは送信されません。

実際、メッセージの送信は関数実装のインプットを探すプロセスであり、paly()関数ポインタは関数実装に直接アラインされ、メッセージを送信する必要はありません。

objc_msgSend には 2 つのパラメータがあります。

  • id メッセージ・レシーバー
  • sel メソッド番号

キャッシュの存在を仮定すると、これらの2つのパラメータは、キャッシュ_t内の対応するclsでidを使用することができ、selは、iOSの基礎となるキャッシュ_tのプロセス解析を理解することによって、ハッシュ添字を取得するキー&マスクを生成し、ここで明確になります。

メッセージ送信におけるいくつかの違い

開発経験に基づいて、メソッドは一般的に4つの方法で呼び出されます:このクラス・オブジェクト・メソッドこのクラス・クラス・メソッド親クラス・オブジェクト・メソッド、親クラス・メソッド

逐次検証の下で、CJPersonを継承したCJStudentクラスを作成し、それぞれのオブジェクト・メソッドとクラス・メソッドを宣言してください。それをCJStudentで呼び出し、clangでコンパイルしてください。

- (void)study{
 [super work];//親クラスのオブジェクト・メソッド
 [self study];//このクラスのオブジェクト・メソッド
}
+ (void)play{
 [super buy];//親クラスのメソッド
 [CJStudent play];//このクラスメソッド
}

clangのresultsセクションに対応しています:

static void _I_CJStudent_study(CJStudent * self, SEL _cmd) {
 //親クラスのオブジェクト・メソッド
 objc_msgSendSuper({self, class_getSuperclass(objc_getClass("CJStudent"))}, sel_registerName("work"));
 //このクラスのオブジェクト・メソッド
 objc_msgSend(self, sel_registerName("study"));
}
static void _C_CJStudent_play(Class self, SEL _cmd) {
 //親クラスのメソッド
 objc_msgSendSuper({self, class_getSuperclass(objc_getMetaClass("CJStudent"))}, sel_registerName("buy"));
 //このクラスメソッド
 objc_msgSend(objc_getClass("CJStudent"), sel_registerName("play"));
}

連結業績は以下の通り:

おわかりのように、ここでの最も明白な違いは,objc_msgSendと書く。_msgSendSuper

基本的に、メッセージ送信の不整合の主な原因は、よく言われるobjc_msgSendSuperにあることが確認でき、クリックしないと見えないobjc_msgSendSuperはどうなのか?

ご覧のように、objc_msgSend との主な違いは、最初のパラメータ objc_super です。

struct objc_super { __unsafe_unretained _Nonnull id receiver; #if !defined(__cplusplus) && !__OBJC2__ __unsafe_unretained _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif };

objc_superは、2つの引数を取る構造体で、1つはidレシーバで、ランタイムが__OBJC2__バージョンになったので、2つ目はクラスsuper_classです。

パラメータの意味を理解することで、上記の結論が理解できるようになります:

  • 親クラスのオブジェクト・メソッドは、親クラスのメソッド・リストで
  • 親クラスのメソッドは、親クラスのメタクラスのメソッド・リストで
  • このクラスのメソッドは、呼び出し元のボディにクラス化されます。
  • superはobjc_msgSendSuperを呼び出し、親メソッドリストを探すようにシステムに指示しますが、呼び出し元のボディはselfのままであることに注意してください。

メソッド検索処理

エントリポイントの検索

ここでは、すべてのソースは、objc_msgSend、古いルール、またはソースコードに見に行くことを指しています。しかし問題なのは、ソースコードのコピーがたくさんあることです。

何か考えてください:

現在の既知の条件によると、呼び出しメソッドは、objc_msgSendを実行し、プロジェクト内のobjc_msgSendシンボリックブレークポイントの下で、そのステップの呼び出しメソッドに実行するようにシンボリックブレークポイントを開きます。ブレークポイントが来ます:

実はobjc_msgSendがobjcのソースコードの中にあることを発見し、ようやく小さなスコープを見つけました。

objcのソースコードを開くか、objc_msgSendを検索してみると、直倒れに関連するものが600以上あり、この方法ではダメなようですが、別の検索キーワードも考えてみましょう。

もう一つの考え方は、objc_msgSendをメソッドで呼び出すには、メソッド名()のメソッドの一般的な書式を呼び出し、objc_msgSend()を検索することができます、検索結果は、.hの部分とアセンブリの部分の2つだけです、まず、.hは除外することができます、.hでは、実装して呼び出すためのソースコードは不可能です、コンパイルだけが残ります、下のobjc_msgSendは、アセンブリで実装されていますよね?一番下のmsgSendはアセンブリで実装されています。

振り返ってみると、objc_msgSendは変数引数で、静的言語cでは効率的に認識できず、確かにアセンブリを使って実装されている可能性が高いです。

いろいろ調べた結果、objc_msgSendの高速ルックアップはアセンブリで実装されていることが確認され、2つの理由が導き出されました:

  • c では、未知の引数を保持したり、関数を通して任意の関数ポインタにジャンプすることはできません。
  • objc_msgSend は一番下にある高頻度イベントであり、高いパフォーマンスが要求されるため、十分な速度が必要です。
  • アセンブリを使用すると、システム機能がフックされるのを効果的に防ぐことができ、より安全です。

クイック検索

我々はobjc_msgSendがアセンブリの実装であることを知っているので、それは唯一のアセンブリを見るのは難しいことができます。ここでは、一般的に使用されるarm64から開始することを選択し、一般的に開始するENTRYの入り口からアセンブリを見て、直接似たようなENTRY objc_msgSendの場所を探索を開始することです!

豆知識:x0~x7にはパラメータが格納され、x0には戻り値も格納されます。

1.最初のパラメータがnullかどうか、つまり、レシーバーselfかどうかを比較する。
2.selfがTaggedPoint型かどうかを判断する。この型はメッセージを送信する必要がない。.
3.最初のパラメータidのx0アドレスの値は、isaであるp13に置かれる。
4.isaを通して_maskクラスを取得する、これがp16がclassと等しい理由である。
5.isaの検索が終了し、最初に内部のキャッシュを見るかどうか、つまり、迅速な検索処理が始まった

と⑤についてはこちらをご覧ください。

GetClassFromIsa_p16内部的に isa_mask を介してクラスを取得

キャッシュ・ルックアップ NORMAL

 * CacheLookup NORMAL|GETIMP|LOOKUP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *	 x1 = selector //番目のパラメータ sel
 *	 x16 = class to be searched //isaによって次のようになる。
 *

3種類のCacheLookup:通常の検索|GETIMP|低速検索

#define SUPERCLASS __SIZEOF_POINTER__
#define CACHE (2 * __SIZEOF_POINTER__)
1.x16CACHEをパンしてキャッシュを取得する_t,キャッシュを設定する_tNSObjectの値を取り出してp10とp11に入れる。p10には8ビットを占有するバケット、p11には4ビットずつ占有する占有とマスクを入れる。
struct cache_t {
 struct bucket_t *_buckets;// 
 mask_t _mask;//4 
 mask_t _occupied;//4 
}
2.w1の_cmd & w11これは、メソッドを見つけるためのハッシュ添え字である。ここでwを使うのは、マスクの型が32ビットで十分であり、小端モードがマスクである最後の4ビットを取るからである。
static inline mask_t cache_hash(cache_key_t key, mask_t mask) {
 return (mask_t)(key & mask);
}
3.パンしてバケツの有効アドレスを取得し、x12バケツからimpを取り出してp17に、selをp9に入れる。
4.バケツに入っているセルと渡されたcmdを比較し始めたが、NoEqualの場合は2fCheckMissの処理に入り、バケツを探すループに入り、そうでない場合はCacheHitのキャッシュヒットになる。
5,CheckMissが呼ばれるが、CheckMissはcbzでselが0かどうかを判断し、0でなければバケットを判断する。 ==buckets,Equal3fにジャンプし始めると、そうでない場合は、バケットを見つけるためにサイクルダウンを開始し、バケットを見つけるためにサイクルダウンを開始し、マルチスレッド更新キャッシュを防ぐために、プロセスを再検索するためにジャンプ1bがある。__objc_msgSend_uncachedスローフロー
.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP 
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
6.上記を繰り返す
    3. 検索を終えてもキャッシュが見つからない場合は、直接JumpMissにジャンプして、強制的に__objc_msgSend_uncached 
.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached

__objc_msgLookup_uncached。

__objc_msgLookup_uncached現在のところ、キャッシュがミスした場合、そのキャッシュは 、に来ることが知られているので、その流れを見てください:

STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
MethodTableLookup
ret

objc_msgLookup_uncachedプロセス内にはMethodTableLookupしかなく、これは文字通りメソッド・リスト検索を意味します。アセンブリのルックアップでもあるのでしょうか?私は下を見続けることしかできません:

.macro MethodTableLookup
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp
	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8, [sp, #(8*16+8*8)]
	// receiver and selector already in x0 and x1
	mov	x2, x16
	bl	__class_lookupMethodAndLoadCache3

__class_lookupMethodAndLoadCache3かなり長い、かなり理解していない特定の最初の段落が、操作のアドレスである見ることができる、直接アドレスパラメータの準備の後、全体的なプロセスの読書に影響を与えません。

__class_lookupMethodAndLoadCache3通常の検索は.

すべてのコールは、同様の実装プロセスがない限り、アップルがオープンソースにしなかったと思うかもしれませんが、そうであれば、ここに探査は終わりに来ているようです。

__objc_msgLookup_uncached__objc_msgLookup_uncached再びobjcに書く_msgSend__objc_msgLookup_uncached自暴自棄になったら、落ち着いて考えてください。objc_msgSendシンボルのブレークポイントを取ってきて、アセンブリ呼び出しがこうなっているか確認してください。

_objc_msgLookup_uncached_objc_msgLookup_uncachedobjc_msgSendの後、確かに , に来ますが、よく見るとアンダースコアが欠けています。__class_lookupMethodAndLoadCache3への呼び出しがあるかどうか確認してください。

_class_lookupMethodAndLoadCache3class_lookupMethodAndLoadCache3の呼び出しは確かに_objc_msgLookup_uncachedにありますが、それを見ると、, で、その前にアンダースコアがありません。そして、それはobjc-runtime-new.mmの4846行目にあるとマークされています。

_class_lookupMethodAndLoadCache3これを見て、私は新しい世界を発見したような気がします。もしかして、底辺が直接電話をかけて検索しているのでしょうか?

_class_lookupMethodAndLoadCache3まず、アノテーションがある場所を探してみると、案の定、.NET Frameworkの実装が見つかりました。

_class_lookupMethodAndLoadCache3また、.NETの呼び出しの前にアドレス・パラメーターの処理がある理由を逆に説明します:

  • (C、C++、静的言語がパラメータリストを決定する必要があるため、準備する必要があります)

これは objc_msgSend の高速ルックアップ処理で、キャッシュ・ルックアップとも呼ばれます。要約すると、高速ルックアップはcache_t内のキャッシュを検索することです、キャッシュヒットは直接終了です;まだ見つからないすべてを検索し、準備作業の前に低速ルックアップを行うために開始し、低速ルックアップ処理にジャンプします。

ゆっくり検索

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } これは、lookUpImpOrForwardを呼び出すだけです。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }

lookUpImpOrForwardメソッドは長く、準備の部分と与えられた部分を見つけ、最後にセクションの検証と結論を与えることに分かれています。

a.準備

1.キャッシュが存在するかどうかを判断し、存在する場合は、直接clsとselを介して直接impを取得し、返す。
2.関連クラス情報判定
 a.与えられたクラスをすべての既知のクラスのリストと照合し、問題があれば内部的に例外を投げる。
 b.決定クラスが実装されているかどうか、実装するために実装されていない、後のクラスのロードの章のこの部分は、主にスーパークラスと親クラスとメタクラスの再帰的な実装に向かってisaに従って、詳細に分析され、オブジェクトのメソッドとクラスのメソッドを準備しながらチェーンを見つける。
 c.クラスが初期化されているかどうかを判断し、初期化されていなければ初期化を解除する。

b.検索部分

コードのルックアップ部分は、1つの画面に収めるにはまだ長すぎるので、2つのシートに分割しています。

1.クラスの準備が整いましたが、再度キャッシュの有無を判断するために、直接clsとselを介して直接impを取得し、返すがある。
2.クラスのメソッドリストに目を通し、メスを見つけたら、まずキャッシュを埋めてからリターンする。ここでは、外側のマルチユース・クラスa{},メスのリネームを防ぐために、ローカル・スコープを形成する。
3.再帰的に親クラスのキャッシュを見つける
 a.impが存在し、メッセージ転送タイプでない場合は、キャッシュに入れられ、それを
 b.そのメソッドが存在し、メッセージ転送タイプである場合、検索は停止され、そのメソッドはキャッシュされない。
4.親クラスのキャッシュが終了し、見つからない場合は、親クラスのメソッドリストを検索し、methが見つかれば、まずキャッシュに埋めてから返す。
5.すべての親クラスを再帰的に検索してもimpが見つからない場合は、メソッド転送処理を開始する。メソッド転送を詳しく分析する,

上記は objc_msgSend のスロー・ルックアップ処理です。一言で言えば、遅いルックアップは、このクラスから親クラス、そして最後にNSObjectへのメソッドルックアップチェーンです。まず、このクラスのmethod_listを見つけ、キャッシュが満たされている見つける;親クラスでキャッシュとmethod_listを見つけるために見つけることができない、キャッシュが満たされている見つける;最後の転送を見つけることができません

c.検証と結論

オブジェクト・メソッド
1.オブジェクト・メソッド - 所有 - 成功;
2.オブジェクト・メソッド - 私にはない - 父のを見つけた - 成功!;
3.オブジェクト・メソッド - 持ってない - パパは持ってない - パパのパパを探す - NSObject - 成功!;
4.オブジェクト・メソッド - 私にはない - お父さんにはない - お父さんのお父さんにはある> NSObject そして、クラッシュする。;
クラスのメソッド
1.クラス・メソッド - 所有 - 成功;
2.クラス・メソッド - 僕は持っていない - パパは持っている - 成功!;
3.クラス・メソッド - あなたは持っていない - お父さんは持っていない - お父さんのお父さんを探している - 」。> NSObject そして、オブジェクト・メソッドはない!
4.クラス・メソッド - あなたは持っていない - お父さんは持っていない - お父さんのお父さんを探している - 」。> NSObject どちらでもない - オブジェクト・メソッドがある - 成功

クラスメソッドの合計だけがオブジェクトメソッドを呼び出すことになるのですか?オブジェクト・メソッドを実装するためにクラス・メソッドを呼び出すというocの世界観には合わないですね。

検証方法の提供

NSObjectカテゴリにオブジェクト・メソッドを定義して実装し、この定義されたオブジェクト・メソッドを任意のクラス名で呼び出す。
分類ステートメント
- (void)instanceMethod{
 NSLog(@"%s--オブジェクト・メソッド",__func__);
}
クラス名呼び出し
int main(int argc, const char * argv[]) {
 @autoreleasepool {
 [CJPerson instanceMethod];
 }
 return 0;
}

実行した結果、うまくいきました。この異例な状況をどう考えるかは、実はisaとsupclassのアライメント図から探ることができます。

説明: クラス名で呼び出すと、メタクラスのメソッド・リストを順番に調べていき、最終的にルート・メタクラスのメソッド・リストを見つけますが、対応するクラス・メソッドを見つけることができません。このとき、ルート・メタクラスの supclass はルート・クラス NSObject を指しているので、NSObject のメソッド・リストを調べに行きます。NSObject のメソッド・リストにはオブジェクト・メソッドが格納されているので、instanceMethod というオブジェクト・メソッドが見つかりました。NSObject のメソッド・リストにはオブジェクト・メソッドが格納されているので、instanceMethod というオブジェクト・メソッドを見つけました。

最後に書く

objc_msgSend は iOS の開発ではバイパスできず、そのプロセスはcache_t のプロセス分析と密接に関係しています。次の章は、メッセージ送信の最後の部分、つまりメッセージ転送のプロセス解析です。ご期待ください。

Read next

vueのkeep-aliveについて知らないかもしれないこと

keep-alive はパッケージのルートの最後の履歴を保存でき、切り替えるときに2つの重要なフックがあります: activated と active です。 スイッチバックするとき、activatedフック機能がトリガーされます。だから、そのような一般的に使用されるページスクロールホイールトップなどの一連の操作を実行することができます:scrollTopは0です。

Oct 28, 2020 · 1 min read