iOS底层学习 - 初探类目、协议、扩展

1,366

Category相关

主要作用

  1. 声明私有方法
  2. 分解体积庞大的类文件
  3. 把Framework的私有方法公开

主要内容

  1. 类方法
  2. 实例方法
  3. Procotol
  4. 属性 [可以添加属性,但是没有set和get方法的实现,不可以添加 成员变量 ]

使用注意

  1. 分类只能增加方法,不能增加成员变量。
  2. 分类方法实现中可以访问原来类中声明的成员变量。
  3. 分类可以重新实现原来类中的方法,但是会***覆盖掉原来的方法***,会导致原来的方法没法再使用(实际上并没有真的替换,而是Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法)。
  4. 当分类、原来类、原来类的父类中有相同方法时,方法调用的优先级:分类(最后参与编译的分类优先) –> 原来类  –> 父类,即先去调用分类中的方法,分类中没这个方法再去原来类中找,原来类中没有再去父类中找。
  5. Category是在runtime时候加载,而不是在编译的时候。
  6. 名字相同的分类 会引起编译报错

为什么不可以添加成员变量而可以添加属性?

为什么不能添加成员变量:

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

typedef struct objc_class *Class;

objc_class结构体的定义如下:

其中相关的类的分类等信息存储在bits.datarw中,后续会进行探究

在上面的objc_class结构体中,ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改*methodLists的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的

为什么可以添加成员属性:

这个我们要从Category的结构体开始分析:

typedef struct category_t {
    const char *name;  //类的名字
    classref_t cls;  //类
    struct method_list_t *instanceMethods;  //category中所有给类添加的实例方法的列表
    struct method_list_t *classMethods;  //category中所有添加的类方法的列表
    struct protocol_list_t *protocols;  //category实现的所有协议的列表
    struct property_list_t *instanceProperties;  //category中添加的所有属性
} category_t;

从Category的定义也可以看出Category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。

Category实际上允许添加属性的,同样可以使用@property,但是不会生成 _变量(带下划线的成员变量),也不会生成添加属性的gettersetter方法,所以,尽管添加了属性,也无法使用点语法调用gettersetter方法。但实际上可以使用runtime去实现Category为已有的类添加新的属性并生成getter和setter方法

如何给Category添加属性的Get和Set方法?

我们可以通过关联对象方式来给Category添加属性对应的方法。

在分类中添加一个cate_name属性,然后添加对应getset方法,并实现相对应关联对象的API,代码如下

-(void)setCate_name:(NSString *)cate_name{
    /**
    ✅参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
    ✅参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
    ✅参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
    ✅参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
    */
    objc_setAssociatedObject(self, @"name",cate_name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSString *)cate_name{
    /**
    ✅参数一:id object : 获取哪个对象里面的关联的属性。
    ✅参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
    */
    return objc_getAssociatedObject(self, @"name");
}

相关关联对象的策略如下:

objc_setAssociatedObject

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

根据代码,可以得出该方法主要实现了关联对象的存储和旧值的释放

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ...
    // retain the new value (if any) outside the lock.
    //✅ 在锁之外保留新值(如果有)。
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        ...存储代码...
    }
    // release the old value (outside of the lock).
    //✅ 最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

