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

你要知道的runtime都在这里

转载请注明出处 http://www.jianshu.com/p/e2c0c67d39ed

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

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

关联对象 Associated Object

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

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

具体需要使用的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修饰符相同,如果对propertyproperty修饰符等有疑问可以查阅本系列教程第三篇文章从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为一个已有类添加属性就是通过类别扩展gettersetter方法。

实例方法

在本系列文章的第二篇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方法中进行统计操作,但是这样太繁琐了。你也可能想到通过继承来实现上述方法,但是你就需要继承UIViewControllerUITableViewControllerUINavigationController等,你在代码中使用过的任意视图控制器,这样一看似乎也挺麻烦的而且代码也不统一。
通过前面的学习我们可以通过使用类别加上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方法会保证在类第一次被加载的时候调用,这样可以保证一定会执行方法交换操作。其次使用GCDdispatch_once来保证交换两个实例方法的实现只进行一次。接下来通过前文介绍的方法来获取自定义的myViewWillAppear:以及UIViewController的选择子和具体的方法描述结构体,最后调用前文介绍的method_exchangeImplementations函数将两个实例方法的实现进行交换就可以了。
可能你看到myViewWillAppear:方法会有疑问,这样不就会导致递归调用吗?需要注意的是,交换两个方法的实现是在运行时进行的,当你调用myViewWillAppear:方法时,实际会执行viewWillAppear:的方法实现,因此不会导致递归调用。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,469评论 33 467
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 694评论 0 2
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 719评论 0 1
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 771评论 0 4