iOS Runtime

xiaoxiao2022-06-11  31

# iOS Runtime

       OC 与C++、Java等语言类似都是面向对象语言,但是它们在很多方面是有所差别。其中区别很明显的一点就是OC是使用了“消息结构”,而非“函数调用”。两者的区别就像下面这样:

//消息结构 Object *obj = [Object new]; [obj performWith:parameter1 and:parameter2];   //函数调用 Object *obj = new Object; obj->perform(parameter1,parameter2);

       对于消息结构语言来说它运行期所执行的代码是由运行环境所决定的,换句话说在编译期对象并不知道要去执行哪个方法,而是在运行期使用动态绑定机制(通过消息传递机制和消息转发机制实现动态绑定)来查找并决定该调用哪个方法,而函数调用语言所调用的函数则是由编译器决定的。这种特性使得OC成为了一门动态语言。(举个例子,我们给button添加一个点击事件,如果没有实现这个方法编译期并不会报错,但是会在运行时(点击button的时候)导致程序崩溃)。

消息传递

     消息传递是runtime的核心,那么到底什么是消息,我们来举一个给对象发送消息的例子。如下:

[objectA messageName:parameter];

     上面例子就是给 objectA 发送了一条“消息”,其中 objectA 为“接受者”,messageName: 是“选择子”,parameter是“参数”,选择子和参数合起来叫做消息。那么这样的消息到底是如何传递执行的呢?首先,编译器会将这条消息转为一条C语言函数—— objc_msgSend()。

id objc_msgSend(id self, SEL op, ...) //上面那个例子会转换为: objc_msgSend(objectA,@selector(messageName:),parameter;

      这个函数的第一个参数是消息的接受者,第二个是选择子,后面的则是消息的参数,参数可以是多个。转换后,objc_msgSend() 函数会根据自身的参数(也就是消息的接受者、选择子)去动态的查找合适的方法去执行。查找的过程就是先从接受者所属类的方法列表中去查找,如果找到就转去执行,如果没有找到就从该类的父类去查找,就这样沿着继承体系一直向上查找,直到找到合适的方法去执行,或者一直找到继承根部(NSObject)还没有找到,这个时候并不会直接报错,而是转去执行“消息转发机制”。

消息转发

      上面我们已经知道了什么时候会执行消息转发机制。下面就来了解一下消息转发机制。整个过程会经过会依次进行三个阶段(如果前面的步骤使得消息得以处理则不会再进行下面的步骤)。

动态方法解析备援的接收者完整的消息转发机制

动态方法解析

       对象在接收到无法在消息传递阶段处理的消息时,首先会调用以下两个方法的其中一个,具体调用哪个取决于该方法是实例方法还是类方法:

+(BOOL)resolveClassMethod:(SEL)sel;   +(BOOL)resolveInstanceMethod:(SEL)sel;

       很显然,如果是实例方法调用 resolveInstanceMethod: ,如果是类方法调用  resolveClassMethod: 。        以 resolveInstanceMethod: 方法为例,从方法名就可以看出,这个方法用来处理实例方法,它会在运行期看看是否能动态添加一个方法来处理当前的这个选择子,它的参数 sel 就是当前那个等待处理的选择子,返回值表示能否添加方法以处理当前选择子。          另外,这里动态添加方法必须是编程者通过重写接收者所属类的 resolveInstanceMethod: 方法,并在该方法中调用函数 class_addMethod(后面会介绍) 将已经实现的方法添加给当前类,这样的话,当系统因无法处理该消息而调用到重写后的resolveInstanceMethod: 方法时自然会执行到我们所写 class_addMethod 函数,从而动态的添加函数中所指定的方法到接受者所属类。换句话说,我们需要重写 resolveInstanceMethod: 方法,在这个方法中为消息接受者所属类添加方法,运行时只需要动态将指定方法添加给类即可,并非系统自行添加,系统只负责调用执行 resolveInstanceMethod:。         接下来我们来介绍一下 class_addMethod 这个函数:

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,                  const char * _Nullable types)

      下面是官方文档给出的解释:

/**   * 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.  */

      这个函数的作用就是根据给出的方法名和实现给一个类添加方法。四个参数分别是要添加方法的那个类(cls)、要添加的方法名(name)、该方法的实现部分(imp)、一个表示方法返回值以及参数类型的字符数组(types)。         下面我们来看个例子:

//创建一个名为 RTMTest 的类 //在 .h 文件中声明一个方法,.m 不去实现这个方法 .h @interface RTMTest : NSObject -(void)printString:(NSString *)string; @end .m @implementation RTMTest @end

  接下来在 viewcontroller 调用这个方法

