简介
我们都知道Objective-C是一门动态语言,也就是说它会将很多工作从编译器推迟到运行时,比如类、方法、属性的确定。这意味着Objective-C不仅仅需要编译器,也需要一个运行时系统。
这个运行时Runtime系统赋予Objective-C这门语言强大的能力,让开发者可以在程序运行时创建、修改类及其对象和它们的属性、方法,同时也使得C语言具有了面向对象的能力。
Objective-C的Runtime不但强大,而且高效,因为它是用C语言和汇编语言写成的。具体一点,它是由一系列数据结构和函数组成的动态公共接口,在/usr/bin/objc/
目录下,在项目中只需引入<objc/runtime.h
>即可看见所有接口。
类与对象
数据结构
在实际操作中,我们可以从三个层面与Runtime系统进行交互,分别是通过OC源码、NSObject定义的方法,以及通过直接调用runtime函数。而这三种方式的运行,都依赖于一些基础性定义,下面就来谈谈。
id
如果要问OC中“动态性”最强的类(型)是哪个,那结果一定是id,因为id可以在编译期代表任意类型而不受约束,这就给程序提供了非常灵活的可能性。
从定义中可以看出,id是一个指向objc_object结构体的指针,objc_object结构体中只有一个名为isa的Class类型,而Class又是一个指向objc_class结构体的指针。
/// id是一个指向objc_object的指针
/// 来自https://opensource.apple.com/source/objc4/objc4-646/runtime/objc-private.h.auto.html
typedef struct objc_object *id;
/// objc_object是一个只含有名为isa的Class组成的结构体
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
void initIsa(Class cls /*indexed=false*/);
void initClassIsa(Class cls /*indexed=maybe*/);
void initProtocolIsa(Class cls /*indexed=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
// ...
private:
void initIsa(Class newCls, bool indexed, bool hasCxxDtor);
// Slow paths for inline control
id rootAutorelease2();
bool overrelease_error();
#if SUPPORT_NONPOINTER_ISA
// Unified retain count manipulation for nonpointer isa
id rootRetain(bool tryRetain, bool handleOverflow);
bool rootRelease(bool performDealloc, bool handleUnderflow);
id rootRetain_overflow(bool tryRetain);
bool rootRelease_underflow(bool performDealloc);
// ...
#endif
#if !NDEBUG
bool sidetable_present();
#endif
};
/// Class是一个指向objc_class的指针
typedef struct objc_class *Class;
/// OBJC1对objc_class的定义
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // 类的类,也就是元类
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/// OBJC2对objc_object的定义
/// 来自https://opensource.apple.com/source/objc4/objc4-646/runtime/objc-runtime-new.h.auto.html
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// ...
}
由此,我们可以得知:对象objc_object的isa指针指向类objc_class,而类objc_class的isa指针又指向类的类,即元类,所有的元类指向根类rootclass,rootclass指向自己,这是水平方向的继承体系,而在垂直方向上的继承体系内,objc_class还有指向父类的superclass指针。另外,为了提高性能,还有缓存cache等。
对象和类的继承关系SEL
在运行时,SEL表示方法的名称,它是类内唯一的C字符串。可以通过sel_registerName在运行时注册SEL。
SEL的定义如下:
typedef struct objc_selector *SEL;
IMP
SEL是方法的名称,IMP则是方法的具体实现,是一个函数指针,是由编译器生产的。当通过OC给对象发送消息时,最终执行的就是IMP所指的代码。SEL和IMP是成对出现的。
IMP的定义如下:
typedef void (*IMP)(void id, SEL, ... );
Method
顾名思义,Method表示一个方法,由三部分组成:
- SEL,方法名称;
- IMP,方法实现;
- types,参数类型和返回值类型。
它的定义如下:
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
操作指南
能够通过OC实现的功能,比如setter、getter、创建对象、添加方法等等,都可以通过Runtime提供的能力实现。下面就单纯从语言设计层面了解一下。
类对象
类定义相关
// 获取当前已注册的所有类
int objc_getClassList ( Class *buffer, int bufferCount );
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定类的元类
Class objc_getMetaClass ( const char *name );
通过实例对象获取类的信息
// 获取给定实例对象的类名
const char * object_getClassName ( id obj );
// 获取实例对象的类
Class object_getClass ( id obj );
// 设置实例对象的类
Class object_setClass ( id obj, Class cls );
实例对象
对整个对象的操作
// 返回指定对象的一份拷贝
id object_copy ( id obj, size_t size );
// 释放指定对象占用的内存
id object_dispose ( id obj );
操作实例变量
// 修改实例变量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 获取实例变量
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 获取实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置实例变量
void object_setIvar ( id obj, Ivar ivar, id value );
对象关联
在常规代码实践中,如果要让一个对象和另一个对象产生关联,只有一个办法——让另一个对象成为它的属性property,这需要事先定义好,也就是要在编译期之前确定好。而如果要在运行时动态关联对象,只有通过Runtime提供的能力才能事先。
方法包含:
-
objc_setAssociatedObject
:设置 -
objc_getAssociatedObject
:获取 -
objc_removeAssociatedObjects
:删除
关联策略包含:
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
static char dynamicKey;
- (void)setAndGetDynamicObject{
if (objc_getAssociatedObject(self, &dynamicKey)) {
objc_removeAssociatedObjects(objc_getAssociatedObject(self, &dynamicKey));
}
id objcSet = [self generateDynamicObject];
objc_setAssociatedObject(self, &dynamicKey, objcSet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
id objcGet = objc_getAssociatedObject(self, &dynamicKey);
NSLog(@"my dynamic object is %@", objcGet);
}
- (id)generateDynamicObject{
NSArray *objecs = @[@1, @3, @"Wang", @YES];
NSUInteger idx = arc4random()%4;
return [objecs objectAtIndex:idx];
}
消息
Objective-C把一切操作称之为发送消息,比如[someone doSth]
表示给someone
发送了一条名为doSth
的消息。只有在运行时,消息才会和实现进行绑定。
发送消息
objc_msgSend
所有的消息,最终都会被编译器用objc_msgSend
发送给接收者,第一个参数是消息接受者receiver,第二个是一个Selector,如果有其他参数直接跟在后面。
objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)
objc_msgSend
通过前两个参数receiver
和selector
便可以容易的定位到具体的方法实现,然后将参数传递给具体的方法并执行,最后返回执行结果。
这里的关键之处在于前面提到的对象和类的数据结构objc_object
和objc_class
。对于objc_class
的数据结构,里面包含指向父类superclass
的指针,以及一个方法分派表,将所有的方法selector和IMP对应起来。而对于objc_object
的数据结构,里面的isa
指针指向它所属的类,也就是指向一个objc_class
。
这样,对象和类的继承体系就被互相联系在一起了,如果某个对象在类的方法分派表中找不见对应的方法,则顺着它的继承关系依次在父类、父类的父类…中寻找,直到最顶层的根类NSObject,如果还没有找见,则给出异常信息。
当然,在实现细节上还有其他要点,比如为了提高消息被处理的速度,运行时系统会缓存方法selector和IMP。
另外,编译器会根据情况在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四个方法中挑选一个来调用。如果消息是传递给超类,那么会调用名字带有Super
的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret
的函数。
self _cmd
当objc_msgSend
找到对应的方法后,在传递参数时,还会附带传递两个隐藏参数self
和_cmd
,前者代表消息的实际接收者,后者表示此消息的selector。实际上,它们是在编译期自动被加入的。
- (id)strange {
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
methodForSelector:
动态绑定很灵活,但在某些特殊的场景中将受到性能影响,比如要在短时间内给某个对象多次发送消息时。
这时候,更加合理的做法是先通过NSObject定义的methodForSelector:
获取方法的地址,然后直接调用,这将省去不少时间开销。
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);
消息转发
如果消息接收者并不能响应某个消息,那么在运行时系统给出异常之前,还会通过消息转发机制寻求其他可能。具体会经过下面三个流程:
- 动态方法解析
- 重定向接收者
- 最后的转发
动态方法解析
可以先通过实现 resolveInstanceMethod:
和 resolveClassMethod:
,然后通过class_addMethod
动态添加一个方法的实现。
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"dynamic method implementation: %@", NSStringFromSelector(_cmd));
}
// 实例方法的动态解析
+ (BOOL)resolveInstanceMethod:(SEL)sel{
SEL aSel = NSSelectorFromString(@"someDynamicMethod");
if (sel == aSel) {
class_addMethod([self class], aSel, (IMP)dynamicMethodIMP, "v@:");
}
return [super resolveInstanceMethod:sel];
}
// 类方法的动态解析
+ (BOOL)resolveClassMethod:(SEL)sel{
SEL aSel = NSSelectorFromString(@"someDynamicMethod");
if (sel == aSel) {
class_addMethod([self class], aSel, (IMP)dynamicMethodIMP, "v@:");
}
return [super resolveClassMethod:sel];
}
重定向接收者
如果经过上面的动态方法解析之后,依然无法处理消息。这时候运行时系统就会通过 forwardingTargetForSelector:
提供一次替换消息接收者的机会。注意:新替换的对象一定不能为self,因为这样会进入死循环。
@implementation RuntimeExample{
RunLoopExample *runloop;
}
- (instancetype)init{
if ((self = [super init])) {
runloop = [[RunLoopExample alloc] init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwarding target");
if ([NSStringFromSelector(aSelector) isEqualToString:NSStringFromSelector(@selector(addTimerForCommonMode))]) {
NSLog(@"forwarding to 'runloop'");
return runloop;
}
return [super forwardingTargetForSelector:aSelector];
}
最终的转发
如果经过上面两个途径依然无法处理某个消息,那么就会进入比较”沉重“的最后一个消息转发步骤了。
通过重写forwardInvocation:
这个在NSObject中定义的方法,根据它的唯一参数——一个封装了原始消息相关信息的NSInvocation对象,将其解构、分析之后给出对应的处理措施。
要注意的是,重写forwardInvocation:
时,还要重写另一个方法methodSignatureForSelector:
,用来生成forwardInvocation:
的唯一参数NSInvocation的方法签名。
@implementation RuntimeExample{
RuntimeHelper *anotherObject;
}
- (instancetype)init{
if ((self = [super init])) {
anotherObject = [[RuntimeHelper alloc] init];
}
return self;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = [anInvocation selector];
if ([anotherObject respondsToSelector:sel]) {
[anInvocation invokeWithTarget:anotherObject];
} else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature && [anotherObject respondsToSelector:aSelector]) {
signature = [anotherObject methodSignatureForSelector:aSelector];
}
return signature;
}
另外,基于消息转发机制,还能模拟继承和多继承。具体而言,就是在forwardInvocation:
中将所有继承对象的方法按规则分派出去,就像是自己的方法一样。
虽然OC的消息转发机制很强大也很灵活,但应该谨慎使用,因为它不但让程序的逻辑变得更加复杂,而且增加了程序的开销,使用过多必然会降低性能。
Method Swizzle
通过上面这些方法已经灵活地实现多数功能,但还是有点限制——需要操纵源码,对于无法获取源码的,比如Apple官方提供的一些能力,显然无法直接修改。在这种情况下,可以用Method Swizzle进行“偷梁换柱”,用一个方法替换另一个方法就能实现直接无法实现的功能。
比如下面的例子在运行时替换了两个方法的实现。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL sel1 = @selector(swizzlingMethod1);
SEL sel2 = @selector(swizzlingMethod2);
Method m1 = class_getInstanceMethod([self class], sel1);
Method m2 = class_getInstanceMethod([self class], sel2);
// 如果此类还没有对应的方法,先注册该方法
BOOL methodAdded = class_addMethod([self class], sel1, method_getImplementation(m2), method_getTypeEncoding(m2));
if (methodAdded) {
NSLog(@"method add and add");
class_replaceMethod([self class], sel2, method_getImplementation(m1), method_getTypeEncoding(m1));
} else {
// 否则,直接交换
NSLog(@"methods exchange");
method_exchangeImplementations(m1, m2);
}
});
}
- (void)swizzlingMethod1{
NSLog(@"swizzling method: %s", __FUNCTION__);
}
- (void)swizzlingMethod2{
NSLog(@"swizzing method: %s", __FUNCTION__);
}
经过实践发现,确实可以轻松的交换两个方法的实现。不过,这种做法会增加程序调试难度,应该尽可能减少使用。另外,也有一些注意事项:
- Method Swizzling的合适时机是
+load
或者+initialize
,前者表示第一次加载时,后者表示第一次使用时的初始化工作。 - Method Swizzling应该总是在
dispatch_once
中执行,保证有且仅有一次运行机会。
对于Method Swizzling最常见的使用场景大概就是替换某些系统方法的默认实现,以便自动化处理一些工作,比如实现一个可自动记录日志和埋点数据的viewWillAppear:
,通过替换UIViewController的原有viewWillAppear:
,就可以实现一个通用的日志收集方案。
网友评论