Method Swizzle黑魔法,修改 ios 系统类库方法 SEL IMP

时间:2023-03-09 05:26:16
Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

版权声明:本文为博主原创文章,未经博主允许不得转载。

一般来说,系统提供的方法已经足够开发了,但是有的时候有些需求用普通方法不好做。

如:在所有的viewcontroll 的viewwillappear:方法之前打个log

你可能会这么做:

1. 建一个uiviewcontroll 父类,重写viewwillappear方法,调用super viewwillappear 方法之前加上log

2. 所有新建的uiviewcontroller 继承第一步生成的

确实你是完成这样的功能,可是你做了那么多的修改,基本每个uiviewcontroller都去修改了父类,这种方法太过于笨重了

本文提供了简单地方法即可实现

我的理解中,object-c 的类调用方法是根据三个元素来定义的。

1. 方法,代表类定义中一个方法类型(typedef struct objc_method *Method)

2. SEL 选择器(typedef struct objc_selector *SEL),一个方法在运行时的名字,常见的有 [self
performSelector:@selector(somemethod:) withObject:nil afterDelay:0.5];
@selector(somemethod:)作为方法的入口

3. 方法的实现入口(typedef id (*IMP)(id, SEL, …))

这三个元素确定了具体调用哪一个函数

直接看代码

  1. #import "UIViewController+Tracking.h"
  2. #import <objc/runtime.h>
  3. @implementation UIViewController (Tracking)
  4. + (void)load {
  5. NSString *className = NSStringFromClass(self.class);
  6. NSLog(@"classname %@", className);
  7. static dispatch_once_t onceToken;
  8. dispatch_once(&onceToken, ^{
  9. Class class = [self class];
  10. // When swizzling a class method, use the following:
  11. // Class class = object_getClass((id)self);
  12. SEL originalSelector = @selector(viewWillAppear:);
  13. SEL swizzledSelector = @selector(xxx_viewWillAppear:);
  14. Method originalMethod = class_getInstanceMethod(class, originalSelector);
  15. Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  16. BOOL didAddMethod =
  17. class_addMethod(class,
  18. originalSelector,
  19. method_getImplementation(swizzledMethod),
  20. method_getTypeEncoding(swizzledMethod));
  21. if (didAddMethod) {
  22. class_replaceMethod(class,
  23. swizzledSelector,
  24. method_getImplementation(originalMethod),
  25. method_getTypeEncoding(originalMethod));
  26. } else {
  27. method_exchangeImplementations(originalMethod, swizzledMethod);
  28. }
  29. });
  30. }

我们category重写了NSObject的 load 方法oc提供了objc/runtime.h类让我们获取这些东西,同时还提供了对类方法操作的函数

我们想的是,直接用一个方法替换掉系统的方法,然后把一些自定义的动作加到方法中

我们只想运行一次就够了,所以使用了 dispatch_once(&onceToken, ^{ …… }
接下来给类添加了新方法

把新方法和系统方法替换

  1. #pragma mark - Method Swizzling
  2. - (void)xxx_viewWillAppear:(BOOL)animated {
  3. NSLog(@"viewWillAppear: %@", self);
  4. [self xxx_viewWillAppear:animated];
  5. }

但是新方法实现的时候,调用的是 [self xxx_viewwillAppear:animated]; 可能你会疑惑

这是因为我们在上面已经用xxx_viewwillAppear 和 viewwillAppear 互换了。所以实际上执行的是系统的viewwillAppear

这个时候可能你又有疑问了,为什么实现是- (void)xxx_viewWillAppear:(BOOL)animated{} 这样的

这是因为 SEL swizzledSelector = @selector(xxx_viewWillAppear:); 拿的就是我们新写的方法。

可以结合这篇博客看,配图很容易懂

http://blog.****.net/yiyaaixuexi/article/details/9374411

以及这篇对SEL讲的比较清楚

http://blog.****.net/fengsh998/article/details/8612969

代码下载地址

https://github.com/holysin/Method_swizzle

轻松学习之 IMP指针的作用

 

可能大家一直看到有许多朋友在Runtime相关文章中介绍IMP指针的概念,那么IMP究竟有什么实际作用呢?让我们先从一个函数看起来。

Method Swizzling

如果对Runtime有一定了解的话,一定听说过或者用过这个函数:

1
void method_exchangeImplementations(Method m1, Method m2)

它通常叫做method swizzling,算是ObjC的"黑魔法"了,作用就是在程序运行期间动态的给两个方法互换实现,比如有这样一种使用场景:

我们的程序中有许多个ViewController,我想在对项目改动最小的情况下,在当每个Controller执行完ViewDidLoad以后就在控制台把自己的名字打印出来,方便我去做调试或者了解项目结构。

有许多朋友会这样说,让所有控制器都继承一个BaseController不就可以了吗?我在这里要解释一下这样做的缺点:假如你的项目里有许多Controller的话,你就需要把项目里凡是没有继承自BaseController的每个Controller都做一次修改了,而且随意更改层级结构会发生意想不到的错误。

其实我们的目的就是重写ViewDidLoad的方法,并在他的方法最后加上几句Log,所以我们需要给UIViewController建立一个category,因为我们知道,如果在Catagory中重写一个方法,就会覆盖它的原有方法实现,但是,这样做以后就没有办法调用系统原有的方法,因为在一个方法里调用自己的方法会是一个死循环。所以我们的解决办法就是,另外写一个方法来和viewDidLoad“交换”,这样外部调用viewDidLoad就会调到新建的这个方法中,同样,我们调用新建的方法就会调用到系统的viewDidLoad中了。

Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

IMP指针

其实,还有一种更加简单的方法可以让我们办到相同的目的,运用IMP指针,IMP就是Implementation的缩写,顾名思义,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP,所以,我们可以直接调用方法的IMP指针,来避免方法调用死循环的问题。

调用一个IMP的方式和调用普通C函数相同,比如:

1
id returnObjc = someIMP(objc,SEL,params...);

不过如果你的项目没有做其他配置的话这样调用编译器是不会通过的,我们来看一下先它的定义:

1
2
3
4
5
if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
else
typedef id (*IMP)(id, SEL, ...); 
endif

在默认情况下你的工程是打开这个配置的

Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

这种情况下IMP被定义为无参数无返回值的函数。所以你需要到工程里搜索到这个选项并把它关闭。这样的麻烦就是,每次使用,你都需要修改工程配置,所以这里我再介绍另外一种办法:重新定义一个和有参数的IMP指针相同的指针类型,在获取IMP时把它强转为此类型。这样运用IMP指针后,就不需要额外的给ViewController写新的方法:

Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

还有一个地方我们需要注意,如果这样直接调用IMP的话就会发生经典的EXC_BAD_ACCESS错误,我们定义的IMP指针是一个有返回值的类型,而其实我们获取的viewDidLoad这个方法是没有返回值的,所以我们需要新定义一个和IMP相同类型的函数指针比如VIMP,把他的返回值定位Void,这样如果你修改的方法有返回值就用IMP,没有返回值就用VIMP。

Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

值得注意的是,如果你重写的方法有返回值,不要忘记在最后做return。

总结

实际上直接调用一个方法的IMP指针的效率是高于调用方法本身的,所以,如果你有一个合适的时机获取到方法的IMP的话,你可以试着调用它。

这是只是IMP使用的场景之一,它还有许多作用,希望大家多多发现。

cocoa当中的函数调用,是一种以消息的方式进行的函数调用,这一点与C++,java是有很大差别的。因此该类型的理解,会涉及到三个重要的概念,class,sel,IMP。

class

每个NSObject的第一个成员变量都是class类型的成员,isa,这个isa的对象可以访问到本类的父类,也可以访问到本类的所有方法的列表。

SEL

这个是方法名称的描述。

IMP

这个是具体的方法的地址。

Class 的含义

Class 被定义为一个指向 objc_class的结构体指针,这个结构体表示每一个类的类结构。而 objc_class 在objc/objc_class.h中定义如下:

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;   /*协议链表*/

};

