美文网首页
iOS Runtime实用详解(一)

iOS Runtime实用详解(一)

作者: handsome5 | 来源:发表于2017-07-28 14:33 被阅读65次

    基本概念

    • 了解 C/C++编译
      • C/C++编译就是将C/C++的代码映射到相应的机器码,编译过程包括几个部分分别是编译,汇编和链接(具体可以去看下汇编语言基础知识)。在函数的编译中,C++和C语言的编译方式是不同的,C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_(不同的编译器有不同的实现)。 在C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。这个重命名的过程是通过一个特殊的算法来实现的,称为名字编码(Name Mangling)。但函数的调用都是在编译的时候会决定调用哪个函数。
    • Object-C是根据C语言所衍生出来的语言,继承了C语言的特性,是扩充C的面向对象编程语言,它与C有必然的联系,但是在编译的时候又有本质的区别,对于oc方法(函数),属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数调用,这就是我们所了解的运行时系统 (runtime system),这里有几个概念必须需要掌握:
      • 动态特性
        Objective-C 具有相当多的动态特性,表现在三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时才会做一些事情。
        • 动态类型:及运行时再决定对象的类型。这类动态类型在日常应用中非常常见。简单说就是id类型。实际上静态类型因为其固定性和可预知性而使用的非常广泛,静态类型是强类型,而动态类型属于弱类型。运行时决定接受者。
        • 动态绑定 :基于动态类型,在某个实例对象被确定后,其类型就被确定了。该对象的属性和响应的消息也被完全确定,这就是动态绑定。
        • 动态加载 根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,让程序在运行时添加代码块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。
      • 消息机制
        • 消息发送是 OC底层的底层操作(引入 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。删除掉一些强制转换语句,可以看到调用方法本质就是发消息,[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。简单点说函数(方法)的调用就发送消息,但是Xcode5.0开始,苹果就不建议开发者直接使用消息机制(5.0后苹果这时引用runtime,runtime对消息机制作了封装,内部就是runtime的底层实现)。但是我们一定要自己实现消息机制发送,怎么发呢?我们在编译设置Build Setting的搜索输入框,输入msg,把Yes设置成No即可发送


          Snip20170728_16.png
         //Person *p  = [[Person alloc] init]; 底层发送是这样
        Person *p  = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
        p = objc_msgSend(p, sel_registerName("init"));
        //把p替换下,
        Person *p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        // p对象发送方法
       objc_msgSend(p, sel_registerName("eat"));
    

    接下来我们去验证下oc底层发送,如何验证请看下面


    Snip20170728_18.png

    关掉工程 command + S,找到消息验证目录,输入clang -rewrite-objc main.m 生成main.cpp


    Snip20170728_19.png
    最后打开main.cpp 把main.cpp拖到底部,如图
    Snip20170728_21.png

    看到的Person跟我们发送的完全一样,其实从这里我们可以发现objc_msgSend的函数调用过程:

    第一步:检测这个selector是不是要忽略的;
    第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉
    第三步:
        调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,
      如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,
    如果仍找不到则继续通过super_class向上查找知道metaclass;
        调用类方法时,首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,
    如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体
    第四步:如果前三步都找不到方法则进入动态方法解析
    消息动态解析具体流程
    第一步:通过resolveInstanceMethod:方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;
    第二步:这步会进入forwardingTargetForSelector:方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三步;
    第三步:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;
    第四步:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash。
    
    • isa指针


      来自网络获取图片.png
    • 消息动态解析


      动态解析流程图(图片来自网络).png
    • runtime
      • runtime 也就是所谓的“运行时”,是一个机制,oc的底层实现,runtime是一套底层的c语言API(包括很多强大实用的c语言类型,c语言函数),需要引入< <objc/runtime.h>;
    • runtime的运用
      • 拦截系统自带的方法调用(Method Swizzling黑魔法),利用runtime 将系统的方法实现和我们自定义的方法实现进行交换
      • runtime动态添加方法
      • 动态创建一个类((比如KVO的底层实现))
      • 实现字典的模型和自动转换
      • 实现NSCoding的自动归档和接档
      • ...
        //获取类方法
        class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
         //获取实例方法
        class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
       案例实现:
    #import "ViewController.h"
    #import <objc/runtime.h>
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    + (void)load {
       
        Method CustomString = class_getClassMethod([NSURL class],@selector(TestURLWithString:));
        Method URLString = class_getClassMethod([NSURL class], @selector(URLWithString:));
        method_exchangeImplementations(URLString, CustomString);
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/中文"];
    }
    @end
    
    #import "NSURL+url.h"
    #import <objc/runtime.h>
    
    @implementation NSURL (url)
    #pragma mark - 这里运用了runtime
    + (instancetype)TestURLWithString:(NSString *)str
    {
        NSURL *url = [NSURL TestURLWithString:str];
        if (!url) {
            NSLog(@"url为空");
        }
        return url;
    }
    @end
    
    • runtime动态添加方法
    #import "Person.h"
    #import <objc/runtime.h>
    
    @implementation Person
    
    //当这个类被调用类一个没有实现的类方法!就会来这里
    //+ (BOOL)resolveClassMethod:(SEL)sel
    //{
    //    
    //}
    
    //当这个类被调用类一个没有实现的对象方法!就会来这里
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
    
        NSLog(@"%@",NSStringFromSelector(sel));
        //动态的添加方法
        if (sel == sel_registerName("eat")) {
            
            /**
             class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, const char *types)
             1.Class 类类型
             2.SEL name 方法编号
             3.IMP implementation的简称,方法的实现,就是一个函数指针,指向一个实现
             4.type 返回类型 command + shift + 0 快速打开文档,搜索class_addMethod里面Parameters types 可以看到一张表,这里v表示无返回值(void)。
             */
            class_addMethod([Person class], sel, (IMP)eat, "v");
        }
        return [super resolveInstanceMethod:sel];
    }
    //记住任何一个函数都有两个参数,一个self,一个cmd,两个隐试参数
    void eat(id self, SEL _cmd) {
        NSLog(@"哥们调用了%@的%@方法",self,NSStringFromSelector(_cmd));
        NSLog(@"哥们吃了");
    }
    
    ////记住任何一个函数都有两个参数,一个self,一个cmd,两个隐试参数,带了个参数
    //void eat(id self, SEL _cmd,id objc) {
    //    
    //    NSLog(@"哥们调用了%@的%@方法%@",self,NSStringFromSelector(_cmd),objc);
    //
    //    
    //}
    
    @end
    
    #import "ViewController.h"
    #import <objc/runtime.h>
    #import "Person.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Person *p  = [[Person alloc] init];
        
        [p performSelector:@selector(eat)];
        
        //带参数的测试 懒加载方法
        //[p performSelector:@selector(eat) withObject:@"测试"];
    }
    @end
    
    • runtime动态创建一个类((KVO的底层实现))
    // KVO的底层实现原理
    // KVO监听的是setter方法
    #import "ViewController.h"
    #import "Person.h"
    #import "NSObject+KVO.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong)Person *p;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
       [super viewDidLoad];
       
       Person *p = [[Person alloc] init];
       
       //KVO的基本写法
       [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        _p = p;
       
       //自定义 kvo底层
       [p test_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
      
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
       NSLog(@"观察到了%@的%@属性的变化了%@",object,keyPath,change);
    
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
       // KVO监听的是setter方法,
       //一个类的类型只能通过isa指针来看
       /*如果name是成员变量的话,KVO就不能监听了*/
       _p.name = @"test";//简单的说底部实现,就是利用runtime创建了个子类对象 
    }
    @end
    
    #import "NSObject+KVO.h"
    #import <objc/message.h>
    
    @implementation NSObject (KVO)
    
    - (void)test_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
    {
       /**
        1.自定义NSTestKVO子类
        2.重写set方法,在内部恢复父类的做法,通知观察者
        3.修改self的isa指针!!恢复指向自定义的NSTestKVO子类!
        */
       
       //动态生成一个类
       NSString *oldClassName = NSStringFromClass([self class]);
       NSString *newClassName = [@"NSTestKVO" stringByAppendingString:oldClassName];
       const char *newName = [newClassName UTF8String];
       
       /**定义一个类
        
        1.继承那个类
        2.类的名称
        */
       
       Class MyClass = objc_allocateClassPair([self class], newName, 0);
       
       //重写setName方法,这里写死了的
       class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
       
       //注册该类
       objc_registerClassPair(MyClass);
       
       //修改self的isa指针,指向自定义的子类MyClass
       object_setClass(self, MyClass);
       
       //将观察者保存到当前对象
       objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    
    void setName(id self, SEL _cmd,NSString *newName){
       
       //保存当前类型
       Class class = [self class];
    
       //改变isa指针
       object_setClass(self, class_getSuperclass(class));
       
       //调用父类的set方法
       objc_msgSend(self, @selector(setName:),newName);
       
       
       //拿出观察者
       id objc = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
       
       //通知观察者, 发送消息,注意设置msg
       objc_msgSend(objc,@selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,nil,nil);
       
       
       //改回子类类型,否则没法继续监听
       object_setClass(self, class);
    }
    @end
    
    • runtime 实现字典的模型和自动转换
      • 案例
    {
      "age" : "<null>",
      "money" : "1000",
      "cat" : {
        "name" : "Persia",
        "price" : "500",
        "fish" : {
          "name" : "鱼",
          "weight" : 50
        }
      },
    
     "students" : [
                      {
                      "name1" : "苦逼的码农",
                      "price1" : 20.8,
                      "publisher1" : "清华大学出版社"
                      }
                     ],
        
      "books" : [
        {
          "name" : "C语言程序设计",
          "price" : 20.8,
          "publisher" : "清华大学出版社",
          "bookUsers" : [
                         {
                       "name1" : "苦逼的码农",
                       "price1" : 20.8,
                       "publisher1" : "清华大学出版社"
                         }
                      ]
     
        },
        {
          "name" : "乔布斯传",
          "price" : 50.2,
          "publisher" : "苹果出版社",
          "bookUsers" : [
                         {
                         "name1" : "乔布斯传",
                         "price1" : 50.2,
                         "publisher1" : "苹果出版社"
                         }
                        ]
        }
      ],
      "name" : "Tom",
      "height" : "181"
    
    }
    
    #import <Foundation/Foundation.h>
    
    @interface NSObject (JSONExtension)
    
    - (void)setDict:(NSDictionary *)dict;
    + (instancetype )objectWithDict:(NSDictionary *)dict;
    // 告诉数组中都是什么类型的模型对象
    -(NSString *)arrayObjectClassWithKey:(NSString *)keyword;
    
    @end
    
    #import "NSObject+JSONExtension.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (JSONExtension)
    
    - (void)setDict:(NSDictionary *)dict {
        
        Class c = self.class;
        while (c &&c != [NSObject class]) {
            
            unsigned int outCount = 0;
            Ivar *ivars = class_copyIvarList(c, &outCount);
            for (int i = 0; i < outCount; i++) {
                Ivar ivar = ivars[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                
                // 成员变量名转为属性名(去掉下划线 _ )
                key = [key substringFromIndex:1];
                // 取出字典的值
                id value = dict[key];
                // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
                if (value == nil || [value isEqual:[NSNull class]]) continue;
                
                // 获得成员变量的类型
                NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
                
                // 如果属性是对象类型
                NSRange range = [type rangeOfString:@"@"];
                if (range.location != NSNotFound) {
                    // 那么截取对象的名字(比如@"Dog",截取为Dog)
                    type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                    
    
                    
                    // 排除系统的对象类型
                    if (![type hasPrefix:@"NS"]) {
                        // 将对象名转换为对象的类型,将新的对象字典转模型(递归)
                        Class class = NSClassFromString(type);
                        value = [class objectWithDict:value];
                        
                    }else if ([type isEqualToString:@"NSArray"]) {
                        
                        // 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
                        NSArray *array = (NSArray *)value;
                        
                        NSLog(@"key====:%@",key);
                        
                        NSMutableArray *mArray = [NSMutableArray array];
                        
                        // 获取到每个模型的类型
                        id class ;
                        if ([self respondsToSelector:@selector(arrayObjectClassWithKey:)]) {
                            
                            NSString *classStr = [self arrayObjectClassWithKey:key];
                            class = NSClassFromString(classStr);
                        }
                       
                        else {
                            
                            NSLog(@"数组内模型是未知类型");
                            return;
                        }
                        // 将数组中的所有模型进行字典转模型
                        for (int i = 0; i < array.count; i++) {
                            
                            
                            [mArray addObject:[class objectWithDict:value[i]]];
                        }
                        
                        value = mArray;
                    }
                }
                
                // 将字典中的值设置到模型上
                [self setValue:value forKeyPath:key];
            }
            free(ivars);
            c = [c superclass];
        }
    }
    + (instancetype )objectWithDict:(NSDictionary *)dict {
        NSObject *obj = [[self alloc]init];
        [obj setDict:dict];
        return obj;
    }
    @end
    
    #import "ViewController.h"
    #import "Person.h"
    #import "User.h"
    #import "NSObject+JSONExtension.h"
    #import "Book.h"
    #import "BookUsers.h"
    #import "Student.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self json];
    }
    
    /// 字典转模型demo
    - (void)json {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"Model2.json" ofType:nil];
        NSData *jsonData = [NSData dataWithContentsOfFile:path];
        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
        
        User *user = [User objectWithDict:json];
        Book *book= user.books[0];
        Student *englistBook = user.students[0];
    
        //NSLog(@"%@",book.name);
    }
    
    model 
    
    #import <Foundation/Foundation.h>
    #import "Cat.h"
    
    @interface User : NSObject
    @property (nonatomic,copy) NSString *name;
    @property (nonatomic,assign) double height;
    @property (nonatomic,assign) int age;
    
    // 属性是一个对象
    @property (nonatomic,strong) Cat*cat;
    // 属性是一个数组
    @property (nonatomic,strong) NSArray *books;
    
    // 属性是一个数组
    @property (nonatomic,strong) NSArray *students;
    
    @end
    #import "User.h"
    
    
    
    @implementation User
    
     //返回数组中都是什么类型的模型对象
    //- (NSString *)arrayObjectClass {
    //   return @"Book";
    //}
    
    - (NSString *)arrayObjectClassWithKey:(NSString *)key {
        if ([key isEqualToString:@"books"]) {
            return @"Book";
        }else
        {
            return @"Student";
        }
    }
    @end
    
    #cat model
    #import <Foundation/Foundation.h>
    #import "Fish.h"
    
    @interface Cat : NSObject
    @property (nonatomic,copy) NSString *name;
    @property (nonatomic,assign) double price;
    // 属性是一个对象
    @property (nonatomic,strong) Fish *fish;
    
    @end
    
    #import "Cat.h"
    
    @implementation Cat
    
    @end
    
    #Book model
    #import <Foundation/Foundation.h>
    #import "Book.h"
    
    @interface Book : NSObject
    @property (nonatomic,copy) NSString *name;
    @property (nonatomic,assign) double price;
    @property (nonatomic,copy) NSString *publisher;
    @property (nonatomic,strong)NSArray *bookUsers;
    @end
    #import "Book.h"
    
    @implementation Book
    //返回数组中都是什么类型的模型对象
    - (NSString *)arrayObjectClass {
        return @"BookUsers";
    }
    @end
    
    #BookUsers model
    #import <Foundation/Foundation.h>
    
    @interface BookUsers : NSObject
    
    
    @property (nonatomic,copy) NSString *name1;
    @property (nonatomic,assign) double price1;
    @property (nonatomic,copy) NSString *publisher1;
    @end
    
    #import "BookUsers.h"
    
    @implementation BookUsers
    
    @end
    

    本文参考文献

    (Runtime基本原理及Demo)http://www.jianshu.com/p/e28f6b279f5f#
    ([iOS] runtime 的使用场景--实战篇)http://www.jianshu.com/p/07b6c4a40a90

    相关文章

      网友评论

          本文标题:iOS Runtime实用详解(一)

          本文链接:https://www.haomeiwen.com/subject/vljxlxtx.html