文章目录
笔记参考
什么是Runtime
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
(就是用C和编译语言写的面向对象特性和Smalltalk式的消息传递机制)
Runtime的作用
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是
OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。
然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
-
苹果系统和UNIX系统
baiios是基于Darwin(unix的分支之一)并非原始的unix系统,因此ios简单du说既是unix的一部分又不zhi同于unix的一个系统。
iOS是由苹果公司为daoiPhone开发的操作系统。它主要是给iPhone、iPodtouch、iPad以及AppleTV使用。就像其基于的MacOSX操作系统一样,它也是以Darwin为基础的。 -
Runtime消息传递
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:
首先,通过obj的isa指针找到它的 class ;
在 class 的 method list 找 foo ;
如果 class 中没到 foo,继续往它的 superclass 中找 ;
一旦找到 foo 这个函数,就去执行它的实现IMP 。
但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。
- OBJC2_UNAVAILABLE
OBJC2.0中,这些东西将被删除。
isa指针指向
类也是对象
类结构体继承自对象结构体,所以类也是对象。isa指针,也是从object结构体继承来的isa。
类对象(objc_class)
- 关于类对象
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。
类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata)
该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。
- 类对象的isa指针
类对象的isa指针指向的我们称之为元类(metaclass),
元类中保存了创建类对象以及类方法所需的所有信息
元类(Meta Class)
- 关于元类
元类中保存了创建类对象以及类方法所需的所有信息
元类(Meta Class)是一个类对象的类。 - 元类的作用
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
(一个对象可以使用自己类以及它父类和超类的方法,所以它元类的指针要指向超类)
类与元类
类中存放实例方法
元类中存放类方法
isa和superClass的指向图
objc_msgSend
objc_msgSend(void /* id self, SEL op, ... */ )
NSArray *databaseRecords = [NSArray array];
转换为C++形式
NSArray *databaseRecords = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
参数——SEL(objc_selector)
typedef struct objc_selector *SEL;
objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:
其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。
- selector命名规则
selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:
同一个类,selector不能重复
不同的类,selector可以重复
Method(objc_method)
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp
}
参数IMP
typedef id (*IMP)(id, SEL, ...);
就是指向最终实现程序的内存地址的指针。
在iOS的Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
类缓存(objc_cache)
当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。
这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。
消息转发机制和三次拯救
消息转发机制
原文连接
这个原语函数参数可变,第一个参数填入消息的接收者,第二个参数就是消息"选择子",后面跟着可选的消息的参数。有了这些参数,objc_msgSend就可以通过接收者的isa指针,到其类对象的方法列表中以选择子的名称为"键"寻找对应的方法。若找到对应的方法,则转到其实现代码执行,否则继续从父类中寻找,如果到根类还是无法找到对应的方法,说明该接收者对象响应该消息,那么就会触发消息转发机制,给开发者最后一次挽救程序crash的机会。
三次拯救
en……这个是学runtime的时候写的,没写完,详见这个🔗
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会,也就是三次拯救。(如果没有找到对应的方法,程序会崩溃,三次拯救是最后的三次机会)
动态方法解析
首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
(添加了函数并返回YES,随便写啥会报错)
代码
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
可以看到虽然没有实现foo:这个函数,但是我们通过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。从打印结果看,成功实现了。
如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector。
备用接收者
如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
@end
[self class]和[super class]
self和super
1. 概念性区别
- self是类,super是预编译指令
- self调用自己方法,super调用父类方法
- [self class]和[super class]输出是一样的
2. 调用方法区别
- 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
- 当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法。
3. 底层实现原理
[self setAge:]
self调用方法实现原理
当使用 self 调用时,会使用 objc_msgSend
函数: id objc_msgSend(id theReceiver, SEL theSelector, …)。
第一个参数是消息接收者,
第二个参数是调用的具体类方法的 selector
第三个参数是 selector 方法的可变参数。
上述例子中的体现
编译器会替换成调用 objc_msgSend 的函数调用,
其中 theReceiver 是 self,theSelector 是 @selector(setAge:),这个 selector 是从当前 self 的 class 的方法列表开始找的 setAge,
当找到后把对应的 selector 传递过去。
super调用实现原理
当使用 super 调用时,会使用 objc_msgSendSuper
函数:id objc_msgSendSuper(struct objc_super *super, SEL op, …)
第一个参数是个objc_super的结构体,
第二个参数还是类似上面的类方法的selector,
上述例子中的体现
当编译器遇到 [super setAge:] 时,执行顺序:
- 构建 objc_super 的结构体,此时这个结构体的第一个成员变量 receiver 就是 子类,和 self 相同。
而第二个成员变量 superClass 就是指父类调用objc_msgSendSuper 的方法,将这个结构体和 setAge 的 sel 传递过去。
- 函数里面在做的事情类似这样:从 objc_super 结构体指向的 superClass 的方法列表开始找 setAge 的 selector,找到后再以 objc_super->receiver 去调用这个 selector
4. [super class]的接收者
关于super
一个指向objc_super数据结构的指针,
传递标识消息发送到的上下文的值,包括接收消息的类的实例变量和开始寻找方法实现接口的超类。
objc_super结构体
删了一些对这部分没用的内容
struct objc_super {
//结构体的第一个成员变量——接收者receiver
__unsafe_unretained _Nonnull id receiver;
//而第二个成员变量 ——superClass 就是指父类调用objc_msgSendSuper 的方法
__unsafe_unretained _Nonnull Class super_class;
};
5. 一个题
@interface student : NSObject
- (void)speak { //方法实现写在这里
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
NSLog(@"123");
}
打印结果
解析:
如上所述,由于方法接收者都是self(objc_super的成员变量接收者receiver也是self),所以打印结果都是self的类student。
class方法
类方法的class返回类本身
+ (Class)class {
return self;
}
实例方法的class方法
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
isa_t
源码
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t(uintptr_t value) : bits(value) { }
位域
什么是位域
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
信息的存取一般以字节为单位。实际上,有时存储一个信息不必用一个或多个字节,例如,“真”或“假”用0或1表示,只需1位即可。在计算机用于过程控制、参数检测或数据通信领域时,控制信息往往只占一个字节中的一个或几个二进制位,常常在一个字节中放几个信息。
优缺点
位段(或称“位域”,Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的
好处:
-
可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
-
位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。
缺点:
- 位域不能是静态类型。不能使用&对位域做取地址运算,因此不存在位域的指针,编译器通常不支持位域的引用(reference)。
- 其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。
声明
在C语言中,位段的声明和结构(struct)类似,但它的成员是一个或多个位的字段,这些不同长度的字段实际储存在一个或多个整型变量中。在声明时,位段成员必须是整形或枚举类型(通常是无符号类型),且在成员名的后面是一个冒号和一个整数,整数规定了成员所占用的位数。
ISA_BITFIELD
源码:
if arm64
ARM处理器本身定位于嵌入式平台,应付轻量级、目的单一明确的程序,现在应用在移动设备上正是得心应手。(用在真机上)
# define ISA_BITFIELD
uintptr_t nonpointer : 1; 是否使用优化的指针 \
uintptr_t has_assoc : 1; 是否有关联对象 \
uintptr_t has_cxx_dtor : 1; 是否有析构函数\
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \存储类地址
uintptr_t magic : 6; 判断初始化完成\
uintptr_t weakly_referenced : 1; // 对象是否曾经或正在被弱引用,如果没有,可以快速释放内存
uintptr_t deallocating : 1; 是否正在被释放 \
uintptr_t has_sidetable_rc : 1; 是否用散列表记录引用计数\
uintptr_t extra_rc : 8 引用计数数量,调用方法查看计数时会显示此值的数目+1.
elif x86_64
x86定位于桌面和服务器,这些平台上很多应用是计算密集型的,比如多媒体编辑、科研计算、模拟等等。(虚拟机上)
# define ISA_BITFIELD
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
__arm__64下的结构
关于nonpointer
class的结构
isa指针介绍
class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容