blog

iOS:謎のKVO

1.オリジン Aspectsがオープンソース化された後、多くの友人からAspectsとObjective-Cの違いを聞かれました。 GitHubでAspectsを見つけ、勉強した結果、Aspectsも...

Jun 24, 2020 · 19 min. read
シェア

はじめに

ほとんどのiOS開発者は、KVOの実装の詳細はほとんど知られていない一方で、KVOはポインタ交換レイヤに限定されて知っています。

KVOの基本に従ってKVOのような操作を実装し、単独で実行すると、すべてうまくいくことがわかりますが、自分の実装とシステムのKVOの実装が同じインスタンス上で同時に実行されると、あらゆる種類の奇妙なバグやクラッシュが次々と出てきます。

これはなぜですか?このような問題を解決するにはどうすればよいのでしょうか?次の章では、アセンブリ・レベルからKVOの謎を解き明かしていきます。

オリジンの側面

SDMagicHookのオープンソース化後、多くの友人から「SDMagicHookとAspectsの違いは何か」という質問を受け、GitHubでAspectsを見つけ、Aspectsもフック操作のisa交換の原理に基づいていることを理解しましたが、両者の具体的な実装やAPI設計にもいくつかの違いがあります。しかし、実装やAPI設計の点で両者にはいくつかの違いがあり、SDMagicHookはAspectsが解決できなかったKVOの衝突問題も解決しています。

1.1 SDMagicHookのAPIは、より使いやすく、より柔軟に設計されています。

SDMagicHookとAspectsの類似点と相違点の具体的な分析は、 github.com/S....

1.2 Aspectsが解決できなかったKVOの競合問題をSDMagicHookが解決

また、AspectsのreadmeにKVOの互換性の問題についての記述がありました:

現在、SDMagicHook は上記の問題を解決しています。

2.組み立てレベルでのKVOの本質を探る

これを理解するには、まずシステムのKVOがどのように実装されているかを理解する必要があります。これを理解するためには、KVO が観測するターゲット・プロパティに値を代入したときに何が起こるかを理解する必要があります。ここでは、自作の Test クラスを例にして、Test クラスのインスタンスの num プロパティに対する KVO 操作を説明します:

numに値を代入するとき、ブレークポイントがKVOクラスのsetNum:のカスタム実装である_NSSetIntValueAndNotify関数に当たっていることがわかります。

では、_NSSetIntValueAndNotifyの内部実装はどうなっているのでしょうか?アセンブリ・コードにヒントがあります:

