您的位置:首页 > 移动开发 > Objective-C

Objective-C的Method Swizzle、对象模型、消息机制、消息转发的详解

2016-03-17 15:29 465 查看
先来引入一个话题


当项目有一个需求是,要对所有的UIViewController的viewWillApear:animte方法进行监听,而项目很大,.m的控制器文件很多,而且该项目已经开发好了,对这个方法监听不可能进入到控制器里一个一个的添加

此时Objective-C有一个运行时的方法特别好的解决这种问题,当然该方法不是说只能解决上述这种情况


比如,做统计,需要对系统的库的某个方法或多个方法进行监听,添加一些自定义的功能在此方法里,这就会用到运行时的Method swizzle


先来了解一下Objective-C语言的发展


本链接详细介绍了Objective-C语言
http://baike.baidu.com/link?url=3Jqn-qSTvdupQz8aCRcjBFuzy7eznY4Ko_fUgKtD6D6MCSfRR7m61wZz4yAkm5XP0heedN1rrC8YduCTdnl41_#reference-[1]-459423-wrap
Objective-C是扩充C的面向对象的编程语言,它是一门动态语言,它是使用C写成一套非常强大的运行时库,采用了smalltalk语言的消息机制,而且编译OC语言的编译器是LLVM(Low Level Virtual Machine 底层虚拟机),所有OC可以兼容C/C++语法

Objective-C

优点

1.完美支持C/C++

2.消息机制(采用smalltalk)

3.强大的运行时系统

4.所有的对象推迟到运行期间才定义类型

5.内存管理的方式(引用计数)

缺点

1.由于是动态语言,很多异常从运行期间抛出来,不是很容易的定位错误位置

2.没有GC(垃圾收集器),所以需要程序员理解内存管理,避免引起内存的循环引用,导致内存泄露

以下讲一个NSArray的案例,把lastObject与自定义的myLastObject方法交换,当项目中使用lastObject方法时,就相当于使用myLastObject方法,并且lastObject方法本身的功能还是存在的

代码:

#import <Foundation/Foundation.h>
@interface NSArray (Swizzle)
@end

@implementation NSArray (Swizzle)

- (id)myLastObject
{
id ret = [self myLastObject];
NSLog(@" 这是替换后的方法被打印了 ***********  myLastObject ");
return ret;
}

@end
使用

#import <Foundation/Foundation.h>
#import <objc/objc.h>
#import <objc/message.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

//1.这个替换操作只需要执行一遍
//原始NSArray方法  lastObject
Method ori_Method =  class_getInstanceMethod([NSArray class], @selector(lastObject));

//替换后的方法  myLastObject
Method my_Method = class_getInstanceMethod([NSArray class], @selector(myLastObject));

//执行替换操作
method_exchangeImplementations(ori_Method, my_Method);

//2.以后使用NSArray的时候调用 lastObject方法就相当于执行myLastObject
//当再次使用NSArray的时候,就相当于执行了mylastObject方法
NSArray *array = @[@"0",@"1",@"2",@"3"];
NSString *string = [array lastObject];
NSLog(@"最终打印 : %@",string);

NSArray * arr = [array copy];
NSLog(@"arr最后 一个是:%@", [arr lastObject]);

}
return 0;
}
这是打印结果



疑问:[self myLastObject]; 这不就造成递归了吗?

原理:当把lastObject和myLastObject交换,实际交换的是方法的IMP(后面会讲到),当我们执行[self lastObject];时,实际上就是执行了lastObject(本身的作用)和myLastObject里注入的新内容,如下图:

method_exchangeImplementations(method *,method*)方法方法就是调换IMP



下面在举一个例子

代码:

#import <UIKit/UIKit.h>
@interface UIViewController (Swizzle)
@end
#import <objc/message.h>

@implementation UIViewController (Swizzle)

+ (void)load
{
NSString *strClass = NSStringFromClass(self.class);
NSLog(@"strClass=%@", strClass);

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

Class class = [self class];

SEL originalMethod = @selector(viewWillAppear:);
SEL destinateMethod = @selector(zr_viewWillAppear:);

Method originMethod = class_getInstanceMethod(class, originalMethod);
Method destMethod = class_getInstanceMethod(class, destinateMethod);

IMP imp = method_getImplementation(destMethod);
const char * conchar = method_getTypeEncoding(destMethod);

BOOL swizzleMethod = class_addMethod(class, originalMethod, imp, conchar);

if(swizzleMethod){
class_replaceMethod(class, destinateMethod, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod, destMethod);
}

});

}

- (void)zr_viewWillAppear:(BOOL)animte
{
[self zr_viewWillAppear:animte];
NSLog(@"**** 替换后的方法 *****viewWillAppear:%@", self);
}

@end


执行

#import "ViewController.h"
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

UIViewController *view = [[UIViewController alloc] init];
}

- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}

@end


结果是:



和上面的解释一样,读者自己领悟

这么好用的Method Swizzle有什么好处和坏处了

使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。

Method swizzling 可以帮助我们写出更好的,更高效的,易维护的代码。但是如果滥用它,也将会导致难以排查的bug。

讨论

这里是一些 Method Swizzling的陷阱:

Method swizzling is not atomic
Changes behavior of un-owned code
Possible naming conflicts
Swizzling changes the method's arguments
The order of swizzles matters
Difficult to understand (looks recursive)
Difficult to debug

