iOS runtime探究(四): 从runtiem开始实践Category添加属性与黑魔法method swizzling你要知道的runtime都在这里

你要知道的runtime都在这里

转载请注明出处 https://cloud.tencent.com/developer/user/1605429

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

  • 从runtime开始: 理解面向对象的类到面向过程的结构体
  • 从runtime开始: 深入理解OC消息转发机制
  • 从runtime开始: 理解OC的属性property
  • 从runtime开始: 实践Category添加属性与黑魔法method swizzling
  • 从runtime开始: 深入weak实现机理

本文是系列文章的第四篇文章从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:的方法实现,因此不会导致递归调用。

备注

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一个会写诗的程序员的博客

第9章 文件IO操作、正则表达式与多线程第9章 文件IO操作、正则表达式与多线程

我们在《第6章 扩展函数与属性》中已经介绍过Kotlin中的类扩展的特性。使用Kotlin的扩展函数功能,我们可以直接为 String 类实现一个 inc() ...

12030
来自专栏开发与安全

《linux c 编程一站式学习》课后部分习题解答

1、假设变量x和n是两个正整数,我们知道x/n这个表达式的结果要取Floor,例如x是17,n是4,则结果是4。如果希望结果取Ceiling应该怎么写表达式呢?...

48160
来自专栏我是攻城师

Apache Pig学习笔记之内置函数(三)

46540
来自专栏AhDung

【手记】注意BinaryWriter写string的小坑——会在string前加上长度前缀length-prefixed

之前以为BinaryWriter写string会严格按构造时指定的编码(不指定则是无BOM的UTF8)写入string的二进制,如下面的代码:

32030
来自专栏编程之旅

iOS设计模式之简单工厂模式

最近在看关于设计模式的书籍,开始觉得在设计程序架构之时,能够灵活运用这些设计模式,代码将变得非常具有美感。一个好的设计模式使得程序更加的灵活,容易修改,易于使用...

8510
来自专栏進无尽的文章

RunTime 之常规操作

有关Runtime的知识总结,我本来想集中写成一篇文章的,但是最后发现实在是太长,而且不利于阅读,最后分成了如下几篇:

12930
来自专栏java思维导图

总结:JDK1.5-JDK1.8各个新特性

作者:iwen-J | 链接:https://my.oschina.net/zhuqingbo0501/blog/1784693 JDK各个版本的新特性 以...

69470
来自专栏程序猿DD

JDK 1.5 - 1.8 各版本的新特性总结

此文章意在借鉴前人经验,留作日后查看。如有侵犯,实属无意。我以后会注意,谢谢博友的提醒。也愿各大博友们能够共同学习和努力。

76860
来自专栏HansBug's Lab

1901: Zju2112 Dynamic Rankings

1901: Zju2112 Dynamic Rankings Time Limit: 10 Sec  Memory Limit: 128 MB Submit: ...

28860
来自专栏HTML5学堂

2015.12.03 HTML5真题练习

HTML5学堂:每天一道题,强壮程序员!今日主要涉及昨日题目的解答,以及一道涉及计时器、时间对象的题目。 HTML5真题【2015.12.02】答案解析 昨日真...

33750

扫码关注云+社区

领取腾讯云代金券