Foundation`_NSSetIntValueAndNotify:
 0x10e5b0fc2 <+0>: pushq %rbp
-> 0x10e5b0fc3 <+1>: movq %rsp, %rbp
 0x10e5b0fc6 <+4>: pushq %r15
 0x10e5b0fc8 <+6>: pushq %r14
 0x10e5b0fca <+8>: pushq %r13
 0x10e5b0fcc <+10>: pushq %r12
 0x10e5b0fce <+12>: pushq %rbx
 0x10e5b0fcf <+13>: subq $0x48, %rsp
 0x10e5b0fd3 <+17>: movl %edx, -0x2c(%rbp)
 0x10e5b0fd6 <+20>: movq %rsi, %r15
 0x10e5b0fd9 <+23>: movq %rdi, %r13
 0x10e5b0fdc <+26>: callq 0x10e7cc882 ; symbol stub for: object_getClass
 0x10e5b0fe1 <+31>: movq %rax, %rdi
 0x10e5b0fe4 <+34>: callq 0x10e7cc88e ; symbol stub for: object_getIndexedIvars
 0x10e5b0fe9 <+39>: movq %rax, %rbx
 0x10e5b0fec <+42>: leaq 0x20(%rbx), %r14
 0x10e5b0ff0 <+46>: movq %r14, %rdi
 0x10e5b0ff3 <+49>: callq 0x10e7cca26 ; symbol stub for: pthread_mutex_lock
 0x10e5b0ff8 <+54>: movq 0x18(%rbx), %rdi
 0x10e5b0ffc <+58>: movq %r15, %rsi
 0x10e5b0fff <+61>: callq 0x10e7cb472 ; symbol stub for: CFDictionaryGetValue
 0x10e5b1004 <+66>: movq 0x36329d(%rip), %rsi ; "copyWithZone:"
 0x10e5b100b <+73>: xorl %edx, %edx
 0x10e5b100d <+75>: movq %rax, %rdi
 0x10e5b1010 <+78>: callq *0x2b2862(%rip) ; (void *)0x000000010eb89d80: objc_msgSend
 0x10e5b1016 <+84>: movq %rax, %r12
 0x10e5b1019 <+87>: movq %r14, %rdi
 0x10e5b101c <+90>: callq 0x10e7cca32 ; symbol stub for: pthread_mutex_unlock
 0x10e5b1021 <+95>: cmpb $0x0, 0x60(%rbx)
 0x10e5b1025 <+99>: je 0x10e5b1066 ; <+164>
 0x10e5b1027 <+101>: movq 0x36439a(%rip), %rsi ; "willChangeValueForKey:"
 0x10e5b102e <+108>: movq 0x2b2843(%rip), %r14 ; (void *)0x000000010eb89d80: objc_msgSend
 0x10e5b1035 <+115>: movq %r13, %rdi
 0x10e5b1038 <+118>: movq %r12, %rdx
 0x10e5b103b <+121>: callq *%r14
 0x10e5b103e <+124>: movq (%rbx), %rdi
 0x10e5b1041 <+127>: movq %r15, %rsi
 0x10e5b1044 <+130>: callq 0x10e7cc2b2 ; symbol stub for: class_getMethodImplementation
 0x10e5b1049 <+135>: movq %r13, %rdi
 0x10e5b104c <+138>: movq %r15, %rsi
 0x10e5b104f <+141>: movl -0x2c(%rbp), %edx
 0x10e5b1052 <+144>: callq *%rax
 0x10e5b1054 <+146>: movq 0x364385(%rip), %rsi ; "didChangeValueForKey:"
 0x10e5b105b <+153>: movq %r13, %rdi
 0x10e5b105e <+156>: movq %r12, %rdx
 0x10e5b1061 <+159>: callq *%r14
 0x10e5b1064 <+162>: jmp 0x10e5b10be ; <+252>
 0x10e5b1066 <+164>: movq 0x2b22eb(%rip), %rax ; (void *)0x00000001120b9070: _NSConcreteStackBlock
 0x10e5b106d <+171>: leaq -0x68(%rbp), %r9
 0x10e5b1071 <+175>: movq %rax, (%r9)
 0x10e5b1074 <+178>: movl $0xc2000000, %eax ; imm = 0xC2000000
 0x10e5b1079 <+183>: movq %rax, 0x8(%r9)
 0x10e5b107d <+187>: leaq 0xf5d(%rip), %rax ; ___NSSetIntValueAndNotify_block_invoke
 0x10e5b1084 <+194>: movq %rax, 0x10(%r9)
 0x10e5b1088 <+198>: leaq 0x2b7929(%rip), %rax ; __block_descriptor_tmp.77
 0x10e5b108f <+205>: movq %rax, 0x18(%r9)
 0x10e5b1093 <+209>: movq %rbx, 0x28(%r9)
 0x10e5b1097 <+213>: movq %r15, 0x30(%r9)
 0x10e5b109b <+217>: movq %r13, 0x20(%r9)
 0x10e5b109f <+221>: movl -0x2c(%rbp), %eax
 0x10e5b10a2 <+224>: movl %eax, 0x38(%r9)
 0x10e5b10a6 <+228>: movq 0x364fab(%rip), %rsi ; "_changeValueForKey:key:key:usingBlock:"
 0x10e5b10ad <+235>: xorl %ecx, %ecx
 0x10e5b10af <+237>: xorl %r8d, %r8d
 0x10e5b10b2 <+240>: movq %r13, %rdi
 0x10e5b10b5 <+243>: movq %r12, %rdx
 0x10e5b10b8 <+246>: callq *0x2b27ba(%rip) ; (void *)0x000000010eb89d80: objc_msgSend
 0x10e5b10be <+252>: movq 0x362f73(%rip), %rsi ; "release"
 0x10e5b10c5 <+259>: movq %r12, %rdi
 0x10e5b10c8 <+262>: callq *0x2b27aa(%rip) ; (void *)0x000000010eb89d80: objc_msgSend
 0x10e5b10ce <+268>: addq $0x48, %rsp
 0x10e5b10d2 <+272>: popq %rbx
 0x10e5b10d3 <+273>: popq %r12
 0x10e5b10d5 <+275>: popq %r13
 0x10e5b10d7 <+277>: popq %r14
 0x10e5b10d9 <+279>: popq %r15
 0x10e5b10db <+281>: popq %rbp
 0x10e5b10dc <+282>: retq

上記のアセンブリコードを擬似コードに変換すると、おおよそ次のようになります:

typedef struct {
 Class originalClass; // offset 0x0
 Class KVOClass; // offset 0x8
 CFMutableSetRef mset; // offset 0x10
 CFMutableDictionaryRef mdict; // offset 0x18
 pthread_mutex_t *lock; // offset 0x20
 void *sth1; // offset 0x28
 void *sth2; // offset 0x30
 void *sth3; // offset 0x38
 void *sth4; // offset 0x40
 void *sth5; // offset 0x48
 void *sth6; // offset 0x50
 void *sth7; // offset 0x58
 bool flag; // offset 0x60
} SDTestKVOClassIndexedIvars;
typedef struct {
 Class isa; // offset 0x0
 int flags; // offset 0x8
 int reserved;
 IMP invoke; // offset 0x10
 void *descriptor; // offset 0x18
 void *captureVar1; // offset 0x20
 void *captureVar2; // offset 0x28
 void *captureVar3; // offset 0x30
 int captureVar4; // offset 0x38
} SDTestStackBlock;
void _NSSetIntValueAndNotify(id obj, SEL sel, int number) {
 Class cls = object_getClass(obj);
 // クラスインスタンスの関連情報を取得する
 SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(cls);
 pthread_mutex_lock(indexedIvars->lock);
 NSString *str = (NSString *)CFDictionaryGetValue(indexedIvars->mdict, sel);
 str = [str copyWithZone:nil];
 pthread_mutex_unlock(indexedIvars->lock);
 if (indexedIvars->flag) {
 [obj willChangeValueForKey:str];
 ((void(*)(id obj, SEL sel, int number))class_getMethodImplementation(indexedIvars->originalClass, sel))(obj, sel, number);
 [obj didChangeValueForKey:str];
 } else {
 // ブロックを生成する
 SDTestStackBlock block = {};
 block.isa = _NSConcreteStackBlock;
 block.flags = 0xC2000000;
 block.invoke = ___NSSetIntValueAndNotify_block_invoke;
 block.descriptor = __block_descriptor_tmp;
 block.captureVar2 = indexedIvars;
 block.captureVar3 = sel;
 block.captureVar1 = obj;
 block.captureVar4 = number;
 [obj _changeValueForKey:str key:nil key:nil usingBlock:&SDTestStackBlock];
 }
}
Foundation`___NSSetIntValueAndNotify_block_invoke:
-> 0x10bf27fe1 <+0>: pushq %rbp
 0x10bf27fe2 <+1>: movq %rsp, %rbp
 0x10bf27fe5 <+4>: pushq %rbx
 0x10bf27fe6 <+5>: pushq %rax
 0x10bf27fe7 <+6>: movq %rdi, %rbx
 0x10bf27fea <+9>: movq 0x28(%rbx), %rax
 0x10bf27fee <+13>: movq 0x30(%rbx), %rsi
 0x10bf27ff2 <+17>: movq (%rax), %rdi
 0x10bf27ff5 <+20>: callq 0x10c1422b2 ; symbol stub for: class_getMethodImplementation
 0x10bf27ffa <+25>: movq 0x20(%rbx), %rdi
 0x10bf27ffe <+29>: movq 0x30(%rbx), %rsi
 0x10bf28002 <+33>: movl 0x38(%rbx), %edx
 0x10bf28005 <+36>: addq $0x8, %rsp
 0x10bf28009 <+40>: popq %rbx
 0x10bf2800a <+41>: popq %rbp
 0x10bf2800b <+42>: jmpq *%rax