由此可见,Class 是指向类结构体的指针,该类结构体含有一个指向其父类类结构的指针,该类方法的链表,该类方法的缓存以及其他必要信息。

NSObject 的class 方法就返回这样一个指向其类结构的指针。每一个类实例对象的第一个实例变量是一个指向该对象的类结构的指针,叫做isa。通过该指针,对象可以访问它对应的类以及相应的父类。如图一所示:

Method Swizzle黑魔法,修改 ios 系统类库方法  SEL IMP

如图一所示,圆形所代表的实例对象的第一个实例变量为 isa,它指向该类的类结构 The object’s class。而该类结构有一个指向其父类类结构的指针superclass, 以及自身消息名称(selector)/实现地址(address)的方法链表。

方法的含义:

注意这里所说的方法链表里面存储的是Method 类型的。图一中selector 就是指 Method的 SEL,  address就是指Method的 IMP。 Method 在头文件 objc_class.h中定义如下:

typedef struct objc_method *Method;

typedef struct objc_ method {

SEL method_name;

char *method_types;

IMP method_imp;

};

一个方法 Method,其包含一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP  - 指向该方法的具体实现的函数指针。

SEL 的含义:

在前面我们看到方法选标 SEL 的定义为:

typedef struct objc_selector   *SEL;

它是一个指向 objc_selector 指针,表示方法的名字/签名。如下所示,打印出 selector。

-(NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b

{

return (a > b) ? a : b;

}

NSLog(@"SEL=%s", @selector(maxIn:theOther:));

输出:SEL=maxIn:theOther:

不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象performSelector相同的 selector 时,会在各自
的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的方法实现IMP,
然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过
selector去查询,我们才能确定具体的执行代码。

IMP 的含义:

在前面我们也看到 IMP 的定义为:

typedef id (*IMP)(id, SEL, ...);

根据前面id 的定义,我们知道 id是一个指向 objc_object 结构体的指针,该结构体只有一个成员isa,所以任何继承自 NSObject 的类对象都可以用id 来指代,因为 NSObject 的第一个成员实例就是isa。

至此,我们就很清楚地知道 IMP  的含义:IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self  指针),
调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码
。我们可以像在C语言里面一样使用这个函数指针。

NSObject 类中的methodForSelector:方法就是这样一个获取指向方法实现IMP 的指针,methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型。

下面的例子展示了怎么使用指针来调用setFilled:的方法实现:

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);

使用methodForSelector:来避免动态绑定将减少大部分消息的开销,但是这只有在指定的消息被重复发送很多次时才有意义,例如上面的for循环。

注意,methodForSelector:是Cocoa运行时系统的提供的功能,而不是Objective-C语言本身的功能。

几个重要的辅助函数,可以在使用过程中起到很好的辅助作用,尤其是在动态编译等起到了比较大的作用。

我们可以通过NSObject的一些方法获取运行时信息或动态执行一些消息:

class   返回对象的类;

isKindOfClass 和 isMemberOfClass检查对象是否在指定的类继承体系中;

respondsToSelector 检查对象能否相应指定的消息;

conformsToProtocol 检查对象是否实现了指定协议类的方法;

methodForSelector  返回指定方法实现的地址。

performSelector:withObject 执行SEL 所指代的方法。