runtime
Objective-C 是一门动态语言,它把很多静态语言在编译和链接时做的事情放到了运行时去处理,它在运行时实现了对类、方法、成员变量、属性等信息的管理机制。
需要
#import <objc/runtime.h>
基于C,为C添加类面向对象的特性。
对象方法:保存到类对象的方法列表。
类方法:保存到元类(Meta Class)的方法列表。
运行时的类与对象
类与对象相关的函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31const char *class_getName(Class cls),获取指定类的类名。
BOOL class_isMetaClass(Class cls),判断指定类是否是一个元类。
Class class_getSuperclass(Class cls),获取指定类的父类。
Class class_setSuperclass(Class cls, Class newSuper),设定指定类的父类。
int class_getVersion(Class cls),获取指定类的版本信息。
void class_setVersion(Class cls, int version),设定指定类的版本信息。
size_t class_getInstanceSize(Class cls),获取实例大小。
Ivar class_getInstanceVariable(Class cls, const char *name),获取指定名字的实例变量。
Ivar class_getClassVariable(Class cls, const char *name),获取指定名字的类变量。
Ivar *class_copyIvarList(Class cls, unsigned int *outCount),获取类的成员变量列表的拷贝。调用后需要自己 free()。
Method class_getInstanceMethod(Class cls, SEL name),获取指定名字的实例方法。
Method class_getClassMethod(Class cls, SEL name),获取指定名字的类方法。
IMP class_getMethodImplementation(Class cls, SEL name),获取指定名字的方法实现。
BOOL class_respondsToSelector(Class cls, SEL sel),类是否响应指定的方法。
Method *class_copyMethodList(Class cls, unsigned int *outCount),获取方法列表的拷贝。调用后需要自己 free()。
BOOL class_conformsToProtocol(Class cls, Protocol *protocol),类是否遵循指定的协议。
Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount),获取协议列表的拷贝。调用后需要自己 free()。
objc_property_t class_getProperty(Class cls, const char *name),获取指定名字的属性。
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount),获取类的属性列表。调用后需要自己 free()。
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types),为类添加方法。
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types),替代类的方法。
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types),给指定的类添加成员变量。这个函数只能在 objc_allocateClassPair() 和 objc_registerClassPair() 之间调用,并且不能为一个已经存在的类添加成员变量。
BOOL class_addProtocol(Class cls, Protocol *protocol),为类添加协议。
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount),为类添加属性。
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount),替代类的属性。
id class_createInstance(Class cls, size_t extraBytes),创建指定类的实例。
id objc_constructInstance(Class cls, void *bytes),在指定的位置创建类的实例。
void *objc_destructInstance(id obj),销毁实例。
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes),创建类和元类。
void objc_registerClassPair(Class cls),注册类到 Runtime。
void objc_disposeClassPair(Class cls),销毁类和对应的元类。
- 类是元类的实例。
- isa,在大多面向对象的语言中,都有类和对象的概念,其中,对象是类的实例,是通过类定义的结构生成出来的。而在 Objective-C 中,类本身也是一个对象,类作为对象时的 isa 指针指向的是元类(Meta Class)。
object_getClass() 可以获得当前对象 isa
#pragma clang diagnostic...
代码,可用于忽略编译器对于未声明的 @selector 的 warning。因为我们的代码中我们需要动态的为一个类创建方法,所以必然不会事先声明。1
2
3
4#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
//调用未声明的 @selector
#pragma clang diagnostic pop使用
objc_registerClassPair()
函数需要注意,不能注册已经注册过的类。- 使用
objc_disposeClassPair()
函数需要注意,如果一个类的实例和子类还存在时,不要去销毁一个类。
运行时的成员变量与属性
- 我们不能用 class_addIvar() 函数为一个已经存在的类添加 Ivar。并且 class_addIvar() 只能在 objc_allocateClassPair() 和 objc_registerClassPair() 之间调用。
- 添加一个属性及对应的成员变量后,我们还能通过 [obj valueForKey:@”propertyName”]; 获得属性值。
关联对象
可用于在类别中添加属性,创建一个category,然后添加property,并且实现setter,gertter。1
2
3
4
5
6
7- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, "name");
}
相关方法:1
2
3void 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)
方法与消息
- 需要
#import <objc/message.h>
- IMP 其实就是 implementation 的缩写,表示方法实现的代码块地址
IMP定义:
id (*IMP)(id, SEL,...)
2.SEL用于唯一标识一个方法名,因此一个类中是不能存在两个同名的方法的,即使参数类型不同也不行。
获取SEL的三种方法:
(1)sel_registerName函数
(2)Objective-C编译器提供的@selector()
(3)NSSelectorFromString()方法
3.Method
将[receiver message]转化为一个消息函数的调用
定义:
objc_msgSend(receiver, selector, arg1, arg2,...)
消息机制
注意:使用objc_msgSend,需要
#import <objc/runtime.h>
1
2
3
4
5
6
7
8
9
10
11
12
13
14 // 通过类名获取类
Class catClass = objc_getClass("Cat");
//注意Class实际上也是对象,所以同样能够接受消息,向Class发送alloc消息
Cat *cat = objc_msgSend(catClass, @selector(alloc));
//发送init消息给Cat实例cat
cat = objc_msgSend(cat, @selector(init));
//发送eat消息给cat,即调用eat方法
objc_msgSend(cat, @selector(eat));
//汇总消息传递过程
objc_msgSend(objc_msgSend(objc_msgSend(objc_getClass("Cat"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("eat"));
- objc_msgSend()报错
Too many arguments to function call ,expected 0,have3
Build Setting–> Apple LLVM 6.0 - Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改为 NO
方法交换
1 | + (void)load{ |
动态加载方法
1 | void run(id self, SEL _cmd, NSNumber *number){ |
消息转发
Method Swizzling(俗称黑魔法)就是在运行时将一个方法的实现代替为另一个方法的实现。Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
1.Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
2.Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
3.Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
消息转发机制基本上分为三个步骤:
第一步:动态方法解析。
第二步:备用接收者。
第三步:完整转发。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52// 第一步,消息接收者没有找到对应的方法时候,会先调用此方法,可在此方法实现中动态添加新的方法
// 返回YES表示相应selector的实现已经被找到,或者添加新方法到了类中,否则返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;
}
// 第二步, 如果第一步的返回NO或者直接返回了YES而没有添加方法,该方法被调用
// 在这个方法中,我们可以指定一个可以返回一个可以响应该方法的对象, 注意如果返回self就会死循环
- (id)forwardingTargetForSelector:(SEL)aSelector {
// if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]) {
// return [[RuntimeMethodHelper alloc] init];
// }
// return [super forwardingTargetForSelector:aSelector];
return nil;
}
// 第三步, 如果forwardingTargetForSelector:返回了nil,则该方法会被调用,系统会询问我们要一个合法的『类型编码(Type Encoding)』
// 函数的最后一个参数 types 是描述方法返回值和参数列表的字符串,我们的代码中的用到的 i@:@ 四个字符分别对应着:返回值 int32_t、参数 id self、参数 SEL _cmd、参数 NSDictionary *dic,即类型编码。
// 若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector: aSelector];
}
return signature;
}
// 当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 在这里进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:[[RuntimeMethodHelper alloc] init]];
}
// // 在这里可以改变方法选择器
// [anInvocation setSelector:@selector(run:)];
// // 改变方法选择器后,需要指定消息的接收者
// [anInvocation invokeWithTarget:self];
}
//- (void)run:(NSNumber *)num {
// NSLog(@"---%@", num); // Print: <RuntimeMethodHelper: 0x7f814b498ee0>, 0x102d79929
//}
// 如果没有实现消息转发 forwardInvocation 则调用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"unresolved method :%@", NSStringFromSelector(aSelector));
}
深入理解黑魔法
当项目规模比较大,各个控制器功能都已经实现,突然增加每个页面都需要修改的需求,实现方式有以下几种:
1.每个页面进行修改
2.继承
3.category
4.Method Swizzling
Method Swizzling本质上就是对IMP和SEL进行交换。Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式。
在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。
在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。
在实现Method Swizzling时,核心代码主要就是一个runtime的C语言API:1
2OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
字典转模型应用
1 | + (instancetype)modelWithDict:(NSDictionary *)dict{ |
runloop运行循环
- 保持程序的持续运行。
- 处理App中的各类事件(触摸事件、定时器事件、Selector事件)
- 节省CPU资源,提高程序性能:没有事件时就进行睡眠状态
内部实现
do-while循环,在这个循环内部不断地处理各种任务(Source\Timeer\Observer),Source和Timer这两个是主动向RunLoop发送消息的,Observer是被动接收消息的。CDRunLoopRunSpecific具体处理runloop的运行情况。
runloop与线程
线程与RunLoop一一对应,其关系是保存在一个全局的 Dictionary 里,key 是 pthread_t, value 是 CFRunLoopRef,RunLoop在第一次获取时创建,在线程结束时销毁
主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动
RunLoop只能选择一个Mode启动,如果当前Mode没有任何Source、Timer、Observer,那么就不会进入RunLoop
- runloop生命周期:第一次获取时创建(currentRunloop),每条线程对应唯一的runloop,线程结束runloop销毁
- 主runloop:在applicationMain中自动创建runloop,主runloop与主线程相关,主线程中runloop自动启动运行,子线程中需要手动创建
- 获取runloop:mainRunloop,currentRunloop(懒加载)
1
2
3
4// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
[NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(run) userInfo:nil repeats:YES];
[currentRunloop run];
iOS中访问和使用RunLoop的API
- Foundation–NSRunLoop
- Core Foundation–CFRunLoopRef(开源)
因为后者是开源的,且前者是在后者上针对OC的封装,所以一般是对CFRunLoopRef进行研究。
两套API对应获取RunLoop对象的方式:
值得注意的是,获取当前RunLoop都是进行懒加载的,也就是调用时自动创建线程对应的RunLoop。
- Foundation
1
2 [NSRunLoop currentRunLoop]; // 当前runloop
[NSRunLoop mainRunLoop];// 主线程runloop
- Core Foundation
1
2CFRunLoopGetCurrent();// 当前runloop
CFRunLoopGetMain();// 主线程runloop
NSRunLoop转化为CFRunLoop:runloop.getCFRunloop
相关类
CFRunLoopRef,mode,source(set),timer(array),observer(array),每个runloop中至少要有一个source或timer
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
runloop的运行模式
- CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop可以包含多个Mode,每个Mode可以包含多个Source、Timer、Observer。
- 每次RunLoop启动时,只能指定其中一个Mode,这个Mode就变成了CurrentMode
- 当启动RunLoop时,如果所在Mode中没有Source、Timer、Observer,那么将不会进入RunLoop,会直接结束
- 如果要切换Mode,只能退出Loop,再重新制定一个Mode进入
CFRunLoopSourceRef 是事件产生的地方
CFRunLoopTimerRef 是基于时间的触发器
CFRunLoopObserverRef 是观察者
系统默认注册了5个Mode
- NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行
- NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode,mode集合
- UITrackingRunLoopMode:界面跟踪 Mode,追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
NSRunLoopCommonModes
- 一个Mode可以将自己标记为“Common”属性,每当 RunLoop 的内容发生变化时,RunLoop会对标记有“Common”属性的Mode进行相适应的切换,并同步Source/Observer/Timer
- 在主线程中,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode这两个Mode都是被默认标记为“Common”属性的,从输出的主线程RunLoop可以查看。
source(timer,input)
- 根据函数调用栈分为:
source0:不基于port,用户主动触发的
source1:基于Port,系统的
observer
- 监听runloop的状态改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/*
第一个参数:分配空间
第一个参数:监听哪个状态
第一个参数:是否持续监听
第一个参数:优先级
第一个参数:回调
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAfterWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
/*
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
*/
switch (activity) {
case kCFRunLoopAfterWaiting:
NSLog(@"即将进入休眠");
break;
default:
break;
}
});
runloop的应用
1.timer
2.imageView的显示
3.performSelector
4.常驻线程:线程完成任务自动挂掉,如果在完成任务后保持runloop,则不会挂掉。1
[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
5.自动释放池
什么时候创建:启动runloop
最后一次销毁:退出runloop
其他时候的创建和销毁:runloop即将进入休眠时会销毁之前的,重新创建一个新的
事件响应
手势识别
界面更新
定时器
PerformSelecter
关于GCD
关于网络请求
参考:iOS 模块详解—「Runtime面试、工作」
Objective-C 的 Runtime
iOS Runtime 几种基本用法简记
iOS运行时(Runtime)详解+Demo
https://www.jianshu.com/p/ed65518ec8db