观 :http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/ 的总结:
1.调用方法的本质:
[receiver message]:
---- objec_msgSend(receiver,selector)
如果有参数 ------objec_msgSend(receiver,selector, arg1,arg2,...)
2. Runtime 的版本:modern(现在)和legacy ::http://www.opensource.apple.com/source/
3. NSObject:
NSProxy:它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
4. Runtime 函数
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc
目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject
类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。
id objc_msgSend(id self, SEL op, ...);
4.1SEL:
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objc中的表示类型(Swift中是Selector
类)。selector
是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL
:
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()
或者 Runtime 系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。
typedef struct objc_selector *SEL;
SEL实质:
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去 找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度 上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么 SEL仅仅是函数名了。
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。
我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
1. sel_registerName函数
2. Objective-C编译器提供的@selector()
3. NSSelectorFromString()方法
4.2 id
typedef struct objc_object *id;
struct objec_object{
Class isa;
} ;
self指向了对象的首地址,而对象的首地址一般是isa变量,isa又是保存了对象的类对象的首地址!
4.3Classs:
struct objc_class { struct objc_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; /*协议链表*/ }; |
也就是说可以动态修改*methodLists
的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。
任性的话可以在Category中添加@dynamic
的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)
objc_class
中也有一个isa
对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]
的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc]
这条消息发给类对象的时候,objc_msgSend()
会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
4.4Method
typedef struct objc_method *Method;
IMP
这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。IMP
指向的方法与objc_msgSend
函数类型相同,参数都包含id
和SEL
类型。每个方法名都对应一个SEL
类型的方法选择器,而每个实例对象中的SEL
对应的方法实现肯定是唯一的,通过一组id
和SEL
参数就能确定唯一的方法实现地址;反之亦然。Cache
为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa
指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache
中查找。Runtime 系统会把被调用的方法存到Cache
中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache
的道理挺像,而我猜苹果为提高Cache
命中率应该也做了努力吧。
4.8 Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;
可以通过class_copyPropertyList
和 protocol_copyPropertyList
方法来获取类和协议中的属性:
返回类型为指向指针的指针,哈哈,因为属性列表是个数组,每个元素内容都是一个objc_property_t
指针,而这两个函数返回的值是指向这个数组的指针。
id LenderClass = objc_getClass("Lender");
unsigned int outCount,i;
objc_property_t *properties = class_copyPropertyList(LenderClass,&outCount);
下面详细叙述下消息发送步骤:
- 检测这个
selector
是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain
,release
这些函数了。 - 检测这个 target 是不是
nil
对象。ObjC 的特性是允许对一个nil
对象执行任何一个方法不会 Crash,因为会被忽略掉。 - 如果上面两个都过了,那就开始查找这个类的
IMP
,先从cache
里面找,完了找得到就跳到对应的函数去执行。 - 如果
cache
找不到就找一下方法分发表。 - 如果分发表找不到就到超类的分发表去找,一直找,直到找到
NSObject
类为止。 - 如果还找不到就要开始进入动态方法解析了,后面会提到。
PS:这里说的分发表其实就是Class
中的方法列表,它将方法选择器和方法实现地址联系起来。
方法中的隐藏参数
我们经常在方法中使用self
关键字来引用实例本身,但从没有想过为什么self
就能取到调用当前方法的对象吧。其实self
的内容是在方法运行时被偷偷的动态传入的。
当objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是
self
指向的内容) - 方法选择器(
_cmd
指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。
获取方法地址
在IMP
那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。