1、Objective-C 简介
Objective-C 是 C 语言的超集,兼容标准 C 语言的大部分语法;在 C 的基础上增加了面向对象的特性,是一门面向对象的语言。
1.1、 面向对象的编程功能
Objective-C 在 C 的基础上增加了面向对象的特性:如封装、继承、多态、消息传递等!
1.1.1、 封装
封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
- 优点1:隐藏内部实现细节,设置访问权限,提高了数据的安全性。
- 优点2:任何出入的数据都要流经接口,通过重写
-set
方法可以起到过滤数据的作用。
1.1.2、 继承
指一个对象直接使用另一对象的属性和方法。Objective-C 的继承具有单继承的特点(不可以多重继承),每个子类都只能有一个直接父类。
当子类继承父类时,子类可以得到父类全部的成员变量与方法。
- 优点:抽取重复代码、建立联系;
- 缺点:耦合性强;
1.1.3、 多态
不同的接收器调用同一方法可以获得不同的结果。实现多态的条件:
- 必须存在继承关系;
- 子类重写父类的方法;
- 父类声明的变量指向子类对象;
Objective-C 指针类型的变量有两个:编译时类型和运行时类型。编译时类型由声明变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现多态。
@interface FatherModel : NSObject
- (void)eat;
@end
@interface BigChildModel : FatherModel
@end
@interface SmallChildModel : FatherModel
@end
{
//此时编译时类型和运行时类型不一致,发生多态
FatherModel *bigModel = [[BigChildModel alloc]init];
FatherModel *smalModel = [[SmallChildModel alloc]init];
[bigModel eat];
[smalModel eat];
//父类 FatherModel 的 -eat方法表现的形式为多种形态,这就叫多态
}
1.1.4、 消息传递
在OOP中,消息传递是指一种在对象之间发送和接收消息的通信模式;对象通过彼此传递消息协同工作。
1.2、 Objective-C 的运行时
Objective-C 既具有与许多面向对象的语言类似的静态语言的特性,又有动态语言的特性。它将许多处理类型、消息和方法决议的工作转移到运行程序的时候进行,而不是在编译或者链接时处理。
Objective-C 主要的动态特性有:
- 动态类型:在运行时决定对象的类型。如声明变量为 id 类型,编译器无法知道其类型,系统运行时决定对象的类型;
- 动态绑定:在程序运行时(而不是编译时)将消息与方法对应起来的处理过程。基于动态类型,在实例对象的类型被确定后,响应的消息也被完全确定。 动态绑定实现了 OOP 的多态性,能够在不影响已有代码的情况下将新对象和代码添加到程序中,从而降低程序之间的耦合;
- 动态加载:根据需求加载 Objective-C 代码,而无需在启动程序时加载它的所有组件。苹果公司提供了
bundle
机制,使用NSBundle
类管理资源包bundle
。 - 对象消息传递:接收器和接收器中被调用的方法在运行时决定!
- 动态方法决议:以动态方式实现方法。如
@dynamic
指令告知编译器与该属性关联的方法以动态方式实现;NSObject
的类方法+resolveClassMethod:
与+resolveInstanceMethod:
以动态方式实现由选择器指定的实例和类方法。 - 对象消息转发机制:当对象收到与其方法不匹配的消息时,通过消息转发机制可以使对象执行用户预先定义的处理过程;将消息发送给能够做出回应的其它接收器;将所有无法识别的消息都发送给同一接收器;既不执行处理过程也不使程序崩溃,默默的吞下消息。
2、Objective-C 的类与对象
在Objective-C中,对象是广义的概念,类也是对象,所以严谨的说法应该是类对象和实例对象。既然实例所属的类称为类对象,那类对象有所属的类吗?有,称之为元类(Metaclass)。
2.1、类
在Objective-C中我们接触最多的也是最基本的就是类与实例,类使用关键字 Class
表示!当我们在创建类或者实例时,是否考虑过类和实例到底是什么?
Objective-C 类本质是一个结构:在 objc-runtime-new.mm
文件中找到Class
是一个objc_class
类型的结构指针,
///Class是一个 objc_class 类型的结构指针:
typedef struct objc_class *Class;
/// objc_object 主要持有一个 isa 指针
struct objc_object {
private:
isa_t isa; ///指向所属的类
public:
Class ISA();// ISA() 假设不是tagged pointer对象
Class getIsa();// getIsa() 允许这是tagged pointer对象
Class changeIsa(Class newCls);//更改现有对象的 isa,如果这是一个新对象,则使用initIsa()来提高性能。
}
// objc_class 的结构成员
struct objc_class : objc_object {
Class super_class; // 指向当前类的父类
cache_t *cache; // 用于缓存指针和 vtable,加速方法的调用
class_data_bits_t bits;
class_rw_t *data() {//存储类的方法、属性、遵循的协议等信息
return bits.data();
}
};
struct class_data_bits_t {
uintptr_t bits;//值是上面的 FAST_ 标志。
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
//ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
struct class_rw_t {
// Symbolication 知道此结构的布局。
uint32_t flags; //运行时使用的一些位标识,比如: `CLS_CLASS(0x1L)`表示该类为普通类,`CLS_META (0x2L)`表示该类为元类;
uint32_t version; // 类的版本信息(默认为0)
const class_ro_t *ro;
method_array_t methods; // 方法
property_array_t properties; //属性
protocol_array_t protocols; // 协议
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
//class_ro_t 存储了该类在编译时就已经确定的属性、方法以及遵循的协议
struct class_ro_t {
uint32_t flags;//标志
uint32_t instanceStart;
uint32_t instanceSize; // 该类的实例变量大小
const uint8_t * ivarLayout;
const char * name;//类的名称
method_list_t * baseMethodList;//基础方法列表
protocol_list_t * baseProtocols;//基础协议列表
const ivar_list_t * ivars;//变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//基础属性列表
//获取基础方法列表
method_list_t *baseMethods() const {
return baseMethodList;
}
};
分析主要的结构成员:
-
isa
指针指向所属的类,是和Class
同类型的objc_class
结构指针;
实例对象的isa
指向所属的类,实例方法存储在所属的类中;
类的isa
指向所属的元类,元类中存储着类方法,当访问类方法时通过该类的isa
指针从元类中寻找方法对应的函数指针; -
super_class
指针指向该类的父类,如果是根类(如NSObject
或NSProxy
), 则super_class
为NULL
; -
cache
缓存最近调用的方法:对象调用方法的过程是个查找methodLists
的过程,如果每次调用都去查找一遍,效率会非常低。 -
ivars
成员变量链表,存储每一个实例变量的地址;是结构体class_ro_t
的成员,在编译时已经确定下来, 运行时不能再改变该内存布局;由于class_rw_t
没有变量表,所以不能在分类增加实例变量; -
methodLists
存放方法的链表,当该类为普通类时,存储实例方法;如果是元类则存储的类方法;
Runtime对于调用过的方法,会以映射的方式保存在cache
中,系统调用方法时首先在cache
中查找,没有找到时才会去methodLists
中遍历获取指定的方法;这对于多次调用的方法,大大的提升了效率! -
protocols
存放协议的链表;
2.1.1、结构成员isa
指针的使用
a:利用 isa
指针获取其所属的类
根类 NSObject
的API提供了用来获取指定对象所属的类的一些方法:
/* @return 返回自身
*/
+ (Class)class {
return self;
}
/* @return 返回该实例所属的类
*/
- (Class)class {
return object_getClass(self);
}
/* 获取 obj 的 isa 指针
* @param obj 指定对象
* 如果是实例对象,那么该实例对象的 isa 指向其所属的类;
* 如果是一个类,那么该类的 isa 指向其所属的元类;
* @note 如果 obj 为 nil ,则返回 nil
*/
Class object_getClass(id obj){
if (obj) return obj->getIsa();
else return Nil;
}
示例1、测试 NSObject
的class
方法
@interface SuperModel : NSObject
@end
@interface Model : SuperModel
@end
{
Model *objct = [[Model alloc] init];
id object_class = objct.class;
id model_class = Model.class;
NSLog(@"objct.class == %@ : %p",object_class,object_class);
NSLog(@"Model.class == %@ : %p",model_class,model_class);
}
/* 打印数据
objct.class == Model : 0x10071f7d8
Model.class == Model : 0x10071f7d8
*/
分析打印数据:实例方法-calss
返回该实例所属的类;类方法 +calss
返回该类自身。
再次获取对象 objct
与类 Model
的 isa
指针:
{
id object_isa = object_getClass(objct);
id model_class_isa = object_getClass(Model);
NSLog(@"objct -> isa == %@ : %p",object_isa,object_isa);
NSLog(@"Model -> isa == %@ : %p",model_class_isa,model_class_isa);
NSLog(@"Model_ISA_isMeta === %d",class_isMetaClass(model_class_isa));//判断一个类是否是元类
}
/* 打印数据
objct -> isa == Model : 0x10071f7d8
Model -> isa == Model : 0x10071f7b0
Model_ISA_isMeta === 1
*/
分析打印数据:实例对象 objct
的 isa
指针指向类Model
;类Model
的 isa
指针指向了元类。
那么元类的 isa
指针指向何处?元类到底是什么?我们留作疑问,稍后在讲解!
b:利用isa
指针判断是否是某个类
根类NSObject
的API提供用来判断指定对象是否是某个类的方法:
/* 判断该类的元类是否是 cls
* @note object_getClass((id)self) 获取该类的元类
*/
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
/* 判断该实例对象所属的类是否是 cls
* @note [self class] 获取该实例所属的类
*/
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
/* 判断该类的元类或者父元类 是否是 cls
* @note for 循环遍历 superclass ,直到根元类时tcls = nil,for 循环停止
*/
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
/* 判断该实例对象所属的类 或者 父类 是否是 cls
* @note for 循环遍历 superclass ,直到根类时tcls = NULL,for 循环停止
*/
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
示例2、
{
//注意:根元类的superclass指向根类
[NSObject isKindOfClass:NSObject.class];// YES
[NSObject isMemberOfClass:NSObject.class];//NO
[SuperModel isKindOfClass:SuperModel.class];//NO
[SuperModel isMemberOfClass:SuperModel.class];//NO
[SuperModel.new isKindOfClass:SuperModel.class];//YES
[SuperModel.new isMemberOfClass:SuperModel.class];//YES
}
2.1.2、父类super_class
根类NSObject
API提供了获取指定类的父类的方法:
@property (readonly) Class superclass;
+ (Class)superclass;
那么这两个方法有何区别呢?我们去研究下它们的底层实现:
/* 首先获取该实例所属的类,接着获取类的父类
*/
- (Class)superclass {
return [self class]->superclass;
}
/* 获取该类的父类
*/
+ (Class)superclass {
return self->superclass;
}
示例3、测试NSObject
的superclass
方法
元类的父类指向何处?父类的元类指向何处?
{
id model_isa_superclass = class_getSuperclass(object_getClass(Model));//获取 Model 元类的父类
id model_superclass_isa = object_getClass(Model.superclass);//获取 Model 父类的元类
NSLog(@"model_isa_superclass == %@ : %p",model_isa_superclass,model_isa_superclass);
NSLog(@"model_superclass_isa == %@ : %p",model_superclass_isa,model_superclass_isa);
NSLog(@"Model_superISA_isMeta == %d",class_isMetaClass(model_superclass_isa));
}
/* 打印数据
model_isa_superclass == SuperModel : 0x10071f710
model_superclass_isa == SuperModel : 0x10071f710
Model_superISA_isMeta == 1
*/
分析打印数据:元类的父类与父类的元类指向相同的类,都指向元类;
2.1.3、获取结构objc_class
的其它成员信息
Runtime 函数库提供了获取结构objc_class
成员信息的函数:
/* 获取指定类的类名
* @param cls 类.
* @return 如果 class 为 nil,则返回空字符串
*/
const char *class_getName(Class cls);
/* 获取类定义的版本号。
*/
int class_getVersion(Class cls);
/* 设置类定义的版本号。
* @param version 新的版本号, int 型数据类型
* @note 可以使用类定义的版本号来提供类表示给其他类的接口的版本控制。
* 这对于对象序列化特别有用,在这种情况下,识别不同类定义版本中实例变量布局的更改非常重要。
*/
void class_setVersion(Class cls, int version);
/* 获取指定类的实例大小(字节数)
* @return 如果 cls 为nil,则返回 0 字节。
*/
size_t class_getInstanceSize(Class cls);
2.1.4、类的实例变量链表:ivars
//变量 Ivar 是个结构指针,指向结构体实例 objc_ivar
typedef struct ivar_t *Ivar;
//变量的结构
struct ivar_t {
int32_t *offset;//变量偏移量
const char *name;//变量名称
const char *type;//变量数据类型
uint32_t size;
uint32_t alignment_raw; // 对齐有时是 -1;使用 alignment()
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
}
2.1.5、获取类的方法链表methodLists
/* 选择器类型 SEL 是一个结构指针,指向结构 objc_selector ;
* SEL 用于在编译源代码时替换选择器值的唯一标识符;
* 所有具有相同选择器值的方法都拥有相同的 SEL 标识符;
*/
typedef struct objc_selector *SEL;
/* 函数指针 IMP 的声明,该函数的前两个参数是默认参数:id 和 SEL
* @param id:对于实例方法来说,self 指向实例对象的地址;对于类方法来说,self 指向类的地址;
* @param SEL 要执行的 method_t 的结构成员
*/
typedef id (*IMP)(id, SEL, ...);
//方法 Method 是一个结构指针,指向结构 method_t
typedef struct method_t *Method;
//变量的结构
struct method_t {
SEL name; //选择器类型:描述了方法的名称
const char *types; //描述了方法参数的数据类型
MethodListIMP imp; //函数指针:提供方法实现的地址
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>{
bool operator() (const method_t& lhs, const method_t& rhs){
return lhs.name < rhs.name;
}
};
};
2.1.6、获取类的协议protocols
//协议的本质也是一个 Objective-C 对象
typedef struct objc_object Protocol;
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// 以下字段并不总是出现在磁盘上。
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
};
//协议链表
struct protocol_list_t {
uintptr_t count;
protocol_ref_t list[0]; // variable-size
};
2.1.7、动态创建一个类
Runtime 函数库提供了相关函数动态创建一个类:
/* 创建一个新类和元类。
*
* @param superclass 创建新类的父类,如果为 nil 则用于创建根类。
* @param name 创建新类的类名称。
* @param extraBytes 在类和元类对象的末尾为索引ivars分配的字节数;通常是 0。
*
* @return 新类,如果无法创建类,则为 nil (如,该名称已经在使用中)。
*
* @note 通过调用 object_getClass(newClass) 来获得指向新元类的指针。
* @note 要创建一个新类,首先调用 objc_allocateClassPair() 函数。
* 然后使用 class_addMethod() 和class_addIvar() 等函数设置类的属性。
* 构建完类后,调用 objc_registerClassPair( )。
* 新类现在可以使用了。
* @note 实例方法和实例变量应该添加到类本身。类方法应该添加到元类中。
*/
Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes);
/* 注册使用 objc_allocateClassPair() 分配的类。
*/
void objc_registerClassPair(Class _Nonnull cls);
/* 用于 Foundation 的键值观察
* 不要自己调用这个函数。
*/
Class _Nonnull objc_duplicateClass(Class _Nonnull original, const char * _Nonnull name,size_t extraBytes);
/* 销毁一个类及其关联的元类。
* @param cls 要销毁的类。它必须是用 objc_allocateClassPair( ) 分配的
* @warning 如果存在此类或子类的实例,则不要调用。
*/
void objc_disposeClassPair(Class _Nonnull cls);
通过上文的学习,我们已经对一个类的结构有了详细的了解!接着我们使用 Runtime 函数库提供的函数动态创建一个类:
void dynamicClass_func(void){
NSLog(@"%s",__func__);
}
{
//以动态方式创建一个类
Class dynamicClass = objc_allocateClassPair(NSObject.class, "DynamicClass", 0);
//以动态方式添加一个方法
Method description = class_getInstanceMethod(NSObject.class, @selector(description));
const char *types = method_getTypeEncoding(description);
class_addMethod(dynamicClass, @selector(dynamicClass_func), (IMP)dynamicClass_func, types);
//注册该类
objc_registerClassPair(dynamicClass);
//使用该类
id dynamicOjct = [[dynamicClass alloc] init];
objc_msgSend(dynamicOjct,NSSelectorFromString(@"dynamicClass_func"));
}
2.2、实例对象
实例对象是我们对类alloc
、new
操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。
调用实例方法时,系统会根据实例的 isa
指针去类的方法列表及父类的方法列表中寻找与SEL
对应的函数指针IMP
。
我们来看下实例对象的定义:
/** isa 指针的 64 位上的一些信息
* @param nonpointer 是否对isa开启指针优化:true代表非指针型ISA,除了地址外,还包含了类的一些信息、对象的引用计数等。
* @param has_assoc:该位表示是否有关联对象
* @param has_cxx_dtor:在ARC环境下标记对象是否通过ARC来管理的
* @param shiftcls:标记当前对象所属类的指针地址
* @param magic:判断当前对象是真的对象还是一段没有初始化的空间
* @param weakly_referenced:是否有弱引用指针指向该对象
* @param deallocating:对象是否正在进行dealloc操作
* @param has_sidetable_rc:标记是否有 sitetable 结构用于存储引用计数
* @param extra_rc:标记对象的引用计数:首先会存储在该字段中,当到达上限后,再存入对应的引用计数表中
*
* @note 非指针型ISA:SUPPORT_NONPOINTER_ISA 64位中存储的内容有:引用计数、析构状态,是否有弱引用指针等等
* @note 一个实例少量的引用计数不会直接存放在 SideTables 中,引用计数存放在extra_rc 中,当其被存满时才会存入相应的SideTables 散列表中
*/
#define ISA_MASK 0x0000000ffffffff8ULL
#define ISA_MAGIC_MASK 0x000003f000000001ULL
#define ISA_MAGIC_VALUE 0x000001a000000001ULL
#define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
#define RC_ONE (1ULL<<45)
#define RC_HALF (1ULL<<18)
/** 苹果将ISA设计成了联合体,在ISA中存储了与该对象相关的一些内存信息,因为 并不需要64个二进制全部都用来存储指针
*/
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD;
};
#endif
};
//对象的结构体实例
struct objc_object {
private:
isa_t isa;
public:
// 一些函数
};
实例对象是 objc_object
结构,该结构体的 isa
变量,指向实例对象所属的类。
2.2.1、self
指针与 super
指针的区别
日常开发中常用的self
与super
指针,我们对它了解多少呢?
示例:测试self
指针与super
指针
@implementation SuperModel
- (instancetype)init{
self = [super init];
if (self) {
id self_object = self.class;
id super_object = super.class;
NSLog(@"1 ---- %@ : %p",self_object,self_object);
NSLog(@"2 ---- %@ : %p",super_object,super_object);
}
return self;
}
@end
@implementation Model
- (instancetype)init{
self = [super init];
if (self) {
id self_object = self.class;
id super_object = super.class;
NSLog(@"3 ==== %@ : %p",self_object,self_object);
NSLog(@"4 ==== %@ : %p",super_object,super_object);
}
return self;
}
@end
{
Model *objct = [[Model alloc] init];//创建实例,调用 -init 方法
}
/* 打印数据
1 ---- Model : 0x105f77730
2 ---- Model : 0x105f77730
3 ==== Model : 0x105f77730
4 ==== Model : 0x105f77730
*/
直觉告诉我们:super.class
获取的是该类的父类,但是打印数据却是该类,这是为什么呢?
使用 clang 将 [super class]
转为 C++ 源码:
/* super 是一个结构指针,指向结构 objc_super
* @param receiver 指向当前实例
* @param 发送消息时搜索的第一个类
*/
struct objc_super2 {
id receiver;
Class current_class;
};
/* 向实例所属类的父类发送消息
* @param super 结构指针:包含接收消息的实例和开始搜索方法实现的父类。
* @param op 选择器类型 SEL
*/
id objc_msgSendSuper(struct objc_super *super, SEL op, ...);
/* [super class] 伪代码
* 接收器仍然是 self
* 但是方法的查找,是从父类开始;
*/
objc_msgSendSuper((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("SuperModel"))}, sel_registerName("class"));
self
与super
的异同:
- 相同之处:
self
与super
消息主体都是self
,指向同一个实例对象。 - 不同之处:查找方法的开始位置,
self
从实例所属类的方法列表开始查询;super
从实例所属类的父类的方法列表开始查询。
self.class
与 super.class
,最终都是在根类 NSObject
找到实现的方法 ;因此super.class
获取的是该类。
2.3、id
类型
id
指针指向结构objc_object
;该类型的实例可以转换为任何一种对象,类似于 C 语言中不确定类型 void *
指针。
typedef struct objc_object *id;
id
类型灵活的体现了 Objective-C 动态类型 的特性:声明变量时指定为id
类型,编译器无法知道其类型;运行时系统通过结构体实例objc_object
的isa
指针找到其所属的类。
2.4、元类
在 Objective-C 中,Runtime 在调用
objc_allocateClassPair()
函数创建类的时候,创建的是一对类:该类以及该类的元类。因此类也被设计为一个对象。
Objective-C 类方法存储在元类上,这是使用元类的根本原因。
类存储示意图.png在给实例对象或类对象发送消息时,寻找方法的规则为:
- 当调用实例方法时,在实例所属类的方法列表查询该方法;
- 当调用类方法时,在该类元类的方法列表查询该方法;
元类与之前的类一样,也是一个对象!那么元类的 isa
指向哪里呢?为了不让这种结构无限延伸下去, Objective-C 设计所有元类的 isa
指向根元类 NSObject
;而根元类的 isa
指向自己;这样就形成了一个完美的闭环。
类关系总结如下:
isa 指针 |
superclass 指针 |
---|---|
实例对象的 isa 指向所属的类 |
实例对象的 superclass 指向所属类的父类 |
类的 isa 指向元类 |
类的 superclass 指向父类 |
元类的 isa 指向根元类 NSObject
|
元类的 superclass 指向父类的元类 |
根元类 NSObject 的 isa 指向根元类自身 |
根元类的 superclass 指向根类 |
根类 NSObject 的 isa 指向根元类 NSObject
|
根类 NSObject 的 superclass 指向 nil
|
2.5、分类
分类 Category
的定义如下:
//Category 本质是一个结构指针,指向结构体实例 category_t
typedef struct category_t *Category;
// 结构 category_t
struct category_t {
const char *name;//分类所属的类的名称
classref_t cls;//分类所属的类
struct method_list_t *instanceMethods;//分类里添加的实例方法列表
struct method_list_t *classMethods;//分类添加的类方法列表
struct protocol_list_t *protocols;//分类实现的协议列表
struct property_list_t *instanceProperties;//分类添加的实例属性列表
struct property_list_t *_classProperties; //分类添加的类属性列表
//获取方法列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
//获取属性列表
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
分类是如何实现的?
2.6、类扩展 Extension
类扩展 Extension 是一个匿名类,与分类 Category
不同:
区别 | 匿名类 | 分类Category
|
---|---|---|
加载时机 | 是类的一部分,在编译期和头文件里的 @interface 以及实现文件里的 @implement 一起形成完整的类,它伴随类的产生而产生,亦随之一起消亡。 |
在运行期决定 |
能否添加变量 | 可以添加实例变量 | 无法添加实例变量;因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局 |
是否依赖于该类源码 | 必须有一个类的源码才能为一个类添加扩展,无法为系统的类比如 NSString 添加扩展 |
不需要知道类的源码即可使用 |
用处 | 一般用来隐藏类的私有信息 | 用来分散类的实现;为原有类增加功能等 |
3、Objective-C 的方法Method
在 Objective-C 中,方法使用关键字 Method
表示。那么方法是什么?如何调用方法呢?方法与函数有何区别呢?
3.1、Method
的本质
在 objc-private.h
文件中找到Method
的声明:一个指向method_t
结构的指针
//方法 Method 是一个结构指针,指向结构 method_t
typedef struct method_t *Method;
//变量的结构
struct method_t {
SEL name; //选择器类型:描述了方法的名称
const char *types; //描述了方法参数的数据类型
MethodListIMP imp; //函数指针:提供方法实现的地址
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>{
bool operator() (const method_t& lhs, const method_t& rhs){
return lhs.name < rhs.name;
}
};
};
/* 选择器类型 SEL 是一个结构指针,指向结构 objc_selector ;
* SEL 用于在编译源代码时替换选择器值的唯一标识符;
* 所有具有相同选择器值的方法都拥有相同的 SEL 标识符;
*/
typedef struct objc_selector *SEL;
/* IMP 的声明:是一个函数的指针;
* 该函数的前两个参数是默认参数:id 和 SEL
* id:对于实例方法来说,self 保存了当前对象的地址;对于类方法来说,self 保存了当前对应类对象的地址;
* SEL:要执行的 method_t 的结构成员
*/
typedef id (*IMP)(id, SEL, ...);
Method
建立了SEL
和IMP
的关联:当向一个对象发送消息时,通过指定的SEL
查询函数指针IMP
,然后执行函数。
3.2、Objective-C 的方法调用
Objective-C 的方法调用实际就是一个消息传递的过程。
调用类方法 [NSString class];
,转为 C++ 代码:
((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("class"));
也就是说:Objective-C 的方法调用,本质是向对象发送消息。主要使用 objc_msgSend()
函数:
/* 该函数由汇编语言写,大致逻辑如下:
* 1、根据 self->isa 获取所属的类;
* 2、获取该类的缓存 cache
* 3、通过选择器类型 SEL 查找与之对应的 Method 的结构成员 IMP;
* 如果找到函数指针 IMP,则执行函数;
* 如果没有找到与 SEL 对应的 Method ;接着执行步骤 4
* 4、遍历该类的方法列表 methodLists ,通过 SEL ,查找与之对应的 IMP;
* 如果找到 IMP 指针,则执行函数;
* 如果没有找到与 SEL 对应的 Method ,则去查询父类的 methodLists;一直查询到根类
* 5、如果到根类也没有找到,那么就抛出 unrecognized selecto
*/
id objc_msgSend(id self, SEL op, ...);
3.2.1、方法查找过程
/** 查找某个类指定选择器的方法实现 IMP ( 标准IMP查找 )
* @param cls 指定的类
* @param sel 指定的选择器
* @param inst 类cls的实例或者子类;如果cls未初始化需要初始化,那么 inst 非空时效率更高
* @param initialize 是否初始化:当没有初始化时 (NO),避免调用 +initialize
* @param cache 是否先去缓存中查找
* @param resolver 是否需要执行动态方法决议;
* @return 返回 IMP
*
* @note 如果不想执行消息转发机制,可以调用 lookUpImpOrNil() 函数,而不是调用该函数
*/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver){
IMP imp = nil;
bool triedResolver = NO;//是否实行动态决议的标记,指标记防止循环调用 动态方法决议
runtimeLock.assertUnlocked();
/******************** 1、先去缓存查找 ******************/
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);//检查该类,如果未知就终止程序
if (!cls->isRealized()) {
realizeClass(cls);//如果未实现,则去实现
}
if (initialize && !cls->isInitialized()) {//初始化但还没完成时
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// 如果 sel == initialize, _class_initialize将发送+initialize,然后在此过程完成后,messenger将再次发送 +initialize。当然,如果这不是由 messenger 调用,那么它就不会发生。2778172
}
retry:
runtimeLock.assertLocked();
/******************** 从该类的缓存中查找 ******************/
imp = cache_getImp(cls, sel);
if (imp) goto done;
/******************* 从该类的方法列表中查找 *****************/
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls); // 找到并缓存至该类
imp = meth->imp;
goto done;
}
}
/******************* 从该类的父类缓存和方法列表中查找 *****************/
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass){
if (--attempts == 0) {// 如果超类链中存在循环,则停止。
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);// 父类缓存中查找
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass); //在超类中找到该方法,在 cls 中缓存它。
goto done;
}else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);// 父类方法列表中查找
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);// 在超类中找到该方法。在这个类中缓存它。
imp = meth->imp;
goto done;
}
}
}
/********************** unrecognized selector 的补救时机 ************************/
// 如果指定选择器 SEL 对应的方法没有实现,而且没有执行方法决议;第一次解决:尝试动态决议
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;//标记为已经执行动态决议
goto retry;
}
// 没有找到 IMP ,动态决议也没有结果:第二次补救机会:消息转发机制
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
/** 根据指定SEL查找某个类的方法实现 IMP
* 相比于lookUpImpOrForward() 函数,该函数不会执行消息转发机制
*/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,bool initialize, bool cache, bool resolver){
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
从 lookUpImpOrForward()
函数可以看出标准 IMP
查找过程:
- 1、先去该类的缓存中查找,如果找到则返回;否则接着查找
- 2、从这个类的方法列表中查找,如果找到,加入缓存列表,并返回 IMP;否则接着查找
- 3、从这个类的父类缓存和方法列表中查找;如果找到;缓存到该类(而非父类),并返回 IMP;否则接着查找;
- 4、如果从父类一直找到根类也没有找到,一般会抛出
unrecognized selector
错误;但该函数还没执行完; - 5、如果允许方法决议:
resolver=YES
,则执行补救措施: - 5.1、 第一次补救:动态方法决议;如果动态方法决议成功,则返回步骤 1 执行;否则往下执行;
- 5.2、第二次补救:消息转发机制,将
IMP
赋值为_objc_msgForward_impcache
,缓存到该类的方法列表,然后返回IMP
;
3.2.2、对象消息传递
在 OOP 中,消息传递是指一种在对象之间发送和接收消息的通信模式。
Objective-C 传递的消息到底是什么?消息与方法有何区别?
苹果对 Objective-C 的消息做了封装,以类 NSInvocation 呈现;NSInvocation
类包含消息的所有元素:目标target
、选择器SEL
、参数和返回值。
Objective-C 的对象消息传递是以动态方式实现的,用于调用类方法与实例方法:
- 类或者实例称为接收器,是消息的目的地;
- 消息本身由选择器和相应的参数构成。
接收器的类型和相应的方法在运行时决定!
Objective-C 的对象消息传递,向接收器发送消息,然后接收器会使用该消息调用相应的方法,并在有需要时返回结果。如果接收器没有相应的方法,也可以使用其它方式处理该消息,如将其发送给另一个对象、检查该消息并自定义其逻辑等!
3.2.3、方法签名
方法签名定义了方法输入参数的数据类型和方法的返回值。
苹果对 Objective-C 的消息做了封装,以类 NSMethodSignature 呈现;
3.3、消息传递过程
向一个 Objective-C 对象发送消息 NSInvocation
时,Runtime 执行了一系列查询逻辑:
- 1、根据
NSInvocation
的target
找到目标对象object_A
; - 2、根据目标对象
object_A
的isa
找到其所属的类Class_A
; - 3、在
Class_A
的结构体实例中首先查询cache
;如果缓存中找到该方法,则执行; - 4、如果缓存中没有找到该方法,则查询
methodLists
;如果找到该方法,则执行; - 5、如果没有找到,则根据
super_class
查找Class_A
的父类Class_A_Super
;然后接着执行步骤 3、4; - 6、如果找到
NSObject
也一直没有找到该方法;一般而言,程序会抛出异常unrecognized selector sent to instance ***
。
Objective-C 不会阻止程序向没有响应方法的对象发送消息。如果出现这种情况,程序默认抛出运行时异常 unrecognized selector
。
然而Objective-C 的运行时为 unrecognized selector
提供了三次补救机会:
- 1、动态方法决议:以动态方式实现方法。
NSObject
的+resolveClassMethod:
类方法与+resolveInstanceMethod:
类方法以动态方式实现由选择器指定的实例和类方法。如果动态方法决议成功,则执行IMP
指向的函数;如果失败,还有第二次补救措施; - 2、消息转发机制:当对象收到无法处理的消息时,将消息转发给能够做出回应的其它接收器;将所有无法识别的消息都发送给同一接收器;既不执行处理过程也不使程序崩溃,默默的吞下消息。Objective-C 提供了两种消息转发机制:
- 2.1、快速转发:重写
- forwardingTargetForSelector:
方法,将无法识别的方法转发给其它对象; - 2.2、完整转发:重写
- forwardInvocation:
方法。
3.3.1、动态绑定
动态绑定是在程序运行时(而不是编译时)将消息与方法对应起来的处理过程。
消息传递过程,就是一个动态绑定的过程!在程序运行前和消息发送前,消息与接收消息的对象不会对应。
在发送消息时,通过消息 NSInvocation
的 -invokeWithTarget:
方法设置接收器;此时NSInvocation
的 target
与SEL
绑定;Runtime 通过搜索指向target
的缓存列表或者方法列表,找到与选择器类型SEL
匹配的Method
,跳转到Method
的结构成员IMP
指向的函数执行代码。
使用动态绑定可以在不影响既有代码的情况下,将新对象和代码连接或添加到系统中,从而降低对象之间的耦合度。
3.3.2、多态性
Objective-C 的对象消息传递功能支持 OOP 多态性:不同的接收器调用同一方法可以获得不同的结果。
对于方法名相同的 在不同类中 的方法,虽然选择器类型SEL
相同,但是执行的 IMP
地址并不一定相同。我们已经知道,在对象消息传递过程中,接收器的类型在运行时确定,之后寻找到target
中的Method
,跳转到Method
的结构成员IMP
指向的函数执行代码。
如果 target
不同,那么结构体实例Method
也不会相同,它的结构成员IMP
也可能不相同,这时执行的代码就不相同。
因此,不同的接收器对同一条消息可以做出不同的响应。
3.3.3、方法交换
通过前面的学习,我们已经对 Objective-C 的对象调方法过程有了大致的了解。我们来实现一个 Runtime 的黑魔法 - 方法交换:为系统类的原有方法增加新的功能。
由于Objective-C的方法执行主要是寻找 IMP
指向的函数执行,假如我们在不改变选择器类型 SEL
的前提下改变method_t
的成员IMP
的值,那么 Runtime 就会顺着SEL
找到method_t
实例,然后执行 被指向它处 IMP
的函数。说的通俗些:方法交换的本质是交换两个方法持有的函数指针,而不是两个方法换了。这就是方法交换的核心原理。
3.4、方法与函数的区别
Objective-C 方法:
- 1、Objective-C 的方法分为实例方法与类方法:实例方法以
-
开头,存储在实例所属的类的methodLists
中 ;类方法以+
开头,存储在元类的methodLists
中 ; - 2、Objective-C 的方法依赖于对象:实例方法只能由类对象调用、类方法只能由类来调用;
- 3、Objective-C 方法的实现必须在
@implementation
与@end
之间; - 4、Objective-C 方法
Method
本质为一个结构指针,指向 结构体实例method_t
; - 5、Objective-C 方法的执行,是根据选择器类型
SEL
找到匹配的函数指针IMP
,执行IMP
处的函数;
C 语言的函数:
- 1、函数能写在文件的任何位置;
- 2、函数归文件所有;
- 3、函数的调用不依赖对象;
- 4、函数的内部不能用类的成员变量名直接去访问类的成员变量;
网友评论