8.Objective-C Associated Objects
参考博客:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。 Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。
Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
面向切面编程:(AOP是Aspect Oriented Program的首字母缩写)这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject
类定义的方法,通过对 runtime 函数的直接调用。
2.1.Objective-C源代码
大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义
2.2.NSObject的方法
Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
2.3.Runtime的函数
Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc
目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject
类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。
objc_msgSend:
方法吧,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP (函数指针,保存了方法地址)并调用,因为 objc_msgSend
是用汇编语言写的,针对不同架构有不同的实现。它的真身是这样的:
id objc_msgSend(id self, SEL _cmd, ...) { Class class = object_getClass(self); IMP imp = class_getMethodImplementation(class, _cmd); return imp ? imp(self, _cmd, ...) : 0; }
3.1.SEL
objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法)。
3.2.id
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档
3.3.Class
之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;
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;
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。
Objective-C 2.0 的头文件虽然没暴露出objc_class结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪:
在objc_class结构体中:ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。
其中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; }
如果你C语言不是特别好,可以直接理解为objc_ivar_list
结构体存储着objc_ivar
数组列表,而objc_ivar
结构体存储了类的单个成员变量的信息;同理objc_method_list
结构体存储着objc_method
数组列表,而objc_method
结构体存储了类的某个方法的信息。
不知道你是否注意到了objc_class
中也有一个isa
对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]
的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc]
这条消息发给类对象的时候,objc_msgSend()
会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。
上图实线是 super_class
指针,虚线是isa
指针。 有趣的是根元类的超类是NSObject
,而isa
指向了自己,而NSObject
的超类为nil
,也就是它没有超类。
3.3.1.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;
}
- 方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
- 方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
- method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
3.3.2.lvar
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 }
可以根据实例查找其在类中的名字,也就是“反射”:
#import <objc/runtime.h> -(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 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。
遍历属性
//遍历UIPageControl属性 unsigned int count = 0; Ivar *ivars = class_copyIvarList([UIPageControl class], &count); for (int i = 0; i < count; i++) { Ivar ivar = ivars[i]; //获取所有私有属性 const char *property = ivar_getName(ivar); NSLog(@"%@",[[NSString alloc]initWithCString:property encoding:NSUTF8StringEncoding]); }
3.4.IMP
IMP在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP
这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。
你会发现IMP
指向的方法与objc_msgSend
函数类型相同,参数都包含id
和SEL
类型。每个方法名都对应一个SEL
类型的方法选择器,而每个实例对象中的SEL
对应的方法实现肯定是唯一的,通过一组id
和SEL
参数就能确定唯一的方法实现地址;反之亦然。
3.5.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; };
Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache的道理挺像,苹果为提高Cache命中率。
3.6.Property
property
标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_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
指针,而这两个函数返回的值是指向这个数组的指针。
举个栗子,先声明一个类:
@interface Lender : NSObject { float alone; } @property float alone; @end
你可以用下面的代码获取属性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以用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)
把上面的代码放一起,你就能从一个类中获取它的属性啦:
#import <objc/runtime.h> #import "Lenaer.h" id LenderClass = objc_getClass("Lender"); unsigned int outCount, i; objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount); for (i = 0; i < outCount; i++) { objc_property_t property = properties[i]; fprintf(stdout, "%s %s ", property_getName(property), property_getAttributes(property)); }
对比下 class_copyIvarList 函数,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
Objc 中发送消息是用中括号([]
)把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
有关消息发送和消息转发机制的原理,可以查看这篇文章。
4.1.objc_msgSend函数
编译器会根据情况在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret
函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend
不再适用,于是objc_msgSend_fpret
被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。
PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。
下面详细叙述下消息发送步骤:
- 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
- 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
- 如果 cache 找不到就找一下方法分发表。
- 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
- 如果还找不到就要开始进入动态方法解析了。
PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。
4.2.方法中的隐藏参数
我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前对象的方法吧。其实self的内容是在方法运行时被偷偷的动态传入的。(之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。)
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是self指向的内容)
- 方法选择器(_cmd指向的内容)
而当方法中的super关键字接收到消息时,编译器会创建一个objc_super结构体:
struct objc_super { id receiver; Class class; };
这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向self的id指针和class的SEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向self的id指针,与调用[self class]相同,所以我们得到的永远都是self的类型。
4.3.获取方法地址
在IMP
那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
可以动态地提供一个方法的实现。例如我们可以用@dynamic
关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
(@dynamic 意思是由开发人员提供相应的代码:对于只读属性需要提供 getter,对于读写属性需要提供 getter 和setter。
@synthesize 意思是,除非开发人员已经做了,否则由编译器生成相应的代码,以满足属性声明。)
可以通过分别重载resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache
和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:
或resolveClassMethod:
来给程序员一次动态添加方法实现的机会。
//RuntimeMain.h文件
// // RuntimeMain.h // RuntimeTest // // Created by Vie on 2017/2/8. // Copyright © 2017年 Vie. All rights reserved. // #import <Foundation/Foundation.h> @interface RuntimeMain : NSObject @end
//RuntimeMain.m文件
// // RuntimeMain.m // RuntimeTest // // Created by Vie on 2017/2/8. // Copyright © 2017年 Vie. All rights reserved. // #import "RuntimeMain.h" #import <objc/runtime.h> #import "RuntimeFoward.h" @implementation RuntimeMain #pragma mark 实例方法动态解析重定向 //动态实例方法解析,如果这里没有找到该执行的方法会指定到重定向forwardingTargetForSelector:方法 +(BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(goToSchool:)) { //用class_addMethod函数完成向特定类添加特定方法实现的操作 //其中 “v@:” 表示返回值和参数(为了兼容32位机型使用"v@:@"),增加f处理float参数,这个符号涉及 Type Encoding return class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:@"); } return [super resolveInstanceMethod:sel]; } //重定向实例方法,Lenaer实现了该实例方法;如果forwardingTargetForSelector:未找方法就转发给forwardInvocation:方法 //forwardingTargetForSelector:仅支持一个对象的返回,也就是说消息只能被转发给一个对象 -(id)forwardingTargetForSelector:(SEL)aSelector{ if(aSelector == @selector(learnClass:)) { return [[NSClassFromString(@"RuntimeFoward") alloc] init]; } //千万别返回self,因为那样会死循环。重定向的类未实现该方法会导致崩溃 return [super forwardingTargetForSelector:aSelector]; } //在forwardInvocation:之前创建一个有效的方法签名 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ //其中 “v@:” 表示返回值和参数(为了兼容32位机型使用"v@:@"),增加f处理float参数,这个符号涉及 Type Encoding return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; } //如果resolveInstanceMethod:和forwardingTargetForSelector:以及forwardInvocation:都未找到方法实现将崩溃,所以可以再最后else定义一个错误日志输出方法处理崩溃 //forwardInvocation:可以将消息同时转发给任意多个对象 //forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。 -(void)forwardInvocation:(NSInvocation *)anInvocation{ return [anInvocation invokeWithTarget:[[RuntimeFoward alloc] init]]; } -(void)myInstanceMethod:(NSString *)string{ NSLog(@"myInstanceMethod = %@", string); } #pragma mark 类方法动态解析重定向 //动态类方法解析,如果这里没有找到该执行的方法会指定到重定向forwardingTargetForSelector:方法 +(BOOL)resolveClassMethod:(SEL)sel{ if (sel==@selector(classFouction:)) { //用class_addMethod函数完成向特定类添加特定方法实现的操作 //其中 “v@:” 表示返回值和参数(为了兼容32位机型使用"v@:@")增加f处理float参数,这个符号涉及 Type Encoding return class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:@f"); } return [class_getSuperclass(self) resolveClassMethod:sel]; } //重定向类方法,Lenaer实现了该类方法;如果resolveClassMethod:和forwardingTargetForSelector:都未找到方法实现将崩溃,所以可以再最后else定义一个错误日志输出方法处理崩溃 +(id)forwardingTargetForSelector:(SEL)aSelector{ if(aSelector == @selector(testNotMe:)) { return NSClassFromString(@"RuntimeFoward") ; } //千万别返回self,因为那样会死循环。重定向的类未实现该方法会导致崩溃 return [super forwardingTargetForSelector:aSelector]; } +(void)myClassMethod:(NSString *)string{ NSLog(@"myClassMethod = %@", string); } @end
//RuntimeFoward.h文件
// // RuntimeFoward.h // RuntimeTest // // Created by Vie on 2017/2/8. // Copyright © 2017年 Vie. All rights reserved. // #import <Foundation/Foundation.h> @interface RuntimeFoward : NSObject @end
//RuntimeFoward.m文件
// // RuntimeFoward.m // RuntimeTest // // Created by Vie on 2017/2/8. // Copyright © 2017年 Vie. All rights reserved. // #import "RuntimeFoward.h" @implementation RuntimeFoward -(void)fowardGet{ NSLog(@"消息转发给RuntimeFoward"); } -(void)fowardWithString:(NSString *)string{ NSLog(@"消息转发给RuntimeFoward,并带参数%@",string); } -(void)learnClass:(NSString *)string{ NSLog(@"消息转发给RuntimeFoward,learnClass并带参数%@",string); } -(NSString *)getInfo:(NSString *)name height:(float)aHeight{ return [NSString stringWithFormat:@"%@身高%f",name,aHeight]; } -(float)getRectangularArea:(float)aWidth height:(float)aHeight{ return aWidth*aHeight; } +(void)testNotMe:(NSString *)string{ NSLog(@"消息转发给RuntimeFoward,testNotMe并带参数%@",string); } @end
使用
#import <objc/message.h> #import "RuntimeMain.h" //调用实例方法 RuntimeMain *runMain=[[RuntimeMain alloc] init]; //调用无参数无返回值方法 ((void (*) (id, SEL)) objc_msgSend) (runMain, sel_registerName("fowardGet")); //调用有参数无返回值方法 ((void (*) (id, SEL,NSString *)) objc_msgSend)(runMain,sel_registerName("fowardWithString:"),@"哈哈哈"); ((void (*) (id, SEL,NSString *)) objc_msgSend)(runMain,sel_registerName("goToSchool:"),@"哈哈哈"); ((void (*) (id, SEL,NSString *)) objc_msgSend)(runMain,sel_registerName("learnClass:"),@"哈哈哈"); //调用返回String有参数方法 NSString *infoString= ((NSString* (*)(id,SEL,NSString *,float)) objc_msgSend)(runMain,sel_registerName("getInfo:height:"),@"张三",175.81); NSLog(@"%@",infoString); //调用返回float方法, float area=((float (*) (id,SEL,float,float)) objc_msgSend)(runMain,sel_registerName("getRectangularArea:height:"),12.0f,12.0f);
NSLog(@"获得长方形面积为%f",area); //调用类方法 [RuntimeMain performSelector:@selector(classFouction:) withObject:@"xxx"]; [RuntimeMain performSelector:@selector(testNotMe:) withObject:@"xxx"];
运行结果:
2017-02-08 19:33:30.608 RuntimeTest[26467:721253] 消息转发给RuntimeFoward 2017-02-08 19:33:30.609 RuntimeTest[26467:721253] 消息转发给RuntimeFoward,并带参数哈哈哈 2017-02-08 19:33:30.609 RuntimeTest[26467:721253] myInstanceMethod = 哈哈哈 2017-02-08 19:33:30.610 RuntimeTest[26467:721253] 消息转发给RuntimeFoward,learnClass并带参数哈哈哈 2017-02-08 19:33:30.611 RuntimeTest[26467:721253] 张三身高175.809998 2017-02-08 19:33:30.612 RuntimeTest[26467:721253] 获得长方形面积为144.000000 2017-02-08 19:33:30.613 RuntimeTest[26467:721253] myClassMethod = xxx 2017-02-08 19:33:30.616 RuntimeTest[26467:721253] 消息转发给RuntimeFoward,testNotMe并带参数xxx
6.1.重定向
在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会
替换对象方法接受者- (id)forwardingTargetForSelector:(SEL)aSelector
方法替换消息的接受者为其他对象:
替换类方法的接受者,需要覆写 + (id)forwardingTargetForSelector:(SEL)aSelector
方法,并返回类对象:
6.2.转发
当动态方法解析未找到执行方法,消息转发机制会被触发。先methodSignatureForSelector:创建有效签名,再forwardInvocation:执行
方法,我们可以重写这个方法来定义我们的转发逻辑:
6.3.转发和多继承
转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中Warrior
和Diplomat
没有继承关系,但是Warrior
将negotiate
消息转发给了Diplomat
后,就好似Diplomat
是Warrior
的超类一样。
消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。
6.4.替代者对象(Surrogate Objects)
转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档。
6.5.转发与继承
尽管转发很像继承,但是NSObject
类不会将两者混淆。像respondsToSelector:
和 isKindOfClass:
这类方法只会考虑继承体系,不会考虑转发链。
在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部开始,实例变量依次根据自己所占空间而产生位移:
上图左边是NSObject
类的实例变量布局,右边是我们写的类的布局,也就是在超类后面加上我们自己类的实例变量,看起来不错。但试想如果哪天苹果更新了NSObject
类,发布新版本的系统的话,那就悲剧了:
我们自定义的类被划了两道线,那是因为那块区域跟超类重叠了。唯有苹果将超类改为以前的布局才能拯救我们,但这样也导致它们不能再拓展它们的框架了,因为成员变量布局被死死地固定了。在脆弱的实例变量(Fragile ivars) 环境下我们需要重新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下会发生什么呢?
在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。
需要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
来代替。
8.Objective-C Associated Objects
在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:
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 };
这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。
//例
//h文件
#import "TKIMRoom.h" @interface TKIMRoom(TKIMRoomExt) @property(nonatomic,copy)NSString *islock; @end
//m文件
#import "TKIMRoomExt.h" #import <objc/runtime.h> @implementation TKIMRoom(TKIMRoomExt) -(NSString *)islock{ return objc_getAssociatedObject(self, "islock"); } -(void)setIslock:(NSString *)islock{ objc_setAssociatedObject(self, "islock", islock, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling 用于改变一个已经存在的 selector 的实现。这项技术使得在运行时通过改变 selector 在类的消息分发列表中的映射从而改变方法的掉用成为可能。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。
//例1:
//NSArray+Swizzle.h文件
// // NSArray+Swizzle.m // RuntimeTest // 通过分类NSarry达到在调用时候,替换方法实现 // Created by Vie on 2017/2/9. // Copyright © 2017年 Vie. All rights reserved. // #import "NSArray+Swizzle.h" @implementation NSArray (Swizzle) -(id)myLastObject{ //别忘记这是我们准备调换IMP的selector,[self myLastObject] 将会执行真的 [self lastObject] 。 id result=[self myLastObject]; NSLog(@"**********替换实现了myLastObject**********"); return result; } @end
//NSArray+Swizzle.m文件
// // NSArray+Swizzle.m // RuntimeTest // 通过分类NSarry达到在调用时候,替换方法实现 // Created by Vie on 2017/2/9. // Copyright © 2017年 Vie. All rights reserved. // #import "NSArray+Swizzle.h" @implementation NSArray (Swizzle) -(id)myLastObject{ //别忘记这是我们准备调换IMP的selector,[self myLastObject] 将会执行真的 [self lastObject] 。 id result=[self myLastObject]; NSLog(@"**********替换实现了myLastObject**********"); return result; } @end
//在main.m文件调用
// // main.m // RuntimeTest // // Created by Vie on 2017/2/7. // Copyright © 2017年 Vie. All rights reserved. // #import <UIKit/UIKit.h> #import "AppDelegate.h" #import <objc/message.h> #import "NSArray+Swizzle.h" int main(int argc, char * argv[]) { @autoreleasepool { #pragma mark Method Swizzling测试 Method original_Method=class_getInstanceMethod([NSArray class], @selector(lastObject)); Method swizzling_Method=class_getInstanceMethod([NSArray class], @selector(myLastObject)); method_exchangeImplementations(original_Method, swizzling_Method); NSArray *array = @[@"0",@"1",@"2",@"3"]; //准备调换IMP的selector,[self lastObject] 将会执行真的 [self myLastObject]。 NSString *string = [array lastObject]; NSLog(@"TEST RESULT : %@",string); return 0; } }
//调用结果
2017-02-09 11:06:02.615 RuntimeTest[39348:924342] **********替换实现了myLastObject********** 2017-02-09 11:06:02.616 RuntimeTest[39348:924342] TEST RESULT : 3
//例2
//UIViewController+Swizzling.h文件
//给UIViewController的viewDidLoad方法进行category
// // UIViewController+Swizzling.h // RuntimeTest // 分类UIViewController,在的+(void)load方法中添加Method Swizzling方法,可以实现页面统计的需求 // Created by Vie on 2017/2/9. // Copyright © 2017年 Vie. All rights reserved. // #import <UIKit/UIKit.h> @interface UIViewController (Swizzling) @end
//UIViewController+Swizzling.m文件
// // UIViewController+Swizzling.m // RuntimeTest // 分类UIViewController,在的+(void)load方法中添加Method Swizzling方法,可以实现页面统计的需求 // Created by Vie on 2017/2/9. // Copyright © 2017年 Vie. All rights reserved. // #import "UIViewController+Swizzling.h" #import <objc/runtime.h> @implementation UIViewController (Swizzling) //由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。 +(void)load{ [super load]; //通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体; //如果是类方法就使用class_getClassMethod()函数获取。 Method original_Method=class_getInstanceMethod([self class], @selector(viewDidLoad)); Method swizzling_Method=class_getInstanceMethod([self class], @selector(swizzling_viewDidLoad)); /** * 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。 * 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。 * 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。 */ if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(swizzling_Method), method_getTypeEncoding(swizzling_Method))) { method_exchangeImplementations(original_Method, swizzling_Method); } } -(void)swizzling_viewDidLoad{ NSString *str=[NSString stringWithFormat:@"%@",self.class]; //将系统的UIViewController对象剔除掉 if (![str containsString:@"UI"]) { NSLog(@" *************统计打点进入页面:%@",str); } //系统调用UIViewController的viewDidLoad方法时,实际上执行的是我们实现的swizzling_viewDidLoad方法。而我们在调用[self swizzling_viewDidLoad];时,执行的是UIViewController的viewDidLoad方法。 [self swizzling_viewDidLoad]; } @end
//在试图控制器里面导入头文件就可使用
#import "UIViewController+Swizzling.h"
//调用结果
2017-02-09 12:58:52.411 RuntimeTest[40579:967313] *************统计打点进入页面:ViewController
9.1.Method Swizzling类簇
在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃。由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?
这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。
//例
//NSArray+Swizzling.h文件
// // NSArray+Swizzling.h // RuntimeTest // // Created by Vie on 2017/2/9. // Copyright © 2017年 Vie. All rights reserved. // #import <Foundation/Foundation.h> @interface NSArray (Swizzling) @end
//NSArray+Swizzling.m文件
// // NSArray+Swizzling.m // RuntimeTest // // Created by Vie on 2017/2/9. // Copyright © 2017年 Vie. All rights reserved. // #import "NSArray+Swizzling.h" #import <objc/runtime.h> @implementation NSArray (Swizzling) +(void)load{ [super load]; //通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体; //如果是类方法就使用class_getClassMethod()函数获取。 Method original_Method=class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method swizzling_Method=class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzling_objectAtIndex:)); /** * 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。 * 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。 * 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。 */ if (!class_addMethod([self class], @selector(objectAtIndex:), method_getImplementation(swizzling_Method), method_getTypeEncoding(swizzling_Method))) { method_exchangeImplementations(original_Method, swizzling_Method); } } - (id)swizzling_objectAtIndex:(NSUInteger)index { NSUInteger count=self.count; if (count-1 < index) { //数组下标越界后执行 NSLog(@" *************%s数组下标越界 %s", class_getName(self.class), __func__); return nil; } else { //数组下标没越界就调用swizzling_objectAtIndex:执行objectAtIndex: return [self swizzling_objectAtIndex:index]; } } @end
//在main.m文件调用
// // main.m // RuntimeTest // // Created by Vie on 2017/2/7. // Copyright © 2017年 Vie. All rights reserved. // #import <UIKit/UIKit.h> #import "AppDelegate.h" int main(int argc, char * argv[]) { @autoreleasepool { #pragma mark Method Swizzling测试2 NSArray *array = @[@"0",@"1",@"2",@"3"]; [array objectAtIndex:4]; return 0; } }
//运行结果
2017-02-09 13:29:56.050 RuntimeTest[41085:985169] *************__NSArrayI数组下标越界 -[NSArray(Swizzling) swizzling_objectAtIndex:]
9.2.Method Swizzling 错误剖析
在上面的例子中,如果只是单独对NSArray
或NSMutableArray
中的单个类进行Method Swizzling
,是可以正常使用并且不会发生异常的。如果进行Method Swizzling
的类中,有两个类有继承关系的,并且Swizzling
了同一个方法。例如同时对NSArray
和NSMutableArray
中的objectAtIndex:
方法都进行了Swizzling
,这样可能会导致父类Swizzling
失效的问题。
对于这种问题主要是两个原因导致的,首先是不要在+ (void)load
方法中调用[super load]
方法,这会导致父类的Swizzling
被重复执行两次,这样父类的Swizzling
就会失效。例如下面的两张图片,你会发现由于NSMutableArray
调用了[super load]
导致父类NSArray
的Swizzling
代码被执行了两次。
还有一个原因就是因为代码逻辑导致Swizzling
代码被执行了多次,这也会导致Swizzling
失效,其实原理和上面的问题是一样的。
我们之所以让自己的类继承NSObject
不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的[receiver message]
背后发生了什么,而只是当做方法或函数调用。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。