<iOS知识体系>分类Category与关联对象底层原理知识

212 阅读7分钟

Category的本质

在Runtime层都是类和对象都是struct表示的,category也不例外,category用结构体struct_t结构体包含:

 1.类的名字(name)            
 2.类(cls)                                                                                               
 3.category中所有给类添加的实例方法的列表(instanceMethods)                                             
 4.category中所有给添加的类方法列表(classMethods)       
 5.category实现的所有协议的列表(protocols)   
 6.category中添加所有属性(instanceProperties)
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; 实例属性列表
   // Fields below this point are not always present on disk.
   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);
};

从catetory的定义中可以看出category可以添加实例方法、类方法、协议、属性。category成员变量列表是只读,所以category不能添加实例变量。

每创建一个分类,在编译时都会生成这样子一个结构体并将分类的方法列表等信息存入这个结构体,在编译阶段分类的相关信息和本类的相关信息分开的。等到运行阶段,会通过runtime加载某个类的所有category数据,把所有category的方法、属性、协议数据分别合并到一个数组中,然后再将分类合并后的数据插入到本类的数据的前面。

1.Category的特点是什么?和Class Extendsion(类拓展)有什么区别?

  • 分类的特点
    分类是运行时决议,编译好的分类的文件,是没把相应分类的内容加到宿主类上的,只有在运行时Runtime才把分类的内容添加到宿主类上。
  • 类拓展的用途是什么
    一般把不想对外公开的一些类的方法、属性、成员变量的时候可以用类的拓展,类拓展代码格式:
    @interface XXX ()
    //私有属性
    //私有方法(如果不实现,编译时会报警,Method definition for 'XXX' not found)
    @end
    
  • 分类和类拓展的区别是什么?
    1.拓展是编译时决议,分类是运行时决议;
    2.分类有声明和实现,拓展只以声明的形式存在,多数情况下寄生于宿主类的.m中;
    3.系统类可以添加分类,但是不能为系统类添加拓展

2.Category使用场景是什么?用途是什么?

1.给现有类添加方法,丰富现有类的功能,比如给NSString添加一些很实用的方法(判断字符串是否是邮箱、转化字符串为MD5);
2.分解代码庞大功能复杂的类。把功能复杂代码很多的类,按照不同功能拆分成不同的类,同一功能放到一个文件里,体现单一职责原则;
3.声明私有方法,比如定义一个分类,只有头文件放到对应宿主.m里,满足私有方法的声明和使用,不暴露集体实现。

3.Category中+load和+initialize调用的顺序是什么样子的?Category中的+load和+initialize方法的区别是什么?

  • 调用顺序
    1.类要优先于分类调用+load方法;
    2.先编译的分类的+load方法会被优先调用;
    3.父类+load优先于子类;
    4.由于分类是objc_msgSend机制,+initialize在所有分类要优先于类调用。在类中+initialize父类优先于子类。
    如果子类没有实现+initialize,会调用父类的+initialize,所以同一个父类的+initialize可能会被调用多次。
    5.编译顺序,决定于编译文件的顺序,通过调整Compile Source中文件的顺序,即可改变编译顺序
  • +load和+initialize 区别在于调用方式和调用时刻不同
    1.调用方式不同:+load是根据函数地址直接调用,+initialize是通过objc_msgSend调用
    2.调用时刻不同:+load方法会在runtime加载类、分类时调用,而且程序运行过程中只调用一次。+initialize是类第一次接收到消息的时候调用,即是类调用alloc时,每一个类只会initialize一次,上面提到子类没有实现+initialize,会调用父类的+initialize。这样父类的initialize方法可能会被调用多次。

4.怎么理解Category方法覆盖的问题?

  • 分类添加的方法可以“覆盖”原类方法
    其实覆盖没有真正的覆盖,如果category里面添加了methodA,那么原类的methodA也是存在的,没有真正覆盖掉。只是系统只会调用后来category添加的methodA。
    因为category的methodA被放到了方法列表的前面,原来类中的methodA被放到了方法列表的后面,在运行时查找方法会找到了category添加的同名方法,就产生了"覆盖"原来类的同名方法的效果。
    如果多个分类有同名方法,那么谁能生效取决于谁最后参与编译。最后参与编译的分类对应的方法就会生效。