NSSetIntValueAndNotify_block_invokeの擬似コードは以下のようになります:

void ___NSSetIntValueAndNotify_block_invoke(SDTestStackBlock *block) {
 SDTestKVOClassIndexedIvars *indexedIvars = block->captureVar2;
 SEL methodSel = block->captureVar3;
 IMP imp = class_getMethodImplementation(indexedIvars->originalClass);
 id obj = block->captureVar1;
 SEL sel = block->captureVar3;
 int num = block->captureVar4;
 imp(obj, sel, num);
}

このブロックの内部実装は、実際には、KVO クラスの indexedIvars から元のクラスを取得し、sel に従って元のクラスから元のメソッド実装を取り出して実行し、最終的に KVO 呼び出しを完了します。KVOクラスのindexedIvarsは、KVO処理全体のキーデータであることがわかりますが、このindexedIvarsはいつ生成されたもので、indexedIvarsにはどのようなデータが含まれているのでしょうか?この問題を解明するには、KVO のソースから始めなければなりません。KVO は isa 交換を使用する必要があるため、最終的には object_setClass メソッドを呼び出す必要があります。object_setClass 関数を手がかりに、条件付きシンボリック・ブレークポイントを設定して object_setClass の呼び出しをトレースし、object_setClass をデバッグするとよいでしょう。スクリーンショットは以下の通りです:

