我们先看一下崩溃堆栈:
0x01c9709f libobjc.A.dylib`objc_msgSend + 19
0x00c3656b UIKit`-[UIScrollView(UIScrollViewInternal) _delegateScrollViewAnimationEnded] + 62
0x00c3665a UIKit`-[UIScrollView(UIScrollViewInternal) _scrollViewAnimationEnded:finished:] + 149
0x00c366da UIKit`-[UIScrollView(UIScrollViewInternal) animator:stopAnimation:fraction:] + 62
0x00ca1a50 UIKit`-[UIAnimator stopAnimation:] + 519
0x00ca2120 UIKit`-[UIAnimator(Static) _advanceAnimationsOfType:withTimestamp:] + 385
0x00ca1c58 UIKit`-[UIAnimator(Static) _LCDHeartbeatCallback:] + 67
0x003562d2 QuartzCore`CA::Display::DisplayLink::dispatch(unsigned long long, unsigned long long) + 110
0x0035675f QuartzCore`CA::Display::TimerDisplayLink::callback(__CFRunLoopTimer*, void*) + 161
0x01e7c376 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 22
0x01e7be06 CoreFoundation`__CFRunLoopDoTimer + 534
0x01e63a82 CoreFoundation`__CFRunLoopRun + 1810
0x01e62f44 CoreFoundation`CFRunLoopRunSpecific + 276
0x01e62e1b CoreFoundation`CFRunLoopRunInMode + 123
0x02a157e3 GraphicsServices`GSEventRunModal + 88
0x02a15668 GraphicsServices`GSEventRun + 104
0x00bc9ffc UIKit`UIApplicationMain + 1211
0x00002c6d ZHCRM`main(argc=1, argv=0xbffff3cc) + 141 at main.m:16
0x00002b95 ZHCRM`start + 53
崩溃的主要原因是因为 scrollview.delegate 在没做完滚动动画的时候释放了, 野指针调用导致。
stackoverflow 上有人讨论过这个问题点击查看。
网上一般的解决方法都是确保处理scrollview delegate 消息的对象在释放前把scrollview.delegate设置为nil。
@implement MyTableViewController
- (void)dealloc {
self.tableview.delegate = nil;
}
这种方法可以解决崩溃, 但是不够收敛,需要改动的地方很多, 而且也不能确保新的开发人员不会再引起。
接下来给大家介绍一种统一的处理方式。
主要思路:
添加一个DelegateWrapper包装对象,确保UIScrollView在释放前,uiscrollview.delegate 不为nil
对象关系就会变成这样
UITableView -> DelegateWrapper -> DelegateHandler(ViewController)
生命周期管理
然后我们通过 weak 指针可以解决 DelegateHandler的释放问题。
@interface DelegateWrapper : NSObject
@property (weak, nonatomic) id delegate;
@end
有了这个对象之后, 设置给 UIScrollerView.delegate 就变成我们的包装对象了。
- (void)setDelegate:(id)delegate
{
DelegateWrapper *wrapper = [DelegateWrapper new];
wrapper.delegate = delegate;
scrollerview.delegate = wrapper;
...
}
通过动态绑定技术,可以很方便的把DelegateWrapper对象嵌入到scrollview 里面,来控制 wrapper 的生命周期。
消息转发
接下来Delegate 还要处理scrollview 发过来的delegate 消息,把它转到真正的delegate对象上。
- (BOOL)respondsToSelector:(SEL)aSelector
{
if (!_delegate) {
NSLog(@"call selector:%s", sel_getName(aSelector));
return NO;
}
return [_delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (_delegate) {
return _delegate;
}
return [super forwardingTargetForSelector:aSelector];
}
好了, 现在有了这些消息转发机制后, 真正的delegate handler可以收到消息了。
调用收敛
现在基本的流程和原理都介绍完了, 接下来的事情就是怎样在一个地方处理了, 以后就都不会有这个问题。
入口点 [UIScrollView setDelegate:]
根据Object C 语言的动态性,使用MethodSwizzing 技术可以很好的把 [UIScrollView setDelegate:] 的方法替换成我们的。这样只要在程序初始化后调用一下,以后就不用担心了。
+ (void)hookMethedClass:(Class)class hookSEL:(SEL)hookSEL originalSEL:(SEL)originalSEL myselfSEL:(SEL)mySelfSEL
{
Method hookMethod = class_getInstanceMethod(class, hookSEL);
Method mySelfMethod = class_getInstanceMethod([MethodsHooker class], mySelfSEL);
IMP hookMethodIMP = method_getImplementation(hookMethod);
class_addMethod(class, originalSEL, hookMethodIMP, method_getTypeEncoding(hookMethod));
IMP hookMethodMySelfIMP = method_getImplementation(mySelfMethod);
class_replaceMethod(class, hookSEL, hookMethodMySelfIMP, method_getTypeEncoding(hookMethod));
}
+ (void)hookUIScrollViewSetDelegate
{
[MethodsHooker hookMethedClass:NSClassFromString(@"UIScrollView")
hookSEL:@selector(setDelegate:)
originalSEL:@selector(originalScrollViewSetDelegate:)
myselfSEL:@selector(myselfScrollViewSetDelegate:)];
}
- (void)myselfScrollViewSetDelegate:(id)delegate
{
WeakDelegate *weakDelegateObject = [[WeakDelegate alloc] init];
weakDelegateObject.delegate = delegate;
objc_setAssociatedObject(self, "weak_delegate_handler", weakDelegateObject, OBJC_ASSOCIATION_RETAIN);
[self originalScrollViewSetDelegate:weakDelegateObject];
}
- (void)originalScrollViewSetDelegate:(id)delegate
{
}
现在只要在didFinishLunch 后调用一下 hookUIScrollViewSetDelegate 这个方法就可以了。