RTMTest *test = [[RTMTest alloc] init]; [test printString:@"runTime"];

 这时候我们可以看到运行结果如下:

RTMTest printString:]: unrecognized selector sent to instance 0x6000006ddbe0'

       很显然是抛出异常,原因是该方法(选择子)无法处理。下面我们来重写一下 resolveInstanceMethod 方法,在 RTMTest 类的 .m 文件中

#import "RTMTest.h" //需要引入 objc/runtime.h 头文件 #import <objc/runtime.h> @implementation RTMTest +(BOOL)resolveInstanceMethod:(SEL)sel{     if (sel == @selector(printString:)) {         class_addMethod([self class], @selector(printString:), class_getMethodImplementation(self, @selector(addMethodPrintString:)), "v@:@");         return YES;     }     return [super resolveInstanceMethod:sel]; } // "v@:@" 表示返回值类型为 void ,有一个参数 -(void)addMethodPrintString:(NSString *)string{     NSLog(@"%@",string); } @end

执行结果如下,显然消息得到了处理

2018-10-23 19:54:17.072048+0800 RuntimeTest[1301:878223] runTime

备援接收者

      如果执行了 resolveInstanceMethod: 方法后消息还未得到处理,便会调用 forwardingTargetForSelector:  方法,从方法名就可以看出这个方法的作用就是为选择子添加代理,也就是说看看能不能把消息转发给其他接收者(称为备援接收者)来处理。

-(id)forwardingTargetForSelector:(SEL)aSelector;

     方法的返回值是备援接收者(若没有找到备援接受者返回nil),参数是等待处理的选择子。同样以上面那个类为例来看看这个方法,同样在 RTMTest 类中声明不实现这个方法,并在 viewcontroller 中调用: 

