Objective-C Runtime 一小时入门教程(上)

Objective-C Runtime 一小时入门教程(上)



一、前言


如果你没有Objective-C基础,请学习了基础的iOS开发再来,这个1小时是给有一定iOS基础的童鞋的。如果你是大牛或者你感觉Objective-C Runtime太简单不用1小时学习的,也请您绕道,这或许只是我的私人笔记了。


请跟着教程“一步步来”,请不要大概地扫两眼就说看不懂——以这种态度写成什么样你也看不懂。这是1小时入门教程,请不要试图在1分钟内入门!


二、本文目标


1小时让你知道什么是Objective-C Runtime,并对它有一定的基本了解,可以在开发过程中运用自如。


三、Objective-C Runtime到底是什么东西?


简而言之,Objective-C Runtime是一个将C语言转化为面向对象语言的扩展。

我们将C++和Objective进行对比,虽然C++和Objective-C都是在C的基础上加入面向对象的特性扩充而成的程序设计语言,但二者实现的机制差异很大。C++是基于静态类型,而Objective-C是基于动态运行时类型。也就是说用C++编写的程序编译时就直接编译成了可令机器读懂的机器语言;用Objective-C编写的程序不能直接编译成可令机器读懂的机器语言,而是在程序运行的时候,通过Runtime把程序转为可令机器读懂的机器语言。Runtime是Objective不可缺少的重要一部分。


四、Objective-C的元素认知


4.1 id和Class


打开/Public Headers/objc.h文件可以看到如下定义:


#if !OBJC_TYPES_DEFINED

/// An opaque type that represents an Objective-C class.

typedef struct objc_class *Class;

 

/// Represents an instance of a class.

struct objc_object {

    Class isa  OBJC_ISA_AVAILABILITY;

};

 

/// A pointer to an instance of a class.

typedef struct objc_object *id;

#endif


Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。


打开/Public Headers/runtime.h文件

objc_class的定义如下:


typedef struct objc_class *Class;

struct objc_class {

Class isa                                 OBJC_ISA_AVAILABILITY; // metaclass

#if !__OBJC2__

Class super_class                         OBJC2_UNAVAILABLE; // 父类

const char *name                          OBJC2_UNAVAILABLE; // 类名

long version                              OBJC2_UNAVAILABLE; // 类的版本信息,默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取

long info                                 OBJC2_UNAVAILABLE; // 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;

long instance_size                        OBJC2_UNAVAILABLE; // 该类的实例变量大小(包括从父类继承下来的实例变量)

struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE; // 该类的成员变量地址列表

struct objc_method_list **methodLists     OBJC2_UNAVAILABLE; // 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;

struct objc_cache *cache                  OBJC2_UNAVAILABLE; // 缓存最近使用的方法地址,用于提升效率;

struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE; // 存储该类声明遵守的协议的列表

#endif

}

/* Use `Class` instead of `struct objc_class *` */


由以上代码可见,类与对象的区别就是类比对象多了很多特征成员,类也可以当做一个objc_object来对待,也就是说类和对象都是对象,分别称作类对象(class object)和实例对象(instance object),这样我们就可以区别对象和类了。


isa:objc_object(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类)其中存放着普通成员变量与动态方法(“-”开头的方法);此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法)。


super_class: 指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为nil。


类与对象的继承层次关系如图(图片源自网络):


Objective-C Runtime 一小时入门教程(上)_第1张图片


所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身。


4.2 SEL


SEL是selector在Objective-C中的表示类型。selector可以理解为区别方法的ID。

typedef struct objc_selector *SEL;


objc_selector的定义如下:


struct objc_selector {

    char *name;                       OBJC2_UNAVAILABLE;// 名称

    char *types;                      OBJC2_UNAVAILABLE;// 类型

};


name和types都是char类型。


4.3 IMP


终于到IMP了,它在objc.h中得定义如下:


typedef id (*IMP)(id, SEL, ...);


IMP是“implementation”的缩写,它是由编译器生成的一个函数指针。当你发起一个消息后(下文介绍),这个函数指针决定了最终执行哪段代码。


4.4 Method


Method代表类中的某个方法的类型。

typedef struct objc_method *Method;


objc_method的定义如下:


struct objc_method {

    SEL method_name                   OBJC2_UNAVAILABLE; // 方法名

    char *method_types                OBJC2_UNAVAILABLE; // 方法类型

    IMP method_imp                    OBJC2_UNAVAILABLE; // 方法实现

}


方法名method_name类型为SEL,上文提到过。

方法类型method_types是一个char指针,存储着方法的参数类型和返回值类型。

方法实现method_imp的类型为IMP,上文提到过。


4.5 Ivar


Ivar代表类中实例变量的类型

typedef struct objc_ivar *Ivar;


objc_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

}


4.6 objc_property_t


objc_property_t是属性,它的定义如下:

typedef struct objc_property *objc_property_t;


objc_property是内置的类型,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等。它的定义如下:


typedef struct {

    const char *name; // 名称

    const char *value;  // 值(通常是空的)

} objc_property_attribute_t;


4.7 Cache


Catch的定义如下:


typedef struct objc_cache *Cache


objc_cache的定义如下:


struct objc_cache {

    unsigned int mask                   OBJC2_UNAVAILABLE;

    unsigned int occupied               OBJC2_UNAVAILABLE;

    Method buckets[1]                   OBJC2_UNAVAILABLE;

};


mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置。

occupied: 实际占用cache buckets的总数。

buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

objc_msgSend(下文讲解)每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。


4.8 Catagory


这个就是我们平时所说的类别了,很熟悉吧。它可以动态的为已存在的类添加新的方法。

它的定义如下:

typedef struct objc_category *Category;


objc_category的定义如下:

struct objc_category {

    char *category_name                           OBJC2_UNAVAILABLE; // 类别名称

    char *class_name                              OBJC2_UNAVAILABLE; // 类名

    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 实例方法列表

    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 类方法列表

    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 协议列表

}


因为是入门,以上就列举这些吧!


五、Objective-C的消息传递


5.1 基本消息传递


在面向对象编程中,对象调用方法叫做发送消息。在编译时,程序的源代码就会从对象发送消息转换成Runtime的objc_msgSend函数调用。

例如某实例变量receiver实现某一个方法oneMethod


[receiver oneMethod];


Runtime会将其转成类似这样的代码


objc_msgSend(receiver, selector);


具体会转换成什么代码呢?

Runtime会根据类型自动转换成下列某一个函数:

objc_msgSend:普通的消息都会通过该函数发送

objc_msgSend_stret:消息中有数据结构作为返回值(不是简单值)时,通过此函数发送和接收返回值

objc_msgSendSuper:和objc_msgSend类似,这里把消息发送给父类的实例

objc_msgSendSuper_stret:和objc_msgSend_stret类似,这里把消息发送给父类的实例并接收返回值

当消息被发送到实例对象时,是如图所示处理的(图片源自网络):


Objective-C Runtime 一小时入门教程(上)_第2张图片


objc_msgSend函数的调用过程:


  • 第一步:检测这个selector是不是要忽略的。

  • 第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉。

  • 第三步:

  • 1.调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根class;

  • 2.当我们调用某个某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根metaclass;

  • 第四部:前三部都找不到就会进入动态方法解析(看下文)。


5.2 消息动态解析


动态解析流程图(图片来自网络):


Objective-C Runtime 一小时入门教程(上)_第3张图片


请参照图片品味以下步骤(实例请看下文《6.6 苍老师的唱歌篇》):


  • 第一步:通过resolveInstanceMethod:方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;

  • 第二步:这步会进入forwardingTargetForSelector:方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三部;

  • 第三部:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;

  • 第四部:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash。


到这里大家可能晕乎乎的,下面看实战篇吧!苍老师必须让你懂!


六、Runtime实战


请大家放心,以下所有实战篇,在最后都会分享Demo给大家!


6.1 苍老师问好篇


苍老师见到我们广大的粉丝们,第一反应当然是:大家好!


#if TARGET_IPHONE_SIMULATOR

#import <objc/objc-runtime.h>

#else

#import <objc/runtime.h>

#import <objc/message.h>

#endif

 

// 自定义一个方法

void sayFunction(id self, SEL _cmd, id some) {

    NSLog(@"%@岁的%@说:%@", object_getIvar(self, class_getInstanceVariable([self class], "_age")),[self valueForKey:@"name"],some);

}

int main(int argc, const char * argv[]) {

    @autoreleasepool {

 

        // 动态创建对象 创建一个Person 继承自 NSObject类

        Class People = objc_allocateClassPair([NSObject class], "Person", 0);

 

        // 为该类添加NSString *_name成员变量

        class_addIvar(People, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));

        // 为该类添加int _age成员变量

        class_addIvar(People, "_age", sizeof(int), sizeof(int), @encode(int));

 

        // 注册方法名为say的方法

        SEL s = sel_registerName("say:");

        // 为该类增加名为say的方法

        class_addMethod(People, s, (IMP)sayFunction, "v@:@");

 

        // 注册该类

        objc_registerClassPair(People);

 

        // 创建一个类的实例

        id peopleInstance = [[People alloc] init];

 

        // KVC 动态改变 对象peopleInstance 中的实例变量

        [peopleInstance setValue:@"苍老师" forKey:@"name"];

 

        // 从类中获取成员变量Ivar

        Ivar ageIvar = class_getInstanceVariable(People, "_age");

        // 为peopleInstance的成员变量赋值

        object_setIvar(peopleInstance, ageIvar, @18);

 

        // 调用 peopleInstance 对象中的 s 方法选择器对于的方法

        // objc_msgSend(peopleInstance, s, @"大家好!"); // 这样写也可以,请看我博客说明

        ((void (*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好");

 

        peopleInstance = nil; //当People类或者它的子类的实例还存在,则不能调用objc_disposeClassPair这个方法;因此这里要先销毁实例对象后才能销毁类;

 

        // 销毁类

        objc_disposeClassPair(People);

 

    }

    return 0;

}


最后的结果是:18岁的苍老师说:大家好!

在使用


objc_msgSend(peopleInstance, s, @"大家好!");


默认会出现以下错误:

objc_msgSend()报错Too many arguments to function call ,expected 0,have3

直接通过objc_msgSend(self, setter, value)是报错,说参数过多。

请这样解决:

Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改为 NO

当然你也可以这样写(推荐):


((void (*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好");


强制转换objc_msgSend函数类型为带三个参数且返回值为void函数,然后才能传三个参数。

此实战内容是,动态创建一个类,并创建成员变量和方法,最后赋值成员变量并发送消息。其中成员变量的赋值使用了KVC和object_setIvar函数两种方式,这些东西大家举一反三就可以了。


你可能感兴趣的:(Objective-C,Runtime)