zoukankan      html  css  js  c++  java
  • 初识 Runtime

    前言

    之前在看一些第三方源码的时候,时不时的能碰到一些关于运行时相关的代码。于是乎,就阅读了一些关于运行时的文章,感觉写的都不错,写此篇文章为了记录一下,同时也重新学习一遍。

    Runtime简介

    • Runtime简称运行时,OC就是运行时机制。
    • C语言中函数的调用在编译的时候就会决定调用哪个函数。
    • 对于OC来说,属于动态调用过程,在编译的时候并不能决定调用哪个函数,只有真正运行的时候才会根据函数的名称找到对应的函数来调用。
    • 事实证明:
      1. 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明就不会报错。
      2. 在编译阶段,C语言调用未实现的函数就会报错。

    Runtime的作用

    发送消息

    • 方法调用的本质就是向对象发送消息。

    • objc_msgSend,只有对象才能发送消息,因此以objc开头。注意:在oc中,不论是实例对象还是Class,都是id类型的对象

    • 让我们来看看方法调用转化成运行时的代码,看看调用方法的真面目吧。

      1. 新建一个命令行工程

      2. 然后在main方法里面写上

        int main(int argc, const char * argv[]) {
            @autoreleasepool {
                NSObject *obj = [[NSObject alloc] init];
            }
            return 0;
        }
        
      3. 用终端跳转到工程所在的根目录,然后命令行运行clang -rewrite-objc main.m

      4. 然后ls查看一下当前目录可以看到有一个main.cpp文件,我们用open main.cpp打开该文件,可以看到一大串代码,我们可以直接翻到底部,可以看到这样的代码:

        int main(int argc, const char * argv[]) {
            /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
                    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
                }
                return 0;
        }
        
      5. 删除掉一些强制转换,将上面的代码简化后:

        int main(int argc, const char * argv[]) {
            /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
                    NSObject *obj = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
                }
            return 0;
            }
        

    总结:到这里,我们可以看到调用方法的本质就是发送消息了,并且可以看到我们写的

    NSObject *obj = [[NSObject alloc] init];
    
    > 上面这条语句发送了两次消息,第一次发送了`alloc`消息,第二次发送了`init`消息。
    
    
    ### 给分类添加属性
    相信大家都知道分类是不可以添加属性的,不过我们可以通过运行时,给分类动态的添加属性。
    
    **原理:**给一个分类声明属性,其本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
    
    #### 实践
    - 我们给`NSObject`添加一个分类,然后声明一个`name`属性。
    
    	```objc
    		#import <Foundation/Foundation.h>
    		@interface NSObject (Extension)
    		@property (nonatomic, copy) NSString *name;
    		@end
    	```
    	
    - 我们创建一个`NSObject`对象,然后给`name`属性赋值,并且打印`name`的值。
       我们可以看到编译成功,但是运行的时候就会华丽丽的崩溃,这就是所谓的不能给分类添加属性的原因了。不过我们可以通过运行时实现。
    - 接下来我们在.m文件重写`name`属性的`setter`以及`getter`方法。
    
    	```objc
    		#import "NSObject+Extension.h"
    		#import <objc/runtime.h>
    		static const char *key = "name";
    		@implementation NSObject (Extension)
    		-(NSString *)name {
    		    // 根据关联的key,获取关联的值
    		    return objc_getAssociatedObject(self, key);
    		}
    		-(void)setName:(NSString *)name {
    		    // 第一个参数:给哪个对象添加关联
    		    // 第二个参数:关联的key,通过这个key获取
    		    // 第三个参数:关联的value
    		    // 第四个参数:关联的策略
    		    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    		}
    		@end
    	```
    	
    
    - 运行程序,编译成功,运行也成功。
    
    ### 动态添加方法
    * **开发使用场景:** 如果一个类方法非常多,加载该类到内存的时候比较耗费资源,需要给每个方法生成映射表,可以使用运行时给该类动态添加方法来解决。(**ps:我感觉这个在开发中不常用**)
    
    #### 实践
    - 创建一个继承`NSObject`的`Student`类,声明一个`study`方法。
    
    	```objc
    		#import <Foundation/Foundation.h>
    		@interface Student : NSObject
    		-(void)study;
    		@end
    	```
    
    - 创建一个`Student`对象,调用`study`方法。
    	
    	```objc
    		Student *s = [[Student alloc] init];
    	   [s performSelector:@selector(study)];
    	```
    	此时会出现一个经典的报错:
    	```objc
    		-[Student study]: unrecognized selector sent to instance 0x7fd719cbb2f0
    	```
        
    	> **错误原因:调用一个未实现的实例方法**
    
    - 我们在`Student`类.m文件动态添加study方法的实现。
    
    	```objc
    		#import "Student.h"
    		#import <objc/runtime.h>
    		@implementation Student
    		// void(*)()
    		// 默认方法都有两个隐式参数,
    		void studyStudent(id self, SEL sel){
    		    NSLog(@"%@--%@",self,NSStringFromSelector(sel));
    		}
    		// 当一个对象调用未实现的方法,会调用该方法处理,并且会把对应的方法列表传进来,我们可以在这个方法里判断,未实现的方法是不是我们想要动态添加的方法。
    		+(BOOL)resolveInstanceMethod:(SEL)sel {    
    		    if (sel == @selector(study)) {
    		        // 第一个参数:给哪个类添加方法
    		        // 第二个参数:添加方法的方法编号
    		        // 第三个参数:添加方法的函数实现(函数地址)
    		        // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
    		        class_addMethod(self, @selector(study), studyStudent, "v@:");
    		        // 注意:此处需要马上结束此方法(否则,如果存在继承关系的话,会调用到父类去)
    		        return YES;
    		    }
    		    return [super resolveInstanceMethod:sel];
    		}
    		@end		
     	```
    ### 交换方法实现
    * 交换方法实现,也就是所谓的`Method Swizzling`。
    * **场景一:**如果你整个项目都做完了,然后产品经理告诉你想统计每一个页面停留的时长。
    * **场景二:**系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有功能。
    
    #### 实践
    **场景一:**统计整个项目每一个页面停留时长。(**解决方法也不是唯一的**)
    
    - 方式一:找到所有的控制器,然后写上:
    
    	```objc
    		-(void)viewWillAppear:(BOOL)animated {
    		   [super viewWillAppear:animated];
    		   [MobClick beginLogPageView:NSStringFromClass([self class])];
    		}
    		-(void)viewWillDisappear:(BOOL)animated {
    		   [super viewWillDisappear:animated];
    		   [MobClick endLogPageView:NSStringFromClass([self class])];
    		}
    	```
    	
    	然后我们一个项目可能有几十个甚至上百个页面需要统计,我们总不可能每个页面都这样写吧。于是乎,就有了**方式二**。
    
    - 方式二:所有的界面都继承于一个基类,然后在基类中写上
    
    	```objc
    		-(void)viewWillAppear:(BOOL)animated {
    		   [super viewWillAppear:animated];
    		   [MobClick beginLogPageView:NSStringFromClass([self class])];
    		}
    		-(void)viewWillDisappear:(BOOL)animated {
    		   [super viewWillDisappear:animated];
    		   [MobClick endLogPageView:NSStringFromClass([self class])];
    		}
    	```
    	
    	可是一开始写项目的时候,并没有使用到继承,所以又papapa地就整个项目的控制器都继承于一个基类,重复地将每一个控制器的继承都该成了我们创建的基类。但是,这样解决真的好么,有可能我们有些界面是继承自`UITableViewController`的,`UICollectionViewController`,等等。那么你就可能会对这些控制器再单独的写上面的代码了。
    	
    	好不容易将整个项目改过来了,然后某天,公司来了一位新人,你告诉他所有的类都要继承自你写的那个基类,新手总是会不经意地犯错误(**也有可能是人家还没有习惯**),有些类忘记继承了,后期排查起来费力费时。那么有没有更好地解决方式呢?**方式三**就可以处理这种问题。
    
    - 方式三:使用`Method Swizzling`实现,给`UIViewController`写一个分类。
    
    	```objc
    		#import "UIViewController+Help.h"
    		#import <objc/runtime.h>
    		@implementation UIViewController (Help)
    		+(void)load {
    		    static dispatch_once_t onceToken;
    		    dispatch_once(&onceToken, ^{
    		        Class class = [self class];
    		        methodSwizzling(class, @selector(viewWillAppear:), @selector(scott_viewWillAppear:));
    		        methodSwizzling(class, @selector(viewWillDisappear:), @selector(scott_viewWillDisappear:));
    		    });
    		}
    		void methodSwizzling(Class class, SEL originSelector, SEL swizzSelector){
    		    Method originMethod = class_getInstanceMethod(class, originSelector);
    		    Method swizzMethod = class_getInstanceMethod(class, swizzSelector);
    		    BOOL isAddMethod = class_addMethod(class, originSelector, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
    		    if (isAddMethod) {
    		        class_replaceMethod(class, swizzSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    		    }else{
    		        method_exchangeImplementations(originMethod, swizzMethod);
    		    }
    		}
    		-(void)scott_viewWillAppear:(BOOL)animated {
    		    [self scott_viewWillAppear:animated];
    		    NSLog(@"调用自定义viewWillAppear");
    		}
    		-(void)scott_viewWillDisappear:(BOOL)animated {
    		    [self scott_viewWillDisappear:animated];
    		    NSLog(@"调用自定义viewWillDissappear");
    		}
    		@end
    	```
    
    ### 字典转模型
    这个单独开一篇给大家讲讲吧。
    
    ## 结束语
    希望通过本文能让大家学习到一些关于Runtime的知识,如果有什么疑问,欢迎大家一起讨论。
  • 相关阅读:
    P4370[Code+#4]组合数问题2【数学,堆】
    牛客挑战赛53G同源数组(Easy Version)【NTT】
    P3577[POI2014]TURTourism【状压dp】
    P1232[NOI2013]树的计数【思维】
    AS3 CookBook学习整理(十一)
    AS3 CookBook学习整理(十五)
    AS3 CookBook学习整理(十四)
    AS3 CookBook学习整理(十二)
    AS3 CookBook学习整理(八)
    AS3 CookBook学习整理(十六)
  • 原文地址:https://www.cnblogs.com/scott-mr/p/8947589.html
Copyright © 2011-2022 走看看