美文网首页
聊聊Objective-C

聊聊Objective-C

作者: 苏沫离 | 来源:发表于2019-01-07 22:39 被阅读0次

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 指针指向该类的父类,如果是根类(如NSObjectNSProxy), 则 super_classNULL
  • 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、测试 NSObjectclass方法
@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 与类 Modelisa 指针:

{
    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
 */

分析打印数据:实例对象 objctisa 指针指向类Model;类Modelisa 指针指向了元类。

那么元类的 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

根类NSObjectAPI提供了获取指定类的父类的方法:

@property (readonly) Class superclass;
+ (Class)superclass;

那么这两个方法有何区别呢?我们去研究下它们的底层实现:

/* 首先获取该实例所属的类,接着获取类的父类
 */
- (Class)superclass {
    return [self class]->superclass;
}

/* 获取该类的父类
*/
+ (Class)superclass {
    return self->superclass;
}
示例3、测试NSObjectsuperclass方法

元类的父类指向何处?父类的元类指向何处?

{
    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、实例对象

实例对象是我们对类allocnew 操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。
调用实例方法时,系统会根据实例的 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 指针的区别

日常开发中常用的selfsuper 指针,我们对它了解多少呢?

示例:测试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"));

selfsuper的异同:

  • 相同之处:selfsuper消息主体都是 self,指向同一个实例对象。
  • 不同之处:查找方法的开始位置,self从实例所属类的方法列表开始查询;super从实例所属类的父类的方法列表开始查询。

self.classsuper.class,最终都是在根类 NSObject 找到实现的方法 ;因此super.class获取的是该类。

2.3、id类型

id指针指向结构objc_object;该类型的实例可以转换为任何一种对象,类似于 C 语言中不确定类型 void *指针。

typedef struct objc_object *id;

id类型灵活的体现了 Objective-C 动态类型 的特性:声明变量时指定为id 类型,编译器无法知道其类型;运行时系统通过结构体实例objc_objectisa指针找到其所属的类。

2.4、元类

在 Objective-C 中,Runtime 在调用 objc_allocateClassPair() 函数创建类的时候,创建的是一对类:该类以及该类的元类。因此类也被设计为一个对象。

Objective-C 类方法存储在元类上,这是使用元类的根本原因。

类存储示意图.png

在给实例对象或类对象发送消息时,寻找方法的规则为:

  • 当调用实例方法时,在实例所属类的方法列表查询该方法;
  • 当调用类方法时,在该类元类的方法列表查询该方法;

元类与之前的类一样,也是一个对象!那么元类的 isa 指向哪里呢?为了不让这种结构无限延伸下去, Objective-C 设计所有元类的 isa 指向根元类 NSObject;而根元类的 isa 指向自己;这样就形成了一个完美的闭环。

类关系示意图.png

类关系总结如下:

isa 指针 superclass指针
实例对象的 isa 指向所属的类 实例对象的 superclass 指向所属类的父类
类的 isa指向元类 类的 superclass指向父类
元类的 isa 指向根元类 NSObject 元类的 superclass 指向父类的元类
根元类 NSObjectisa指向根元类自身 根元类的 superclass 指向根类
根类 NSObjectisa指向根元类 NSObject 根类 NSObjectsuperclass指向 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建立了SELIMP的关联:当向一个对象发送消息时,通过指定的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、根据NSInvocationtarget找到目标对象object_A
  • 2、根据目标对象object_Aisa找到其所属的类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:方法。
Runtime 的方法查询逻辑.png
3.3.1、动态绑定

动态绑定是在程序运行时(而不是编译时)将消息与方法对应起来的处理过程。

消息传递过程,就是一个动态绑定的过程!在程序运行前和消息发送前,消息与接收消息的对象不会对应。

在发送消息时,通过消息 NSInvocation-invokeWithTarget:方法设置接收器;此时NSInvocationtargetSEL 绑定;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、函数的内部不能用类的成员变量名直接去访问类的成员变量;

参考文章:
格物致知iOS类与对象
新手也看得懂的 iOS Runtime 教程
iOS 消息发送与转发详解

相关文章

网友评论

      本文标题:聊聊Objective-C

      本文链接:https://www.haomeiwen.com/subject/stndlqtx.html