美文网首页
runtime 2 稍微高级那么一点(建议看本文底部链接原文!)

runtime 2 稍微高级那么一点(建议看本文底部链接原文!)

作者: mkb2 | 来源:发表于2016-09-10 18:37 被阅读97次
    1.执行一个小任务
    - (NSString *)movieTitle
    {
        return @"Futurama: Into the Wild Green Yonder";
    }
    

    在 Objective-C 中 selector 只是一个 C 的数据结构,用于表示一个你想在一个对象上执行的 Objective-C 方法。在 runtime 中的定义像这样…

    typedef struct objc_selector  *SEL;
    

    ** 使用**的时候是这样子

    SEL aSel = @selector(movieTitle);
    

    Message(消息)

    [target getMovieTitleForObject:obj];
    

    消息是方括号 ‘[]’ 中的那部分,由你要向其发送消息的对象(target),你想要在上面执行的方法(method)还有你发送的参数(arguments)组成。 Objective-C 的消息和 C 函数调用是不同的。事实上,你向一个对象发送消息并不意味着它会执行它。Object(对象)会检查消息的发送者,基于这点再决定是执行一个不同的方法还是转发消息到另一个目标对象上。


    Class 如果你查看一个类的runtime信息,你会看到这个…

    typedef struct objc_class *Class;
    typedef struct objc_object {
        Class isa;
    } *id;
    

    前者是类的结构体
    后者是对象的结构体


    objc_object 只有一个指向类的isa 指针,就是我们说的术语 “isa pointer”(isa 指针)。这个isa 指针是当你向对象发送消息时,Objective-C Runtime检查一个对象并且查看它的类是什么然后开始查看它是否响应这些selectors 所需要的一切。最后我么看到了id 指针。默认情况下id 指针除了告诉我们它们是 Objective-C 对象外没有其他用了。当你有一个id 指针,然后你就可以问这个对象是什么类的,看看它是否响应一个方法,等等,然后你就可以在知道这个指针指向的是什么对象后执行更多的操作了


    在 Objective-C 中的一个类实现看起来像这样:

    @interface MyClass : NSObject {
        // vars
        NSInteger counter;
    }
    // methods
    -(void)doFoo;
    @end
     
    但是 runtime 不只要追踪这些
     
    #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_*protocols          OBJC2_UNAVAILABLE;
    #endif
    

    1.我们可以看到,一个类有其父类的引用,它的名字,实例变量,方法,缓存还有它遵循的协议。runtime 在响应类或实例的方法时需要这些信息。

    1. 那么 Class 定义的是对象还是对象本身?它是如何实现的 (译注:读者需要区分 Class 和 class 是不同的,正如 Nil 和 nil 的用途是不同的)?

    之前我说过 Objective-C 类也是对象,runtime 通过创建 Meta Classes 来处理这些。当你发送一个消息像这样 [NSObject alloc] 你正在向类对象发送一个消息,这个类对象需要是 MetaClass 的实例,MetaClass 也是 root meta class 的实例。当你说继承自 NSObject 时,你的类指向 NSObject 作为自己的 superclass。然而,所有的 meta class 指向 root metaclass 作为自己的 superclass。所有的 meta class 只是简单的有一个自己响应的方法列表。所以当你向一个类对象发送消息如 [NSObject alloc],然后实际上 objc_msgSend() 会检查 meta class 看看它是否响应这个方法,如果他找到了一个方法,就在这个 Class 对象上执行(译注:class 是一个实例对象的类型,Class 是一个类(class)的类型。对于完全的 OO 来说,类也是个对象,类是类类型(MetaClass)的实例,所以类的类型描述就是 meta class)。

    类和类类型之间的关系

    从你开始 Cocoa 开发时,那些教程就说如继承自 NSObject 然后开始写一些代码,你享受了很多继承自苹果的类所带来的便利。有一件事你从未意识到的是你的对象被设置为使用 Objective-C 的 runtime。当我们为我们的类的一个实例分配了内存,像这样…

    MyObject *object = [[MyObject alloc] init];
    

    最先执行的消息是 +alloc。如果你查看下文档, 它说“新的实例对象的 isa 实例变量被初始化为指向一个数据结构,那个数据结构描述了这个类;其他的实例变量被初始化为 0。”所以继承自苹果的类不仅仅是继承了一些重要的属性,也继承了能在内存中轻松分配内存的能力和在内存中创建满足 runtime 期望的对象结构(设置 isa 指针指向我们的类)。

    那么 Class Cache 是什么?(objc_cache *cache)

    当 Objective-C runtime 沿着一个对象的 isa 指针检查时,它会发现一个对象实现了许多的方法。然而你可能只调用其中一小部分的方法,也没有意义每次检查时搜索这个类的分发表(dispatch table)中的所有 selector。所以这个类实现了一个缓存,当你搜索一个类的分发表,并找到合适的 selector 后,就会把它放进缓存中。所以当 objc_msgSend() 在一个类中查找 selector 时会先查找类缓存。有个理论是,当你在一个类上调用了一个消息,你很可能之后还会调用它。所以如果我们考虑到这点,就意味着当我们有个子类继承自 NSObject 叫做 MyObject 并且运行了以下的代码

    MyObject *obj = [[MyObject alloc] init]; 
     
    @implementation MyObject
    - (id)init {
        if(self = [super init]) {
            [self setVarA:@”blah”];    
        }
        return self;
    }
    @end
    

    发生了以下的事:

    (1) [MyObject alloc] 首先被执行。MyObject 没有实现 alloc 方法,所以我们不能在这个类中找到 +alloc 方法,然后沿着 superclass 指针会指向 NSObject。

    (2) 我们询问 NSObject 是否响应 +alloc 方法,它可以。+alloc 检查消息的接收者类,是 MyObject,然后分配一块和我们的类同样大小的内存空间,并初始化它的 isa 指针指向 MyObject 类,我们现在有了一个实例对象,最终把类对象的 +alloc 方法加入 NSObject 的类缓存(class cache)中(lastly we put +alloc in NSObject's class cache for the class object )。

    (3) 到现在为止,我们发送了一个类消息,但是现在我们发送一个实例消息,只是简单的调用 -init 或者我们设计的初始化方法。当然,我们的类会响应这个方法,所以 -(id)init 加入到缓存中。(译注:要是 MyObject 实现了 init 方法,就会把 init 方法加入到 MyObject 的 class cache 中,要是没有实现,只是因为继承才有了这个方法,init 方法还是会加入到 NSObject 的 class cache 中)。

    (4) 然后 self = [super init] 被调用。super 是个 magic keyword,指向对象的父类,所以我们得到了 NSObject 并调用它的的 init 方法。这样可以确保 OOP(面相对象编程) 的继承功能正常,这个方法可以正确的初始化父类的变量,之后你(在子类中)可以初始化自己的变量,如果需要可以覆盖父类的方法。在 NSObject 的例子中,没什么重要的要做,但并不总是这样。有时要做些重要的初始化。

    运行时能干啥?

    1.Objective-C 消息转发
    2.动态添加方法
    3.给分类添加属性
    4.获取类中的各种详情,属性名称,方法名称列表等
    5.替换方法
    6.字典转模型

    1. Objective-C 消息分发

    判断这个类,有没有这个方法,如果没有,实现forwardingTargetForSelector这个方法,然后将这个消息让其他的对象处理,例如,现在重写nsstring,这个类,他不能执行jump这个方法,那让谁来执行?forwardingTargetForSelector返回来的对象"hello world"执行。然后就改成大写!!!
    感觉就是CA这个类实例化了一个对象,然后我们通过performSelector:@selector(jump),然后重写- (id)forwardingTargetForSelector:(SEL)aSelector这个方法,对消息进行处理,~

    @interface CA : NSObject
     -(void)f;
     
     @end
     
     @implementation CA
     
     - (id)forwardingTargetForSelector:(SEL)aSelector
     {
         if (aSelector == @selector(uppercaseString))
         {
             return@"hello world";
         }
     }
    
     a = [[CA alloc]init];
     NSLog(@"%@",[a performSelector:@selector(uppercaseString)];
    

    打印如下 该测试代码的输出为:HELLO WORLD

    在 Objective-C 中向一个不知道如何响应这个方法的对象发送消息是完全合法的(甚至可能是一种潜在的设计决定)。苹果的文档中给出的一个原因是模拟多继 承,Objective-C 不是原生支持的,或者你可能只是想抽象你的设计并且隐藏幕后处理这些消息的其他对象/类。这一点是 runtime 非常需要的。它是这样做的 1. Runtime 检查了你的类和所有父类的 class cache 和分发表,但是没找到指定的方法。2. Objective_C 的 Runtime 会在你的类上调用 + (BOOL) resolveInstanceMethod:(SEL)aSEL。 这就给了你一个机会去提供一个方法实现并且告诉 runtime 你已经解析了这个方法,如果它开始查找,这回就会找到这个方法。你可以像这样实现…定义一个函数…

    void fooMethod(id obj, SEL _cmd)
    { 
        NSLog(@"Doing Foo");
    }
    

    然后你可以像这样使用 class_addMethod() 解析它…

    +(BOOL)resolveInstanceMethod:(SEL)aSEL
    {
        if(aSEL == @selector(doFoo:))
        {
                class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
                return YES;
        }
        return [super resolveInstanceMethod];
    }
    

    在 class_addMethod() 最后一部分的 "v@:" 是方法的返回和参数类型。你可以在 Runtime Guide 的 Type Encoding 章节看到完整介绍。 3. Runtime 然后调用 – (id)forwardingTargetForSelector:(SEL)aSelector。这样做是为了给你一次机会(因为我们不能解析这个方法 (参见上面的 #2))引导 Objective-C runtime 到另一个可以响应这个消息的对象上,在花费昂贵的处理过程调用 – (void)forwardInvocation:(NSInvocation *)anInvocation 之前调用这个方法也是更好的。你可以像这样实现

    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if(aSelector == @selector(mysteriousMethod:))
        {        
            return alternateObject;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    显然你不想从这个方法直接返回 self,否则可能会产生一个死循环。 4. Runtime 最后一次会尝试在目标对象上调用 – (void)forwardInvocation:(NSInvocation *)anInvocation。如果你从没看过 NSInvocation,它是 Objective-C 消息的对象形式。一旦你有了一个 NSInvocation 你可以改变这个消息的一切,包括目标对象,selector 和参数。所以你可以这样做…

    -(void)forwardInvocation:(NSInvocation *)invocation
    {  
        SEL invSEL = invocation.selector;    
        if([altObject respondsToSelector:invSEL]) {        
            [invocation invokeWithTarget:altObject];    
        } else {        
            [self doesNotRecognizeSelector:invSEL];    
        }
    }
    
    

    如果你继承自 NSObject,默认它的 – (void)forwardInvocation:(NSInvocation *)anInvocation 实现只是简单的调用 -doesNotRecognizeSelector:,你可以在最后一次机会里覆盖这个方法去做一些事情。(译注:对这块内容有兴趣的同学可以参见:http://www.cnblogs.com/biosli/p/NSObjectinherit2.html

    2. Non Fragile ivars(Modern Runtime)(非脆弱的 ivar)
    3.Objective-C 关联对象

    最近在 Mac OS X 10.6 雪豹 中新引入了关联引用。Objective-C 不能动态的添加一些属性到对象上,和其他的一些原生支持这点的语言不一样。所以之前你都不得不努力为未来要增加的变量预留好空间。在 Mac OS X 10.6 中,Objective-C 的 Runtime 已经原生的支持这个功能了。如果我们想向一个已有的类添加变量,看起来像这样…

    #import  //Cocoa
    #include  //objc runtime api’s 
     
    @interface NSView (CustomAdditions)
    @property(retain) NSImage *customImage;
    @end 
     
    @implementation NSView (CustomAdditions) 
     
    static char img_key; //has a unique address (identifier)
     
    - (NSImage *)customImage
    {    
        return objc_getAssociatedObject(self,&img_key);
    }
     
    - (void)setCustomImage:(NSImage *)image
    {    
        objc_setAssociatedObject(self, &img_key,image, OBJC_ASSOCIATION_RETAIN);
    } 
     
    @end
     
    objc_setAssociatedObject() 的选项,你可以在 runtime.h 文件中找到。
     
    /* Associated Object support. */ 
     
    /* objc_setAssociatedObject() options */
    enum {    
        OBJC_ASSOCIATION_ASSIGN = 0,    
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,    
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,    
        OBJC_ASSOCIATION_RETAIN = 01401,    
        OBJC_ASSOCIATION_COPY = 01403
    };
    

    这些和 @property 语法中的选项意思一样。


    运行时的作用
    1.发送消息
    2.交换方法
    3.动态添加方法
    4.给分类添加属性
    5.字典转模型
    6.消息分发

    3.动态添加方法
    • 开发使用场景:如果一个类的方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
    • 经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
    • 简单使用fly举例说明,应该就是在.h文件不去申明有这个函数,可以避免加载到内存中消耗资源,在.m文件中,我们可以去添加这个函数,并且使用resolveInstanceMethod函数,动态的添加fly这个函数
    //RTDog.m文件
    void fly()
    {
        NSLog(@"自定义的方法fly");
    }
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        
        if (sel == @selector(fly)) {
            // 动态添加eat方法
            
            // 第一个参数:给哪个类添加方法
            // 第二个参数:添加方法的方法编号
            // 第三个参数:添加方法的函数实现(函数地址)
            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
            class_addMethod(self, @selector(fly), fly, "v@:");
            
        }
        
        return [super resolveInstanceMethod:sel];
    }
    
    //控制器中调用方法
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //动态添加方法
        RTDog *dog = [[RTDog alloc] init];
        [dog performSelector:@selector(fly)];
    }
    
    

    期间不会报错,但是会有很多的警告,就是说-- 你也没给我这个方法的声明啊~,但是调用的时候是正常的

    4.给分类添加属性

    原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间


    众所周知,分类只能添加方法,不能添加属性。如果在分类中声明了一个name属性,只不过是添加了setName:,name两个方法,并没有添加属性。
    下面说说如何通过运行是给分类添加属性(面试的时候总问这个事,字典转模型也是通过这个)

    #import "RTDog.h"
    
    @interface RTDog (Property)
    @property(copy,nonatomic)NSString *name;
    @end
    
    //声明一个属性,会告诉系统,有set和get方法,外边调用才有使,内部通过关联,直接设置给他赋值
    - (void)setName:(NSString *)name{
        objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (NSString *)name{
        return objc_getAssociatedObject(self, @"name");
    }
    

    • 字典转模型的方式二:Runtime
    - 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
    - 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
    

    先看看这几个属性

    
    typedef struct objc_class *Class;
    
    struct objc_class {
    
      Class isa; // 指向metaclass
    
       
    
      Class super_class ; // 指向其父类
    
      const char *name ; // 类名
    
      long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
    
      long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    
      long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
    
      struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址
    
      struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
    
      struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
    
      struct objc_protocol_list *protocols; // 存储该类遵守的协议
    
        }
    
    + (instancetype)modelWithDic:(NSDictionary *)dic{
        
        //1.思路 通过运行是,获取模型中所有的属性,然后我们通过KVC赋值
        id obj = [[self alloc] init];
        
        //1.1 通过运行时,获取具体的属性
        /**
         *  class_copyIvarList  获取类中所有的属性
         * Ivar 成员属性的意思
         * 第一个参数:表示获取哪个类中的成员属性
         * 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
         * 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到
         *
         */
        /* 类似下面这种写法
         
         Ivar ivar;
         Ivar ivar1;
         Ivar ivar2;
         // 定义一个ivar的数组a
         Ivar a[] = {ivar,ivar1,ivar2};
         
         // 用一个Ivar *指针指向数组第一个元素
         Ivar *ivarList = a;
         
         // 根据指针访问数组第一个元素
         ivarList[0];
         
         */
        unsigned int count;
        Ivar *ivarList = class_copyIvarList(self, &count);
        
        for (int i = 0; i<count; i++) {
            Ivar var = ivarList[i];
            //获取属性名字
            NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(var)];
            NSLog(@"属性名字是什么?%@",propertyName);
            NSString *key = [propertyName substringFromIndex:1];
            id value = dic[key];
            
            //二级转换,如果字典中还是字典的话,应该在将他们转换成模型
            if ([value isKindOfClass:[NSDictionary class]]) {
                // 字典转模型
                // 获取模型的类对象,调用modelWithDict
                // 模型的类名已知,就是成员属性的类型
                
                
                //获取成员属性类型
                NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(var)];
                //裁剪字符串
                NSRange range = [type rangeOfString:@"\""];
                
                type = [type substringFromIndex:range.location + range.length];
                
                range = [type rangeOfString:@"\""];
                
                // 裁剪到哪个角标,不包括当前角标
                type = [type substringToIndex:range.location];
                
                //根据字符串,生成类对象
                Class modelClass = NSClassFromString(type);
                if (modelClass) {
                    value = [modelClass modelWithDic:value];
                }
            }
            
            //判断如果是数组的话,我们也一点要记得将数组中的字典装换
            if ([value isKindOfClass:[NSArray class]]) {
                //判断数组的字典,对应的类中有没有实现字典转模型的协议
                if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                    // 转换成id类型,就能调用任何对象的方法
                    id idSelf = self;
                    NSString *type = [idSelf arrayContainModelClass][key];
                     //生成模型
                    Class modelClass = NSClassFromString(type);
                    NSMutableArray *arr = [NSMutableArray array];
                    for (NSDictionary *modelDic in value){
                        id model = [modelClass modelWithDic:modelDic];
                        [arr addObject:model];
                    }
                }
            }
            
            //给模型赋值KVC
            if (value) {
                [obj setValue:value forKey:key];
            }
        }
        
        return obj;
    }
    

    亲测,结果如下:

    运行结果成功

    记住了哈,凡是用到了copy的,一定要记得free();

    要记得free

    本文大量的事例,基本都是参考了一下几篇文章,自己理解的有可能偏颇,更建议直接参看原文

    让你快速上手Runtime
    理解 Objective-C Runtime
    Objective C运行时(runtime)技术的几个要点总结
    runtime 运行时机制 完全解读http://my.oschina.net/panyong/blog/298631)
    类和对象的处理
    [http://www.cocoachina.com/ios/20141105/10134.html](Objective-C Runtime 运行时之二:成员变量与属性)
    Objective-C Runtime 运行时之三:方法与消息
    本文的demo在仓库,如果有需要可以看看,但是不要当真,里面有很多的bug,见谅

    相关文章

      网友评论

          本文标题:runtime 2 稍微高级那么一点(建议看本文底部链接原文!)

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