简单说一下Objective-C
简单说一下Objective-C,它在C语言的基础上加入了面向对象的特性,同时还继承了SmallTalk的消息传递的特性。Objective-C会将编译和执行延迟到远行时。Objective-C中对象之间互相调用方法,可以看成对象之间互相传递消息,在对象发送消息后,所有消息的回应都会在运行时才会决定,并交给类别自行决定如何处理收到的消息。⚠️因此,一个类别收到消息并不能保证一定会做出回应。
下面主要讲一下Objective-C的主要特性运行时(RunTime)。
Runtime
Objective-C runtime 是一个主要由C和汇编写运行时库(Runtime Library)。Runtime 有两个版本:Modern Runtime(现代的 Runtime) 和 Legacy Runtime(过时的 Runtime)。Modern Runtime:覆盖所有64位的Mac OS X 应用和所有iPhone OS 的应用,Objective-C 2.0后采用。Legacy Runtime 覆盖所有32位的MAC OS X 应用。
Runtime的交互方法
-
方式一: Objective-C源代码。
大部分时间我们只管写ObjC代码,runtime系统会自动工作。例如:
[receiver message]
编译器会转化为objc_msgSend(receiver,selector)
含有参数的话objc_msgSend(receiver,selector,arg1,arg2,...)
消息的执行会使用到编译器为实现动态语言特性而创建的数据结构和函数,例如Objc中的类,方法和协议等都是在runtime中由一些数据结构定义的。 -
方式二:NSObject方法。
Cocoa 中大多数类都是继承NSObject类。最特殊的例外是NSProxy,他是抽象超类,他实现了一些消息转发的相关方法。有的NSObject 中的类方法起到了抽象接口的作用,如description方法需要你重载它并为你定义的类提供描述内容。还有一些方法可以在运行时获取类的信息,检查一些特性。如:
class
isKindOfClass
isMemberOfClass
respondsToSelector
confromsToProtocol
检查对象是否实现了指定协议类的方法methodForSelector
则返回指定方法实现的地址。 -
方式三:Runtime函数。
runtime的头文件在
/usr/include/objc
目录下。对于RunTime的函数可以查看官方文档。
Runtime 术语
objc_msgSend:
方法的真身->id objc_msgSend(id self,SEL op, ...);
-
SEL:是
selector
类型。selector是方法选择器,可以理解为区分方法的ID。SEL的数据结构是typedef struct objc_selector *SEL;
它就是映射到方法的C字符串。你可以使用@selector()
或者Runtime系统的sel_registerName
函数来获取一个SEL类型的方法选择器。⚠️不同类中相同名字的方法所对应的方法选择器是相同的,但是方法名字相同而变量类型不同也会导致她们具有相同的方法选择器。
-
id:
objc_msgSend
第一个参数是id
,它是指向类实例的指针typedef struct objc_object *id
,那么objc_object
是什么?->struct objc_object {Class isa;};
objc_object
结构体包含一个isa
指针,根据isa
指针就可以找到对象所属的类。⚠️
isa
指针不总是指向实例对象所属的类,我们应该用class
确定实例对象的类而不是用isa
。在KVO的实现机理中,被观察者的isa
指针指向中间的类而不是真实的类,这种叫做isa-swizzling
技术官方文档 。 Class:之所以说
isa
是指针是因为Class
其实是指向objc_class
结构体的指针:typedef struct objc_class *Class
, 接下来看一下objc_class
中的结构:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;//指向metaclass
#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;
/* Use `Class` instead of `struct objc_class *` */
我们可以看到运行时一个类关联的超类指针,类名和成员变量,方法,缓存以及附属的协议列表。在objc_class
中「ivars」是「objc_ivar_list」的指针;「methodLists」是「objc_method_list」指针的指针。也就是说我们可以动态修改「*methodLists」的值来添加方法,这也是category的实现原理,同样解释了category不添加属性的原因。深入理解 Object-C category。 在category中我们也可以添加@dynamic
的属性,并利用运行期动态提供存取的方法。或者使用关联度对象「AssociatedObject」其中objc_ivar_list
和 objc_method_list
分别对应成员变量和方法列表,看一下关于属性,方法以及各自对应的列表方法的数据结构:
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
} OBJC2_UNAVAILABLE;
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 {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp 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;
} OBJC2_UNAVAILABLE;
我们可以看到objc_class 中有一个isa 对象,这是因为ObjC类本身也是一个对象,为了处理类和对象之间的关系,runtime创建了一个叫做元类(meta class)的东西,他用来表示类对象本身所具备的元数据。类方法就定义于此,这些方法可以理解成类对象的实例方法。一个类只有一个类对象,类对象仅有一个相关的元类。当一个类发送[NSObjec alloc] 消息时,事实上是 把这个消息发送给类对象,这个类对象必须有一个元类的实例,而这个元类的同时也是一个类的根元类(root meta class)的实例。所有的元类最终指向根元类的超类。所有的元类的方法列表都能响应消息的类方法。所以当[NSObject_alloc] 这条消息发送给类对象的时候,objc_msgSend() 会去它的元类里面查找能够响应消息的方法,找到后这个类对象执行方法。
上图是super_class 指针,虚线是isa 指针。可以看到根元类的超类是NSObject ,而他的isa指向了自己,也就是它没有超类。
- Method:代表某个方法的类型。typedef struct objc_method *Method , objc_method在上面方法的列表中存储方法名,类型和实现:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
SEL:方法名类型。
方法类型method_types是个char类型,其实存储这方法的参数类型和返回值类型。
method_imp指向的是方法的实现,本质是一个函数指针。
- Ivar:类中实例变量类型。typedef struct objc_ivar *Ivar; 看一下 objc_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
}
我们还可以根据实例来查找其在类中的名字,拿来主义,也就是“反射”:
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}
class_copyIvarList 函数获取的不仅有函数的实例变量,还有属性。
- IMP:typedef id(*IMP)(id,SEL,...); 函数指针,这个是有编译器生成的。IMP 指向这个方法的实现。再次我们可以利用IMP来直接执行方法而不通过消息传递机制。但是在本人看来这个方法虽然效率很高省去了消息传递过程但是对于程序的调试不太方便(此句仅代表个人看法)。IMP和 objc_msgSend函数的参数都包含id和SEL类型。没个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL 参数就能确定唯一的方法实现地址。
- objc_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中,下次查找效率会更高(对于这点本人知之甚少)。
- 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指针。
然后我们可以根据property_getName函数来查找属性名称。const char *property_getName(objc_property_t property)
我们还可以利用class_getProperty 和 protocol_getProperty 通过给出的名称来在类和协议中获取属性的引用:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以用property_getAttributes函数来发掘属性的名称和@encode类型字符串:
const char *property_getAttributes(objc_property_t property)
把上面的代码放一起,你就能从一个类中获取它的属性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(class, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
对比下 class_copyIvarList
函数,使用 class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
Runtime运行时先看这些,愿我们都可以站在巨人的肩膀来提升自我😄。
参考文献:
http://yipingmi.top/391/
http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
http://www.cocoachina.com/ios/20141008/9844.html
https://zh.wikipedia.org/wiki/Objective-C
http://www.cnblogs.com/yswdarren/p/3619303.html