您的位置:首页 > 移动开发 > IOS开发

iOS runtime探究(四): 从runtiem开始实践Category添加属性与黑魔法method swizzling

2017-03-28 17:20 579 查看

你要知道的runtime都在这里

转载请注明出处 http://blog.csdn.net/u014205968/article/details/67639335

本文主要讲解
runtime
相关知识,从原理到实践,由于包含内容过多分为以下五篇文章详细讲解,可自行选择需要了解的方向:

从runtime开始: 理解面向对象的类到面向过程的结构体

从runtime开始: 深入理解OC消息转发机制

从runtime开始: 理解OC的属性property

从runtime开始: 实践Category添加属性与黑魔法method swizzling

从runtime开始: 深入weak实现机理

本文是系列文章的第四篇文章从runtiem开始: 实践Category添加属性与黑魔法method swizzling,本文将会介绍比较常用的
runtime
关联对象
以及
runtime
对方法的处理和一个交换方法实现的方法。

关联对象 Associated Object

如果我们想为系统的类添加一个方法可以采用类别的方式进行扩展,相对来说比较简单,但如果要添加一个属性或称为成员变量,通常采用的方法就是继承,这样就比较繁琐了,如果不想去继承那就可以通过
runtime
来进行关联对象操作。

使用
runtime
关联对象
添加属性与我们自定义类时定义的属性其实是两个不同的概念,通过
关联对象
添加属性本质上是使用
类别
进行扩展,通过添加
setter
getter
方法从而在访问时可以使用点语法进行方法,在使用上与自定义类定义的属性没有区别。

具体需要使用的C函数如下:

//为一个实例对象添加一个关联对象,由于是C函数只能使用C字符串,这个key就是关联对象的名称,value为具体的关联对象的值,policy为关联对象策略,与我们自定义属性时设置的修饰符类似
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通过key和实例对象获取关联对象的值
id objc_getAssociatedObject(id object, const void *key);
//删除实例对象的关联对象
void objc_removeAssociatedObjects(id object);


通过注释和函数名不难发现上诉三个方法分别是设置关联对象、获取关联对象和删除关联对象。

需要说明一下
objc_AssociationPolicy
,具体的定义如下:

/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
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. */
};


这些关键词很眼熟,没错,就是
property
使用的修饰符,具体含义也与
property修饰符
相同,如果对
property
property修饰符
等有疑问可以查阅本系列教程第三篇文章从runtime开始: 理解OC的属性property或本博客另外两篇关于
property
的讲解文章:iOS @property探究(一): 基础详解iOS @property探究(二): 深入理解

说了这么多,接下来举个具体的栗子,为一个已有类添加一个关联对象。

@interface Person : NSObject

@property (nonatomic, copy) NSString* cjmName;
@property (nonatomic, assign) NSUInteger cjmAge;

@end

@implementation Person

@synthesize cjmName = _cjmName;
@synthesize cjmAge = _cjmAge;

@end

@interface NSArray (MyPerson)

- (void)setPerson:(Person*)person;
- (Person*)person;

@end

@implementation NSArray (MyPerson)

- (void)setPerson:(Person *)person {
objc_setAssociatedObject(self, "_person", person, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (Person*)person {
return objc_getAssociatedObject(self, "_person");
}
@end


这个栗子设置的关联对象其实没有任何实际意义,通过代码可以看出,使用
runtime
为一个已有类添加属性就是通过类别扩展
getter
setter
方法。

实例方法

在本系列文章的第二篇iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制,我们详细介绍了
runtime
对方法的底层处理,以及发送消息和消息转发机制,这里就不再赘述了,如有需要可以查看相关文章,本文会介绍OC层面对方法的相关操作,同时会介绍
method swizzling
的方法。

先来回顾一下实例方法相关的结构体和底层实现,有如下代码:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;

- (void)showMyself;

- (void)helloWorld;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}

- (void)showMyself {
NSLog(@"Hello World, My name is %@ I\'m %ld years old.", self.name, self.age);
}

- (void)helloWorld {
NSLog(@"Hello World");
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
[p showMyself];
unsigned int count = 0;
Method *methodList = class_copyMethodList([p class], &count);
for (int i = 0; i < count; i++) {
SEL s = method_getName(methodList[i]);
NSLog(@"%@", NSStringFromSelector(s));
if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
IMP imp = method_getImplementation(methodList[i]);
imp();
}
}
}
return 0;
}