RTMTest.m @implementation RTMTest +(BOOL)resolveInstanceMethod:(SEL)sel{     return YES; } -(id)forwardingTargetForSelector:(SEL)aSelector{     if (aSelector == @selector(printString:)) {         //将该消息转给 ViewController 的实例对象去处理         return [[ViewController alloc] init];     }     return  [super forwardingTargetForSelector:aSelector]; } @end ViewController.h //在 ViewController 中实现这个方法 -(void)printString:(NSString *)string{     NSLog(@"%@",string); }

 可以看到运行结果,显然成功地将等待处理的消息转给 ViewController 执行了

2018-10-23 20:32:49.201074+0800 RuntimeTest[1641:1298070] runTime

完整的消息转发机制

      如果执行完上一步消息还没有得到处理,就会启用完整的消息转发机制:         首先调用 methodSignatureForSelector:  方法获取待处理方法的方法签名(包含方法的返回值类型、参数),如果获取不到返nil并调用 doesNotRecognizeSelector: 从而程序崩溃,如果获取到则返回该方法签名,继续往下进行,创建一个 NSInvocation 对象,将待处理消息的相关细节封装到这个对象中,然后调用-(void)forwardInvocation:(NSInvocation *)anInvocation 方法。    

@implementation RTMTest + (BOOL)resolveInstanceMethod:(SEL)sel {     return YES; } - (id)forwardingTargetForSelector:(SEL)aSelector {     return nil; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {     if (aSelector == @selector(printString:)) {         return [NSMethodSignature signatureWithObjCTypes:"v@:@"];     }          return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation {     if ([anInvocation selector] == @selector(printString:)) {         ViewController *viewController = [[ViewController alloc] init];         //将待处理消息发送给对象 viewController         [anInvocation invokeWithTarget:viewController];     } } @end

执行结果如下:

2018-10-23 21:37:49.109146+0800 RuntimeTest[2146:2009193] runTime

 显然,成功地将待处理消息转发给了viewController并得以处理(前提是ViewController中实现了这个方法)。

doesNotRecognizeSelector:

     如果进行了以上步骤消息还没有得到处理就会调用 doesNotRecognizeSelector: 方法抛出异常。举个例子,如果我们给一个button添加点击事件而没有实现该方法,这个时候再去点击这个button我们会发现程序会崩溃并打印如下错误信息:

-[ViewController touchButton]: unrecognized selector sent to instance 0x7fe3d1c0b200

此异常表示选择子最终未能得到处理。

 Runtime的应用

一、给关联对象添加属性

       很多时候我们需要通过“分类机制”将类的实现代码分模块写,使得代码便于管理,或者有时候我们会想为某个系统类添加一个属性,但是因为要添加一个属性就继承一个类似乎有些麻烦,这个时候我们就想能不能通过分类给它添加一个属性。但是我们会发现分类中是不能声明属性的。   下面我们就来看看为什么不能在分类中添加属性:

//创建一个 UIView 的分类 //UIView+category.h @interface UIView (category) //为这个分类添加一个属性 @property(nonatomic,copy) NSString *viewName; @end

如果不手动在 .m 为这个属性添加存取方法,我们会发现会有以下警告信息

Property 'viewName' requires method 'setViewName:' to be defined - use @dynamic or provide a method implementation in this category   Property 'viewName' requires method 'viewName' to be defined - use @dynamic or provide a method implementation in this category

以上警告信息告诉我们需要为 viewName 属性添加set ,get方法,或者用  @dynamic 修饰该属性( @dynamic 修饰表示被修饰属性不需要自动添加存取方法),这就说明系统并不会为分类中声明的属性添加存取方法。那我们试着在 .m 文件中为这个属性添加存取方法:

#import "UIView+category.h" #import <objc/runtime.h> @implementation UIView (category) -(void)setViewName:(NSString *)viewName{     self.viewName = viewName;    //分类中不能通过下划线访问属性 } -(NSString *)viewName{     return self.viewName; } @end viewController.m UIView *view = [[UIView alloc] initWithFrame:self.view.frame]; [self.view addSubview:view]; view.viewName = @"runtimeTestView"; NSLog(@"%@",view.viewName);

在分类中不能通过下划线访问属性,而在 setViewName: 方法中再使用 self.viewName = viewName 赋值显然会导致死循环(self.viewName会调用set方法),运行以上代码我们会发现程序因陷入死循环而crash,调取一下当前的函数调用栈会发现一直在调用 setViewName: 方法:

frame #169392: 0x000000010d1fe05f RuntimeTest`-[UIView(self=0x00007fa61b503110, _cmd="setViewName:", viewName=0x000000010d200130) setViewName:] at UIView+category.m:14     frame #169393: 0x000000010d1fe05f RuntimeTest`-[UIView(self=0x00007fa61b503110, _cmd="setViewName:", viewName=0x000000010d200130) setViewName:] at UIView+category.m:14     frame #169394: 0x000000010d1fe05f RuntimeTest`-[UIView(self=0x00007fa61b503110, _cmd="setViewName:", viewName=0x000000010d200130) setViewName:] at UIView+category.m:14     frame #169395: 0x000000010d1fe05f RuntimeTest`-[UIView(self=0x00007fa61b503110, _cmd="setViewName:", viewName=0x000000010d200130) setViewName:] at UIView+category.m:14

      接下来我们看一下如何用runtime来解决这个问题——通过下面这个方法为类添加关联对象:

/**   * Sets an associated value for a given object using a given key and association policy.  *   * @param object The source object for the association.  * @param key The key for the association.  * @param value The value to associate with the key key for object. Pass nil to clear an existing association.  * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”  *   * @see objc_setAssociatedObject  * @see objc_removeAssociatedObjects  */ OBJC_EXPORT void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,                          id _Nullable value, objc_AssociationPolicy policy)

以上是官方文档中给出的解释,根据给定的 key 和给定的内存管理策略为给定对象添加关联对象。四个参数分别是要添加属性的对象、关联对象的 key 、关联的对象、内存管理策略(也就是属性关键字)。    内存管理策略(枚举类型)与正常声明属性一样,选择需要的属性关键字,对应相应的内存管理策略

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. */ };

看完这个函数,我们来举个例子

//其他地方不变,在分类的 .m 文件中添加如下代码 #import "UIView+category.h" #import <objc/runtime.h> @implementation UIView (category) //将该属性用 dynamic 修饰,告诉编译器不用自动为该属性添加存取方法,这样就不会有警告信息 @dynamic viewName; //关联对象对应的 key static char kViewNameKey; -(void)setViewName:(NSString *)viewName{     //添加关联对象     objc_setAssociatedObject(self, &kViewNameKey, viewName,OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -(NSString *)viewName{     //根据唯一 key 获取该属性(也就是所添加的关联对象);     return objc_getAssociatedObject(self, &kViewNameKey); } @end

运行结果如下:

2018-10-27 15:14:11.076839+0800 RuntimeTest[1916:1662387] runtimeTestView

显然成功地在分类中为view添加了一个可正常存取的属性。

二、NSCoding自动归档和自动解档

实现 NSCoding 自动归档和自动解档的关键在于 runtime 提供的一个函数:

/**   * 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 _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount)      OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

      从文档可以看出,这个函数用于描述类的实例变量(包括所有属性和变量(@interface{//包括这里声明的变量})),两个参数分别是要描述的类和返回数组的长度,函数返回值是一个包含了所有属性和变量的数组。下面我们验证一下它是否真的有这个功能:

//创建一个名为 RTMPerson 的类 //在 .h 文件中声明一下属性 @interface RTMPerson : NSObject{     NSString *address; } @property(nonatomic,copy) NSString *name; @property(nonatomic,copy) NSString *number; @property(nonatomic,assign) NSInteger age; @end 在viewController中调用 RTMPerson *person = [[RTMPerson alloc] init]; unsigned int count = 0; Ivar *personvar = class_copyIvarList([person class], &count); for (int i = 0; i < count; i++) {     NSLog(@"%s",ivar_getName(personvar[i]));     //获取到的是一个C语言字符串,需要转成NSString     NSLog(@"%@",[NSString stringWithUTF8String:ivar_getName(personvar[i])]); }

运行结果

2018-10-27 16:19:41.372623+0800 RuntimeTest[2547:2370122] address 2018-10-27 16:19:41.372793+0800 RuntimeTest[2547:2370122] address 2018-10-27 16:19:41.372904+0800 RuntimeTest[2547:2370122] _name 2018-10-27 16:19:41.373027+0800 RuntimeTest[2547:2370122] _name 2018-10-27 16:19:41.373142+0800 RuntimeTest[2547:2370122] _number 2018-10-27 16:19:41.373257+0800 RuntimeTest[2547:2370122] _number 2018-10-27 16:19:41.373381+0800 RuntimeTest[2547:2370122] _age 2018-10-27 16:19:41.373498+0800 RuntimeTest[2547:2370122] _age

可以看到的确是遍历到了所有属性包括变量,这样的话我们就可以用它的这个功能来遍历类的所有属性,以达到自动归档和自动解档的效果。接下来我们对上面那个 RTMPerson 进行归档解档,如下:

//同样需要引入runtime头文件 -(instancetype)initWithCoder:(NSCoder *)aDecoder{     if (self = [super init]) {         unsigned int count = 0;         Ivar *selfIvar = class_copyIvarList([self class], &count);         for (int i = 0;i < count; i++) {             NSString *key = [NSString stringWithUTF8String:ivar_getName(selfIvar[i])];             [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];         }     }     return self; } -(void)encodeWithCoder:(NSCoder *)aCoder{     unsigned int outCount;     Ivar * ivars = class_copyIvarList([self class], &outCount);     for (int i = 0; i < outCount; i ++) {         Ivar ivar = ivars[i];         NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];         [aCoder encodeObject:[self valueForKey:key] forKey:key];     } } //在viewcontroller中试一下有没有归档成功     RTMPerson *person = [[RTMPerson alloc] init];     person.name = @"runtime";     person.number = @"666666";     person.age = 10;     NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person];     RTMPerson *newPerson = [NSKeyedUnarchiver unarchiveObjectWithData:data];     NSLog(@"====%@ %ld",newPerson.name,(long)newPerson.age);

运行结果如下

2018-10-27 16:52:05.869214+0800 RuntimeTest[3142:2726449] ====runtime 10

显然已经成功实现了自动归档解档,方便许多。

三、字典转模型

先来看看这个函数

/**   * 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 _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)     OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

与上面提到的 class_copyIvarList() 类似,不同的是class_copyPropertyList() 用来描述对象的所有属性,不包括变量,返回值是包含了所有属性的数组。我们可以利用这个函数,用以下方法实现用字典初始化一个对象,也就是字典转模型:

- (instancetype)initWithDict:(NSDictionary *)dict {     self = [super init];     if (self) {         //存放所有属性名         NSMutableArray * keys = [NSMutableArray array];         unsigned int count = 0;         //获取所有属性         objc_property_t * selfProperty = class_copyPropertyList([self class], &count);         for (int i = 0; i < count; i ++) {             //将获取到的属性转换为NSString类型             NSString * propertyName = [NSString stringWithUTF8String:property_getName(selfProperty[i])];             //添加到keys             [keys addObject:propertyName];         }         free(selfProperty);         for (NSString * key in keys) {             //如果字典中没有该属性的值则不进行赋值语句             if ([dict valueForKey:key] == nil) continue;             //如果有,赋值             [self setValue:[dict valueForKey:key] forKey:key];         }     }     return self;      //viewcontroll中调用 RTMPerson *person2 = [[RTMPerson alloc] initWithDict:@{@"name":@"zhangsan",@"number":@"888888",@"age":@10}]; NSLog(@"name=%@ number=%@ age=%ld",person2.name,person2.number,(long)person2.age); }

运行结果

2018-10-27 17:54:47.106223+0800 RuntimeTest[3734:3382900] name=zhangsan number=888888 age=10

 

转载请注明原文地址: https://www.6miu.com/read-4930373.html

最新回复(0)