Objective-C 运行时
运行时(runtime)这个术语有多重的意思。到目前为止,我们用它形容应用在用户电脑中运行的一段时间。运行时可以和编译时(compile-time)做对比,编译时是在程序运行之前,还在使用Xcode构建程序的一段时间。
Objective-C运行时是OS X以及iOS系统执行Objective-C代码的一部分。它还负责动态的追踪记录哪些类存在,这些类定义了哪些方法,以及查看消息是否恰当地在对象之间传递。
内省
内省(introspection)是Objective-C运行时的一个特性:它能够让对象在程序运行的时候回答关于自身的问题。例如,这里有一个NSObject方法叫做respondsToSelector:
- (BOOL)respondsToSelector:(SEL)aSelector;
其中的一个实参是一个选择器(一个方法的名字)。如果对象实现了该选择器的名字的方法,就会返回YES;如果没有实现,就返回NO。使用respondsToSelector:
即是内省的例子。
动态查找并执行方法
对象发送消息的时候,它会开始搜索要执行的方法。通常会从接收者的isa指针指向的类开始进行搜索,然后根据继承层级搜索,直到找到需要的方法。
动态查找并执行方法构成了Objective-C消息发送机制的基础,它也是Objective-C运行时的另一大特性。
查找并执行方法是C函数 objc_msgSend()
的工作。这个函数的实参是接收消息的对象,被执行的方法的选择器,以及这个方法的所有实参。
// 动态查找,并执行方法 objc_msgSend
NSString *nameString = @"miky";
NSString *capsName = objc_msgSend(nameString, @selector(uppercaseString));
NSLog(@"%@",capsName);
类以及继承层级的管理
Objective-C 运行时不仅负责记录正在使用哪些类,还负责记录那些包含到程序中的库以及框架使用的类。
#import <Foundation/Foundation.h>
#import <objc/message.h>
#import <objc/runtime.h>
// 获取给定类的继承层级
// 这个函数会获取一个类对象后得到它的父类,然后继续获取父类的父类,直到最后再也没有父类。通常,最后的类会是NSObject。
NSArray *BNRHierarchyForClass(Class cls) {
// 声明一个数组用来保存类及其父类组成的列表,创建一个层级
NSMutableArray *classHierarchy = [NSMutableArray array];
// 继续追踪继承层级,知道再也没有父类
for (Class c = cls; c != Nil; c = class_getSuperclass(c)) {
NSString *className = NSStringFromClass(c);
[classHierarchy insertObject:className atIndex:0];
}
return classHierarchy;
}
// 获取给定类的方法列表
// 方法(Method)方法是一类结构的名字,这类结构的成员包括方法的选择器(SEL类型的变量)以及一个函数指针(function pointer)
// 函数指针指向执行程序中内存数据段的一大块代码。这个函数指针是IMP类型的变量。
NSArray *BNRMethodsForClass(Class cls) {
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(cls, &methodCount);
NSMutableArray *methodArray = [NSMutableArray array];
for (int m = 0; m < methodCount ; m++) {
// 获取当前的方法
Method currentMethod = methodList[m];
// 获取当前方法的选择器
SEL methodSelector = method_getName(currentMethod);
// 给数组增加字符串表达式
[methodArray addObject:NSStringFromSelector(methodSelector)];
}
return methodArray;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建一个字典数组,每个字典都会保存类的名称、层级以及给定类的方法列表
NSMutableArray *runtimeClassesInfo = [NSMutableArray array];
// 声明一个变量,用来保存注册表的数量。
unsigned int classCount = 0;
// 创建一个指针指向应用当前加载的所有注册类的列表
// 通过引用返回注册类的数量
Class *classList = objc_copyClassList(&classCount);
// 列表单中的每个类 遍历类列表
for (int i = 0; i < classCount; i++) {
// 将类的列表作为一个C语言数组处理,获取其中的一个类
Class currentClass = classList[i];
// 将类的名称作为字符串处理
NSString *className = NSStringFromClass(currentClass);
// 输出类的名称
// NSLog(@"className : %@",className);
NSArray *hierarchy = BNRHierarchyForClass(currentClass);
NSArray *methods = BNRMethodsForClass(currentClass);
NSDictionary *classInfoObject = @{@"classname" : className,
@"hierarchy" : hierarchy,
@"methods" : methods};
[runtimeClassesInfo addObject:classInfoObject];
}
// 现在已经不需要这个类列表的缓存区了,释放它
free(classList);
// 按照字母顺序给这些类排序,打印出来
NSSortDescriptor *alphaAsc = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
NSArray *sortedArray = [runtimeClassesInfo sortedArrayUsingDescriptors:@[alphaAsc]];
NSLog(@"There are %ld classes registered with this program`s Runtime",sortedArray.count);
NSLog(@"%@",sortedArray);
}
return 0;
}
objc_copyClassList()
函数会返回一个由指定类对象的指针组成的C数组。
按照惯例,调用名字中包含“copy”或“create”的函数时所使用的内存,例如objc_copyClassList()
函数,如果不再需要,就必须释放。这种情况我们称之为创建规则(create rule)。与之类似,如果调用名字中包含“get”的函数时所使用的内存不归你所有,就不必释放。这种情况我们称之为获取规则(get rule)。注意这两条规则和手动内存管理中使用的规则类似。
KVO 的工作原理
KVO 是苹果公司的API依赖于运行时函数的另一个例子。在第36章讨论KVO 的时候,如果对象使用存取器,被观察的对象可以自动通知属性中的变化。
运行时,如果向某个对象发送addObserver:forKeyPath:option:context:
消息,那么这个方法可以:
- 决定被观察对象的类,并使用
objc_allocateClassPair()
函数给这个类定义一个新的子类。 - 改变对象的isa指针,让它指向新的子类(高效改变对象的类型)。
- 覆盖被观察对象的存取器,发送KVO消息。
例如,一个类的location属性的存方法代码如下:
- (void)setLocation:(NSPoint)location {
_location = location;
}
在新的子类中,存取器会被覆盖如下:
- (void)setLocation:(NSPoint)location {
[self willChangeValueForKey:@"location"];
[super setLocation:location];
[self didChangeValueForKey:@"location"];
}
子类的存取器实现会调用原始类的实现,然后将它们用简明的KVO通知消息封装起来。这些新的类以及方法都会在运行时使用Objective-C运行时函数定义。
对类新增观察者之后,会有一个新的子类:
className = "NSKVONotifying_类名";
hierarchy = (NSObject, 类名 ,"NSKVONotifying_BNRTowel");
methods = ("setLocation:" , class , dealloc , "_isKVOA");