最近在处理iOS crash 防护时,发现Unrecognized Selector的防护存在一些日常开发被忽略的情况。若直接使用业内防护方案的话,有可能存在相对隐蔽不易发现的功能失效。本文就针对Unrecognized Selector防护中遇到的问题进行分析,揭秘整个防护过程隐藏的问题。
添加Unrecognized Selector防护后,首页触发刷新就会弹出防护警告,看内容比较疑惑,UITableView中的setContentInset: 正常情况下是有对应实现的,怎么可能会触发crash拦截呢?
心想业界的方案都是这样写的,应该可以完美防护住Unrecognized Selector了吧。但是前面遇到问题就解释不清了,于是乎,只能调试抓现场呀。
通过打印发现此时的UITableView已经被改了,变成NSKVONotifying_UITableView,这个就是给UITableView的contentInset做了KVO的监听,那问题来了,KVO的实现不应该是类似以下伪代码实现:
- (void)setContentInset:(UIEdgeInsets)edge {
[self willChangeValueForKey:@"contentInset"];
[super setContentInset:edge];
[self didChangeValueForKey:@"contentInset"];
}
同样的对UITableView的其他属性,如contentOffset、frame,进行监听是不会进入到消息转发的,那为什么contentInset属性比较特殊呢?
苹果不开源呀!这时候只能用lldb来看看具体实现了,首先进入lldb后,对addObserver:forKeyPath:options:context:进行调试
(lldb) breakpoint set -n '[NSObject addObserver:forKeyPath:options:context:]'
Foundation`-[NSObject(NSKeyValueObserverRegistration) addObserver:forKeyPath:options:context:]:
......
0x18aafcd30 <+84>: add x0, x0, #0x3ac ; _NSKeyValueObserverRegistrationLock
0x18aafcd34 <+88>: mov w1, #0x0
0x18aafcd38 <+92>: bl 0x1904ce9f0
0x18aafcd3c <+96>: mov x0, x19
0x18aafcd40 <+100>: bl 0x1904ce8a0
0x18aafcd44 <+104>: mov x1, x21
0x18aafcd48 <+108>: bl 0x18ab3657c ; NSKeyValuePropertyForIsaAndKeyPath
0x18aafcd4c <+112>: mov x3, x0
0x18aafcd50 <+116>: mov x0, x19
0x18aafcd54 <+120>: mov x2, x20
0x18aafcd58 <+124>: mov x4, x22
0x18aafcd5c <+128>: mov x5, x23
0x18aafcd60 <+132>: bl 0x18b3c1c20 ; objc_msgSend$_addObserver:forProperty:options:context:
0x18aafcd64 <+136>: adrp x0, 376784
0x18aafcd68 <+140>: add x0, x0, #0x3ac ; _NSKeyValueObserverRegistrationLock
......
从上述代码看真正添加Observer的实现应该在_addObserver:forProperty:options:context: 方法中,可以发现替换isa指针的方法是isaForAutonotifying
(lldb) breakpoint set -n 'isaForAutonotifying'
Foundation`-[NSKeyValueUnnestedProperty isaForAutonotifying]:
......
0x18aafd1f8 <+60>: ldrb w8, [x0, x22]
0x18aafd1fc <+64>: cbz w8, 0x18aafd20c ; <+80>
0x18aafd200 <+68>: adrp x8, 376776
0x18aafd204 <+72>: ldrsw x23, [x8, #0x228]
0x18aafd208 <+76>: b 0x18aafd2bc ; <+256>
0x18aafd20c <+80>: mov x0, x19
0x18aafd210 <+84>: bl 0x18b3c49a0 ; objc_msgSend$_isaForAutonotifying
0x18aafd214 <+88>: adrp x8, 376776
0x18aafd218 <+92>: add x8, x8, #0x220 ; _MergedGlobals
0x18aafd21c <+96>: ldrsw x23, [x8, #0x8]
......
(lldb) breakpoint set -n '_isaForAutonotifying'
Foundation`-[NSKeyValueUnnestedProperty _isaForAutonotifying]:
0x18aafd4c0 <+0>: pacibsp
0x18aafd4c4 <+4>: stp x20, x19, [sp, #-0x20]!
0x18aafd4c8 <+8>: stp x29, x30, [sp, #0x10]
0x18aafd4cc <+12>: add x29, sp, #0x10
0x18aafd4d0 <+16>: mov x19, x0
0x18aafd4d4 <+20>: ldp x8, x2, [x0, #0x8]
0x18aafd4d8 <+24>: ldr x0, [x8, #0x8]
0x18aafd4dc <+28>: bl 0x18b3c99e0 ; objc_msgSend$automaticallyNotifiesObserversForKey:
0x18aafd4e0 <+32>: cbz w0, 0x18aafd504 ; <+68>
0x18aafd4e4 <+36>: ldr x0, [x19, #0x8]
0x18aafd4e8 <+40>: bl 0x18ab6ad08 ; _NSKeyValueContainerClassGetNotifyingInfo
0x18aafd4ec <+44>: cbz x0, 0x18aafd508 ; <+72>
0x18aafd4f0 <+48>: mov x20, x0
0x18aafd4f4 <+52>: ldr x1, [x19, #0x10]
0x18aafd4f8 <+56>: bl 0x18aaecbe8 ; _NSKVONotifyingEnableForInfoAndKey
0x18aafd4fc <+60>: ldr x0, [x20, #0x8]
0x18aafd500 <+64>: b 0x18aafd508 ; <+72>
0x18aafd504 <+68>: mov x0, #0x0
0x18aafd508 <+72>: ldp x29, x30, [sp, #0x10]
0x18aafd50c <+76>: ldp x20, x19, [sp], #0x20
0x18aafd510 <+80>: retab
(lldb) breakpoint set -n '_NSKVONotifyingEnableForInfoAndKey'
_NSKVONotifyingEnableForInfoAndKey 方法汇编实现太长了,以下就直接翻译本文比较关心的部分伪代码:
......
char *argtype = method_copyArgumentType(m, 2);
IMP replacementSetter = (IMP)&_NSSetObjectValueAndNotify;
if (argtype[0] <= '?' && argtype[0] != '#') {
NSLog(@"KVO only supports -set<Key>: methods that take id, NSNumber-supported scalar types, and some structure types. Autonotifying will not be done for invocations of -[%@ %s].", notifyingInfo->_originalClass, sel_getName(method_getName(m)));
} else {
if (argtype[0] == '{') {
if (strcmp(argtype, @encode(CGPoint)) == 0) {
replacementSetter = (IMP)&_NSSetPointValueAndNotify;
} else if (strcmp(argtype, @encode(NSRange)) == 0) {
replacementSetter = (IMP)&_NSSetRangeValueAndNotify;
} else if (strcmp(argtype, @encode(CGRect)) == 0) {
replacementSetter = (IMP)&_NSSetRectValueAndNotify;
} else if (strcmp(argtype, @encode(CGSize)) == 0) {
replacementSetter = (IMP)&_NSSetSizeValueAndNotify;
} else {
replacementSetter = (IMP)&_CF_forwarding_prep_0;
}
} else {
switch (argtype[0]) {
case: 基础数据类型 {
// 获取基础数据类型的IMP
......
} break;
}
}
free(argtype);
SEL selector = method_getName(m);
NSKVONotifyingSetMethodImplementation(notifyingInfo, selector, replacementSetter, key);
NSKeyValueSetter *notifyingSetter = [NSObject _createValueSetterWithContainerClassID:notifyingInfo->_notifyingClass key:key];
[notifyingSetter setMethod:class_getInstanceMethod(notifyingInfo->_notifyingClass, selector)];
if (replacementSetter == (IMP)&_CF_forwarding_prep_0) {
NSKVONotifyingSetMethodImplementation(notifyingInfo, @selector(forwardInvocation:), (IMP)&NSKVOForwardInvocation, nil);
Class otherClass= notifyingInfo->_notifyingClass;
const char *methodName = sel_getName(selector);
int nameLength = strlen(methodName);
const char *prefix = kOriginalImplementationMethodNamePrefix;
char buffer[29] = {0};
strlcpy(buffer, prefix, nameLength+strlen(prefix));
strlcat(buffer, methodName, nameLength+strlen(prefix));
SEL newForwardingSelector = sel_registerName(buffer);
IMP originalIMP = method_getImplementation(m);
const char *originalTypeEncoding = method_getTypeEncoding(m);
class_addMethod(otherClass, newForwardingSelector, originalIMP, originalTypeEncoding);
}
}
发现非 CGSize、CGPoint、CGRect、NSRange的结构体的IMP指向了 _CF_forwarding_prep_0 ,具体实现可以看下下面代码:
_CF_forwarding_prep_0: // top of stack is used as marg_list
stmfd sp!, {r0-r3} // push args to marg_list
stmfd sp!, {fp, lr} // setup stack frame: sp -= 8, marg_list @ sp+8
.save {fp, lr}
.setfp fp, sp, #4
add fp, sp, #4
.pad #8
sub sp, sp, #8 // pad the stack: sp -= 8, marg_list @ sp+16
add r1, sp, #16 // use marg_list as return strage pointer
add r0, sp, #16 // load marg_list
bl ___forwarding___ // call through
sub sp, fp, #4 // restore stack
ldmfd sp!, {fp, lr} // destroy stack frame
cmp r0, #0 // check for forwarding completion
bne LContinue // circle back around if we're not done or failed
ldmfd sp!, {r0-r3} // load return value registers from marg_list
bx lr // return
CF_forwarding_prep_0中的___forwarding___ 的功能就是进行三次消息转发,也就是解释了UIEdgeInsets的属性添加KVO后触发更新时,先进入消息转发,再调用到原方法。
添加KVO后,UITableView实例的isa指针指向NSKVONotifying_UITableView
当开发者调用UITableView的setContentInset方法时,实际就是走的NSKVONotifying_UITableView:
iOS开发:『Crash 防护系统』(一)Unrecognized Selector【https://cloud.tencent.com/developer/beta/article/1492750】
Foundation 【https://github.com/apportable/Foundation】