通过
clang
转写后可以找到如下与实例方法相关的定义:

struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void  *_imp;
};

static struct /*_method_list_t*/ {
unsigned int entsize;  // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[7];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
7,
{{(struct objc_selector *)"initWithName:age:", "@32@0:8@16Q24", (void *)_I_Person_initWithName_age_},
{(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
{(struct objc_selector *)"helloWorld", "v16@0:8", (void *)_I_Person_helloWorld},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
{(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
{(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};


上一篇文章iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制已经详细介绍了上述结构体,这里不再赘述了。

通过上述代码可以看出,一个实例方法在底层就是一个方法描述和一个C函数的具体实现,我们可以通过如下代码获取这个方法描述结构体:

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
unsigned int count = 0;
Method *methodList = class_copyMethodList([p class], &count);
for (int i = 0; i < count; i++) {
SEL s = method_getName(methodList[i]);
NSLog(@"%@ %s", NSStringFromSelector(s), method_getTypeEncoding(methodList[i]));
if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
IMP imp = method_getImplementation(methodList[i]);
imp();
}
}
}
return 0;
}


首先看一下
Method
是什么,在
objc/runtime.h
中可以找到相关定义:

typedef struct objc_method *Method;


它是一个指向结构体
struct objc_method
的指针,这里的结构体
struct objc_method
其实就是前文中
.cpp
文件中的
struct _objc_method
结构体,通过
class_copyMethodList
方法就可以获取到相关类的所有实例方法,具体函数声明如下:

/**
* Describes the instance methods implemented by a class.
*
* @param cls The class you want to inspect.
* @param outCount On return, contains the length of the returned array.
*  If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Method describing the instance methods
*  implemented by the class—any instance methods implemented by superclasses are not included.
*  The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
*
*  If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
*
* @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
* @note To get the implementations of methods that may be implemented by superclasses,
*  use \c class_getInstanceMethod or \c class_getClassMethod.
*/
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);


通过注释可以看出,第一个参数是相关类的类对象(如有疑问可以查阅本系列文章的前两篇文章),第二个参数是一个指向
unsigned int
的指针,用于指明
Method
的数量,通过该方法就能够获取到所有的实例方法,接下来可以通过
method_getName
方法获取成员变量
_cmd
,这是一个选择子
selector
可以通过方法
NSStringFromSelector
获取到实例方法的名称。通过方法
method_getTypeEncoding
就可以获得函数类型
method_type
。通过方法
method_getImplementation
就可以获取到实例方法的具体实现
imp
,这个具体实现就是我们自定义的实例方法的一个C函数,因此,如果该方法内不访问任何其他实例变量并且没有任何参数就可以直接执行该函数。

上述代码的输出结果如下:

2017-03-27 12:36:12.342715 OCTest[4135:952839] initWithName:age: @32@0:8@16Q24
2017-03-27 12:36:12.342795 OCTest[4135:952839] showMyself v16@0:8
2017-03-27 12:36:12.342843 OCTest[4135:952839] helloWorld v16@0:8
2017-03-27 12:36:12.342866 OCTest[4135:952839] Hello World
2017-03-27 12:36:12.342884 OCTest[4135:952839] .cxx_destruct v16@0:8
2017-03-27 12:36:12.342911 OCTest[4135:952839] name @16@0:8
2017-03-27 12:36:12.342929 OCTest[4135:952839] setName: v24@0:8@16
2017-03-27 12:36:12.342951 OCTest[4135:952839] age Q16@0:8
2017-03-27 12:36:12.342966 OCTest[4135:952839] setAge: v24@0:8Q16


我们也可以通过
class_addMethod
函数动态的为一个类添加实例方法,具体的栗子可以查看前文从runtime开始: 深入理解OC消息转发机制这里不再赘述。

Method Swizzling

通过前面的介绍,我们知道一个实例方法在底层就是一个方法描述加上方法类型和具体的C函数实现,
Foundation
等框架都是闭源的,我们没有办法直接修改代码,通常情况下可以通过继承、类别、关联属性等手段添加属性或实例方法,在某些情况下通过上述方法实现的代码还是比较复杂或繁琐。接下来本文将介绍一种方法用于交换两个实例方法的实现,从而达到修改闭源代码的效果,这个方法就是
Method Swizzling


Method Swizzling
方法的本质就是修改前文介绍的方法描述结构体,方法描述结构体
struct _objc_method
中有一个
struct objc_selector
类型的成员变量
_cmd
,这就是我们常用的
selector
选择子,同时也有一个函数指针
_imp
,这个函数指针就指向实例方法的具体实现。了解了这些我们就可以手动修改
selector
对应的
_imp
,也就是修改实例方法的具体实现,下面举个栗子:

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
Method method1 = class_getInstanceMethod([p class], @selector(helloWorld));
Method method2 = class_getInstanceMethod([p class], @selector(showMyself));
method_exchangeImplementations(method1, method2);

[p showMyself];
[p helloWorld];
}
return 0;
}


上述代码使用了一个C函数:

/**
* Exchanges the implementations of two methods.
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
*  \code
*  IMP imp1 = method_getImplementation(m1);
*  IMP imp2 = method_getImplementation(m2);
*  method_setImplementation(m1, imp2);
*  method_setImplementation(m2, imp1);
*  \endcode
*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);


通过注释和函数名称不难发现,该函数用于交换两个方法的实现,也就是说前文讲述的结构体
struct _objc_method
中的函数指针
_imp
被交换了,原来的选择子
@selector(helloWorld)
对应着方法
helloWorld
的实现,原来的选择子
@selector(showMyself)
对应着方法
showMyself
的实现。如下图所示:



通过上述方法将两个结构体的
_imp
成员变量进行了一次交换操作,也就是说选择子
@selector(helloWorld)
对应着方法
showMyself
的实现,而选择子
@selector(showMyself)
对应着方法
helloWorld
的实现,如下图所示:



因此上述代码的输出结果如下:

2017-03-27 15:35:54.077598 OCTest[6061:1472928] Hello World
2017-03-27 15:35:54.077853 OCTest[6061:1472928] Hello World, My name is Jiaming Chen I'm 22 years old.


runtime
强大到可以改变一个实例方法的具体实现,但是上面的例子好像并没有什么用,没有人会闲的没事去交换两个实例方法的实现。

考虑一个需求,现在需要为每一个页面添加一个手势用于执行某项固定操作,比如添加一个长按收拾,用户可以在任意界面长按后弹出一个视图或是执行某项操作,又比如需要统计每个视图打开的次数,你可能会想到在每一个的视图控制器的
viewDidLoad
方法中添加这个手势或在
viewDidAppear
方法中进行统计操作,但是这样太繁琐了。你也可能想到通过继承来实现上述方法,但是你就需要继承
UIViewController
UITableViewController
UINavigationController
等,你在代码中使用过的任意视图控制器,这样一看似乎也挺麻烦的而且代码也不统一。

通过前面的学习我们可以通过使用类别加上
Method Swizzling
来实现在不修改使用方式的前提下执行自定义操作了。

具体栗子如下:

@interface UIViewController (MyUIViewController)

@end

@implementation UIViewController(MyUIViewController)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

SEL originalSelector = @selector(viewWillAppear:);
Method originalMethod = class_getInstanceMethod([self class], originalSelector);

SEL exchangeSelector = @selector(myViewWillAppear:);
Method exchangeMethod = class_getInstanceMethod([self class], exchangeSelector);

method_exchangeImplementations(originalMethod, exchangeMethod);
});
}

- (void)myViewWillAppear:(BOOL)animated {
[self myViewWillAppear:animated];
NSLog(@"MyViewWillAppear %@", self);
}

@end


首先需要使用类方法
load
来进行实例方法实现的交换操作,因为
load
方法会保证在类第一次被加载的时候调用,这样可以保证一定会执行方法交换操作。其次使用
GCD
dispatch_once
来保证交换两个实例方法的实现只进行一次。接下来通过前文介绍的方法来获取自定义的
myViewWillAppear:
以及
UIViewController
的选择子和具体的方法描述结构体,最后调用前文介绍的
method_exchangeImplementations
函数将两个实例方法的实现进行交换就可以了。

可能你看到
myViewWillAppear:
方法会有疑问,这样不就会导致递归调用吗?需要注意的是,交换两个方法的实现是在运行时进行的,当你调用
myViewWillAppear:
方法时,实际会执行
viewWillAppear:
的方法实现,因此不会导致递归调用。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ios 面向对象 结构