zoukankan      html  css  js  c++  java
  • iOS runtime(一)(runtime 分析理解)

      本文主要是我对学习runtime或其它知识过程中的串联起来写成的,里面也包括了引用外部相关的内容,也包括自己的理解,对runtime做个总结记录的同时把runtime的各个知识点衔接起来,希望能对读者有所帮助,文章有点长,希望读者做好心理准备。在仔细读完这文章后相信大家都对runtime有一定的理解。那么学了runtime有什么用呢,这就是我下一篇文章 iOS runtime (二) (开源库分析) 要写的内容,主要讲解的是一些优秀的开源库是如何运用runtime,提高我们的开发效率的。


    (一)runtime是什么?

      从程序设计语言开始

      Objective-C语言它是扩充C的面向对编程语言。OC语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理,也正因为这样,才让它能在C语言的基本上赋予更多的特性,如:面向对象,runtime 运行时等。

      runtime是什么?

      我认为runtime应该包括了两部分:runtime 系统和runtime接口

      runtime 系统:当我们的iOS程序启动时,其实runtime系统其实已经运行起来了,它相当于为OC语言而出现的操作系统又或者说一个运行库。它会为我们的代码所写的所有继承于NSObject类生成对应的元类(后面后讲),类(类对象),然后编译器和runtime系统相互协作才得以让OC能够实现如:运行时动态生成类、对象以和方法,消息传递机制,消息转发机制,以及Method Swizzling等等

      runtime接口:runtime接口是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的OC代码,底层都是基于runtime接口实现的。

      我们平常开发当中,其实有已经有意无意已经跟runtime打交道。因为与runtime打交道主要有三种方式:

      (1)平常使用OC写代码,当代码中使用到OC的类与方法,runtime系统其实已经在隐式的被使用着。

      (2)使用NSObject的某些方法如:isKindOfClass:、isMemberOfClass:等等的这些接口时,就是显示使用runtime接口提供的功能。

      (3)runtime提供的一套C语言API。


     (二)对象模型

    首先我们要区分两个名词,类对象(即类,这样称乎是因为在OC中类也是一个对象),实例对象(通过某个类创建的具体实例)。

    Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

    typedef struct objc_class *Class;

    查看objc/runtime.h中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;  // 类的版本信息,默认为0
        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;

    在__OBJC2__以前,我们是可以看到结构体的各个成员的,下面简单描述一下与对象模型相关的两个字段:

    isa:每个对象(包括类对象和实例对象)中都会包含它,表明当前的对象是属于哪个类的。实例对象的isa指针指向它的类对象。而类对象的isa指针指向它的元类(metaclass)。后面会介绍到。

    super_class:指向它的父类的指针。

    meta-class是一个类对象的类。

    1、当我们向一个对象发送消息时,会到这个实例对象所属的这个类对象的方法列表中查找方法

    2、而向一个类对象发送消息时,会在这个类对象的meta-class的方法列表中查找。

    每个类对象都会有一个单独的meta-class。meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个的闭环。

    假设我们有如下代码:

    @interface SuperClass : NSObject
    @end
    
    @interface SubClass : SuperClass
    @end

    @implementation SubClass

    - (void)message

    {

    }

    @end

     

    根据上面的说明,对象模型如下图,图摘自网络:

       

     对于NSObject继承体系来说,图中的Root class即为NSObject.

    那么我们知道了OC中的对象模型是这样子的,那苹果为什么这样做,这样做有什么用呢?接下来就到第二部分。


    (三)消息机制

      在iOS中,我们要区分一下方法与函数。方法是属于类或者对象的,而函数则不一定,可以独立于类与对象之外。函数的调用是一步到位,程序直接跳到函数的地址去执行。而方法的调用,它是对类和对象而言,向对应的类或对象发送一条消息,通过runtime系统的消息机制,找到实际要调用的函数地址,然后跳到地址中执行。当我们执行上一部分Subclass类的如下代码时

    [subClassInstance message]

    编译器调用的其实是

    objc_msgSend(subClassInstance, selector) //selector参数,编译器传递的会是sel_registerName("message")。

    如果方法带有参数,那么调的会是

    objc_msgSend(subClassInstance, selector, arg1, arg2, ...)

     还记得我们上一部分讲的对象模型吗?objc_class中有一个属性

    struct objc_method_list **methodLists;这个里面存储的是类的方法链表。链表内每个元数都是一个方法。那么方法又是什么,我们可以在runtime.h中可以看到,每个方法其实也是一个结构体。

    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;  //选择器,Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;  //IMP实际上是一个函数指针,指向方法实现的地址
    }                                    

    就上一部分SubClass中的而然,SubClass中的methodLists中就会有一个方法。method_name的值为:sel_registerName("message"),method_imp则指向messge方法实现的地址。(这个地址是编译链接期已经决定了的,这里我还这样理解,我们可以运行时为类添加新的方法,修改方法的实现把它修改成另一个已经存在的实现(这就是后面会说到的Method swizzling),但是不能在运行时创建一个新的实现。)  

      那么objc_msgSend会帮我们完成动态绑定的所有事情:通过传进来的receiver subClassInstance,及selector找到对应的Method。method中已经关联了方法实现的地址,接下来就把参数作为方法实现的参数传进去进行调用,把调用的反回值作为objc_msgSend的返回值。这就是OC中的消息发送。

    下图演示了这样一个消息的基本框架,以下基本来自官方文档截图及翻译:

    当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个我们在稍后讨论。

      现在先回头看看对象模型中objc_class中的struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存

    上面说的是方法第一次被调用的流程。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法,走上述流程。所以OC的对象模型是消息传递机制中方法查找的基础。

      接下来我们会讲消息的转发机制,如上面所说:如果最后没有定位到selector,则会走消息转发流程。实在没必要重复造轮子,对于这部分详情可以参考消息转发,作者已经写得够好了。但为了不跳链接也能有个好的理解,我下面是对链接内容的删简版,去掉具体代码的实现。

    1、当没有找到SEL的IMP时,resolveInstanceMethod方法就会被调用,它给类利用class_addMethod添加方法的机会。

    2、经过resolveInstanceMethod如果对象还是不能执行到对应的IMP那么就进入下一个阶段,进入到

    流程到了这里,系统给了个将这个SEL转给其他对象的机会。

    3、如果第二步返回的是nil或Self,那就就会进入methodSignatureForSelector这个函数和后面的forwardInvocation:是最后一个寻找IML的机会。这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行。

    真正执行从methodSignatureForSelector:返回的NSMethodSignature。在这个函数里可以将NSInvocation多次转发到多个对象中,这也是这种方式灵活的地方。(forwardingTargetForSelector只能以Selector的形式转向一个对象)。


    (四)Method Swizzling 

       在第三部分,我们讨论了消息机制,基于消息机制,才会有Method Swizzling,Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

      下面摘自Method Swizzling一文。

      例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

    这种情况下,我们就可以使用Method Swizzling,如在代码所示:

    #import <objc/runtime.h>
    
    @implementation UIViewController (Tracking)
    
    + (void)load {
            static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];         
            // When swizzling a class method, use the following:
                        // Class class = object_getClass((id)self);
    
            SEL originalSelector = @selector(viewWillAppear:);
            SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
            BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
            if (didAddMethod) {
                    class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    #pragma mark - Method Swizzling
    - (void)xxx_viewWillAppear:(BOOL)animated {
            [self xxx_viewWillAppear:animated];
        NSLog(@"viewWillAppear: %@", self);
    }
    @end

    在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。

    上面的例子很好地展示了使用method swizzling来一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。在此我们说说使用method swizzling需要注意的一些问题:

    Swizzling应该总是在+load中执行

    在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

    Swizzling应该总是在dispatch_once中执行

    与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

     注意事项

    Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

    1、总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。

    2、避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。

    3、明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。


     (五)成员变量与属性

      成员变量

      此时再次看回objc_class中的struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表

      链表中包含类的所有成员变量,里面的每个元素都是一个Ivar.

      Ivar是表示实例变量的类型,其实际是一个指向objc_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。它表示基地址偏移字节。

    在编译我们的类时,编译器生成了一个 ivar布局。我们对 ivar 的访问就可以通过 对象地址 + ivar偏移字节的方法。

    使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过 对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar。详情参考文章,里面也说明了为什么runtime允许动态添加方法和属性,但是不允许添加成员变量(objc_setAssociatedObject除外的方式)。

    属性

    1、定义:

    objc_property_t:声明的属性的类型,是一个指向objc_property结构体的指针

    typedef struct objc_property *objc_property_t;

    2、操作函数:

    // 获取所有属性
    class_copyPropertyList

    说明:使用class_copyPropertyList并不会获取无@property声明的成员变量

    // 获取属性名
    property_getName
    // 获取属性特性描述字符串
    property_getAttributes
    // 获取所有属性特性
    property_copyAttributeList

    说明:property_getAttributes函数返回objc_property_attribute_t结构体列表,objc_property_attribute_t结构体包含name和value,常用的属性如下:

    属性类型  name值:T value:变化的
    编码类型  name值:C(copy) &(strong) W(weak) 空(assign) 等 value:无
    非/原子性 name值:空(atomic) N(Nonatomic)  value:无
    变量名称  name值:V  value:变化

    成员变量中讲了这么多,如果感觉还有点模糊,没有关系,下面通过一个例子,大家就能明白了,动手做一下会更好理解,也可以熟悉一下相关API。此时新建一个工程。添加一个类MyValueAndProperty如下:

    //MyValueAndProperty.h
    #import <Foundation/Foundation.h>
    
    @interface MyValueAndProperty : NSObject
    {
        BOOL _myValue;
    }
    @property (nonatomic, strong) NSString *myProperty;
    @end
    //MyValueAndProperty.m

    #import "MyValueAndProperty.h"

    @implementation MyValueAndProperty
    @end

    接着就是在ViewController中使用,这里我简单的把代码放在了viewDidLoad中。

    //ViewController.m
    #import
    "ViewController.h" #import <objc/runtime.h> #import "MyValueAndProperty.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; unsigned int propertyCount = 0; MyValueAndProperty *myValueAndProperty = [[MyValueAndProperty alloc] init]; objc_property_t *properties = class_copyPropertyList([myValueAndProperty class], &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { objc_property_t property = properties[i]; //属性名 const char * name = property_getName(property); //属性描述 const char * propertyAttr = property_getAttributes(property); NSLog(@"属性描述为 %s 的 %s ", propertyAttr, name); //属性的特性 unsigned int attrCount = 0; objc_property_attribute_t * attrs = property_copyAttributeList(property, &attrCount); for (unsigned int j = 0; j < attrCount; j ++) { objc_property_attribute_t attr = attrs[j]; const char * name = attr.name; const char * value = attr.value; NSLog(@"属性的描述:%s 值:%s", name, value); } } unsigned int valueCount = 0; Ivar *ivars = class_copyIvarList([myValueAndProperty class], &valueCount); for (unsigned int i = 0; i < valueCount; i++) { Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); const char *type = ivar_getTypeEncoding(ivar); NSLog(@"成员变量名:%s, 类型:%s", name, type); } // Do any additional setup after loading the view, typically from a nib. }

    接着运行程序,输出结果为:

    2016-06-29 11:44:11.326 ValueAndProperty[45184:53842645] 属性描述为 T@"NSString",&,N,V_myProperty 的 myProperty 
    2016-06-29 11:44:11.326 ValueAndProperty[45184:53842645] 属性的描述:T 值:@"NSString"
    2016-06-29 11:44:11.326 ValueAndProperty[45184:53842645] 属性的描述:& 值:
    2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 属性的描述:N 值:
    2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 属性的描述:V 值:_myProperty
    2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 成员变量名:_myValue, 类型:B
    2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 成员变量名:_myProperty, 类型:@"NSString"

    说明:T@"NSString",&,N,V_myProperty 这句是对属性的总体描述,接下来四行是对描述的分解,每个描述特征以逗号作为分隔,所以会有四个描述特征,1、有name为T的value为@"NSString"(即属性为NSString类型), 2、name为&(即编码类型strong),3、name为N(即非原子性),4、name为V,值为_myProperty(即编译器给我们生成一个成员变量_myProperty)。接下来最后打印出来的两行:就是把所有的成员变量打印出来,从打印出来的结果就可以知道确实是有_myProperty成员变量。

    读到这里可能大家会觉得那些&,N,V,是怎么来的,有什么,或产生疑惑。没关系,接下来就会讲解这部分内容。


    (六)类型编码(Type Encodings)

      下面主要讲解一下理论性的东西。首先类型编码是什么回事?类型编码就是编译器把所有的方法(包括它的返回值、参数、等等),属性,编译成一个个具有一定规则的字符串保存起来,我们可以通过runtime相关的API接口获取到

    要获取方法的Type Encodings可使用下面接口:

    const char *method_getTypeEncoding(Method m);

    获取属性的Type Encodings可使用下面接口:

    const char *property_getAttributes(objc_property_t property)

    那么这里规则是怎么样的?规则有点多,相当于语言的言法,这里就不进行截图和讲解,详情查看官方文档Type Encodings章节。想看中文的可以参考文章。重点是要明白这节开头说的加粗那句话。

    到这里,我们就知道第五节的数些字符是怎么来的。


      总结:到这里本文对runtime相关内容的讨论已经完成,当然还有runtime还有很多其它方面的东西没有讲,如:id类型、对象关联等等。这些读者可以自己去研究。接下来就会分析runtime实践中优秀的开源库iOS runtime (二) (开源库分析),以便知道学习了runtime这么多知识点后,究竟可以怎么用,可以用来做些什么事情。

    作者:xianmingchen
    出处:http://www.cnblogs.com/chenxianming/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任权利。
  • 相关阅读:
    Sql Server 2008卸载后再次安装一直报错
    listbox 报错 Cannot have multiple items selected when the SelectionMode is Single.
    Sql Server 2008修改Sa密码
    学习正则表达式
    Sql Server 查询第30条数据到第40条记录数
    Sql Server 复制表
    Sql 常见面试题
    Sql Server 简单查询 异步服务器更新语句
    jQuery stop()用法以及案例展示
    CSS3打造不断旋转的CD封面
  • 原文地址:https://www.cnblogs.com/chenxianming/p/5616073.html
Copyright © 2011-2022 走看看