为什么说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 代码,底层都是基于它来实现的
- 在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;
}
- 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;
- 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,其实类也是一个对象)- 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
- 分析结构体 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,它存储类的普通成员变量与普通成员方法(-开头的方法)。
- 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);
}