电脑装配网

避免iOS崩溃:揭秘Unrecognized Selector防护挑战和实用策略

 人阅读 | 作者yiyi | 时间:2023-07-20 02:18

前言

最近在处理iOS crash 防护时,发现Unrecognized Selector的防护存在一些日常开发被忽略的情况。若直接使用业内防护方案的话,有可能存在相对隐蔽不易发现的功能失效。本文就针对Unrecognized Selector防护中遇到的问题进行分析,揭秘整个防护过程隐藏的问题。

一、问题表现

添加Unrecognized Selector防护后,首页触发刷新就会弹出防护警告,看内容比较疑惑,UITableView中的setContentInset: 正常情况下是有对应实现的,怎么可能会触发crash拦截呢?

二、Unrecognized Selector 防护实现

先来看下我的防护系统中Unrecognized Selector是怎么实现的,我们采用的是Hook消息转发中的 forwardingTargetForSelector方法,然后再判断是否重写forwardingTargetForSelector或者forwardInvocation方法,若重写了,则本类需要处理消息转发,就不防护了,否则就返回CrashGuardProxy对象。

心想业界的方案都是这样写的,应该可以完美防护住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 ; _NSKeyValueObserverRegistrationLock0x18aafcd34 <+88>: mov w1, #0x00x18aafcd38 <+92>: bl 0x1904ce9f00x18aafcd3c <+96>: mov x0, x190x18aafcd40 <+100>: bl 0x1904ce8a00x18aafcd44 <+104>: mov x1, x210x18aafcd48 <+108>: bl 0x18ab3657c ; NSKeyValuePropertyForIsaAndKeyPath0x18aafcd4c <+112>: mov x3, x00x18aafcd50 <+116>: mov x0, x190x18aafcd54 <+120>: mov x2, x200x18aafcd58 <+124>: mov x4, x220x18aafcd5c <+128>: mov x5, x230x18aafcd60 <+132>: bl 0x18b3c1c20 ; objc_msgSend$_addObserver:forProperty:options:context:0x18aafcd64 <+136>: adrp x0, 3767840x18aafcd68 <+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, 3767760x18aafd204 <+72>: ldrsw x23, [x8, #0x228]0x18aafd208 <+76>: b 0x18aafd2bc ; <+256>0x18aafd20c <+80>: mov x0, x190x18aafd210 <+84>: bl 0x18b3c49a0 ; objc_msgSend$_isaForAutonotifying0x18aafd214 <+88>: adrp x8, 3767760x18aafd218 <+92>: add x8, x8, #0x220 ; _MergedGlobals0x18aafd21c <+96>: ldrsw x23, [x8, #0x8]......

在该方法中发现真正替换isa的方法是_isaForAutonotifying,直接调试:

(lldb) breakpoint set -n '_isaForAutonotifying'Foundation`-[NSKeyValueUnnestedProperty _isaForAutonotifying]:0x18aafd4c0 <+0>: pacibsp0x18aafd4c4 <+4>: stp x20, x19, [sp, #-0x20]!0x18aafd4c8 <+8>: stp x29, x30, [sp, #0x10]0x18aafd4cc <+12>: add x29, sp, #0x100x18aafd4d0 <+16>: mov x19, x00x18aafd4d4 <+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 ; _NSKeyValueContainerClassGetNotifyingInfo0x18aafd4ec <+44>: cbz x0, 0x18aafd508 ; <+72>0x18aafd4f0 <+48>: mov x20, x00x18aafd4f4 <+52>: ldr x1, [x19, #0x10]0x18aafd4f8 <+56>: bl 0x18aaecbe8 ; _NSKVONotifyingEnableForInfoAndKey0x18aafd4fc <+60>: ldr x0, [x20, #0x8]0x18aafd500 <+64>: b 0x18aafd508 ; <+72>0x18aafd504 <+68>: mov x0, #0x00x18aafd508 <+72>: ldp x29, x30, [sp, #0x10]0x18aafd50c <+76>: ldp x20, x19, [sp], #0x200x18aafd510 <+80>: retab

_NSKeyValueContainerClassGetNotifyingInfo主要是生成 NSKVONotifying_xxx 类以及创建_isKVOA、dealloc、class方法;

_NSKVONotifyingEnableForInfoAndKey就是对监听属性的处理,这个就是要找的,看看具体实现:

(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_liststmfd sp!, {r0-r3} // push args to marg_liststmfd sp!, {fp, lr} // setup stack frame: sp -= 8, marg_list @ sp+8.save {fp, lr}.setfp fp, sp, #4add fp, sp, #4.pad #8sub sp, sp, #8 // pad the stack: sp -= 8, marg_list @ sp+16add r1, sp, #16 // use marg_list as return strage pointeradd r0, sp, #16 // load marg_listbl ___forwarding___ // call throughsub sp, fp, #4 // restore stackldmfd sp!, {fp, lr} // destroy stack framecmp r0, #0 // check for forwarding completionbne LContinue // circle back around if we're not done or failedldmfd sp!, {r0-r3} // load return value registers from marg_listbx lr // return

CF_forwarding_prep_0中的___forwarding___ 的功能就是进行三次消息转发,也就是解释了UIEdgeInsets的属性添加KVO后触发更新时,先进入消息转发,再调用到原方法。

在回顾下整个KVO添加以及方法调用过程,未添加KVO时,UITableView存在setContentInset方法

添加KVO后,UITableView实例的isa指针指向NSKVONotifying_UITableView

当开发者调用UITableView的setContentInset方法时,实际就是走的NSKVONotifying_UITableView:

正常情况下forwardingTargetForSelector:是没有对应实现的,一定会进入到forwardInvocation: ,但是我们增加了crash防护是替换了forwardingTargetForSelector:方法,判断是会认为需要防护的,就会被我们的防护代码给误拦截了,也导致无法进入到KVO的消息分发了。

三、问题解决

知道问题后修改起来就容易多了,只要在拦截处判断下是否有对象方法实现,不管是正常IMP还是_CF_forwarding_prep_0都算有IMP了,至于原类怎么处理的不需要管,防护系统就不防护这类的情况。

四、参考资料

iOS开发:『Crash 防护系统』(一)Unrecognized Selector【https://cloud.tencent.com/developer/beta/article/1492750】

Foundation 【https://github.com/apportable/Foundation】

作者:binweichen

来源:微信公众号:腾讯VATeam

出处:https://mp.weixin.qq.com/s/bHdtetOb3LQGRfRO9AFVQA


文章标签:

本文链接:『转载请注明出处』