Runtime巩固
Runtime是什么?
Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的。
消息转发
- 方法:与一个类相关的一段实际代码,并给出一个特定的名字。例:
- (int)meaning { return 42; }
消息:发送给对象的名称和一组参数。示例:向0x12345678对象发送
meaning
并且没有参数。-
选择器:表示消息或方法名称的一种特殊方式,表示为类型SEL。选择器本质上就是不透明的字符串,它们被管理,因此可以使用简单的指针相等来比较它们,从而提高速度。(实现可能会有所不同,但这基本上是他们在外部看起来的样子。)例如:
@selector(meaning)
。 - 消息发送:接收信息并查找和执行适当方法的过程。
objc_msgSend
就负责消息发送。在runtime的objc/message.h
中能找到它的API。objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)`
消息发送的主要步骤
消息发送的时候,在C语言函数中发生了什么事情?编译器是如何找到这个方法的呢?消息发送的主要步骤如下:
- 首先检查这个selector是不是要忽略。比如Mac OS X开发,有了垃圾回收就不会理会retain,release这些函数。
- 检测这个selector的target是不是nil,OC允许我们对一个nil对象执行任何方法不会Crash,因为运行时会被忽略掉。
- 如果上面两步都通过了,就开始查找这个类的实现IMP,先从cache里查找,如果找到了就运行对应的函数去执行相应的代码。
- 如果cache中没有找到就找类的方法列表中是否有对应的方法。
- 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到NSObject类为止。
OC的方法本质
在编译时你写的 OC 函数调用的语法都会被翻译成一个 C 的函数调用 objc_msgSend()
。比如,下面两行代码就是等价的:
OC
[array insertObject:foo atIndex:5];
C
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
动态特性:方法解析和消息转发
没有方法的实现,程序会在运行时挂掉并抛出 unrecognized selector sent to …
的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:
- Method resolution
- Fast forwarding
- Normal forwarding
动态方法解析: Method Resolution
+ (BOOL)resolveInstanceMethod:
或者 + (BOOL)resolveClassMethod:
,让你有机会提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。还是以 foo 为例,你可以这么实现:void fooMethod(id obj, SEL _cmd) { NSLog(@"Doing foo"); } + (BOOL)resolveInstanceMethod:(SEL)aSEL { if(aSEL == @selector(foo:)){ class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:"); return YES; } return [super resolveInstanceMethod]; }
这里第一字符v
代表函数返回类型void
,第二个字符@
代表self的类型id
,第三个字符:
代表_cmd的类型SEL
。
快速转发: Fast Rorwarding
- (id)forwardingTargetForSelector:(SEL)aSelector
方法。如果此方法返回的是nil 或者self,则会进入消息转发机制(- (void)forwardInvocation:(NSInvocation *)invocation
),否则将会向返回的对象重新发送消息。- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(foo:)){ return [[BackupClass alloc] init]; } return [super forwardingTargetForSelector:aSelector]; }
完整消息转发: Normal Forwarding
与上面不同,可以理解成完整消息转发,是可以代替快速转发做更多的事。
- (void)forwardInvocation:(NSInvocation *)invocation { SEL sel = invocation.selector; if([alternateObject respondsToSelector:sel]) { [invocation invokeWithTarget:alternateObject]; } else { [self doesNotRecognizeSelector:sel]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; if (!methodSignature) { methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"]; } return methodSignature; }
forwardInvocation:
方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的消息对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。例如:我们可以为了避免直接闪退,可以当消息没法处理时在这个方法中给用户一个提示,也不失为一种友好的用户体验。
其中,参数invocation
是从哪来的?在forwardInvocation:
消息发送前,runtime系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation对象。所以重写forwardInvocation:
的同时也要重写methodSignatureForSelector:
方法,否则会抛出异常。当一个对象由于没有相应的方法实现而无法响应某个消息时,运行时系统将通过forwardInvocation:
消息通知该对象。每个对象都继承了forwardInvocation:
方法,我们可以将消息转发给其它的对象。
区别: Fast Rorwarding 对比 Normal Forwarding?
需要重载的API方法的用法不同
- 前者只需要重载一个API即可,后者需要重载两个API。
- 前者只需在API方法里面返回一个新对象即可,后者需要对被转发的消息进行重签并手动转发给新对象(利用
invokeWithTarget:
)。
转发给新对象的个数不同
- 前者只能转发一个对象,后者可以连续转发给多个对象。例如下面是完整转发:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector==@selector(run)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector: aSelector]; } -(void)forwardInvocation:(NSInvocation *)anInvocation { SEL selector =[anInvocation selector]; RunPerson *RP1=[RunPerson new]; RunPerson *RP2=[RunPerson new]; if ([RP1 respondsToSelector:selector]) { [anInvocation invokeWithTarget:RP1]; } if ([RP2 respondsToSelector:selector]) { [anInvocation invokeWithTarget:RP2]; } }
应用实战:消息转发
1、特定崩溃预防处理
没有实现方法而会导致奔溃
#import "NSObject+CrashLogHandle.h" @implementation NSObject (CrashLogHandle) - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { //方法签名 return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"NSObject+CrashLogHandle---在类:%@中 未实现该方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector)); } @end
因为在category中复写了父类的方法,会出现下面的警告:
解决办法就是在Xcode的Build Phases中的资源文件里,在对应的文件后面 -w ,忽略所有警告。
2、苹果系统API迭代造成API不兼容的奔溃处理
if (isOperatingSystemAtLeastVersion(11, 0, 0)) { scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else { viewController.automaticallyAdjustsScrollViewInsets = NO; }
解决方式二:消息转发
消息机制总结
Objective-C 中给一个对象发送消息会经过以下几个步骤:
Objective-C 中给一个对象发送消息会经过以下几个步骤:
-
在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
-
如果没有找到,Runtime 会发送
+resolveInstanceMethod:
或者+resolveClassMethod:
尝试去 resolve (解决)这个消息; -
如果 resolve 方法返回 NO,Runtime 就发送
-forwardingTargetForSelector:
允许你把这个消息转发给另一个对象; -
如果没有新的目标对象返回, Runtime 就会发送
-methodSignatureForSelector:
和-forwardInvocation:
消息。你可以发送-invokeWithTarget:
消息来手动转发消息或者发送-doesNotRecognizeSelector:
抛出异常。
关联对象:为分类添加“属性”
runtime提供了給我们3个API以管理关联对象(存储、获取、移除):
//关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) //获取关联的对象 id objc_getAssociatedObject(id object, const void *key) //移除关联的对象 void objc_removeAssociatedObjects(id object)
其中的参数
id object
:被关联的对象const void *key
:关联的key,要求唯一id value
:关联的对象objc_AssociationPolicy policy
:内存管理的策略
@property
并不会自动生成实例变量以及存取方法,所以一般使用关联对象为已经存在的类添加 “属性”。解决方案:可以使用两个方法 objc_getAssociatedObject
以及 objc_setAssociatedObject
来模拟属性 的存取方法,而使用关联对象模拟实例变量。用法解析
NSObject+AssociatedObject.m
#import "NSObject+AssociatedObject.h" #import <objc/runtime.h> @implementation NSObject (AssociatedObject) - (void)setAssociatedObject:(id)associatedObject { objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)associatedObject { return objc_getAssociatedObject(self, _cmd); } @end
- (void)viewDidLoad { [super viewDidLoad]; NSObject *objc = [[NSObject alloc] init]; objc.associatedObject = @"Extend Category"; NSLog(@"associatedObject is = %@", objc.associatedObject); }
查看OBJC_ASSOCIATION_RETAIN_NONATOMIC
,可以发现它是一个枚举类型,完整枚举项如下所示:
实战场景
- UITapGestureRecognizer+NSString.h
#import <UIKit/UIKit.h> @interface UITapGestureRecognizer (NSString) //类拓展添加属性 @property (nonatomic, strong) NSString *dataStr; @end
- UITapGestureRecognizer+NSString.m
#import "UITapGestureRecognizer+NSString.h" #import <objc/runtime.h> //定义常量 必须是C语言字符串 static char *PersonNameKey = "PersonNameKey"; @implementation UITapGestureRecognizer (NSString) - (void)setDataStr:(NSString *)dataStr{ objc_setAssociatedObject(self, PersonNameKey, dataStr, OBJC_ASSOCIATION_COPY_NONATOMIC); } -(NSString *)dataStr{ return objc_getAssociatedObject(self, PersonNameKey); } @end
如此一来,响应事件的方法就可以根据事件激活方携带过来的信息进行下一步操作了,比如根据它携带过来的某个参数进行网络请求等等。
应用到此知识点的第三方框架有 Masonry MJRefresh
关联对象:封装关联的Block体,作为属性
- UIAlertView+Handle.h
#import <UIKit/UIKit.h> // 声明一个button点击事件的回调block typedef void (^ClickBlock)(NSInteger buttonIndex) ; @interface UIAlertView (Handle) @property (copy, nonatomic) ClickBlock callBlock; @end
- UIAlertView+Handle.m
#import "UIAlertView+Handle.h" #import <objc/runtime.h> @implementation UIAlertView (Handle) - (void)setCallBlock:(ClickBlock)callBlock { objc_setAssociatedObject(self, @selector(callBlock), callBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (ClickBlock )callBlock { return objc_getAssociatedObject(self, _cmd); // return objc_getAssociatedObject(self, @selector(callBlock)); } @end
调用
- (void)popAlertViews3 { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil]; [alert setCallBlock:^(NSInteger buttonIndex) { if (buttonIndex == 0) { [self doCancel]; } else { [self doContinue]; } }]; [alert show]; } // UIAlertViewDelegate protocol method - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ void (^block)(NSInteger) = alertView.callBlock; block(buttonIndex); }
block去实现button的点击回调
#import <UIKit/UIKit.h> #import <objc/runtime.h> // 导入头文件 // 声明一个button点击事件的回调block typedef void(^ButtonClickCallBack)(UIButton *button); @interface UIButton (Handle) // 为UIButton增加的回调方法 - (void)handleClickCallBack:(ButtonClickCallBack)callBack; @end
#import "UIButton+Handle.h" // 声明一个静态的索引key,用于获取被关联对象的值 static char *buttonClickKey; @implementation UIButton (Handle) - (void)handleClickCallBack:(ButtonClickCallBack)callBack { // 将button的实例与回调的block通过索引key进行关联: objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 设置button执行的方法 [self addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside]; } - (void)buttonClicked { // 通过静态的索引key,获取被关联对象(这里就是回调的block) ButtonClickCallBack callBack = objc_getAssociatedObject(self, &buttonClickKey); if (callBack) { callBack(self); } } @end
关联观察者对象
AFNetworking为菊花控件监听NSURLSessionTask以获取网络进度的分类
为了获取某个数据,但这个获取的过程只需要执行一次即可,让某个对象的方法获得的数据结果作为“属性”与这个对象进行关联。
方法交换
先给要替换的方法的类添加一个Category,然后在Category中的+(void)load
方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。
由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。
- Swizzling应该总在+load中执行
- Swizzling应该总是在dispatch_once中执行
- Swizzling在+load中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象。
- 为了避免Swizzling的代码被重复执行,我们可以通过GCD的dispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。
防止UI控件短时间多次激活事件
给按钮添加分类,并添加一个点击事件间隔的属性,执行点击事件的时候判断一下是否时间到了,如果时间不到,那么拦截点击事件。
UIButton是UIControl的子类,因而根据UIControl新建一个分类即可
- UIControl+Limit.m
#import "UIControl+Limit.h" #import <objc/runtime.h> static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval"; static const char *UIControl_ignoreEvent="UIControl_ignoreEvent"; @implementation UIControl (Limit) #pragma mark - acceptEventInterval - (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval { objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -(NSTimeInterval)acceptEventInterval { return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue]; } #pragma mark - ignoreEvent -(void)setIgnoreEvent:(BOOL)ignoreEvent{ objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN); } -(BOOL)ignoreEvent{ return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue]; } #pragma mark - Swizzling +(void)load { Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:)); Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:)); method_exchangeImplementations(a, b);//交换方法 } - (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event { if(self.ignoreEvent){ NSLog(@"btnAction is intercepted"); return;} if(self.acceptEventInterval>0){ self.ignoreEvent=YES; [self performSelector:@selector(setIgnoreEventWithNo) withObject:nil afterDelay:self.acceptEventInterval]; } [self swizzled_sendAction:action to:target forEvent:event]; } -(void)setIgnoreEventWithNo{ self.ignoreEvent=NO; } @end
- ViewController.m
-(void)setupSubViews{ UIButton *btn = [UIButton new]; btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)]; [btn setTitle:@"btnTest"forState:UIControlStateNormal]; [btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal]; btn.acceptEventInterval = 3; [self.view addSubview:btn]; [btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside]; } - (void)btnAction{ NSLog(@"btnAction is executed"); }
数组越界问题
- NSArray+CrashHandle.m
@implementation NSArray (CrashHandle) // Swizzling核心代码 // 需要注意的是,好多同学反馈下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。 + (void)load { Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); } // 为了避免和系统的方法冲突,我一般都会在swizzling方法前面加前缀 - (id)cm_objectAtIndex:(NSUInteger)index { // 判断下标是否越界,如果越界就进入异常拦截 if (self.count-1 < index) { @try { return [self cm_objectAtIndex:index]; } @catch (NSException *exception) { // 在崩溃后会打印崩溃信息。如果是线上,可以在这里将崩溃信息发送到服务器 NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__); NSLog(@"%@", [exception callStackSymbols]); return nil; } @finally {} } // 如果没有问题,则正常进行方法调用 else { return [self cm_objectAtIndex:index]; } }
- ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; // 测试代码 NSArray *array = @[@0, @1, @2, @3]; [array objectAtIndex:3]; //本来要奔溃的 [array objectAtIndex:4]; }
UINavigationController安全的 safePopToViewController
UINavigationController+APSafeTransition.h
KVO的实现原理
比如:web加载进度
[_vWeb addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
[_webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
//WkWebView的 回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"estimatedProgress"]) { [_progressView setProgress:_webView.estimatedProgress animated:YES]; if (_webView.estimatedProgress >= 1.0) { [UIView animateWithDuration:0.3 delay:0.1 options:UIViewAnimationOptionCurveEaseInOut animations:^(){ _progressView.alpha = 0; } completion:^(BOOL finish){ _progressView.progress = 0; }]; } } if ([keyPath isEqualToString:@"title"]) { if (object == self.webView) { self.title = self.webView.title; } } }
- (void)dealloc{ [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; [_webView removeObserver:self forKeyPath:@"title"]; }