Runtime和Runloop的初识
这几天都在看一些iOS进阶的底层知识,所以理所当然的要好好学习一下runtime和runloop,在一开始的时候发现确实晦涩难懂,也可能是因为我比较笨吧【叹气】,但是不学不行啊特别是看来好多文档发现只要是个稍微有点深度的文章都会提到这俩货就更不能不学了,所以只要硬着头皮学咯,不过幸运的是我发现从实用方面入手就容易懂多了,万幸万幸,看来有时候从结果推理论也是个不错的方式呢。然后这里就把这几天的学习心得简单总结一下,这里关于runtime的说法很多都参考了OC最实用的runtime总结,面试、工作你看我就足够了! 关于runloop的则主要参考深入理解RunLoop
1:runtime
Objective-C是一门动态语言,什么是动态语言呢?动态语言就是指在执行静态语言的编译和链接时能动态的操作的一种语言。所以除了运行静态语言的编译器以外,还需要一个运行时系统来执行的编译功能。这个运行时系统被称之为runtime。【这句话是我废了老大的劲儿才想好的,也不知道对不对】
runtime的功能
- 动态交换两个方法,这里的方法包括系统自带的方法
- 动态添加对象的成员变量和成员方法
- 获得某个类的所有成员方法、所有成员变量
runtime的实际应用
1. “黑魔法”Method Swizzling使用
这是一个很有意思的小技巧,名字为什么叫的这么玄乎不得考,但是确实挺好玩的,也很实用,Method Swizzling
其实主要基于的就是下面这个方法
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
举例的话我想那一个日常提别容易出现问题的报错来展示——数组越界。在我们日常的工作中常常会因为数组越界让整个项目crash,这个在我们调试的时候确实是必要的,但是在用户使用的时候却最好不要出现。这个时候就要想办法做到哪怕数组越界了也不能导致奔溃了。简单的做法就是替换NSArray的objectAtIndex
方法
首先你需要给你想要替换的系统方法建一个对应的category,数组越界那肯定是要给NSArray这个类做一个category了。紧接着你需要在category中的+ (void)load
添加Method Swizzling
方法。下面是代码展示:
+ (void)load {
SEL safeSel = @selector(safeObjectAtIndex:);
SEL sysSel = @selector(objectAtIndex:);
Class myClas = NSClassFromString(@"__NSArrayI");
Method safeMethod = class_getInstanceMethod(myClas, safeSel);
Method sysMethod = class_getInstanceMethod(myClas, sysSel);
method_exchangeImplementations(sysMethod, safeMethod);
}
- (id)safeObjectAtIndex:(NSUInteger)index {
if (index > (self.count-1)) {
NSAssert(NO, @"数组越界啦~");
return nil;
} else {
return [self safeObjectAtIndex:index];
}
}
由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。所以可以安心的在load中做method的替换。另外你会发现,在runtime下__NSArrayI
才是NSArray真正的类,这个也是需要注意的点。
然后再看看我们的具体实现的safeObjectAtIndex
方法,这里的NSAssert是断言,平时用于开发阶段调试程序中的Bug,通过为NSAssert()
传递条件表达式来断定是否属于Bug。然后是关于在safeObjectAtIndex
里调用safeObjectAtIndex
,其实这个时候safeObjectAtIndex
已经不是原来的那个safeObjectAtIndex
,而是objectAtIndex
,因为你在load的时候已经把他们俩互换了,所以不会发生循环引用,而且这个是必须要写的,不然你会让原来正常的objectAtIndex
出现问题。
2. 在分类中设置属性,给任何一个对象设置属性
比方说我们想让UIButton多一个btnNumber属性,这时候就可以用category和runtime给他加上这个属性。
需要用到的方法 <objc/runtime.h>
- set方法,将值value 跟对象object 关联起来(将值value 存储到对象object 中)
参数 object:给哪个对象设置属性
参数 key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
参数 value:给属性设置的值
参数policy:存储策略 (assign 、copy 、 retain就是strong)void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
- 利用参数key 将对象object中存储的对应值取出来
id objc_getAssociatedObject(id object , const void *key)
首先创建一个UIButton的category,然后给这个category加一个Number的属性。记得把btnNumber属性写到.h里,然后再.m中重写Getter和Setter方法。
.h:
@property (nonatomic, copy) NSString *btnNumber;
.m:
char btnNumberKey;
- (void)setBtnNumber:(NSString *)btnNumber {
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
objc_setAssociatedObject(self, &btnNumberKey, btnNumber, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)btnNumber {
return objc_getAssociatedObject(self, &btnNumberKey);
}
这样你就能让你的UIButton多一个btnNumber的属性了,这里要建议大家起这类属性名字的时候最好让他变得特别,不然跟系统自己的属性有所冲突就难办了。
2:runloop
从字面上理解,runloop可以简单直译成运行环。就是一个用来跑任务的环儿,为什么是个环而不是一根棍子呢?因为他跑完你这个任务还要接着等着跑别的任务。你不把app杀掉他就一直等着你给它活儿干。【真是敬业】也就是 "等待->接受消息->处理->处理完接着等"从而形成一个环儿。
所以runloop可以简单理解为一个一直在等待消息进来的对象,是一个让线程能随时处理事件但并不退出的机制。
由于我还没有特别深入的了解runloop的主要作用,所以这里暂时只说我懂的那部分。直接说runloop的应用吧。
说到runloop的运用主要还是要看runloop的mode,目前苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 "Common"。使用时注意区分这个字符串和其他 mode name。
所以runloop的主要应用还是在针对UItrackingRunloopmode 和 kCFRunloopDefaultMode之间,UITracking主要是追踪scrollView的滑动状态,而DefaultMode主要是用于app平时所处的状态。
大家在使用NSTimer的时候肯定有一步是吧Timer加进一个mode里对吧。这个mode就是runloop的典型运用之一。下面有一个这样的场景,一个页面中有一个自动轮播图,然后这个页面又有scrollView的滚动需求,那么在滚动scrollView的时候轮播图不动了怎么办。比如下面这个淘宝的首页,首页的head是一个自动滚动的轮播图,但是想要在滑动的时候轮播图也能自己动的话就需要用到runloop的mode了。
淘宝网首页.png这个时候就是因为runloop的mode中你只把timer加到了NSDefaultRunLoopMode中,而scrollview的滚动是在UITrackingRunLoopMode中控制的。想要在滑动屏幕的同时也能顺利的让轮播图滚动,你就需要把timer同时加进NSDefaultRunLoopMode和UITrackingRunLoopMode,也就是NSRunLoopCommonModes中啦~
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];