详细的讨论以下地址 有说明
http://blog.csdn.net/yiyaaixuexi/article/details/9374411
以下说说Objective-C的对象模型

我们都知道OC有强大的运行时系统,那么OC的对象模型是怎么一会事儿了?

C语言没有类,但是有结构体(是个程序员都知道



OC的很多库都是直接或者间接集成子NSObject类(即是协议也是类)

一个类,可以有多个属性,可以有多个方法(含类方法和实例方法)

OC的每个对象都有一个isa指针, 简单的意思isa指针指向这个类,

id是万能类型,它的运行时类型是objc_object

Class的运行时类型是objc_class

它们都是结构体

以下是一个类的构成部分在运行时

struct objc_class {

Class isa OBJC_ISA_AVAILABILITY;

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;

} OBJC2_UNAVAILABLE;

我们可以在runtime.h头文件中看到这个

isa 每个类都有一个isa指针

super_class
父类是谁

instance_size 本类的实例大小

objc_ivar_list 本类的所有属性在这个列表中

objc_method_list
本类所有的方法在这个列表中

objc_cache 本类缓存

objc_protocal_list 本类所有协议列表

所以对于一个OC的类在运行时系统时是在objc_class这个结构体中很清楚的描述着

下面说说消息机制

学过其他的语言的同学都知道,除了Objective-C语言调用方法是发送消息机制外,基本上主流的语言都是通过实例调用方法

举个例子:

C#调用一个方法

object.console();

OC调用一个方法

[object console];

写法上有小点儿区别,原理可大不相同

C#是直接调用实例对象方法

OC是发送一个消息,一个消息是一个SEL类型,方法名就是一个字符串,编译时发送消息会转换成objc_msgSend方法

这个方法需提供两个参数,接收对象和消息名称

objc_msgSend(receiver, selector)

所有整个发送消息的过程都在这个方法里进行了

前面说过,每个对象都有一个isa指针代表着所属的类,每个类都有方法列表和属性列表,当接收到消息是,系统首先会根据isa指针查找相应的类,然后在这个类的方法列表去寻找对应的方法(这个方法可能在本类也可能在父类),如果找到了,那就会正常执行,如果没有找到,就会抛出异常

下面说说消息转发

当Objective-C发送消息时,在runtime去当前类和父类中去寻找对应的方法,找不到时,不会马上崩溃,而是可以做消息转发处理,如果没有做消息转发处理,那就会奔溃

有三种方式处理消息转发

第一种:

+ (BOOL)resolveInstanceMethod:(SEL)sel; 实例方法的消息转发

+ (BOOL)resolveClassMethod:(SEL)sel; 类方法的消息转发

第二种:

- (id)forwardingTargetForSelector:(SEL)aSelector;

第三种

先生成方法签名,在通过forwardInvocation来实现消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)anInvocation

第一种:

+ (BOOL)resolveInstanceMethod:(SEL)sel; 实例方法的消息转发

+ (BOOL)resolveClassMethod:(SEL)sel; 类方法的消息转发

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface Person : NSObject

- (void)run;

@end
@implementation Person

void run(id self, SEL _cmd){
NSLog(@"%@ %s", self, sel_getName(_cmd));
}

//第一种:该方法是是实例方法的转发
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//在这里给run方法添加一个实现
if(sel == @selector(run)){
//v@:意思是  v表示void无返回值,@表示self, :表示参数_cmd
class_addMethod(self, sel, (IMP)run, "v@:");
}
return [super resolveInstanceMethod:sel];
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {

//1.实例方法run被声明了,但是没有实现
//被调用,会抛出异常, 若实现了消息转发,则会调用消息转发定义的方法
Person *p = [[Person alloc] init];
[p run];

}
return 0;
}
<span style="font-size:18px;"></span>
<span style="font-size:18px;"></span>


第二种消息转发

- (id)forwardingTargetForSelector:(SEL)aSelector;

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface Car : NSObject

@end
@implementation Car
- (void)run
{
NSLog(@"通过Person类的消息转发,调用Car类的方法");
}
@end

@interface Person : NSObject
- (void)run;
@end
@implementation Person

//第二种,消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(run)){
return [[Car alloc] init];
} else {
return nil;
}
}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
//第二种消息转发方式
//快速转发路径
Person *p = [[Person alloc] init];
[p run];
}
return 0;
}


第三种消息转发

先生成方法签名,在通过forwardInvocation来实现消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)anInvocation

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface Car : NSObject
- (void)run;
@end
@implementation Car
- (void)run
{
NSLog(@"Car类中的run方法,第三种方法通过签名实现转发");
}
@end

@interface Person : NSObject
- (void)run;
@end
@implementation Person

//第三种,用来生成方法签名,然后这个签名就是给forwardInvocation中的参数NSInvocation调用的,用来转发消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if([NSStringFromSelector(aSelector) isEqualToString:@"run"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL selector = [anInvocation selector];
Car *car = [[Car alloc] init];
if([car respondsToSelector:selector]){
[anInvocation invokeWithTarget:car];
}
}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
//第三种, 先生成一个方法签名,然后通过forwardInvocation中的参数来实现转发消息
Person *p = [[Person alloc] init];
[p run];
}
return 0;
}


总结消息转发:当已声明方法,未实现方式时,可以通过消息转发来处理,避免直接奔溃,

消息转发有以上三种方式来处理,如果未实现以上三种转发方式,那就真的奔溃了,亲~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: