Most iOS developers’ knowledge of KVO is limited to the ISA pointer exchange layer, but the implementation details of KVO are poorly understood. If you follow the KVO basic principles to implement a set of kVO-like operations and run them independently, you will find everything is fine. However, once your implementation and the system’s KVO implementation work on the same instance at the same time, all kinds of weird bugs and crashes will emerge in endless. Why is that? How can such problems be solved? Next we will try to start from the assembly level to uncover the mystery of KVO……
1. The origin of Aspects
After the open source of SDMagicHook, many partners ask “what is the difference between SDMagicHook and Aspects”. I found Aspects on GitHub and learned about them and found that Aspects are also hook operations based on the principle of ISA exchange. However, there are some differences in implementation and API design, and SDMagicHook also solves KVO conflict problems that Aspects failed to solve.
1.1 THE API design of SDMagicHook is more friendly and flexible
Specific similarities and differences between SDMagicHook and Aspects can be found at github.com/larksuite/S… .
1.2 SDMagicHook solves the KVO conflict problem that Aspects fails to solve
I also noticed a description of the KVO compatibility issue in the Aspects readme:
Could SDMagicHook have the same problem? After testing SDMagicHook, it turns out that the problem is more complex and trickier than the Aspects author suggests, The specific performance of the problem will vary with the call sequence and times of the system KVO(hereinafter referred to as native-KVO) and the self-implemented class KVO(custom-KVO), as follows:
- Call custom-KVO first and then call native-KVO, native-KVO and custom-KVO both work fine
- Call native KVO first and then call custom-kvo. If custom-kVO runs normally, native KVO will crash
- Call native KVO first, then call Custom-kvo, then call native KVO. Native KVO runs normally, but Custom-kVO fails, and there is no crash
At present, SDMagicHook has solved all kinds of problems mentioned above, and THE specific implementation scheme will be introduced in detail in the following article.
2. Explore the essence of KVO from the assembly level
To understand this problem, first of all, we need to study how the system’S KVO is implemented, and the system’s KVO implementation is quite complex, where should we start? To understand this problem, we first need to understand what happens when an assignment is performed on the target attribute observed by KVO. KVO = KVO; KVO = KVO; KVO = KVO; KVO = KVO;
When we assign num, we can see that the breakpoint matches the KVO implementation of setNum:, the _NSSetIntValueAndNotify function
So what’s the internal implementation of _NSSetIntValueAndNotify? Some clues can be found in the assembly code:
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 ; The < 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 ; The < 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 Copy the code
The above assembly code translates into pseudocode roughly as follows:
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); // Get the information associated with the class instance 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 { / / generated block 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]; } } Copy the code
Object_getIndexedIvars (CLS) getIndexeDivars (KVO); IndexedIvars ->flag is true if the developer has overwritten the willChangeValueForKey: or didChangeValueForKey: method Class_getMethodImplementation (indexedIvars->originalClass, sel))(obj, sel, number) Otherwise, the stack block implemented by default as NSSetIntValueAndNotify_block_invoke is used and all necessary parameters such as indexedIvars, the instance observed by KVO, SEL corresponding to the observed attribute, and assignment parameters are captured and the block is added Passed as an argument to the [obj _changeValueForKey: STR key:nil key:nil usingBlock:&SDTestStackBlock] call. What is indexedIvars obtained from object_getIndexedIvars(CLS) in the pseudocode? Block. invoke = ___ NSSetIntValueAndNotify_block_invoke First let’s look at the assembly implementation of NSSetIntValueAndNotify_block_invoke:
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 Copy the code
___NSSetIntValueAndNotify_block_invoke translates to pseudocode as follows:
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); } Copy the code
The internal implementation of this block actually takes the original class from the indexedIvars of the KVO class, then executes the original method implementation from the original class according to SEL and finally completes a KVO call. We found that indexedIvars of the KVO class was a key data throughout the entire KVO process. When was this indexedIvars generated? What data does indexedIvars contain? To figure this out, we have to look at the source of KVO. We know that since KVO uses isa, it must eventually call the object_setClass method. Set conditional symbol breakpoints to track calls to object_setClass.
<Test: 0x600003DF01B0 > and NSKVONotifying_Test <Test: 0x600003DF01B0
Yes, we have now successfully located the ISA exchange scene of KVO, but to find where the KVO class was generated we need to go back down the call stack, Finally we locate the KVO class generation function _NSKVONotifyingCreateInfoWithOriginalClass, its assembly code is as follows:
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 ; The < 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 ; The < 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 ; The < 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 ; The < 523 > + 0x10c557f17 <+414>: cmpq $-0x1, 0x382409(%rip) ; _NSKVONotifyingCreateInfoWithOriginalClass.kvoLog + 7 0x10c557f1f <+422>: jne 0x10c557fbc ; The < 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 ; The < 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 ; The < 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 ; The < 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 ; The < 428 > + 0x10c557fd4 <+603>: callq 0x10c775102 ; symbol stub for: __stack_chk_fail Copy the code
Pseudocode translated as follows:
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 { // Error handling omitted...... return nil } return newCls; } Copy the code
Through _NSKVONotifyingCreateInfoWithOriginalClass before this pseudo code you will find our indexedIvars frequently mentioned here is the initial im. Objc_allocateClassPair (Class _Nullable superclass, Const char * _Nonnull name, size_t extraBytes), “The number of bytes to allocate for indexed ivars at The end of The class and metaclass objects.” This means that when generating a new class with objc_allocateClassPair we can specify extraBytes to create extra space for this class to store some data. When generating KVO class, the system will allocate an extra 0x68 bytes of space. Its specific memory layout and usage are described by a structure as follows:
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; Copy the code
3. How to solve the native KVO Crash caused by Custom-kVO
After reading this, I believe you have a general understanding of the details of KVO implementation. Then we will return to the original question, why “call native KVO first and then call Custom-kvo, custom-Kvo runs normally, native KVO crash”? Let’s also use the Test class mentioned above as an example:
NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test Custom -KVO = SD_NSKVONotifying_Test_abcd; custom-KVO = SD_NSKVONotifying_Test_abcd; If we hadn’t allocated an extra 0x68 bytes of space for storing KVO critical information, as native KVO does, So when we send the test setNum: message then setNum: method call super implementation to the KVO _NSSetIntValueAndNotify method is in accordance with the SDTestKVOClassIndexedIvars * object_getIndexedIvars = object_getIndexedIvars(CLS) * object_getIndexedIvars = object_getIndexedIvars(CLS)
After finding the root cause of the problem, we can use the same method as native KVO to generate SD_NSKVONotifying_Test_abcd and also allocate 0x68 its own space. Copy indexedIvars of NSKVONotifying_Test to SD_NSKVONotifying_Test_abcd
In general, if we copy the indexedIvars of the Native KVO class to the custom-Kvo class, it is enough to copy the indexedIvars of the native KVO class to the custom-Kvo class. However, SDMagicHook only does this. Because SDMagicHook schedules methods in the form of message forwarding on the generated new classes, the problem becomes more complicated all of a sudden. Examples are as follows:
Since message forwarding is used, we will point the setNum: implementation of SD_NSKVONotifying_Test_abcd to _objc_msgForward, and then generate a new SEL__sd_B_abcd_setNum: to point to the native implementation of its subclass, NSKVONotifying_TestsetNum: void _NSSetIntValueAndNotify(id obj, SEL SEL, int number) When the test instance receives a setNum: message, the message forwarding mechanism will be triggered. Then SDMagicHook’s message scheduling system will finally implement the callback to the Hook native method by sending a __sd_B_abcd_setNum: message to the test instance. Void _NSSetIntValueAndNotify(id obj, SEL SEL, int number) So __sd_B_abcd_setNum: is passed as sel argument to _NSSetIntValueAndNotify. Then, when _NSSetIntValueAndNotify internally tried to get the original class Test from indexedIvars and then found the corresponding method __sd_B_abcd_setNum from Test and called it, the crash occurred because it could not find the corresponding function implementation. To solve this problem, we also need to add a new __sd_B_abcd_setNum: method to the Test class and point its implementation to the setNum: implementation as follows:
At this point, the problem of “call native KVO first and then call custom-kvo, custom-kVO runs normally, and native KVO crash” can be solved smoothly.
4. How to solve the problem of custom-kVO failure caused by native KVO
At present, there is still a problem “Call native KVO and then call Custom-Kvo and then call native KVO. Native KVO runs normally, while Custom-kVO fails and there is no crash”. Why is this a problem? This time, we still take the Test class as an example. First, we instantiate an instance of Test with the Test class, and then perform the native-kVO operation on the num attribute of Test. At this time, the ISA of Test points to the NSKVONotifying_Test class. Custom -KVO = SD_NSKVONotifying_Test_abcd; custom-KVO = sd_nSKVonotifying_test; NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test = NSKVONotifying_Test;
WHY? !!!!! The original native – KVO will hold a global dictionary _NSKeyValueContainerClassForIsa. NSKeyValueContainerClassPerOriginalClass key and for KVO operation of the original class The NSKeyValueContainerClass instance stores KVO class information for value.
So when we do KVO on the test instance again, Native – KVO will Test class as key from NSKeyValueContainerClassPerOriginalClass before store to find the NSKeyValueContainerClass directly obtained from KVO class NSKVONotifying_Test then sets the object_setClass method to the test instance and then custom-kvo is invalidated.
To solve this problem, I have two ideas: 1. Modify the KVO data related to NSKVONotifying_Test; 2. Then consider that scenario 1 is not desirable, because the data associated with NSKVONotifying_Test is shared by all Test instances during KVO operations, and any changes may have a global impact on the KVO of the Test instance. Object_setclass (NSKVONotifying_Test) is used to hook the object_setclass function (NSKVONotifying_Test). We check to skip this setclass if the current ISA pointer is SD_NSKVONotifying_Test_abcd and SD_NSKVONotifying_Test_abcd inherits from NSKVONotifying_Test Operation.
However, this is not enough, because Custom-kvo uses a special message forwarding mechanism to schedule the hook methods. If the custom KVO is performed first and then the native KVO is performed, the observed properties will be repeatedly called. Therefore, we do native KVO before the first custom-kVO operation on an instance, so that our custom-kVO method scheduling can work properly. The code is as follows:
conclusion
The essence of KVO is to generate a new class based on the ISA of the observed instance and store various key data related to KVO operations in the extra space of this class, and then the new class performs the role of a middleman with the extra space to store various data to complete the complex method scheduling.
The KVO implementation of the system is relatively complex, and many functions are called at deep levels. At the beginning, we might as well comb out the main operation path from the end of the whole function call stack. After we have a general understanding of THE KVO operation, we will comprehensively analyze each process and details from the global perspective. It is in this way that we achieved a quick understanding and understanding of KVO.
At this point, a good compatible native KVO custom-kVO is complete. In retrospect, this solution was tricky, but it had to be tricky given the limitations of iOS. Rather than advocating the use of tricky operations like this one, we wanted to use this example to show you the nature of KVO and how we analyze and solve problems. If you can draw inspiration from this article, it’s “ungrateful,” and if you can apply the ideas and methods presented in this article to the various problems you encounter in development, “it would be really great!”
More share
Open source | Objective – C & Swift the lightweight Hook
Toutiao Android ‘second’ level compilation speed optimization
Evolution of Bytedance distributed table storage system
Bytedance self-developed consistent online KV & Table Storage Practices – Part 1
Bytedance – Feishu audio and video Mobile team
The team mainly serves feishu audio and video products, constantly optimizing and exploring user experience, r&d process, compilation and optimization, and architecture direction in order to meet the rapid product iteration while maintaining high user experience. We are recruiting students in The direction of Android/iOS and full stack platform architecture in Shanghai for a long time. If you want to have in-depth communication or need to be promoted within the department or submit your resume, you can contact [email protected] (title: Bytedance – Feishuomobile direct promotion).
Welcome to the Bytedance technical team