概念
RunTime 又叫运行时, 是一套底层的 C 语言的 API, 是 iOS 内部核心之一, 我们平时编写的 OC 代码, 底层都是基于它来实现的. 比如
[receiver message];
// 底层运行时会被编译器转化为
objc_msgSend(receiver, selector)
// 如果还有其他参数
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为
objc_msgSend(receiver, selector, arg1, arg2, ...)
为什么需要 RunTime
- Object - C是一门动态语言, 他会将一些工作放在代码运行时才处理而非编译时. 也就是说, 有很多类和成员变量在我们编译的时候是不知道的, 而在运行的时候, 我们说编写的代码会转变成完整的确定的代码寻衅.
- 因此, 编译器是不够的, 我们还需要一个运行时系统 (RunTime System) 来处理编译后的代码
- Runtime 基本是用 C 和 汇编写的, 由此可见苹果为了动态系统的高效而做出的努力. 苹果和 GUN 各自维护一个开源的 Runtime 版本, 这个俩个版本之间都在努力保持一致.
- RunTime 的作用
OC 在三种层面上与 RunTime 系统进行交互 - 通过 Object - C 源代码
- 只需要编写 OC 代码, Runtime 系统自动在幕后搞定一切, 调用方法, 编译器会将 OC 代码转换成运行时代码, 在运行时确定数据结构和函数.
- 通过 Foundation 框架的 NSObject 类定义的方法
- cocoa 程序中绝大部分分类都是 NSObject 类的子类, 所以都继承了 NSObject 的行为. (NSProxy 类是个例外, 它是个抽象类)
- 一些情况下, NSObject 类仅仅定义了完成某件事情的模块, 并没有体统所需的代码. 例如 - description 方法, 该方法返回类内容的字符串标示, 该方法主要用来调试程序. NSObject 类并不知道该类的内容, 所以它只是返回类的名字和对象的地址, NSObject 的子类可以重新实现.
- 还有一些 NSObject 的方法可以从 Runtime 系统中获取信息, 允许对象进行自我检
- class 方法返回对象的类
- isKindOfClass: 和 isMemberOfClass: 方法检查对象时候存在于指定的类的继承体系中 (是否是其子类或者父类的成员变量)
- respondToSelector: 检查对象能否响应指定的消息
- conformsToProtocol: 检查对象是否实现了制定协议类的方法
- methodForSelector: 返回制定方法实现的地址.
- 通过对 Runtime 库函数的直接调用
- runtime 系统是具有公共接口的动态共享库. 头文件存放于 /usr/include/objc 目录下, 这意味着我们使用时只需要引入 objc/Runtime.h 头文件即可.
- 许多函数可以让你使用纯 C 代码来实现 Objc 同样的工嗯呢, 除非是写一些 Objc 与其他语言的桥接或是底层的 debug 工作, 你在写 objc 代码时一般不会用到这些 C 语言函数.
Runtime 的相关术语
SEL
- 它是 selector 在 objc 中标示 (Swift 中 是 selector 类). selector 是方法选择器, 其实作用就和名字一样, 日常生活中, 我们通过人名辨别谁是谁, 注意 Objc 在相同类中不会有两个相同命名的方法. selector 对方法名进行包装, 以便找到对应的方法实现. 他的数据结构是:
我们可以看出它是个映射到方法的 C 字符串, 你可以通过 Objc 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器.typedef struct Objc_selector *SEL;
注意: 不用类中相同名字的方法所对应的 selector 是相同的, 由于变量的类型不同, 所以不会导致他们调用方法实现混乱.
id
- id 是一个参数类型, 它是指向某个类的实例的指针. 定义如下:
以上定义, 看到 objc_object 结构体包含一个 isa 指针, 根据 isa 指针就可以找到对象所属的类.typedef struct object_object *id; struct objc_object { Class isa ; };
注意: isa 指针在代码寻衅时并不总指向实例对象所属的类型, 所以不能依靠它来确定类型, 要想确定类型还是需要对象的 classs 方法. PS: KVO 的实现激励就是贱被观察对象的 isa 指针指向一个中间类而不是真实类型.
class
typedef struct objc_class *Class
- Class 其实是指向 objc_class 结构体的指针. objc_class 的数据结构如下:
从 Objc_class 可以看到, 一个运行时类中关联了它的父类指针, 类名, 成员变量, 方法, 缓存以及附属协议.struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表 - 成员变量列表
struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE;
- 方法列表
struct objc_method_list {
struct objc_method_list *obsolete
OBJC2_UNAVAILABLE;
int method_count
OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space
OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1]
OBJC2_UNAVAILABLE;
- 由此可见, 我们可以动态修改 *methodList 的值来添加成员方法, 这也是 category 实现的原理, 同样的解释了 category 不嗯呢添加属性的原因
- objc_ivar_list 结构体用来纯纯成员变量的列表, 而 objc_ivar 则是存储了单个成员变量的信息. 同理, objc_method_list 结构体存储着方法数组的列表, 而单个方法的信息则由 objc_method 结构体储存.
- 值得注意的是, objc_class 中也有一个 isa 指针, 这说明 Objc 类本身也是一个对象. 为了处理类和对象的关系, Runtime 库创建了一个种讲座 Meta Class (元类) 的东西, 类对象所属的类叫做元类. Meta Class 表述了类对象本身所具备的元数据.
- 我们所熟悉的类方法, 就源自于 Meta Class. 我们可以理解为类方法就是类对象的实例方法. 每个类仅有一个类对象, 而每个对象仅有一个与之相关的元类.
- 当你发出一个类似 [NSObject alloc] (类方法)的消息时, 实际上, 这个消息呗发送给了一个类对象 (Class Object), 这个类对象必须是一个院内的实例, 而这个元类同时也是一个根元类(Root Meta Class) 的实例. 所有元类的 isa 指针最终都指向根元类.
- 所以当 [NSObject alloc] 这条消息发送给类对象的时候, 运行时代码 objc_msgSend() 会去它元类中查找能够响应消息的方法实现, 如果找到了, 就会对这个类对象执行方法调用.
- 最后objc_class 中还有一个 objc_cache, 缓存, 它的作用很重要, 下面再说.
Method
Method 代表类中某个方法的类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name
OBJC2_UNAVAILABLE;
char *method_types
OBJC2_UNAVAILABLE;
IMP method_imp
OBJC2_UNAVAILABLE;
}
- objc_method 存储方法名, 方法类型和方法实现:
- 方法名类型为 SEL
- 方法类型 method_types 是个 char 指针, 储存方法的参数类型和返回类型 method_imp 指向了方法的实现, 本子是一个函数指针.
Ivar 是表示成员变量的类型
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name
OBJC2_UNAVAILABLE;
char *ivar_type
OBJC2_UNAVAILABLE;
int ivar_offset
OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space
OBJC2_UNAVAILABLE;
#endif
}
- 其中 ivar_offset 是基地址编译字节
IMP
IMP 在 objc.h 中的定义是
typedef id (*IMP)(id, SEL, ...);
- 它就是一个函数指针, 他这个编译器生成的. 单利发起一个 objc 消息之后, 最终他会执行那段代码, 就是由这个函数指针制定的. 而 IMP 这个函数指针就是指向了这个方法的实现.
- 如果得到了 执行某个实例某个方法的入口, 我们就可以绕开消息传递阶段, 直接执行方法, 这在后面 cache 中会提到.
- 你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同, 参数都包含 id 和 SEL 类型. 每个方法名都对应一个 SEL 类型的方法选择器, 而每个实例对象的 SEL 对应的方法实现肯定是唯一的, 通过一组 id 和 SEL 参数就能确定唯一的方法实现地址.
- 而一个确定的方法也只有唯一的一组 id 和 SEL 参数
Cache
Cache 定义如下
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */
OBJC2_UNAVAILABLE;
unsigned int occupied
OBJC2_UNAVAILABLE;
Method buckets[1]
OBJC2_UNAVAILABLE;
};
- cache 为方法调用的性能进行优化, 每当实例对象接收到一个消息时, 它不在直接在 isa 指针指向的类方法列表中遍历查找响应的方法, 因为每次都要查找效率太低了, 而是优先在 cache 中查找.
- Runtime 系统会把被调用的方法存到 cache 中, 如果一个方法被调用, 那么它可能今后还会被调用, 下次查找的时候就会效率更高. 就想计算机组成原理中 CPU 逃过主存先访问 cache 一样.
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;// 这个更常用
通过 class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int
*outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto,
unsigned int *outCount)
返回的属性列表, 列表中每个元素都是一个 objc_property_t 指针
#import <Foundation/Foundation.h>
@interface Person : NSObject
/** name */
@property (strong, nonatomic) NSString *name;
/** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
@end
以上是一个 person 类, 有三个属性. 让我们用上述方法获取类的运行时属性.
unsigned int outCount = 0;
objc_property_t *properties = class_copyPropertyList([Person
class], &outCount);
NSLog(@"%d", outCount);
for (NSInteger i = 0; i < outCount; i++) {
NSString *name = @(property_getName(properties[i]));
NSString *attributes =
@(property_getAttributes(properties[i]));
NSLog(@"%@--------%@", name, attributes);
}
打印结果
test[2321:451525] 3
test[2321:451525] name--------T@"NSString",&,N,V_name
test[2321:451525] age--------Ti,N,V_age
test[2321:451525] weight--------Td,N,V_weight
property_getName 用来查找属性的名称, 返回 c 字符串
property_getAttributes函数挖掘属性的真实名称和 @encode 类型, 返回 c 字符串.
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char
*name, BOOL isRequiredProperty, BOOL isInstanceProperty)
class_getProperty 和 protocol_getProperty 通过给出属性名在类和协议中获得属性的引用
Runtime 与消息
- 一些 Runtime 属于结束之后, 接下来要说到消息. 体会苹果官方文档的 messages
aren’t bound to method implementations until Runtime. 消息知道运行时才会与方法实现进行绑定. - 这里要清楚一点, objc_msgSend 方法看起来好像返回了数据, 其实objc_msgSend 从不返回数据, 而是你的方法在运行时实现被调用后才会返回数据. 下面消息叙述消息发送的步骤;
- 首先检测这个 selector 是不是要忽略. 比如 Mac OS X 开发, 有了垃圾回收就不会理会 retain, release 这些函数.
- 检测这个 selector 的 target 是不是 nil, Objc 允许我们对一个 nil 对象执行任何方法不会Crash, 因为运行时会被忽略掉.
- 如果上面两步都通过了, 那么就开始查找一个类的实现IMP, 先从 cache 里查找, 如果找到了就寻衅对应的函数去执行相应的代码.
- 如果 cache 找不到就找类的方法列表中是够有对应的方法.
- 如果类的方法列表中找不到就到父类方法列表中查找, 一直找到 NSObject 类为止. 如果还找不到, 就要开始进入动态方法解析了, 之后再说.
- 在消息的传递中, 编译器会根据情况在 objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, objc_msgSendSuper_stret 这四个方法中选择一个调用. 如果消息是传递给父类, 那么会调用名字带有 super 的函数, 如果消息返回值是数据结构而不是简单值时, 会调用名字带有 stret 的函数.
方法中隐藏参数
疑问:
- 我们经常用到关键字 self, 但是 self 是如何获取当前方法的对象呢? 其实, 这也是 Runtime 系统的作用, self 是在方法运行时被动传入的.
- 当 objc_msgSend 找到方法对应实现, 它将直接调用该方法实现, 并将消息中所有参数都传递给方法实现, 同事, 它还将传递俩个隐藏参数
- 接受消息对象 (self 所指向的内容, 当前方法的对象指针)
- 方法选择器(_cmd 指向的内容, 当前方法的 SEL 指针)
- 因为在源代码方法的定义中, 我们平没有发现这俩个参数的声明, 他们是在代码被编译时被插入方法实现的. 尽管这些参数没有跟被明确声明, 在源代码中我们仍然可以应用它们.
- 这俩个参数, self 更实用. 它是在方法实现中访问消息接受者对象的实例变量的途径.
- 这时我们可能会想到另一个关键字 super, 实际上 super 关键字接收到消息时, 编译器会创建一个 objc_super 结构体
struct objc_super {id receiver; Class class; };
- 这个结构体指明了消息应该被传递给定的父类. receiver 仍然是 self 本身, 当我们想通过 [super class] 获取父类时, 编译器其实是向 self 的 id 指针和class 的 SEL 传递给了 objc_msgSendSuper 函数. 只有在 NSObject 类中才能找到 class 方法, 然后 class 方法底层被转换为 Object_getClass(), 接着底层编译器将代码为 objc_msgSend (objc_super -> receiver, @selecor(class)), 传入的第一个参数是指向 self 和 id 指针, 与调用 [self class] 相同, 所以我们得到的永远都是 self 的类型. 因此你会发现
获取方法地址// 这句话并不能获取父类的类型, 只能获取当前类的类型名 NSLog(@"%@", NSStringFromClass([super class]));
NSObject 类中有一个实例方法: methodForSelector, 你可以用它来获取某个方法选择器对应 IMP, 例子:
当方法被当做函数调用时, 俩个隐藏参数也必须明确给出, 上面的例子调用了 1000 次函数, 你也可以尝试给 target 发送 1000 次 setFilled: 消息会花多久.void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES);
虽然可以更高效的调用方法, 但是这种做法很少用, 除非是需要大量重复调用某个方法的情况, 才会选择使用以免消息发送泛滥
注意: methodForSelector: 方法是由 Runtime 系统提供的, 而不是 objc 自身的特性动态方法解析
可以动态提供一个方法实现. 如果我们使用关键字 @dynamic 在类的实现文件中修饰一个属性, 表明我们会为这个属性提供存取方法, 编译器不会默认为我们生成这个属性的 setter 和 getter 方法, 需要我们自己提供.
@dynamic propertyName;
这时, 我们可以通过分别重载 resolveInstanceMethod: 和 resolveClassMethod: 方法添加实例方法实现和类方法实现.
当 RunTime 系统再 Cache 和类的方法列表(包括父类)中找不到要执行的方法时, Runtime 会调用 resolveInstanceMethod: 或 resolveClassMethod: 来给我们一次动态添加方法实现的机会. 我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP)
dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
上面的例子为 resolveThisMethodDynamically 方法添加了实现内容, 就是 dynamicMethodIMP 方法中的代码. 其中 "v@:" 标示返回值和参数, 这个符号标示含义见: Type Encoding
注意
动态方法解析会在消息转发机制侵入前执行, 动态方法解析器将会首先给予提供该方法选择器对应的 IMP 机会. 如果你想让该方法选择器被传送到转发机制, 就让 resolveInstanceMethod: 方法返回 NO.
消息转发
- 重定向
消息转发机制执行前, RunTime 系统允许我们替换消息的接受者为其他对象. 通过(id)forwardingTargetForSelector:(SEL)aSelector
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(mysteriousMethod:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector]; }
如果此方法返回nil或者self, 则会计入消息转发机制 (forwardInvocation:), 否则将向返回的对象重新发送消息.
- 转发
当动态方法解析不做处理返回 NO 时, 则会触发消息转发机制. 这时 forwardInvocation: 方法会被执行, 我们可以重写这个方法来定义我们的转发逻辑
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
还有一部分没写,,,,,,
转发和继承
这块先不写, 写下面比较好理解的部分
健壮的事例变量
在 RunTime 的现行版本中, 最大的特点就是健壮的实例变量. 当一个类被编译的时候, 实例变量的内存布局就形成了, 它表明访问类的实例变量的位置. 实例变量一次根据自己所占的空间而产生位移.(此处有省略, 有时间看书补回来)
runtime 实现的机制是什么, 怎么用, 一般用于干嘛, 你还能记得你所使用的相关的头文件或者某些方法的名称吗
- 需要导入 <objc/message.h> <objc/runtime>
- runtime, 运行时机制, 它是一套 C 语言库
- 实际我们编写的所有 OC 代码, 最终都是转成了 runtime 库的东西, 比如类转成了 runtime 库里面的结构体等数据类型, 方法转成了 runtime 库里面的 C 语言函数, 平时调方法都是转成了 objc_msSend (所以说 OC 有个消息发送机制)
- 因此, 可以说 runtime 是 OC 的底层实现, 是 OC 的幕后执行者.
- 有了 runtime 库, 能做什么事情呢, runtime 库里面包含了跟类, 成员变量, 方法相关的 API, 比如获取类里面的所有成员变量, 为类动态添加成员变量, 动态改变类的方法实现, 为类动态添加新的方法
- 因此, 有了runtime, 想怎么改就怎么改
写不动了, 在等等吧
网友评论