Runtime

为什么说Objective-C是动态语言

  • 概念
    动态语言 是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化,类型的检查是在运行时做的,优点为方便阅读,清晰明了,缺点为不方便调试。所谓的动态类型语言,意思就是类型的检查是在运行时做的

    静态类型语言 的类型判断是在运行前判断(如编译阶段),比如C#、java就是静态类型语言,主要优点在于其结构非常规范,便于调试,方便类型安全

  • Objective-C是动态语言

    • Objective-C 可以通过Runtime 这个运行时机制,在运行时动态的添加变量、方法、类等

    • Objective-C具有相当多的动态特性,基本的,也是经常被提到和用到的有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)

    • object-c类的类型和数据变量的类型都是在运行是确定的,而不是在编译时- 确定。例如:多态特性,我们可以使用父类对象来指向子类对象,并且可以用来调用子类的方法

所以说 Objective-C是动态语言

Runtime 数据结构

Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的

    1. 在Runtime 之前先来看下 结构体

结构体简单用法

struct Books
{
    char  title[50];
    char  author[50];
    char  subject[100];
    int   book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};


struct Student {
    char *name;
    int num;
    int age;
    char group;
    float score;
    
} stu1 = {"Tom",12,16,'A',1233.9};


int main(int argc, const char * argv[]) {
    
  
    //*********结构体*********
    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);

    
    //*********结构体指针*********
    //pstu指向结构体变量stu1的地址
    struct Student *pstu = &stu1;
    
    //指针引用结构体变量成员的方式是: (*指针变量名).成员名
    //注意,*p 两边的括号不可省略,因为成员运算符“.”的优先级高于指针运算符“*”,所以如果 *p 两边的括号省略的话,那么 *p.num 就等价于 *(p.num) 了。
    printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", (*pstu).name, (*pstu).num, (*pstu).age, (*pstu).group, (*pstu).score);
    
    //->”是“指向结构体成员运算符
    printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", pstu->name, pstu->num, pstu->age, pstu->group, pstu->score);


    return 0;
}

    1. Class 的在OC底层中的定义

typedef struct objc_class *Class; 很明显是一个 结构体类型的指针

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */


/// An opaque type that represents an Objective-C class. 不透明的类型,表示一个Objective-C类
typedef struct objc_class *Class;
    1. id 的在OC底层中的定义

typedef struct objc_object *id; 很明显也是一个 结构体类型的指针

/// Represents an instance of a class.表示类的实例 
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class. 指向类实例的指针
typedef struct objc_object *id;

可以发现:
每个objective-c对象都有一个隐藏的 数据结构,这个数据结构是Objective-C对象的第一个成员变量,它就是isa指针。

  • 这个指针指向哪呢?
    指向Class isa,指向的是这个 对象所对应的类(Class,其实类也是一个对象)

    1. NSObject 的在OC底层中的定义
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

不考虑@interface关键字在编译时的作用,可以把NSObject更接近C语言结构表示为:


//第一步变形
struct NSObject {
    Class isa ;
}

//第二步变形:根据第1点,Class 在底层的定义,变形
struct NSObject{
     struct objc_class *isa
}

到这一步为止,所以还得研究 结构体 objc_class

    1. 分析结构体 objc_class
struct objc_class {
    
    //isa 指针
    //对象也有一个isa指针,指向Class isa,指向的是这个 对象所对应的类(Class,其实类也是一个对象)
    //指向metaclass,
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    //指向其父类,如果这个类是根类,则为NULL。
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    
    //类名
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;

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

    //一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    long info                                                OBJC2_UNAVAILABLE;

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

    //用于存储每个成员变量的地址
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;

    // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;

    //指向最近使用的方法的指针,用于提升效率;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;

    //存储该类遵守的协议
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */


/// An opaque type that represents an Objective-C class. 不透明的类型,表示一个Objective-C类
typedef struct objc_class *Class;

附加解释:

Class isa:指向metaclass,也就是静态的Class。对象obj也有一个isa指针,指向Class isa,指向的是这个 对象所对应的类(Class,其实类也是一个对象)
这是解释类方法也就是实例方法,因为类也是对象

metaclass的isa指向根metaclass,如果该metaclass是根metaclass则指向自身;
metaclass 的super_class指向父metaclass
如果该metaclass是根metaclass则指向该metaclass对应的类;

重点就是isa指针,Objective-C对类对象和实例对象中的isa所指向的类结构做了不同的命名,类对象中isa指向类结构称为metaclass,它存储类的static类成员变量与static类成员方法(+开头的方法);实例对象中的isa指向类结构称作class,它存储类的普通成员变量与普通成员方法(-开头的方法)。

    1. SEL 的在OC底层中的定义

也是一个结构体指针

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

Rumtime 消息机制

提到消息机制,不能不提到 SEL,SEL又叫选择器,是表示一个方法的selector的指针,他的定义typedef struct objc_selector *SEL; 正因为是结构体指针,所以也有人说SEL是一个对象

比如
创建一Person个类 继承NSObject、
创建一Son个类 继承Person、
创建一个Student 继承NSObject,分别实现如下代码

- (instancetype)init
{
    self = [super init];
    if (self) {
        SEL sel1 = @selector(eat);
        NSLog(@"sel : %p", sel1);
    }
    return self;
}
- (void)eat {
    NSLog(@"吃了");
    
}

输出如下:

2019-01-19 17:55:05.029309+0800 RuntimeDemo[3106:91635] sel : 0x10552fa4f

2019-01-19 17:55:05.029515+0800 RuntimeDemo[3106:91635] sel : 0x10552fa4f
2019-01-19 17:55:05.029619+0800 RuntimeDemo[3106:91635] sel : 0x10552fa4f

2019-01-19 17:55:05.029757+0800 RuntimeDemo[3106:91635] sel : 0x10552fa4f

Son 继承Person,所以会调用父类的init 方法,会打印两次
会发现有的SEL,打印的地址相同

由此证明,不同类的相同SEL是同一个对象。

所以在 Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致 Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差

不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP

  • 问题一: 不同对象调用相同的方法怎么找到SEL?

一般的调用方法如下

- (void) setUpTest01 {
    Person *p = [[Person alloc] init];
    [p eat];
}

也可以这么些

- (void)setUpTest02 {
    
    Person *p = [Person alloc];
    
    p = [p init];
    
    //[p eat];
    [p performSelector:@selector(eat)];
}

Runtime进行方法调用本质上是发送消息,发送消息是怎么发送的呢?

通过Runtime 就会发现,在底层使用了 objc_msgSend 函数

//消息机制
- (void)setUpTest03 {
    //Person *p = [Person alloc];
    //类方法 其实类也是一个对象,oc 表示类 类型,swift 表示元 类型
    Person *p = objc_msgSend([Person class], @selector(alloc));
    
    //p = [p init];
    p = objc_msgSend(p, @selector(init));

    //objc_msgSend(p,@selector(eat))    //编译报错
    /**
         1. target -> build setting ,搜索 msg
         2. Enable Strict Cheaking of objc_msgSend Calls 设置为No
         、设置以后就不会编译错误了,因为oc 不推荐使用底层去实现
     */
 
    objc_msgSend(p,@selector(eat));
    //objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>) //... 可扩展参数
}
  • 解读 objc_msgSend
- self,调用当前方法的对象。
- _cmd,当前被调用方法的SEL
objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>) //... 可扩展参数

继续分解,有些函数不懂没关系

- (void)setUpTest04 {
    //Person *p = [Person alloc];
    //类方法 其实类也是一个对象,oc 表示类 类型,swift 表示元 类型
    //Person *p = objc_msgSend(objc_getClass("Person"), @selector(alloc));
    Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

    //p = objc_msgSend(p, @selector(init));
    p = objc_msgSend(p, sel_registerName("init"));
    
   // objc_msgSend(p,@selector(eat));
    objc_msgSend(p,sel_registerName("eat"));
}

工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。
因此,如果我们想到这个方法集合中查找某个方法时,只需要去 找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度 上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么 SEL仅仅是函数名了。

  • IMP

    IMP实际上是一个函数指针,指向方法实现的首地址

    /// A pointer to the function of a method implementation. 
    #if !OBJC_OLD_DISPATCH_PROTOTYPES
    typedef void (*IMP)(void /* id, SEL, ... */ ); 
    #else
    typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
    #endif
    
    • 解读参数
      第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),
      第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

    • SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的 IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针 了

  • Method

struct objc_method {

     //方法名
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    
    //方法的实现
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}   

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL

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

KVO的底层实现

  • 1.KVO是基于runtime机制实现的
  • 2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
  • 3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
  • 4.每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
  • 5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
 
     1. command + Q 关闭工程项目
     2. 在次运行项目,在Person 创建对象之后加上一个断点
     3. 在 控制台可以看出 _p 目录下 NSObject ——> isa 又个isa 指针,指向(class)Person
     4. 调用 [_p FF_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil] 之后
     5. 在 控制台可以看出 _p 目录下 NSObject ——> isa 又个isa 指针,指向(class)FFKVOPerson
     (self->_p->isa:NSKVONotifying_Person),
     6. 监听属性的值age 是否被修改,其实是在NSKVONotifying_Person类里面重写了set 方法,一旦改变,就是通知父类做一系列操作
     7. 当我 用成员变量_name 时候,无法监听,所以证明只能观察重写的 set 方法
     

代码如下:


#import "NSObject+KVO.h"
#import <objc/message.h>


@implementation NSObject (KVO)

- (void)FF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
    NSLog(@"%@",self);
    
    //1. 动态添加一个类
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [@"FFKVO" stringByAppendingString:oldClassName];
    const char *newClass = [newClassName UTF8String];
    
    //定义一个类
    Class myClass = objc_allocateClassPair([self class], newClass, 0);
    
    //重写setAge(添加一个set方法)
    class_addMethod(myClass, @selector(setAge:), (IMP)setAge, "v@:");
    
    //注册这个类
    objc_registerClassPair(myClass);
    
    //改变isa 指针的指向
    //NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;
    object_setClass(self, myClass);

    //关联对象
    objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    
}

//默认参数!!
void setAge(id self,SEL _cmd ,int age) {
    
    //1保存一下自己
    id class = [self class];
    
    //2.让自己指向父类
    object_setClass(self, class_getSuperclass([self class]));
   
    NSLog(@"修改完毕 %d",age);
    
   
    //3.
    objc_msgSend(self,@selector(setAge:),age);
   
    //取出观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
    
    NSDictionary *dic = @{@"new": [NSNumber numberWithInt:age]};
    
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"age",self,dic,nil);
    
    //4.改回类型 针对 3
    object_setClass(self, class);
    
    
}


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 727评论 0 2
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,170评论 0 7
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 一、Runtime简介 Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    林安530阅读 1,059评论 0 2
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 913评论 0 6