主要参考链接:
http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
(Good)刨根问底Objective-C Runtime
http://www.cocoachina.com/ios/20141224/10740.html
1 OC与Runtime的交互方式
OC从三种不同的层级上与Runtime系统进行交互,分别是通过Objective-C源代码,通过Foundation框架的NSObject类定义的方法,通过对runtime函数的直接调用。
1.1 Objective-C源代码
大部分情况下你就只管写你的OC代码就行,runtime系统自动在幕后辛勤劳作着。
1.2 NSObject的方法
Cocoa中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
1.3 Runtime的函数
其实[receiver message]会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。
现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。
Objective-C的Runtime铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个Objc程序员需要了解的。
Runtime系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现OC中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写OC代码时一般不会直接用到这些函数的,除非是写一些OC与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对Runtime函数的详细文档。
2 Runtime术语
id objc_msgSend ( id self, SEL op, ... );
2.1 SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在OC中的表示类型(Swift中是Selector类)。 selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL :
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用OC编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是OC中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法拿走不谢),Cocoa中有好多长长的方法哦。
2.2 Id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
id 在 objc.h 中定义如下:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
就像注释中所说的这样 id 是指向一个 objc_object 结构体的指针。
id 这个struct的定义本身就带了一个 *, 所以我们在使用其他NSObject类型的实例时需要在前面加上 *, 而使用 id 时却不用。
2.3 objc_object结构体
那 objc_object 又是啥呢:
struct objc_object { Class isa; };
objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以顺藤摸瓜找到对象所属的类。
这个时候我们知道Objective-C中的object在最后会被转换成C的结构体Class,而在这个struct中有一个 isa 指针,指向它的类别 Class(PS:这里还不是Meta Class)。
2.4 Class、objc_class
在 objc.h 中定义如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
我们可以看到Class本身指向的也是一个C的struct objc_class。
继续看在runtime.h中objc_class定义如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#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;
该结构体中,isa指向所属Class, super_class指向父类别。另外,还关联了它的类名,成员变量列表,方法列表,缓存,还有附属的协议。
其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表:
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_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;
}
2.5 Isa指针
Isa指针有两类:
1、在oc对象转化为c结构体时,用于指向对应Class结构体的isa指针;
2、在Class结构体内部实现里,isa指针作为一个成员变量,指向该class的MetaClass;
情况一:
情况二:
Class在设计中本身也是一个对象。而这个Class对象的对应的类,我们叫它 Meta Class。即Class结构体中的isa指向的就是它的Meta Class。
2.6 元类(MetaClass)
一个ObjC类同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(root meta class)的实例。你会说NSObject的子类时,你的类就会指向NSObject做为其超类。但是所有的元类都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当[NSObject alloc]这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
简单理解:
objc_class中存放的是实例方法列表,而Meta Class中存放的是类方法列表;可以把Meta Class理解为一个Class对象的Class。简单的说:
1、当我们发送一个消息给一个NSObject对象实例时,这条消息会在对象的类的方法列表里查找;
2、当我们发送一个消息给一个类时,这条消息会在类的Meta Class的方法列表(即类方法)里查找,而 Meta Class本身也是一个Class,它跟其他Class一样也有自己的isa 和 super_class 指针。看下图:
注意最左边son(Instance)的isa指针,其实与其他isa指针不同。
上图实线是super_class指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。
· 每个Class都有一个isa指针指向一个唯一的MetaClass;
· 每一个Meta Class的isa指针都指向最上层的Meta Class(图中的NSObject的Meta Class);
· 最上层的Meta Class的isa指针指向自己,形成一个回路;
· 每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身;
· 最上层的NSObject Class的super class指向nil;
2.7 SEL与objc_selector
打开objc.h文件,看下SEL的定义如下:
typedef struct objc_selector *SEL;
SEL是一个指向objc_selector结构体的指针。而objc_selector 的定义并没有在runtime.h中给出定义。我们可以尝试运行如下代码:
SEL sel = @selector(foo);
NSLog(@"%s", (char *)sel);
NSLog(@"%p", sel);
const char *selName = [@"foo" UTF8String];
SEL sel2 = sel_registerName(selName);
NSLog(@"%s", (char *)sel2);
NSLog(@"%p", sel2);
输出如下:
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
Objective-C在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID。只要方法名称相同,那么它们的ID就是相同的。
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。
当然,不同的类可以拥有相同的selector。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
2.8 Method
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;
} OBJC2_UNAVAILABLE;
· 方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
· 方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
· method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
2.9 Ivar
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
} OBJC2_UNAVAILABLE;
PS: OBJC2_UNAVAILABLE之类的宏定义是苹果在OC中对系统运行版本进行约束的黑魔法,有兴趣的可以查看源代码。
这里我们注意第三个成员ivar_offset。它表示基地址偏移字节。
2.10 IMP函数指针
IMP 在 objc.h 中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而IMP这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址;反之亦然。
2.11 Cache
在 runtime.h 中Cache的定义如下:
typedef struct objc_cache *Cache
还记得之前 objc_class 结构体中有一个 struct objc_cache *cache 吧,它到底是缓存啥的呢,先看看 objc_cache 的实现:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
· mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置;
· occupied: 实际占用cache buckets的总数;
· buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
Cache 为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在 isa 指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统 会把 被调用的 方法存到Cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。
2.12 方法列表
objc_method_list就是用来存储当前类的方法链表,objc_method存储了类的某个方法的信息。
3 消息
OC中发送消息是用中括号([ ])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
3.1 objc_msgSend消息发送机制
看起来像是 objc_msgSend返回了数据,其实 objc_msgSend 从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
1. 检测这个selector是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain、release这些函数了。
2. 检测这个 target 是不是nil对象。ObjC 的特性是允许对一个nil对象执行任何一个方法不会 Crash,因为会被忽略掉。
3. 如果上面两个都过了,那就开始查找这个类的IMP,先从cache里面找,完了找得到就跳到对应的函数去执行。
4. 如果cache找不到就找一下方法分发表。(Class中的方法列表)
5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
如果还找不到就要开始进入动态方法解析和消息转发的机制,后面会提到。
其实编译器会根据情况在 objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或objc_msgSendSuper_stret 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
3.2 方法中的隐藏参数
我们经常在方法中使用 self 关键字来引用实例本身,但从没有想过为什么 self就能取到调用当前方法的对象吧。其实 self 的内容是在方法运行时被偷偷地动态传入的。
当 objc_msgSend 找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
– 接收消息的对象(也就是 self 指向的内容)
– 方法选择器( _cmd 指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中, self 引用了接收者对象,而 _cmd 引用了方法本身的选择器:
- strange {
id target = getTheReceiver();
SEL method = getTheMethod();
if( target ==self || method == _cmd ) return nil;
return [target performSelector: method];
}
在这两个参数中, self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:
struct objc_super { id receiver; Class class; };
这个结构体指明了消息应该被传递给特定超类的定义。
3.3 获取方法地址
在IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject 类中有个 methodForSelector: 实例方法,你可以用它来获取某个方法选择器对应的IMP,举个栗子:
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);
PS: methodForSelector: 方法是由Cocoa的Runtime系统提供的,而不是OC自身的特性。
3.4 Category处理机制
3.4.1 objc_category
在runtime.h中查看定义:
typedef struct objc_category *Category;
同样也是指向一个 objc_category 的C结构体,定义如下:
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
char *class_name OBJC2_UNAVAILABLE;
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
通过上面的结构体,大家可以很清楚的看出存储的内容。我们继续往下看,打开objc源代码,在objc-runtime-new.h中我们可以发现如下定义:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
上面的定义需要提到的地方有三点:
1. name 是指class_name 而不是category_name;
2. cls是要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象;
3. instanceProperties表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的;
3.4.2 Category加载机制
1. 打开objc源代码,找到objc-os.mm, 函数_objc_init为runtime的加载入口,由libSystem调用,进行初始化操作。
2. 之后调用objc-runtime-new.mm -> map_images加载map到内存;
3. 之后调用objc-runtime-new.mm ->_read_images初始化内存中的map, 这个时候将会load所有的类、协议还有Category。NSObject的+load方法就是这个时候调用的;
这里贴上Category被加载的代码:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist = _getObjc2CategoryList(hi, &count);
for (i = 0; i< count; i++) {
category_t*cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
//Category's target class is missing (probably weak-linked).
//Disavow any knowledge of this category.
catlist[i] = nil;
if(PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with missing weak-linked target class", cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
BOOL classExists = NO;
if(cat->instanceMethods || cat->protocols || cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if(cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if(PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if(cat->classMethods || cat->protocols
/*|| cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if(cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if(PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)", cls->nameForLogging(), cat->name);
}
}
}
}
1) 循环调用了_getObjc2CategoryList方法,这个方法的实现是:
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
方法中最后一个参数__objc_catlist就是编译器刚刚生成的category数组。
2) load完所有的categories之后,开始对Category进行处理。
从上面的代码中我们可以发现:实例方法被加入到了当前的类对象中, 类方法被加入到了当前类的Meta Class中(cls->ISA);
Step 1. 调用addUnattachedCategoryForClass方法
Step 2. 调用remethodizeClass方法, 在remethodizeClass的实现里调用attachCategoryMethods
static void attachCategoryMethods(Class cls, category_list *cats, bool flushCaches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
method_list_t **mlists = (method_list_t **)_malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, flushCaches);
_free_internal(mlists);
}
这里把一个类的category_list的所有方法取出来生成了method list。这里是倒序添加的,也就是说,新生成的category的方法会先于旧的category的方法插入。
之后调用attachMethodLists将所有方法前序添加进类的method list中,如果原来类的方法列表是a,b,Category的方法列表是c,d。那么插入之后的方法列表将会是c,d,a,b。
3.4.3 小发现
看上面被编译器转换的代码,我们发现Category头文件被注释掉了,结合上面category的加载过程。这就是我们即使没有import category的头文件,都能够成功调用到Category方法的原因。
runtime加载完成后,Category的原始信息在类结构中将不会存在。
4 objc runtime的成员变量和属性
4.1 objc_ivar类型
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
} OBJC2_UNAVAILABLE;
PS: OBJC2_UNAVAILABLE 之类的宏定义是苹果在 OC 中对系统运行版本进行约束的黑魔法,有兴趣的可以查看源代码。
这里我们注意第三个成员ivar_offset。它表示基地址偏移字节。
在编译我们的类时,编译器生成了一个 ivar布局,显示了在类中从哪可以访问我们的ivars 。看下图:
上图中,左侧的数据就是地址偏移字节,我们对 ivar 的访问就可以通过 对象地址 + ivar偏移字节的方法。但是这又引发一个问题,看下图:
我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。而Objective-C Runtime中使用了Non Fragile ivars,看下图:
在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。
需要注意的是在健壮的实例变量下,不要使用 sizeof(SomeClass) ,而是用class_getInstanceSize([SomeClass class]) 代替;也不要使用offsetof(SomeClass, SomeIvar) ,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))来代替。
4.2 偏移示例
我们来看一个例子:
@interface Student : NSObject
{
@private NSInteger age;
}
@end
@implementation Student
- (NSString *)description
{
return [NSString stringWithFormat: @"age = %d", age];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *student = [[Student alloc] init];
student->age = 24;
}
return 0;
}
上述代码,Student有两个被标记为private的ivar,这个时候当我们使用 -> 访问时,编译器会报错。那么我们如何设置一个被标记为private的ivar的值呢?
通过上面的描述,我们知道ivar是通过计算字节偏量来确定地址,并访问的。我们可以改成这样:
@interface Student : NSObject
{
@private int age;
}
@end
@implementation Student
- (NSString *)description
{
NSLog(@"current pointer = %p", self);
NSLog(@"age pointer = %p", &age);
return [NSString stringWithFormat: @"age = %d", age];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *student = [[Student alloc] init];
Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age");
int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar));
NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar));
*age_pointer = 10;
NSLog(@"%@", student);
}
return 0;
}
上述代码的输出结果为:
2014-11-08 18:24:38.892 Test[4143:466864] age ivar offset = 8
2014-11-08 18:24:38.893 Test[4143:466864] current pointer = 0x1001002d0
2014-11-08 18:24:38.893 Test[4143:466864] age pointer = 0x1001002d8
2014-11-08 18:24:38.894 Test[4143:466864] age = 10
我们可以清晰的看到指针地址的变化和偏移量,和我们上述描述一致。
4.3 Property实现机制
使用clang -rewrite-objc main.m重写题目中的代码,我们发现Sark类中的name属性被转换成了如下代码:
struct Sark_IMPL{
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
// @property(nonatomic, copy) NSString *name;
/* @end */
// @implementationSark
static NSString*_I_Sark_name(Sark *self, SEL _cmd)
{
return (*(NSString**)((char*)self + OBJC_IVAR_$_Sark$_name));
}
static void_I_Sark_setName_(Sark *self, SEL_cmd, NSString *name)
{
objc_setProperty(self, _cmd, __OFFSETOFIVAR__(struct Sark,_name), (id)name,0,1);
}
类中的Property属性被编译器转换成了Ivar,并且自动添加了我们熟悉的Set和Get方法。
我们这个时候回头看一下objc_class结构体中的内容,并没有发现用来专门记录Property的list。我们翻开objc源代码,在objc-runtime-new.h中,发现最终还是会通过在class_ro_t结构体中使用property_list_t存储对应的propertyies。
而在刚刚重写的代码中,我们可以找到这个property_list_t:
static struct/*_prop_list_t*/{
unsigned int entsize; //sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
}_OBJC_$_PROP_LIST_Sark__attribute__ ((used, section("__DATA,__objc_const"))) = {
sizeof(_prop_t),1,name
};
static struct_class_ro_t _OBJC_CLASS_RO_$_Sark __attribute__((used, section ("__DATA,__objc_const")))={
0, __OFFSETOFIVAR__(structSark,_name),sizeof(struct Sark_IMPL),
(unsigned int)0,0,"Sark",
(const struct _method_list_t*)&_OBJC_$_INSTANCE_METHODS_Sark,0,
(const struct _ivar_list_t*)&_OBJC_$_INSTANCE_VARIABLES_Sark,0,
(const struct _prop_list_t*)&_OBJC_$_PROP_LIST_Sark,
};
解惑
1)为什么能够正常运行,并调用到speak方法?
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。这个时候的obj已经相当于一个Sark的实例对象(但是和使用[Sark new]生成的对象还是不一样的),我们回想下Runtime的第二篇博文中objc_object结构体的构成就是一个指向Class的isa指针。
这个时候我们再回想下上一篇博文中objc_msgSend的工作流程,在代码中的obj指向的Sark Class中能够找到speak方法,所以代码能够正常运行。
2) 为什么self.name的输出为?
我们在测试代码中加入一些调试代码和Log如下:
- (void)speak
{
unsigned int numberOfIvars = 0;
Ivar *ivars = class_copyIvarList([self class], &numberOfIvars);
for(const Ivar *p = ivars; p < ivars+numberOfIvars; p++) {
Ivar const ivar = *p;
ptrdiff_t offset = ivar_getOffset(ivar);
const char *name = ivar_getName(ivar);
NSLog(@"Sark ivar name = %s, offset = %td", name, offset);
}
NSLog(@"my name is %p", &_name);
NSLog(@"my name is %@", *(&_name));
}
@implementation Test
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"Test instance = %@", self);
void *self2 = (__bridge void *)self;
NSLog(@"Test instance pointer = %p", &self2);
id cls = [Sark class];
NSLog(@"Class instance address = %p", cls);
void *obj = &cls;
NSLog(@"Void *obj = %@", obj);
[(__bridge id)obj speak];
}
return self;
}
@end
输出结果如下:
2014-11-11 00:56:02.464 Test[10475:1071029] Test instance =
2014-11-11 00:56:02.464 Test[10475:1071029] Test instance pointer = 0x7fff5fbff7c8
2014-11-11 00:56:02.465 Test[10475:1071029] Class instance address = 0x1000023c8
2014-11-11 00:56:02.465 Test[10475:1071029] Void *obj =
2014-11-11 00:56:02.465 Test[10475:1071029] Sark ivar name = _name, offset = 8
2014-11-11 00:56:02.465 Test[10475:1071029] my name is 0x7fff5fbff7c8
2014-11-11 00:56:02.465 Test[10475:1071029] my name is
Sark中Propertyname最终被转换成了Ivar加入到了类的结构中,Runtime通过计算成员变量的地址偏移来寻找最终Ivar的地址,我们通过上述输出结果,可以看到 Sark的对象指针地址加上Ivar的偏移量之后刚好指向的是Test对象指针地址。
这里的原因主要是因为在C中,局部变量是存储到内存的栈区,程序运行时栈的生长规律是从地址高到地址低。C语言到头来讲是一个顺序运行的语言,随着程序运行,栈中的地址依次往下走。
看下图,可以清楚的展示整个计算的过程:
我们可以做一个另外的实验,把Test Class 的init方法改为如下代码:
@interface Father : NSObject
@end
@implementation Father
@end
@implementation Test
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"Test instance = %@", self);
id fatherCls = [Father class];
void *father;
father = (void *)&fatherCls;
id cls = [Sark class];
void *obj;
obj = (void *)&cls;
[(__bridge id)obj speak];
}
return self;
}
@end
你会发现这个时候的输出变成了:
2014-11-08 21:40:36.724 Test[4845:543231] Test instance =
2014-11-08 21:40:36.725 Test[4845:543231] ivar name = _name, offset = 8
2014-11-08 21:40:36.726 Test[4845:543231] Sark instance = 0x7fff5fbff7b8
2014-11-08 21:40:36.726 Test[4845:543231] my name is 0x7fff5fbff7c0
2014-11-08 21:40:36.726 Test[4845:543231] my name is
关于C语言内存分配和使用的问题可参考这篇文章http://www.th7.cn/Program/c/201212/114923.shtml。
5 动态方法解析
你可以动态地提供一个方法的实现。例如我们可以用 @dynamic 关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName: 和 propertyName 方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod: 和 resolveClassMethod: 方法分别动态添加实例方法实现和类方法实现。因为当 Runtime 系统在 Cache 和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod: 或 resolveClassMethod: 来给程序员一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作:
void dynamicMethodIMP(id self, SEL _cmd) {
//implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod: aSEL];
}
@end
上面的例子为 resolveThisMethodDynamically 方法添加了实现内容,也就是 dynamicMethodIMP 方法中的代码。其中“ v@: ”表示返回值和参数,这个符号涉及Type Encoding
PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:或 instancesRespondToSelector: 方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让 resolveInstanceMethod: 返回 NO 。
6 消息转发
6.1 重定向
在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载 -
(id)forwardingTargetForSelector:(SEL)aSelector 方法替换消息的接受者为其他对象:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector: aSelector];
}
毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回 self ,因为那样会死循环。
6.2 转发
当动态方法解析不作处理返回 NO 时,消息转发机制会被触发,这时forwardInvocation: 方法会被执行,我们可以重载这个方法来定义我们的转发逻辑:
- (void) forwardInvocation: (NSInvocation *)anInvocation {
if ([someOtherObject respondsToSelector: [anInvocation selector]])
[anInvocation invokeWithTarget: someOtherObject];
else [super forwardInvocation: anInvocation];
}
该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation: 方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。
当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation: 消息通知该对象。每个对象都从 NSObject 类中继承了forwardInvocation: 方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector: 。通过实现我们自己的 forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。
forwardInvocation: 方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。 forwardInvocation: 方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意: forwardInvocation: 方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将 negotiate 消息转发给其它对象,则这个对象不能有 negotiate 方法。否则, forwardInvocation: 将不可能会被调用。
6.3 转发和多继承
转发和继承相似,可以用于为OC编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中 Warrior和 Diplomat 没有继承关系,但是 Warrior 将 negotiate 消息转发给了Diplomat 后,就好似 Diplomat 是 Warrior 的超类一样。
消息转发弥补了 OC 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。
6.4 替代者对象(Surrogate Objects)
转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看 官方文档 。
6.5 转发与继承
尽管转发很像继承,但是 NSObject 类不会将两者混淆。像respondsToSelector:和isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个 Warrior 对象如果被问到是否能响应 negotiate消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] ) ...
结果是 NO ,尽管它能够接受 negotiate 消息而不报错,因为它靠转发消息给Diplomat 类来响应消息。
如果你为了某些意图偏要“弄虚作假”让别人以为 Warrior 继承到了 Diplomat的 negotiate 方法,你得重新实现 respondsToSelector: 和isKindOfClass: 来加入你的转发算法:
- (BOOL)respondsToSelector:(SEL)aSelector {
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了 respondsToSelector: 和 isKindOfClass: 之外,instancesRespondToSelector: 中也应该写一份转发算法。如果使用了协议,conformsToProtocol: 同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个 methodSignatureForSelector: 来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现 methodSignatureForSelector: :
- (NSMethodSignature*)methodSignatureForSelector: (SEL)selector {
NSMethodSignature* signature = [super methodSignatureForSelector: selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector: selector];
}
return signature;
}
7 动态关联属性——Objective-CAssociated Objects
7.1 关联的概念
关联是指把两个对象相互关联起来,使得其中的一个对象作为另外一个对象的一部分。关联特性只有在Mac OS X V10.6以及以后的版本上才是可用的。
在 OS X 10.6 之后,Runtime系统让OC支持向对象动态添加变量。涉及到的函数有以下三个:
void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );
这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
这些常量对应着引用关联值的政策,也就是 OC 内存管理的引用计数机制。
7.2 作用
在类的定义之外为类增加额外的存储空间
使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用。
关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(在垃圾自动回收环境下也不会导致资源不可回收)。
7.3 具体使用
7.3.1 创建关联
创建关联要使用到Objective-C的运行时函数:
objc_setAssociatedObject来把一个对象与另外一个对象进行关联。该函数需要四个参数:源对象,关键字,关联的对象和一个关联策略。当然,此处的关键字和关联策略是需要进一步讨论的。
■ 关键字是一个void类型的指针。每一个关联的关键字必须是唯一的。通常都是会采用静态变量来作为关键字。
■ 关联策略表明了相关的对象是通过赋值,保留引用还是复制的方式进行关联的;还有这种关联是原子的还是非原子的。这里的关联策略和声明属性时的很类似。这种关联策略是通过使用预先定义好的常量来表示的。
下面的代码展示了如何把一个字符串关联到一个数组上。
列表7-1 把一个字符串关联到一个数组
static char overviewKey;
NSArray * array = [[NSArray alloc] initWidthObjects: @"One", @"Two", @"Three", nil];
//为了演示的目的,这里使用initWithFormat:来确保字符串可以被销毁
NSString * overview = [[NSString alloc] initWithFormat: @"@", @"First three numbers"];
objc_setAssociatedObject(array, &overviewKey, overview, OBJC_ASSOCIATION_RETAIN);
[overview release];
//(1) overview仍然是可用的
[array release];
//(2)overview 不可用
在(1)处,字符串overview仍然是可用的,这是因为OBJC_ASSOCIATION_RETAIN策略指明了数组要保有相关的对象。当数组array被销毁的时候,也就是在(2)处overview也就会被释放,因此而被销毁。如果此时还想使用overview,例如想通过log来输出overview的值,则会出现运行时异常。
7.3.2 获取相关联的对象
获取相关联的对象时使用Objective-C函数objc_getAssociatedObject。接着上面列表7-1的代码,我们可以使用如下代码来获取与array相关联的字符串:
NSString * associatedObject = (NSString *)objc_getAssociatedObject(array, &oveviewKey);
7.3.3 断开关联
断开关联是使用objc_setAssociatedObject函数,传入nil值即可。
接着列表7-1中的程序,我们可以使用如下的代码来断开字符串overview和arry之间的关联:
objc_setAssociatedObject(array, &overviewKey, nil, OBJC_ASSOCIATION_ASSIGN);
其中,被关联的对象为nil,此时关联策略也就无关紧要了。
使用函数objc_removeAssociatedObjects可以断开所有关联。通常情况下不建议使用这个函数,因为他会断开所有关联。只有在需要把对象恢复到“原始状态”的时候才会使用这个函数。
7.3.4 一个完整的实例程序
- (void)viewDidLoad {
[super viewDidLoad];
// static const char associated Button key;
UIButton *btn = [UIButton buttonWithType: UIButtonTypeCustom];
[btn setTitle: @"点我" forState: UIControlStateNormal];
[self.view addSubview: btn];
[btn setFrame: CGRectMake(50, 50, 50, 50)];
btn.backgroundColor = [UIColor redColor];
[btn addTarget: self action: @selector(click:) forControlEvents: UIControlEventTouchUpInside];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)click: (UIButton *)sender
{
NSString *message = @"你是谁";
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"提示" message: @"我要传值·" delegate: self cancelButtonTitle: @"确定" otherButtonTitles: nil];
alert.delegate = self;
[alert show];
//#import 头文件
//objc_setAssociatedObject需要四个参数:源对象,关键字,关联的对象和一个关联策略。
//1 源对象alert
//2 关键字 唯一静态变量key associatedkey
//3 关联的对象 sender
//4 关键策略 OBJC_ASSOCIATION_ASSIGN
// enum {
// OBJC_ASSOCIATION_ASSIGN = 0, 若引用/**< Specifies a weak reference to the associated object. */
// OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
// * The association is not made atomically. */
// OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
// * The association is not made atomically. */
// OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
// * The association is made atomically. */
// OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
// * The association is made atomically. */
// };
objc_setAssociatedObject(alert, @"msgstr", message, OBJC_ASSOCIATION_ASSIGN);
//把alert和message字符串关联起来,作为alertview的一部分,关键词就是msgstr,之后可以使用objc_getAssociatedObject从alertview中获取到所关联的对象,便可以访问message或者btn了
// 即实现了关联传值
objc_setAssociatedObject(alert, @"btn property", sender, OBJC_ASSOCIATION_ASSIGN);
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
//通过 objc_getAssociatedObject获取关联对象
NSString *messageString = objc_getAssociatedObject(alertView, @"msgstr");
UIButton *sender = objc_getAssociatedObject(alertView, @"btn property");
NSLog(@"%ld", buttonIndex);
NSLog(@"%@", messageString);
NSLog(@"%@", [[sender titleLabel] text]);
//使用函数objc_removeAssociatedObjects可以断开所有关联。通常情况下不建议使用这个函数,因为他会断开所有关联。只有在需要把对象恢复到“原始状态”的时候才会使用这个函数。
}
终端打印:
2015-07-22 16:18:35.294 test[5174:144121] 0
2015-07-22 16:18:35.295 test[5174:144121] 你是谁
2015-07-22 16:18:35.295 test[5174:144121] 点我
8 Objc源码分析
Objc4源码下载地址
http://www.opensource.apple.com/tarballs/objc4/
9 总结
我们之所以让自己的类继承 NSObject 不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上Runtime系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的[receiver message] 背后发生了什么,而只是当做方法或函数调用。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。
9.1 类本质
OC中类的本质其实就是“结构体+函数指针”的组合,当然,对于NSObject而言,首先它只是包含一个objc_class类型的成员isa,而objc_class是一个结构体类型,继承自objc_object,
9.2 方法执行流程
1. 检测这个selector是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain、release这些函数了。
2. 检测这个 target 是不是nil对象。ObjC 的特性是允许对一个nil对象执行任何一个方法不会 Crash,因为会被忽略掉。
3. 如果上面两个都过了,那就开始查找这个类的IMP,先从cache里面找,完了找得到就跳到对应的函数去执行。
4. 如果cache找不到就找一下方法分发表。(Class中的方法列表)
5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
6. 如果还找不到就要开始进入 动态方法解析和消息转发的机制,后面会提到。
7.在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载 -
(id)forwardingTargetForSelector:(SEL)aSelector 方法替换消息的接受者为其他对象
9.3 Method Swizzling
参考链接:Method Swizzling和AOP实践
http://tech.glowing.com/cn/method-swizzling-aop/
Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。
上一篇文章有讲到每个类里都有一个 Dispatch Table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在 Dispatch Table 里做点改动,让这个方法的名字(SEL)对应到另个 IMP 。
首先定义一个类别,添加将要 Swizzled 的方法:
@implementation UIViewController (Logging)
- (void)swizzled_viewDidAppear:(BOOL)animated
{
// call original implementation
[self swizzled_viewDidAppear: animated];
// Logging
[Logging logWithEventName: NSStringFromClass([selfclass])];
}
代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。调用 viewDidAppear: 会调用你实现的 swizzled_viewDidAppear:,而在 swizzled_viewDidAppear: 里调用 swizzled_viewDidAppear: 实际上调用的是原来的 viewDidAppear: 。
接下来实现 swizzle 的方法 :
@implementation UIViewController (Logging)
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if(didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
这里唯一可能需要解释的是 class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations 替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
最后,我们只需要确保在程序启动的时候调用 swizzleMethod 方法。比如,我们可以在之前 UIViewController 的 Logging 类别里添加 +load: 方法,然后在 +load: 里把 viewDidAppear 给替换掉:
@implementation UIViewController (Logging)
+ (void)load
{
swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load: 是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个 +load: 消息。
其实,这里还可以更简化点:直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。
void (gOriginalViewDidAppear)(id, SEL, BOOL);
void newViewDidAppear(UIViewController *self, SEL _cmd, BOOLanimated)
{
// call original implementation
gOriginalViewDidAppear(self, _cmd, animated);
// Logging
[Logging logWithEventName: NSStringFromClass([selfclass])];
}
+ (void)load
{
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
gOriginalViewDidAppear = (void*)method_getImplementation(originalMethod);
if(!class_addMethod(self, @selector(viewDidAppear:),(IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {
method_setImplementation(originalMethod, (IMP) newViewDidAppear);
}
}
通过 Method Swizzling ,我们成功把逻辑代码跟处理事件记录的代码解耦。当然除了 Logging ,还有很多类似的事务,如 Authentication 和 Caching。这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。这种程序设计问题,业界也给了他们一个名字-Cross Cutting Concerns。
而像上面例子用 Method Swizzling 动态给指定的方法添加代码,以解决 Cross Cutting Concerns 的编程方式叫:Aspect Oriented Programming
10 参考链接
– Objective-C Runtime Programming Guide
– Objective-C runtime之运行时的基本特点
– Understanding the Objective-C Runtime
(Good)刨根问底Objective-C Runtime
http://www.cocoachina.com/ios/20141224/10740.html
Objective-C Runtime Programming Guide
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html
深入理解Objective-C的Runtime机制
http://www.csdn.net/article/2015-07-06/2825133-objective-c-runtime/1
Objective-C Runtime运行时之一:类与对象
http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/
Objective-C Runtime运行时之二:成员变量与属性
http://southpeak.github.io/blog/2014/10/30/objective-c-runtime-yun-xing-shi-zhi-er-:cheng-yuan-bian-liang-yu-shu-xing/
Objective-C Runtime运行时之三:方法与消息
http://southpeak.github.io/blog/2014/11/03/objective-c-runtime-yun-xing-shi-zhi-san-:fang-fa-yu-xiao-xi-zhuan-fa/
Objective-C Runtime运行时之四:MethodSwizzling
http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/
Objective-C Runtime运行时之五:协议与分类
http://southpeak.github.io/blog/2014/11/08/objective-c-runtime-yun-xing-shi-zhi-wu-:xie-yi-yu-fen-lei/
Objective-C Runtime运行时之六:拾遗
http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/
iOS用Runtime解决服务器返回NSNull问题
http://blog.csdn.net/uxyheaven/article/details/48299599
[Objective-C]关联(objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects)
http://blog.csdn.net/onlyou930/article/details/9299169
objc_setAssociatedObject使用
http://my.oschina.net/wupengnash/blog/482377
ios专题- objc runtime动态增加属性
http://www.cnblogs.com/luoguoqiang1985/p/3551966.html
objc_getAssociatedObject, objc_setAssociatedObject
http://zhy584520.iteye.com/blog/1742493
IOS中延时执行的几种方式的比较和汇总
http://blog.csdn.net/chenyong05314/article/details/24695897
IOS关于取消延迟执行函数的种种。performSelector与cancelPreviousPerformRequestsWithTarget
http://blog.csdn.net/samuelltk/article/details/8994313
iOS设置 延迟执行 与 取消延迟执行 方法 以及对runloop初步认识
http://www.cnblogs.com/someonelikeyou/p/5509878.html
Method Swizzling和AOP实践
http://tech.glowing.com/cn/method-swizzling-aop/
Objective-C Runtime
http://tech.glowing.com/cn/objective-c-runtime/