-
前言
近期项目不急,所以有时间来看看自己想学的东西,记得去面试的时候很多面试官都问到runtime的知识点,自己虽然了解一点这方面的知识,但都很零碎。所以这段时间好好研究总结一下。runtime的资料网上很多,觉得很多都讲得比较晦涩难懂(个人观点)。我通过自己的学习总结一遍,主要讲一些常用的、实用的方法。
-
什么是runtime?
runtime简称运行时。就是系统在运行的时候的一些机制,其中最主要的就是消息机制。OC的函数调用成为消息消息发送。属于动态过程。在编译的时候并不能决定真正的调用函数(在编译阶段,OC可以调用任何函数,即使这个 函数并未实现,只要声明就不会报错。而C语言在编译时就会报错——对于C语言,函数在编译的时候会决定调用那个函数,编译完成之后直接顺序执行),只有在真正运行时才会根据函数名称找到对应的函数来调用。
runtime是OC底层的一套C语言的API(使用时需引入<objc/runtime.h>或<objc/message.h>),其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的,编译器最终会将OC代码转换为运行时的代码。比如:
[receiver message];
底层运行时会被编译器转化为:
obje_msgSend(receiver,selector)。
如果还有其他参数,比如:
[recevier message:(id)arg1,arg2...]
底层运行时会被编译器转化为:
objc_msgSend(recevier,Selector,arg1,arg2,...)
以上代码你可能看不出他的价值,但我们只需要了解OC是一门动态语言,他会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定代码运行。所以,编译器是不够用的,我们就需要一个运行时系统(Runtime system)来处理编译后的代码。
-
runtime可实现的功能
(1) 动态交换两个方法的实现(特别是交换系统自带的方法)
(2)动态添加对象的成员变量和方法
(3)获取某个类的所有成员方法和成员变量
-
runtime的相关运用?
(1)拦截并替换系统自带的方法(Swizzle)。如拦截viewDidLoad、alloc、imageNamed等等;
(2)动态的添加对象的成员变量和方法;
(3)实现字典和模型的转换(利用runtime遍历模型对象的所有属性, 根据属性名从字典中取出对应的值, 设置到模型的属性上,反之亦然如:MJExtension);
(4)将某些OC代码转换为运行时代码,探究其底层的实现。如Block的实现、KVO(利用runtime动态产生一个类);
(5)NSCoding(归档和解档, 利用runtime遍历模型对象的所有属性);
(6)用于封装框架(想怎么改就怎么改);
(7)动态交换两个方法的实现
-
runtime常见的函数
(1)class_copyPropertyList 获取一份拷贝的成员列表数组
(2)property_getName 获取成员名称
(3)objc_msgSend 给对象发送消息
(4)object_getIvar 从Ivar(成员变量)对象中取值
(5)object_setIvar 赋值函数
(6)class_getInstanceVariable 获取成员对象的Ivar
(7)class_copyMethodList 遍历某个类所有的方法
(8)class_copyIvarList 遍历某个类所有的成员变量
(9)class_getClassMethod 获取类的方法
(10)method_exchangeImplementations 交换方法
(11)class_replaceMethod 修改类的方法
(12)method_setImplementation 来直接设置某个方法的IMP
(13)class_…..
这些都是学习runtime必须要知道的函数!!!!(10、11、12归根结底,都是偷换了selector的IMP)
-
runtime的一些术语的数据结构
想要全面了解Runtime机制,我们必须先了解Runtime的一些术语及他们对应的数据结构
SEL
SEL是selector 的简写,俗称方法选择器,实质存储的是方法的名称(Swift中是Selector类)。selector是方法选择器,其作用就和名字一样,就像日常生活中,我们通过人名去辨别谁是谁。在OC相同的类不会有两个命名相同的方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:
typedef struct objc_selector *SEL;
我们可以看出它是个映射到方法的 C 字符串,我们可以通过 Objc 编译器器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。注意:不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。
Class
Class是指向objc_class结构体的指针。它的数据结构是:typedef struct objc_class *Class;
objc_class 的数据结构如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
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;
#endif
} OBJC2_UNAVAILABLE;
从objc_class 可以看到,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表:
// 成员变量列表
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
由此可见,我们可以动态修改 *methodList 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。这里可以参考下美团技术团队的文章:深入理解 Objective-C: Category。objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar 则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法的信息则由 objc_method 结构体存储。值得注意的时,objc_class 中也有一个 isa 指针,这说明 Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做 Meta Class(元类) 的东西,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。
Method
Method 代表类中某个方法的类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
objc_method 存储了方法名,方法类型和方法实现:
-
- 方法名类型为 SEL
- 方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型
- method_imp 指向了方法的实现,本质是一个函数指针
Ivar
Ivar 是表示成员变量的类型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
其中 ivar_offset 是基地址偏移字节
IMP
IMP implement 的简写,俗称方法实现,看源码得知它就是一个函数指针,在objc.h中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。
如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache 中会提到。
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。
而一个确定的方法也只有唯一的一组 id 和 SEL 参数。
Cache
Cache 定义如下:
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。
Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。
id
id 是一个参数类型,它是指向某个类的实例的指针。定义如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
以上定义,看到 objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对象所属的类。注意:isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。(KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型,详见:KVO章节。)
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
注意:
返回的是属性列表,列表中每个元素都是一个 objc_property_t 指针
-
下面我通过demo 我一个个来讲解
在使用runtime时必须导入<objc/runtime.h> ,这里就以Student类为例,现在创建的xiaoming对象,有+(void)study和+(void)run两个类方法和-(void)study1和-(void)run1实例方法,有name和age两个属性。
一、获取属性列表
使用objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
其中cls 参数表示需要获取属性的类。outCount表示返回包含返回的属性数组的长度
首先从文档里看他的结构
/** * Describes the properties declared by a class. * * @param cls The class you want to inspect. * @param outCount On return, contains the length of the returned array. * If e outCount is c NULL, the length is not returned. * * @return An array of pointers of type c objc_property_t describing the properties * declared by the class. Any properties declared by superclasses are not included. * The array contains c *outCount pointers followed by a c NULL terminator. You must free the array with c free(). * * If e cls declares no properties, or e cls is c Nil, returns c NULL and c *outCount is c 0. */ OBJC_EXPORT objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
示例
unsigned int count; objc_property_t *propertyList = class_copyPropertyList([Student class], &count); for (unsigned int i=0; i<count; i++) { const char *propertyName = property_getName(propertyList[i]); NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]); }
打印结果为:
2017-09-12 14:35:32.131 RunTimeProduct[4680:110834] property---->name
2017-09-12 14:35:32.131 RunTimeProduct[4680:110834] property---->age
二、获得一个类的所有成员变量
使用方法:Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
其中cls 参数表示需要获取成员变量的类。outCount表示返回包含返回的属性数组的长度,用来存放属性的个数
还要用到方法:const char *ivar_getName(Ivar v)(获得成员变量的名字)和
const char *ivar_getTypeEndcoding(Ivar v)(获得成员变量的类型)
/** * Describes the instance variables declared by a class. * * @param cls The class 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 Ivar describing the instance variables declared by the class. * Any instance variables declared by superclasses are not included. The array contains *outCount * pointers followed by a NULL terminator. You must free the array with free(). * * If the class declares no instance variables, or cls is Nil, NULL is returned and *outCount is 0. */ OBJC_EXPORT Ivar *class_copyIvarList(Class cls, unsigned int *outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
示例
unsigned int count; Ivar *ivarList = class_copyIvarList([Student class], &count); for (unsigned int i = 0; i < count; i++) { // 取出i位置对应的成员变量 Ivar myIvar = ivarList[i]; const char *ivarName = ivar_getName(myIvar); const char *type = ivar_getTypeEncoding(myIvar); NSLog(@"成员变量名:%s 成员变量类型:%s",ivarName,type); }
打印结果为:
2017-09-12 16:45:20.492 RunTimeProduct[6085:173368] 成员变量名:_name 成员变量类型:@"NSString"
2017-09-12 16:45:20.493 RunTimeProduct[6085:173368] 成员变量名:_age 成员变量类型:Q
三、获取协议列表
使用方法:Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
其中cls 参数表示需要获取协议列表的类。outCount表示返回包含返回的属性数组的长度
/** * Describes the protocols adopted 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 Protocol* describing the protocols adopted * by the class. Any protocols adopted by superclasses or other protocols are not included. * The array contains *outCount pointers followed by a NULL terminator. You must free the array with free(). * * If cls adopts no protocols, or cls is Nil, returns NULL and *outCount is 0. */ OBJC_EXPORT Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
示例
unsigned int count; __unsafe_unretained Protocol **protocolList = class_copyProtocolList([Student class], &count); for (unsigned int i = 0; i < count; i++) { Protocol *myProtocal = protocolList[i]; const char *protocolName = protocol_getName(myProtocal); NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]); }
四、获取方法列表
使用的方法:
Method *class_copyMethodList(Class cls, unsigned int *outCount)
其中cls 参数表示需要获取方法列表的类。outCount表示返回包含返回的属性数组的长度
/** * 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 count; Method *methodList = class_copyMethodList([Student class], &count); for (unsigned int i = 0; i < count; i++) { Method method = methodList[i]; NSLog(@"method---->%@", NSStringFromSelector(method_getName(method))); }
打印结果为:
2017-09-12 14:52:07.997 RunTimeProduct[4929:120846] method---->run1
2017-09-12 14:52:07.997 RunTimeProduct[4929:120846] method---->study1
2017-09-12 14:52:07.997 RunTimeProduct[4929:120846] method---->age
2017-09-12 14:52:07.997 RunTimeProduct[4929:120846] method---->setAge:
2017-09-12 14:52:07.997 RunTimeProduct[4929:120846] method---->.cxx_destruct
2017-09-12 14:52:07.997 RunTimeProduct[4929:120846] method---->name
2017-09-12 14:52:07.998 RunTimeProduct[4929:120846] method---->setName:
五、获得某个类的类方法
使用方法:
Method class_getClassMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
其中cls 参数表示需要获取类方法的类。SEL name 要获取的方法的名字
/** * Returns a pointer to the data structure describing a given class method for a given class. * * @param cls A pointer to a class definition. Pass the class that contains the method you want to retrieve. * @param name A pointer of type c SEL. Pass the selector of the method you want to retrieve. * * @return A pointer to the c Method data structure that corresponds to the implementation of the * selector specified by aSelector for the class specified by aClass, or NULL if the specified * class or its superclasses do not contain an instance method with the specified selector. * * @note Note that this function searches superclasses for implementations, * whereas c class_copyMethodList does not. */ OBJC_EXPORT Method class_getClassMethod(Class cls, SEL name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
示例
Class stuClass = object_getClass([Student class]); SEL oriSEL = @selector(run); Method oriMethod = class_getClassMethod(stuClass, oriSEL);
NSLog(@"method---->%@", NSStringFromSelector(method_getName(oriMethod)));
打印结果为:
2017-09-12 14:56:47.605 RunTimeProduct[4989:123342] method---->run
六、获得实例方法
使用方法:
Method class_getInstanceMethod(Class cls, SEL name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
其中cls 参数表示需要获取的实例的类。SEL name 要获取的方法的名字
/** * Returns a specified instance method for a given class. * * @param cls The class you want to inspect. * @param name The selector of the method you want to retrieve. * * @return The method that corresponds to the implementation of the selector specified by * e name for the class specified by e cls, or c NULL if the specified class or its * superclasses do not contain an instance method with the specified selector. * * @note This function searches superclasses for implementations, whereas c class_copyMethodList does not. */ OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
示例
Method cusMethod = class_getInstanceMethod([Student class], @selector(run1)); NSLog(@"method---->%@", NSStringFromSelector(method_getName(cusMethod)));
打印结果为:
2017-09-12 15:02:46.873 RunTimeProduct[5132:127840] method---->run1 由此可知Student里有这个实例方法
七、给实例动态添加方法
使用方法:BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
参数详解:
Class cls 需要添加方法的类 SEL name 在添加方法的类里,这个添加方法的名字(可以根据爱好写)即就是添加的方法在本类里面叫做的名字,但是方法的格式一定要和你需要添加的方法的格式一样,比如有无参数。(有几个小伙伴问我Demo里面的findInSelf这个方法没有找到,请看这里呀。这个就是里面为啥没有findInSelf方法而可以直接调用的原因) IMP imp 就是Implementation的缩写,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP。这里需要的是IMP,所以你不能直接写方法,需要用到一个方法:
const char *types:这一个也很有意思,我刚开始也很费解,结果看了好多人的解释我释然了,知道吗,我释然啦。这个东西其实也很好理解:
比如:”v@:”意思就是这已是一个void类型的方法,没有参数传入。
再比如 “i@:”就是说这是一个int类型的方法,没有参数传入。
再再比如”i@:@”就是说这是一个int类型的方法,又一个参数传入。
/** * Adds a new method to a class with a given name and implementation. * * @param cls The class to which to add a method. * @param name A selector that specifies the name of the method being added. * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd. * @param types An array of characters that describe the types of the arguments to the method. * * @return YES if the method was added successfully, otherwise NO * (for example, the class already contains a method implementation with that name). * * @note class_addMethod will add an override of a superclass's implementation, * but will not replace an existing implementation in this class. * To change an existing implementation, use method_setImplementation. */ OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
示例代码
/** OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) Class cls 需要添加方法的类 SEL name 在添加方法的类里,这个添加方法的名字(可以根据爱好写)即就是添加的方法在本类里面叫做的名字,但是方法的格式一定要和你需要添加的方法的格式一样,比如有无参数。(有几个小伙伴问我Demo里面的findInSelf这个方法没有找到,请看这里呀。这个就是里面为啥没有findInSelf方法而可以直接调用的原因) IMP imp 就是Implementation的缩写,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP。这里需要的是IMP,所以你不能直接写方法,需要用到一个方法: const char *types:这一个也很有意思,我刚开始也很费解,结果看了好多人的解释我释然了,知道吗,我释然啦。这个东西其实也很好理解: 比如:”v@:”意思就是这已是一个void类型的方法,没有参数传入。 再比如 “i@:”就是说这是一个int类型的方法,没有参数传入。 再再比如”i@:@”就是说这是一个int类型的方法,又一个参数传入。 OBJC_EXPORT IMP class_getMethodImplementation(Class cls, SEL name) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0); 这个方法也是runtime的方法,就是获得对应的方法的指针,也就是IMP。 用这个方法添加的方法是无法直接调用的,必须用performSelector:调用。为甚么呢??? 知道为甚么了吧,你添加方法是在运行时添加的,你在编译的时候还没有这个本类方法,所以当然不行啦。 */ Student *xiaoming = [[Student alloc]init]; class_addMethod([Student class], @selector(addMothodName), class_getMethodImplementation([self class], @selector(addMothod:)), "v@:@:"); [xiaoming performSelector:@selector(addMothodName) withObject:@"lyj"];
- (void)addMothod:(NSString *)name{ NSLog(@"动态添加的方法 %@",name); }
打印结果为:
2017-09-12 15:13:17.380 RunTimeProduct[5357:134692] 动态添加的方法 lyj
八、交换两个方法的实现,拦截系统自带的方法调用功能
使用的方法:
void method_exchangeImplementations(Method m1 , Method m2)
其中Method m1 ,Method m2就是我们要交换的两个方法
案例1:方法简单的交换
创建一个Student类,其中实现两个方法,并在.h中声明
+ (void)run { NSLog(@"跑步"); } + (void)study { NSLog(@"学习"); }
在控制器中调用以下代码
[Student run];
[Student study];
打印结果为:
2017-09-11 17:15:58.513 RunTimeProduct[18066:171816] 跑步
2017-09-11 17:15:58.514 RunTimeProduct[18066:171816] 学习
下面通过runtime 实现方法交换,类方法的获取class_getClassMethod,对象方法用class_getInstanceMethod
// 获取两个类的类方法 Method m1 = class_getClassMethod([Student class], @selector(run)); Method m2 = class_getClassMethod([Student class], @selector(study)); // 开始交换方法实现 method_exchangeImplementations(m1, m2); [Student run]; [Student study];
打印结果为:
2017-09-11 17:30:22.138 RunTimeProduct[18247:179351] 学习
2017-09-11 17:30:22.138 RunTimeProduct[18247:179351] 跑步
这里就实现了一个简单的方法替换
案例2:拦截系统方法
比如:比如iOS8 升级 iOS9 后需要版本适配,根据不同系统使用不同样式图片(拟物化和扁平化),如何通过不去手动一个个修改每个UIImage的imageNamed:方法就可以实现为该方法中加入版本判断语句?
实现步骤:
1、为UIImage创建一个分类(UIImage+Category)
2、在分类中实现一个自定义的方法,方法中写要在系统方法中加入的语句,实现版本判断
+ (UIImage *)lyj_imageName:(NSString *)imageName { double version = [[UIDevice currentDevice].systemVersion doubleValue]; if (version >= 9.0) { //如果系统版本是9.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片 imageName = [imageName stringByAppendingString:@"_ios9"]; NSLog(@"changName = %@",imageName); } return [UIImage lyj_imageName:imageName]; }
3.在分类中重新UIImage的load方法,实现方法交换(只要能让其执行一次方法交换语句,load就再适合不过了)
//dispatch_once这里不是“单例”,是保证方法替换只执行一次. static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取两个类的类方法 Method m1 = class_getClassMethod([self class], @selector(imageNamed:)); Method m2 = class_getClassMethod([self class], @selector(lyj_imageName:)); // 开始交换方法实现 method_exchangeImplementations(m1, m2); });
注意:在自定义的方法中,最后一定要在调用一下系统的方法,让其有系统方法的功能,但由于方法交换,系统的方法名已经变成了我们自定义的方法名,这就实现了系统方法的拦截。
九、在分类中设置属性,给任何一个对象设置属性
众所周知,分类中是无法设置属性的,如果在分类的声明中写属性只能为其生成get和set方法的声明,但无法生成成员变量。虽然点语法能调用出来,但程序执行就会Crash。那有人会想为什么不使用全局变量?但全局变量在整个内存中只有一份,我们创建多个对象修改其属性值都会修改同一个变量,这样就无法保证像属性一样每个对象都拥有其自己的属性值。这时我们就可以借助runtime来为分类增加属性了。
这里需要用到两个方法:
1、void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
这里set方法,将值value 跟对象object 关联起来(将值value 存储到对象object 中)
参数 object:给哪个对象设置属性
参数 key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
参数 value:给属性设置的值
参数policy:存储策略 (assign 、copy 、 retain就是strong)
2、id objc_getAssociatedObject(id object , const void *key)
利用参数key 将对象object中存储的对应值取出来
步骤:
1、创建一个分类,比如给UIbutton添加一个点击事件的block,设置Title字体颜色的属性lyj_textColor和倒圆角的属性lyj_Radius。
2、先在.h 中@property 声明出get 和 set 方法,方便点语法调用
//先说明一个block typedef void (^clickBlock)(void); /**设置点击事件*/ @property (nonatomic,copy) clickBlock click; @property (nonatomic, retain)UIColor *lyj_textColor; @property (nonatomic, assign) double lyj_Radius;
3、在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值
static const void *associatedKey = "associatedKey"; //Category中的属性,只会生成setter和getter方法,不会生成成员变量 - (void)setClick:(clickBlock)click { objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC); [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside]; if (click) { [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside]; } } - (clickBlock)click { return objc_getAssociatedObject(self, associatedKey); } -(void)buttonClick{ if (self.click) { self.click(); } } - (UIColor *)lyj_textColor { return objc_getAssociatedObject(self, @selector(lyj_textColor)); } - (void)setLyj_textColor:(UIColor *)lyj_textColor { objc_setAssociatedObject(self, @selector(lyj_textColor), lyj_textColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self setTitleColor:lyj_textColor forState:UIControlStateNormal]; } - (double)lyj_Radius { return [objc_getAssociatedObject(self, "lyj_Radius") doubleValue]; } - (void)setLyj_Radius:(double)lyj_Radius { objc_setAssociatedObject(self, "lyj_Radius", @(lyj_Radius), OBJC_ASSOCIATION_ASSIGN); self.layer.cornerRadius = lyj_Radius; self.layer.masksToBounds = YES; }
4、在要使用的地方直接点语法就可以设置相关的属性了
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = self.view.bounds; button.lyj_textColor = [UIColor redColor]; button.backgroundColor = [UIColor yellowColor]; button.lyj_Radius = self.view.bounds.size.width/2; [button setTitle:@"测试" forState:UIControlStateNormal]; [self.view addSubview:button]; button.click = ^{ NSLog(@"buttonClicked"); };