object_setClass にブレークポイントを設定した後、レジスタ rdi と rsi 内のパラメータがそれぞれNSKVONotifying_Test として出力されることを確認します

KVOクラスが生成される場所を見つけるには、コールスタックに沿ってバックトラックし、最終的にKVOクラス_NSKVONotifyingCreateInfoWithOriginalClassの生成関数を見つける必要があります:

Foundation`_NSKVONotifyingCreateInfoWithOriginalClass:
-> 0x10c557d79 <+0>: pushq %rbp
 0x10c557d7a <+1>: movq %rsp, %rbp
 0x10c557d7d <+4>: pushq %r15
 0x10c557d7f <+6>: pushq %r14
 0x10c557d81 <+8>: pushq %r12
 0x10c557d83 <+10>: pushq %rbx
 0x10c557d84 <+11>: subq $0x20, %rsp
 0x10c557d88 <+15>: movq %rdi, %r14
 0x10c557d8b <+18>: movq 0x2b463e(%rip), %rax ; (void *)0x000000011012d070: __stack_chk_guard
 0x10c557d92 <+25>: movq (%rax), %rax
 0x10c557d95 <+28>: movq %rax, -0x28(%rbp)
 0x10c557d99 <+32>: xorl %eax, %eax
 0x10c557d9b <+34>: callq 0x10c55b452 ; NSKeyValueObservingAssertRegistrationLockHeld
 0x10c557da0 <+39>: movq %r14, %rdi
 0x10c557da3 <+42>: callq 0x10c7752b8 ; symbol stub for: class_getName
 0x10c557da8 <+47>: movq %rax, %r12
 0x10c557dab <+50>: movq %r12, %rdi
 0x10c557dae <+53>: callq 0x10c775ba0 ; symbol stub for: strlen
 0x10c557db3 <+58>: movq %rax, %rbx
 0x10c557db6 <+61>: addq $0x10, %rbx
 0x10c557dba <+65>: movq %rbx, %rdi
 0x10c557dbd <+68>: callq 0x10c775666 ; symbol stub for: malloc
 0x10c557dc2 <+73>: movq %rax, %r15
 0x10c557dc5 <+76>: leaq 0x29d604(%rip), %rsi ; _NSKVONotifyingCreateInfoWithOriginalClass.notifyingClassNamePrefix
 0x10c557dcc <+83>: movq $-0x1, %rcx
 0x10c557dd3 <+90>: movq %r15, %rdi
 0x10c557dd6 <+93>: movq %rbx, %rdx
 0x10c557dd9 <+96>: callq 0x10c77510e ; symbol stub for: __strlcpy_chk
 0x10c557dde <+101>: movq $-0x1, %rcx
 0x10c557de5 <+108>: movq %r15, %rdi
 0x10c557de8 <+111>: movq %r12, %rsi
 0x10c557deb <+114>: movq %rbx, %rdx
 0x10c557dee <+117>: callq 0x10c775108 ; symbol stub for: __strlcat_chk
 0x10c557df3 <+122>: movl $0x68, %edx
 0x10c557df8 <+127>: movq %r14, %rdi
 0x10c557dfb <+130>: movq %r15, %rsi
 0x10c557dfe <+133>: callq 0x10c775762 ; symbol stub for: objc_allocateClassPair
 0x10c557e03 <+138>: movq %rax, %rbx
 0x10c557e06 <+141>: testq %rbx, %rbx
 0x10c557e09 <+144>: je 0x10c557f17 ; <+414>
 0x10c557e0f <+150>: movq %rbx, %rdi
 0x10c557e12 <+153>: callq 0x10c775816 ; symbol stub for: objc_registerClassPair
 0x10c557e17 <+158>: movq %r15, %rdi
 0x10c557e1a <+161>: callq 0x10c7754ec ; symbol stub for: free
 0x10c557e1f <+166>: movq %rbx, %rdi
 0x10c557e22 <+169>: callq 0x10c77588e ; symbol stub for: object_getIndexedIvars
 0x10c557e27 <+174>: movq %rax, %r15
 0x10c557e2a <+177>: movq %r14, (%r15)
 0x10c557e2d <+180>: movq %rbx, 0x8(%r15)
 0x10c557e31 <+184>: movq 0x2b4748(%rip), %rdx ; (void *)0x000000010d7fd1f8: kCFCopyStringSetCallBacks
 0x10c557e38 <+191>: xorl %edi, %edi
 0x10c557e3a <+193>: xorl %esi, %esi
 0x10c557e3c <+195>: callq 0x10c774778 ; symbol stub for: CFSetCreateMutable
 0x10c557e41 <+200>: movq %rax, 0x10(%r15)
 0x10c557e45 <+204>: movq 0x2b49e4(%rip), %rcx ; (void *)0x000000010d7f6bb8: kCFTypeDictionaryValueCallBacks
 0x10c557e4c <+211>: xorl %edi, %edi
 0x10c557e4e <+213>: xorl %esi, %esi
 0x10c557e50 <+215>: xorl %edx, %edx
 0x10c557e52 <+217>: callq 0x10c774454 ; symbol stub for: CFDictionaryCreateMutable
 0x10c557e57 <+222>: movq %rax, 0x18(%r15)
 0x10c557e5b <+226>: leaq -0x38(%rbp), %rbx
 0x10c557e5f <+230>: movq %rbx, %rdi
 0x10c557e62 <+233>: callq 0x10c775a3e ; symbol stub for: pthread_mutexattr_init
 0x10c557e67 <+238>: movl $0x2, %esi
 0x10c557e6c <+243>: movq %rbx, %rdi
 0x10c557e6f <+246>: callq 0x10c775a44 ; symbol stub for: pthread_mutexattr_settype
 0x10c557e74 <+251>: leaq 0x20(%r15), %rdi
 0x10c557e78 <+255>: movq %rbx, %rsi
 0x10c557e7b <+258>: callq 0x10c775a20 ; symbol stub for: pthread_mutex_init
 0x10c557e80 <+263>: movq %rbx, %rdi
 0x10c557e83 <+266>: callq 0x10c775a38 ; symbol stub for: pthread_mutexattr_destroy
 0x10c557e88 <+271>: cmpq $-0x1, 0x3824a0(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken + 7
 0x10c557e90 <+279>: jne 0x10c557fa4 ; <+555>
 0x10c557e96 <+285>: movq (%r15), %rdi
 0x10c557e99 <+288>: movq 0x366528(%rip), %rsi ; "willChangeValueForKey:"
 0x10c557ea0 <+295>: callq 0x10c7752b2 ; symbol stub for: class_getMethodImplementation
 0x10c557ea5 <+300>: movb $0x1, %cl
 0x10c557ea7 <+302>: cmpq 0x38248a(%rip), %rax ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange
 0x10c557eae <+309>: jne 0x10c557ec9 ; <+336>
 0x10c557eb0 <+311>: movq (%r15), %rdi
 0x10c557eb3 <+314>: movq 0x366526(%rip), %rsi ; "didChangeValueForKey:"
 0x10c557eba <+321>: callq 0x10c7752b2 ; symbol stub for: class_getMethodImplementation
 0x10c557ebf <+326>: cmpq 0x38247a(%rip), %rax ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange
 0x10c557ec6 <+333>: setne %cl
 0x10c557ec9 <+336>: movb %cl, 0x60(%r15)
 0x10c557ecd <+340>: movq 0x36715c(%rip), %rsi ; "_isKVOA"
 0x10c557ed4 <+347>: leaq 0x1ff(%rip), %rdx ; NSKVOIsAutonotifying
 0x10c557edb <+354>: xorl %ecx, %ecx
 0x10c557edd <+356>: movq %r15, %rdi
 0x10c557ee0 <+359>: callq 0x10c558057 ; NSKVONotifyingSetMethodImplementation
 0x10c557ee5 <+364>: movq 0x365154(%rip), %rsi ; "dealloc"
 0x10c557eec <+371>: leaq 0x1ef(%rip), %rdx ; NSKVODeallocate
 0x10c557ef3 <+378>: xorl %ecx, %ecx
 0x10c557ef5 <+380>: movq %r15, %rdi
 0x10c557ef8 <+383>: callq 0x10c558057 ; NSKVONotifyingSetMethodImplementation
 0x10c557efd <+388>: movq 0x36519c(%rip), %rsi ; "class"
 0x10c557f04 <+395>: leaq 0x433(%rip), %rdx ; NSKVOClass
 0x10c557f0b <+402>: xorl %ecx, %ecx
 0x10c557f0d <+404>: movq %r15, %rdi
 0x10c557f10 <+407>: callq 0x10c558057 ; NSKVONotifyingSetMethodImplementation
 0x10c557f15 <+412>: jmp 0x10c557f84 ; <+523>
 0x10c557f17 <+414>: cmpq $-0x1, 0x382409(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog + 7
 0x10c557f1f <+422>: jne 0x10c557fbc ; <+579>
 0x10c557f25 <+428>: movq 0x3823f4(%rip), %r14 ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog
 0x10c557f2c <+435>: movl $0x10, %esi
 0x10c557f31 <+440>: movq %r14, %rdi
 0x10c557f34 <+443>: callq 0x10c7758e2 ; symbol stub for: os_log_type_enabled
 0x10c557f39 <+448>: testb %al, %al
 0x10c557f3b <+450>: je 0x10c557f79 ; <+512>
 0x10c557f3d <+452>: movq %rsp, %rbx
 0x10c557f40 <+455>: movq %rsp, %rax
 0x10c557f43 <+458>: leaq -0x10(%rax), %r8
 0x10c557f47 <+462>: movq %r8, %rsp
 0x10c557f4a <+465>: movl $0x8200102, -0x10(%rax) ; imm = 0x8200102
 0x10c557f51 <+472>: movq %r15, -0xc(%rax)
 0x10c557f55 <+476>: leaq -0x63f5c(%rip), %rdi
 0x10c557f5c <+483>: leaq 0x296c1d(%rip), %rcx ; "KVO failed to allocate class pair for name %s, automatic key-value observing will not work for this class"
 0x10c557f63 <+490>: movl $0x10, %edx
 0x10c557f68 <+495>: movl $0xc, %r9d
 0x10c557f6e <+501>: movq %r14, %rsi
 0x10c557f71 <+504>: callq 0x10c7751aa ; symbol stub for: _os_log_error_impl
 0x10c557f76 <+509>: movq %rbx, %rsp
 0x10c557f79 <+512>: movq %r15, %rdi
 0x10c557f7c <+515>: callq 0x10c7754ec ; symbol stub for: free
 0x10c557f81 <+520>: xorl %r15d, %r15d
 0x10c557f84 <+523>: movq 0x2b4445(%rip), %rax ; (void *)0x000000011012d070: __stack_chk_guard
 0x10c557f8b <+530>: movq (%rax), %rax
 0x10c557f8e <+533>: cmpq -0x28(%rbp), %rax
 0x10c557f92 <+537>: jne 0x10c557fd4 ; <+603>
 0x10c557f94 <+539>: movq %r15, %rax
 0x10c557f97 <+542>: leaq -0x20(%rbp), %rsp
 0x10c557f9b <+546>: popq %rbx
 0x10c557f9c <+547>: popq %r12
 0x10c557f9e <+549>: popq %r14
 0x10c557fa0 <+551>: popq %r15
 0x10c557fa2 <+553>: popq %rbp
 0x10c557fa3 <+554>: retq
 0x10c557fa4 <+555>: leaq 0x382385(%rip), %rdi ; _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce
 0x10c557fab <+562>: leaq 0x2b9886(%rip), %rsi ; __block_literal_global.8
 0x10c557fb2 <+569>: callq 0x10c7753d8 ; symbol stub for: dispatch_once
 0x10c557fb7 <+574>: jmp 0x10c557e96 ; <+285>
 0x10c557fbc <+579>: leaq 0x382365(%rip), %rdi ; _NSKVONotifyingCreateInfoWithOriginalClass.onceToken
 0x10c557fc3 <+586>: leaq 0x2b982e(%rip), %rsi ; __block_literal_global
 0x10c557fca <+593>: callq 0x10c7753d8 ; symbol stub for: dispatch_once
 0x10c557fcf <+598>: jmp 0x10c557f25 ; <+428>
 0x10c557fd4 <+603>: callq 0x10c775102 ; symbol stub for: __stack_chk_fail

擬似コードへの変換は以下の通り:

typedef struct {
 Class originalClass; // offset 0x0
 Class KVOClass; // offset 0x8
 CFMutableSetRef mset; // offset 0x10
 CFMutableDictionaryRef mdict; // offset 0x18
 pthread_mutex_t *lock; // offset 0x20
 void *sth1; // offset 0x28
 void *sth2; // offset 0x30
 void *sth3; // offset 0x38
 void *sth4; // offset 0x40
 void *sth5; // offset 0x48
 void *sth6; // offset 0x50
 void *sth7; // offset 0x58
 bool flag; // offset 0x60
} SDTestKVOClassIndexedIvars;
Class _NSKVONotifyingCreateInfoWithOriginalClass(Class originalClass) {
 const char *clsName = class_getName(originalClass);
 size_t len = strlen(clsName);
 len += 0x10;
 char *newClsName = malloc(len);
 const char *prefix = "NSKVONotifying_";
 __strlcpy_chk(newClsName, prefix, len);
 __strlcat_chk(newClsName, clsName, len, -1);
 Class newCls = objc_allocateClassPair(originalClass, newClsName, 0x68);
 if (newCls) {
 objc_registerClassPair(newCls);
 SDTestKVOClassIndexedIvars *indexedIvars = object_getIndexedIvars(newCls);
 indexedIvars->originalClass = originalClass;
 indexedIvars->KVOClass = newCls;
 CFMutableSetRef mset = CFSetCreateMutable(nil, 0, kCFCopyStringSetCallBacks);
 indexedIvars->mset = mset;
 CFMutableDictionaryRef mdict = CFDictionaryCreateMutable(nil, 0, nil, kCFTypeDictionaryValueCallBacks);
 indexedIvars->mdict = mdict;
 pthread_mutex_init(indexedIvars->lock);
 static dispatch_once_t onceToken;
 dispatch_once(&onceToken, ^{
 bool flag = true;
 IMP willChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(willChangeValueForKey:));
 IMP didChangeValueForKeyImp = class_getMethodImplementation(indexedIvars->originalClass, @selector(didChangeValueForKey:));
 if (willChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange && didChangeValueForKeyImp == _NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange) {
 flag = false;
 }
 indexedIvars->flag = flag;
 NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA), NSKVOIsAutonotifying, nil)
 NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc), NSKVODeallocate, nil)
 NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class), NSKVOClass, nil)
 });
 } else {
 // エラー処理プロセスの省略......
 return nil
 }
 return newCls;
}
typedef struct { Class originalClass; // offset 0x0 Class KVOClass; // offset 0x8 CFMutableSetRef mset; // offset 0x10 CFMutableDictionaryRef mdict; // offset 0x18 pthread_mutex_t *lock; // offset 0x20 void *sth1; // offset 0x28 void *sth2; // offset 0x30 void *sth3; // offset 0x38 void *sth4; // offset 0x40 void *sth5; // offset 0x48 void *sth6; // offset 0x50 void *sth7; // offset 0x58 bool flag; // offset 0x60 } SDTestKVOClassIndexedIvars;

カスタムKVOによるネイティブKVOのクラッシュを解決するには?

KVO 実装の詳細については概ねご理解いただけたと思います。では、元の質問に戻りますが、native-KVO を呼び出した後に custom-KVO を呼び出すと、custom-KVO は問題なく実行されるのに native-KVO がクラッシュするのはなぜでしょうか?前述の Test クラスを例にとって考えてみましょう:

問題の根本的な原因を突き止めた後、native-KVO の例に従って、SD_NSKVONotifying_Test_abcd に 0x68 の余分な領域を確保し、カスタム KVO 操作を行う際に NSKVONotifying_Test の indexedIvars を SD_NSKVONotifying_Test_abcd にコピーします。カスタムKVOを行う場合は、indexedIvarsをSD_NSKVONotifying_Test_abcdにコピーします:

一般に、ネイティブ KVO の上にカスタム KVO を作成する場合は、 ネイティブ KVO クラスの indexedIvars をカスタム KVO クラスにコピーすればよいのですが、 SDMagicHook ではこれが十分ではありません。 SDMagicHook はメッセージ転送を使用して生成された新しいクラスのメソッドをディスパッチするからです。なぜなら、SDMagicHook は生成された新しいクラスのメソッドをディスパッチするために メッセージ転送を使用しているからです。以下に例を示します:

メッセージ転送が使用されるため、SD_NSKVONotifying_Test_abcd の setNum: の対応する実装は _objc_msgForward を指すようになり、新しい SEL__sd__B_abcd_setNum: が生成されてそのサブクラスのネイティブ実装を指すようになります。void _NSSetIntValueAndNotify(id obj, SEL sel, int number)NSKVONotifying_TestsetNum:実装、つまり関数です。テストインスタンスが setNum: メッセージを受信すると、まずメッセージ転送メカニズムが起動し、SDMagicHook のメッセージスケジューリングシステムは最終的に __sd_B_abcd_setNum: メッセージをテストインスタンスに送信し、フックされたネイティブメソッドへのコールバックを実装します。void _NSSetIntValueAndNotify(id obj, SEL sel, int number)_NSSetIntValueAndNotifysd_B_abcd_setNum:の実装関数は , ですので、__sd_B_abcd_setNum:は関数のsel引数として渡されます。_NSSetIntValueAndNotifyそして、この関数が内部でindexedIvarsから元のクラスTestを取得し、Testから__sd_B_abcd_setNum:に対応するメソッドを見つけて呼び出そうとすると、対応する関数の実装が見つからずにクラッシュします。 この問題を解決するには、Testに新しい__sd_B_abcd_setNum:メソッドを追加し、その実装をsetNum:メソッドに向ける必要があります。この問題を解決するには、Testクラスに新しい__sd_B_abcd_setNum:メソッドを追加し、その実装をsetNum:実装に向ける必要があります:

この時点で、「最初にネイティブKVOを呼び出してからカスタムKVOを呼び出すと、カスタムKVOは問題なく動くが、ネイティブKVOがクラッシュする」という問題をスムーズに解決できます。

4. ネイティブKVOがカスタムKVOを無効にする問題の解決方法

1.NSKVONotifying_TestのKVOデータを変更する。 2.インターセプトシステムのsetclass操作をフックする。というのも、NSKVONotifying_Test の KVO データは、Test クラスのインスタンスが KVO 操作を行う際に共有されるため、Test クラスのインスタンスの KVO に影響を与える可能性があるからです。そのため、FishHook を使用してシステムの object_setclass 関数をフックする必要があります。システムが NSKVONotifying_Test をパラメータとするインスタンスに対して setclass 操作を実行すると、現在の isa ポインタが SD_NSKVONotifying_Test_abcd と SD_NSKVONotifying_Test_abcd および SD_NSKVONotifying_Test_abcd であるかどうかがチェックされます。NSKVONotifying_Test_abcd は NSKVONotifying_Test を継承しているため、 setclass 操作はスキップされます。

しかし、custom-KVO はフックされたメソッドをディスパッチするために特別なメッセージ転送メカニズムを使用するため、custom-KVO を最初に実行し、次に native-KVO を実行すると、観測されたプロパティへの呼び出しが重複してしまうため、これでは十分ではありません。そのため、インスタンスに対する最初の custom-KVO 操作の前に native-KVO を実行し、custom-KVO のメソッド・スケジューリングが適切に機能するようにします。コードは以下の通りです:

まとめ

KVOの本質は、観測されたインスタンスのisaに基づいて新しいクラスを生成し、このクラスの余分なスペースにKVO操作に関連する様々なキーデータを格納し、この新しいクラスが余分なスペースに格納された様々なデータの助けを借りて複雑なメソッドスケジューリングを完了するために仲介役として機能することです。

KVOの実装は比較的複雑で、関数呼び出しも比較的深いものが多いので、最初のうちは、関数呼び出しスタック全体の末尾から主要な操作経路を整理し、KVOの動作を大まかに理解した上で、グローバルな視点から様々な処理や詳細を分析するのがよいでしょう。そうすることで、KVOの素早い理解が得られます。

これで、ネイティブKVOとうまく連携するカスタムKVOができました。振り返ってみると、この解決策はまだトリッキーすぎますが、iOSシステムの制限のもとでの選択にすぎません。私はこのようなトリッキーな操作の使用を推奨しているわけではありませんが、この例を使ってKVOの本質と問題を分析し解決する方法を紹介したいだけです。この記事から何らかのインスピレーションを得ることができれば、この記事は "悪くない "ですし、この記事で紹介したアイデアや方法を、様々な難問に遭遇した自分の開発に役立てることができれば、"それは本当に良いことだ "と思います!「素晴らしい

より多くの共有

オープンソース|Objective-C & Swift最軽量フックソリューション

今日のアンドロイド「秒単位」コンパイル速度最適化

バイトジャンプ分散テーブルストレージシステムの進化

ByteDanceセルフ・リサーチ 強力な一貫オンラインKV&テーブル・ストレージの実践 - 前編

バイトジャンプ - Flying Book Audio/Video モバイルチーム

チームは主にフライングブックのオーディオおよびビデオ製品にサービスを提供し、製品の性能、安定性などのユーザーエクスペリエンス、R&Dプロセス、コンパイルと最適化、継続的な最適化と綿密な調査のアーキテクチャの方向性、同時に製品の急速なイテレーションを満たすために、ユーザーエクスペリエンスの高いレベルを維持するために。私たちは東京でAndroid / iOSとフルスタックプラットフォームアーキテクチャの学生を募集しています。詳細な話をしたい、または部門内で昇進する必要がある場合、または履歴書を送信する場合は、qiuzehui@bytedance.com。

ByteHopテクノロジーチームへようこそ

Read next

webpackの質問まとめ

I.一般的なビルドツールは何ですか?その長所と短所を教えてください。webpackを選んだ理由は?\nGrunt、Gulp、Fis3、Rollup、Npm Script、webpack。\n&lt;1&gt; Gruntの利点は以下の通りです:\n-

Jun 24, 2020 · 8 min read