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:

  1. Call custom-KVO first and then call native-KVO, native-KVO and custom-KVO both work fine
  2. Call native KVO first and then call custom-kvo. If custom-kVO runs normally, native KVO will crash
  3. 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