5.能否给Category添加实例变量?那如何给分类添加实例变量?

  • 不能给category添加实例变量:
    从分类的底层原理可以知道,category成员变量列表是只读,所以category不能添加实例变量。
  • 直接添加肯定是不行,可以间接的添加:
    • 给分类添加全局变量,并且手动重写setter/getter方法实现。但是全局变量有很多隐患,对象销毁时无法销毁,不建议采用。
    • 用关联对象方法添加实例变量。通过runtime提供的关联对象的方法可以简洁的给分类添加成员变量。通过下面提供的关联对象下API,可以实现给分类添加实例变量:
      添加关联对象
      void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
      获得关联对象
      id objc_getAssociatedObject(id object, const void * key)
      移除所有的关联对象
      void objc_removeAssociatedObjects(id object)
      
  • 关联对象添加实例变量原理:
    其实关联对象并没有添加到实例变量到被关联对象内存中,关联对象有一个全局内容管理器(AssociationsManager),关联对象通过上面提到的三个API,操作实例变量存取、移除。从而完成了对分类的添加实例变量。

关联对象的底层原理

  • 关联对象的使用场合
    默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现。
    关联对象提供以下API:

     // 1.添加关联对象:
    void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
    // 2.获得关联对象:
    id objc_getAssociatedObject(id object, const void * key)
    // 3.移除所有的关联对象
    void objc_removeAssociatedObjects(id object)  
    

    在添加关联对象的方法中有一个policy属性,它是一个枚举值,对应我们平时定义属性时设置的修饰词:

  • 关联对象的基本使用
    使用关联对象需要#import <objc/runtime.h>

    // 给DJTPerson创建一个分类,添加一个name属性
    @interface DJTPerson (Test)
    @property (nonatomic, copy) NSString *name;
    @end
    
    @implementation DJTPerson (Test)
    /**
      * 第一种写法:创建一个void*类型的指针作为key,它存着自己的地址,只要唯一就行
      */
    static const void *DJTNameKey = &DJTNameKey;
    - (void)setName:(NSString *)name {
      objc_setAssociatedObject(self, DJTNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name{
      return objc_getAssociatedObject(self, &DJTNameKey);
    }
    
    /**
      * 第二种写法:既然是void*类型,使用char类型的地址,节省空间
      */
    static const char DJTNameKey;
    - (void)setName:(NSString *)name{
        objc_setAssociatedObject(self, &DJTNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
      }
    
    - (NSString *)name{
       return objc_getAssociatedObject(self, &DJTNameKey);
      }
      
    /**
      * 第三种写法:使用属性名作为key,其实也是传的内存地址
      */
    - (void)setName:(NSString *)name {
        objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
     }
    
    - (NSString *)name {
       return objc_getAssociatedObject(self, @"name");
    }
    
    /**
      * 第四种写法:使用get方法的@selector作为key,这种写法的好处是它返回的是一个结构体指针,写错方法名会有错误提示
      */
    - (void)setName:(NSString *)name {
         objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
      }
    
    - (NSString *)name {
        //@selector(name)等价于隐式参数_cmd
         return objc_getAssociatedObject(self, @selector(name));
        // return objc_getAssociatedObject(self, _cmd);
     }
     
    @end
    
  • 关联对象的底层原理
    关联对象并不是在被关联对象本身的内存中,通过分析底层实现,它存储在由AssociationsManager管理的全局统一的一个AssociationsHasMap中,关系如下:


    从上图中可以看出,关联对象底层实现依赖下面四个核心对象:

    • AssociationsManager
    • AssociationsHasMap
    • ObjectAssociationMap
    • ObjectionAssociation

    它们之间的关系:
    AssociationsHasMap里面存储着某个对象的关联对象Map表,即ObjectAssociationMap,这个表存储了多个关联对象,因为在分类里可以给对象添加多个属性,也就是设置多个关联对象,ObjectAssociationMap中就是我们添加的关联对象,由于ObjectAssociation存储值和策略,当我们将关联对象设置为nil时,AssociationMap自动删除这条关联对象;当我们调用objc_removeAssociatedObjects(id object)方法时,就是移除某个对象的关联对象,即上图中AssocicationHasMap需要移除对象的关联对象Map表。