动态语言
OC是一门不折不扣的动态语言,所以它的很多机制都是动态运行时决定的。这点和C语言不一样,C语言是静态绑定,也就是编译后所有的一切都已经决定了。这一点和C语言的函数指针有些类似,很多时候函数指针在编译的时候并不知道会指向哪个函数,所以此时就是动态绑定。
举几个OC动态类型的例子,最为直接的就是id类型了、还有关联对象、动态绑定、消息转发、方法调配、这些技术都是动态类型很好的证明。
OC对象结构
在介绍动动态性之前,我们先来看看OC对象的一些结构。
#import<objc/runtime>这是OC运行时函数库,里面定义了很多结构体。
首先看对象的结构:
typdef struct objc_object { Class isa; } *id;
对象结构中非常简单,只有一个isa指针,isa指针后面我们会介绍。
接下来我们看类的结构体
struct objc_class { Class isa #if !__OBJC2__ Class super_class const char *name long version long info long instance_size struct objc_ivar_list *ivars struct objc_method_list **methodLists struct objc_cache *cache struct objc_protocol_list *protocols #endif } typedef struct objc_class *Class;
可以看到很多信息都在Class中定义着,里面信息如下
字段 | 含义 |
isa | isa指针 |
super_class | 父类指针 |
name | 类名 |
version | 类的版本信息,默认为0 |
info | 供运行期使用的一些位标识 |
instance_size | 实例的大小 |
ivars | 实例变量列表 |
methodLists | 方法列表 |
cache | 指向最近调用的方法,用于优化调用方法的速度 |
protocols | 协议列表 |
接下来我们逐个介绍一下:
isa指针和super_class
在OC中,严格意义上讲是没有类这种概念的,每一个类都是一个对象,只不过类对象是一个单例。
isa指针存在于每一个对象中,类普通实例的isa指针指向类,类的isa指针指向它的元类(类方法全部都在元类中存放)。元类的isa指针指向根元类,也就是NSObject的isa所指向的元类。
super_class只有类和元类才有,它们分别指向自己的父类和父元类,而为了让NSObject成为所有类的根类,让NSObject的元类的父类指针也指向了NSObject。
这样说可能也不是很好理解,看下面这张图应该就很快理解了。
等下我们说到消息传递的时候还会在说到isa和super_class。
ivars属性列表
struct objc_ivar_list { int ivar_count #ifdef __LP64__ int space #endif /* variable length structure */ struct objc_ivar ivar_list[1] }
space作用还不太清楚...求指教啊。
下面是实例变量结构
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,这个是实例变量在编译时的偏移量,是由编译时决定的。
methodLists方法列表
struct objc_method_list { struct objc_method_list *obsolete int method_count #ifdef __LP64__ int space #endif /* variable length structure */ struct objc_method method_list[1] }
该结构有方法链表和方法总数。
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
里面有函数名,返回值类型和函数实现,接下来看看SEL和IMP定义
typedef struct objc_selector *SEL; typedef id (*IMP)(id, SEL, ...);
可以看到SEL是objc_selector,(*IMP)(id,SEL,...)是id,我没有找到objc_selector的结构所以这里也没法说什么...
cache缓存列表
typedef struct objc_cache *Cache #define CACHE_BUCKET_NAME(B) ((B)->method_name) #define CACHE_BUCKET_IMP(B) ((B)->method_imp) #define CACHE_BUCKET_VALID(B) (B) #ifndef __LP64__ #define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask)) #else #define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask)) #endif struct objc_cache { unsigned int mask /* total = mask + 1 */ unsigned int occupied Method buckets[1] };
缓存列表里面包含了已缓存的方法,用于快速的调用,不需要在去方法列表里面查询了。
protocol协议列表
struct objc_protocol_list { struct objc_protocol_list *next; long count; Protocol *list[1]; };
协议列表包含了协议数量和协议的指针。
id
id类型实际上就是一个objc_object的typedef,是一个对象实例,而且还是一个指针,所以用id来定义对象的时候就不需要加*号了。
id类型往往需要我们使用”自省“机制来保证使用安全,所谓自省其实就是看看这个对象是不是某个类的实例,或者是不是其子类。
自省用一下两个方法:
isKindOfClass:(Class)class 判断是不是其类族对象
isMemberOfClass:(Class)class 判断是不是类本身对象
关联对象
关联对象在我之前的博客中已经有介绍了,这里就不再说了。
OC消息机制和消息转发
别的语言调用函数,OC则叫做发送消息,这是因为所有OC的方法调用实际上底层都是通过
objc_msgSend(id self , SEL cmd,...)来发送的。
该函数的作用就是传递给一个对象某个方法,后面的不定参数列表是方法所需要的参数。
这里说一下OC的消息传递机制,首先对一个对象发送消息,它会先检查自己的类中有没有该方法,如果没有就找他的父类中有没有,如果还没有则会进行消息转发。
在看例子之前先说一下,Xcode6貌似默认行为不让我们使用objc_msgSend了,所以需要先设置一下
把这一项设置为No就可以了。
这里看个例子,
EqualObject *object1 = [EqualObject new]; EqualObject *object2 = [EqualObject new]; object1.name = @"xiaoming"; object2.name = @"xiaoming"; BOOL isEqual = objc_msgSend(object1,@selector(isEqualToEqualObject:),object2); if(isEqual) { NSLog(@"equal"); }
EqualObject是我们自己实现的类,它有一个判断是否相等的方法isEqualToEqualObject:,如果name相等就人为两个对象相等。
这里我们直接传递消息,不通过OC语法,运行程序可以看到equal被打印了出来。
然后我们再看看消息转发机制。
当该对象包括其父类都没有这个方法的时候会启动,消息转发机制分为两大阶段。
第一阶段先看对象所属类是否有能力动态添加方法,已处理这个位置的选择子,这叫做动态解析(dynamic method resolution)。
第二阶段设计“完整的消息转发机制”。如果运行期系统已经把第一阶段执行完了,那么接受者自己就没法再以动态新增方法的手段来处理与消息相关的方法调用。这又分为两个小步。
首先,请接受者看看有没有其他对象能处理这条消息。若有,则在运行时转给那个对象,于是消息转发过程结束。若没有“备用的接受者”,则启动完成的消息转发机制,运行起系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。
动态方法解析
遇到无法解析的信息后,首先将调用其所属类的下列类方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
该方法参数就是未知的选择子,返回BOOL类型那个,表示这个类是否能新增一个实力方法已处理这个选择子。假如是类方法,那么会调用
+(BOOL)resolveClassMethod:(SEL)selector
使用这种方法的前提是相关的实现已经写好了,只等运行时动态的插入就行,比如CoreData中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。
下面我们看个例子
void showMessage(id self, SEL _cmd, id value) { if([value isKindOfClass:[NSString class]]) { NSLog(@"%@",(NSString *)value); } } +(BOOL)resolveInstanceMethod:(SEL)sel { NSString *selString = NSStringFromSelector(sel); if([selString isEqualToString:@"showMessage:"]) { class_addMethod(self, sel, (IMP)showMessage, "v@:@"); return YES; } else { return [super resolveInstanceMethod:sel]; } }
向刚才EqualObject添加以上实现带代码,然后在客户端调用:
objc_msgSend(object1,@selector(showMessage:),@"Hello");
能够看到程序并没有报错,而且还打印出了Hello!
备援接受者
当没有使用动态方法解析后,还是出发备用接受者,该步骤会触发该方法
-(id)forwardingTargetForSelector:(SEL)selector
方法参数是未知的选择子,如果找到备用对象返回对象,否则返回nil。
可以利用该方法来模拟多重继承机制(实际为组合),因为外部看不到,所以感觉上就像是本身处理该消息。
下面看一个例子:
把刚才添加的代码注释掉,添加一个新类OtherObject,添加showMessage方法,然后在EqualObject中添加以下代码
-(id)forwardingTargetForSelector:(SEL)aSelector { NSString *selectString = NSStringFromSelector(aSelector); if([selectString isEqualToString:@"showMessage:"]) { return other; } return nil; }
会发现Hello依旧出现了!,而且如果你在OtherObject中的showMessage方法中打上断点,会发现方法执行到了OtherObject中...
完整的消息转发
如果消息没有转发,那么回来到这一步,首先创建NSInvocation对象,然后把未处理的信息细节全部都封装于其中。
此对象包含选择子、目标(target)和参数。在触发NSInvocation对象时,“消息派发系统”会把消息派给目标。
此步骤会调用以下方法:
-(void)forwardInvocation:(NSInvocation *)invocation
这个方法实现简单,只要改变调用目标,使消息在新目标上得以调用即可。但是这样和备用接受者实现就一样了,所以一般都不会这样写。
比较有用的实现是再出发消息前,先以某种方式改变消息内容,比如追加另一个参数,或是改装选择子。
如果发现不该由该类执行,那么需要调用超类的该方法,继承体系中每个类都有机会处理此调用请求,如果到NSObject还不能处理会调用doesNotRecognizerSelector抛出异常。
实现上面那个方法的时候需要同时实现。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
需要先对NSInvocation签名然后才能使用NSInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *sig; sig = [other methodSignatureForSelector:aSelector]; return sig; } -(void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation invokeWithTarget:other]; }
方法调配
在运行时,我们还可以使用方法调配技术来改变SEL指向的IMP,比如说目前方法名和对应IMP如下
方法 | IMP |
methodA | IMPA |
methodB | IMPB |
但是当我们使用方法调配后,就可以出现一下情况
方法 | IMP |
methodA | IMPB |
methodB | IMPA |
其实我们修改的本质是方发表的映射,修改了选择子的指向
该技术主要用到的方法如下
void method_exchangeImplementations(Method m1, Method m2)
该函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:
Method class_getInstanceMethod(Class aClass , SEL aSelector)
现在我们向EqualObject中添加methodA和methodB
-(void)methodA { NSLog(@"methodA"); } -(void)methodB { NSLog(@"methodB"); }
然后客户端这样写
EqualObject *object1 = [EqualObject new]; Method methodA = class_getInstanceMethod([EqualObject class], @selector(methodA)); Method methodB = class_getInstanceMethod([EqualObject class], @selector(methodB)); method_exchangeImplementations(methodA, methodB); [object1 methodA];
运行后就会打印出methodB。
利用该技术进行黑盒调试
要进行黑盒调试,主要用到的就是该技术和category
下面来看看具体该如何编写:
首先添加一个EqualObject的category,然后添加一个新方法plusMethodA
-(void)plusMethodA { [self plusMethodA]; NSLog(@"this is plus version"); }
这里看着像是会无限递归,但是实际上plusMethodA选择子已经指向了methodA的IMP,所以并不会出现无限调用的情况。
客户端代码
EqualObject *object1 = [EqualObject new]; Method methodA = class_getInstanceMethod([EqualObject class], @selector(methodA)); Method methodB = class_getInstanceMethod([EqualObject class], @selector(plusMethodA)); method_exchangeImplementations(methodA, methodB); [object1 methodA]
输出结果为
2015-08-13 09:06:01.672 Equal[8282:5413229] methodA 2015-08-13 09:06:01.673 Equal[8282:5413229] this is plus version
可以看到我们没有继承一个类就做到了扩展某个方法,用于调试打印一些输出信息会很有用!
常用Runtime总结
关联对象:
设置一个关联对象
void objc_setAssociatedObject(id object, void *key ,id value, objc_AssociationPolicy policy)
获取关联对象
void objc_getAssociatedObject(id object, void *key)
删除该对象所有的关联对象
void objc_removeAssociatedObjects(id object)
消息传递
向某个对象/父类 发送消息
objc_msgSend(Super)
方法调配
交换两个方法的实现
void method_exchangeImplementation(Method m1, Method m2)
得到该法的指针
Method class_getInstanceMethod(Class aClass, SEL aSelector)
动态创建对象
创建新的类
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
给类增加新的方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
注册新的类
void objc_registerClassPair(Class cls)
获得对象的isa指针所指向的对象
Class object_getClass(id obj)