存储的相关代码如下。主要操作如下

  1. 获取到关联对象管理类
  2. 根据指针地址获取到当前类的关联对象哈希表
  3. 根据key寻找对应的对象,找到就替换,未找到则插入
  4. 如果传入value为nil。如果有值,则移除
{
        //✅ 关联对象的管理类
        AssociationsManager manager;
        //✅ 获取关联的 HashMap -> 存储当前关联对象
        AssociationsHashMap &associations(manager.associations());
        //✅ 对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            //✅ 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                //✅ 根据key去获取关联属性的迭代器
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    //✅ 替换设置新值
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    //✅ 到最后了 - 直接设置新值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                //✅ 如果AssociationsHashMap从没有对象的关联信息表,
                //✅ 那么就创建一个map并通过传入的key把value存进去
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            //✅ 如果传入的value是nil,并且之前使用相同的key存储过关联对象,
            //✅ 那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }

根据以上探究,我们可以知道:

  • 关联对象存储的值,并不是在类中,而是单独存储的
  • 当类释放时,会清楚相关的所有的关联对象
    • 会在类的dealloc方法中进行释放

objc_getAssociatedObject

根据传入的key,获取到value的查找表的操作

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        //✅ 关联对象的管理类
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        //✅ 生成伪装地址。处理参数 object 地址
        disguised_ptr_t disguised_object = DISGUISE(object);
        //✅ 所有对象的迭代器
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            //✅ 内部对象的迭代器
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                //✅ 找到 - 把值和策略读取出来
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

Runtime 系列 3-- 给 category 添加属性

Category实现原理

深入理解Category

Extension相关

主要作用

  1. 声明私有属性
  2. 声明私有方法
  3. 声明私有成员变量

特点

  1. 编译时决议,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类,直接编译进ro
  2. 只以声明的形式存在,多数器情况下寄生与宿主类的.m中。伴随着类的产生而产生,也随着类的消失而消失
  3. Extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展
  4. 如果以文件单独存在的extension,并没有import进相关文件,编译时是不会链接的,因为底层编译器会自动过滤未使用的类,方法等

Extension与Category区别

  • Extension
    • 编译器决议,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
    • 伴随着类的产生而产生,也随着类的消失而消失。
    • Extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展
  • Category
    • 是运行期决议的
    • 类扩展可以添加实例变量,分类不能添加实例变量
    • 原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。

Procotol相关

概念

Protocol(协议)的声明看起来类似一个类的接口,不同的是Protocol没有父类也不能定义实例变量。Protocol是一种特殊的程序设计结构,用于声明专门被别的类实现的方法。因为OC是单继承的,由于不支持多继承,所以很多时候都是用Protocol和Category来代替实现多继承。Protocol只能定义公用的一套接口,但不能提供具体的实现方法。也就是说,它只告诉你要做什么,但具体怎么做,它不关心。

Protocol的基本用途:

  1. 可以用来声明一大堆方法(不能声明成员变量
  2. 要某个类遵守了这个协议,就相当于拥有这个协议中的所有方法声明
  3. 只要父类遵守了某个协议,就相当于子类也遵守了
  4. 如果这个协议只用在某个类中,应该把协议定义在该类中; 如果这个协议用在很多类中,就应该定义在单独文件中

主要内容

  1. 声明方法
  2. 属性[原理和实现同Category] 我们还是应该尽量把属性定义在主接口中,而不应该定义在协议中

为什么自定义的协议后面会有 <NSObject>

协议也能继承。既可以继承自自定义的协议,也可以继承自系统的协议。 我们在定义协议的时候,一般都是直接继承

因为这个协议中定义了一些基本的方法,由于我们使用的所有类都继承NSObject这个基类,而这个基类遵守了这个协议,那么也就实现了其中的那些方法,这些方法当然可以由NSObject及其子类对象调用,但是在不知道遵守者类型的时候需要用到id <协议名>这样的指针,这个指针在编译期并不知道自己指向哪个对象,唯一能调用的便是协议中的方法,然而有时候又需要用一些基本的方法,比如要辨别id <协议名>这个指针所指的对象属于哪个类,就要用到-isMemberOf:这个方法,而这个方法是这个协议中的方法之一,所以,我们自定义的协议都需要继承。本段一开始便说道:``中的方法在NSObject基类中实现了,那么无需再关心实现了,直接调用`中的方法吧。

Protocol使用

  1. 写在头文件中
    • 可以当做是给这个类添加了一些外部接口
    • 写在头文件中,类内部自然能通过self调用,外部也可以调用里面的方法,子类可以实现或者重写里面的方法。
  2. 写在文件的类扩展中
    • 可以当做是给这个类添加了一些私有接口
    • 而在类扩展中,内部可以调用,外部不能调用、子类不能重写实现和重写,相当于是私有方法。

如果子类自身又遵循了这个协议,但并没有实现,那么在运行时,系统会一级级往上查找,直到找到父类的方法实现。也就是说,只要知道苹果的私有方法名,并且确保自己的类是这个私有方法所属类的子类,就可以在子类中通过只声明不实现的方式执行父类中该私有方法的实现

主要作用

  1. 某一个类需要委托其他类处理某些事件,最具代表性性的便是UITableView的那些代理方法。这些方法其实还是代理的方法,只不过定义的地方可能会在委托者类中,通过调用这些方法,可以:将委托者中的数据传递给代理;将代理的数据传递给委托者;将委托者的事件抛给代理去处理...
  2. 给某几个特定的类添加统一的接口,这些接口是从这些类中抽象出的共同的行为,这样便可以减少重复的代码。

主要特点

  1. 是一种软件设计模式
  2. 以**@protocol**形式体现
  3. 传递方式一对一
  4. protocol 里的方法由谁实现,由谁调用

参考资料

iOS 之 Protocol 详解

Protocol