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这么多知识点后,究竟可以怎么用,可以用来做些什么事情。

 

posted on 2016-06-25 11:42  chenxianming  阅读(811)  评论(0编辑  收藏  举报

导航