iOS-分类Category详解和关联对象

 Category的实现原理

  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

 

CategoryClass Extension的区别是什么?

  • Class Extension在编译的时候,它的数据就已经包含在类信息中
  • Category是在运行时,才会将数据合并到类信息中

 

Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

  • load方法
  • load方法在runtime加载类、分类的时候调用
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用 (子类不会覆盖父类的load方法 因为load是根据函数地址直接调用 而不是是通过objc_msgSend调用具体的可以看这个链接 里面有详细的load 和 initialize的区别)

 

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果 实现方式
  1. 因为分类中的属性只是生成了 set 和 get 方法的声明,其实现和下划线变量都没有生成 所以我们需要将为做的事情完成
    #import "MJPerson.h"
    
    @interface MJPerson (Test)
    //{
    //    int _weight;
    //}
    
    //分类里面声明属性 只是生成 属性的set 和 get 方法 但是并没有去实现和生成_变量
    @property (assign, nonatomic) int weight;
    
    
    @property (copy, nonatomic) NSString* name;
    
    //- (void)setWeight:(int)weight;
    //- (int)weight;
    
    @end
    #define MJKey [NSString stringWithFormat:@"%p", self]
    
    @implementation MJPerson (Test)
    
    NSMutableDictionary *names_;
    NSMutableDictionary *weights_;
    + (void)load
    {
        weights_ = [NSMutableDictionary dictionary];
        names_ = [NSMutableDictionary dictionary];
    }
    
    - (void)setName:(NSString *)name
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        names_[MJKey] = name;
    }
    
    - (NSString *)name
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        return names_[MJKey];
    }
    
    - (void)setWeight:(int)weight
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        weights_[MJKey] = @(weight);
    }
    
    - (int)weight
    {
    //    NSString *key = [NSString stringWithFormat:@"%p", self];
        return [weights_[MJKey] intValue];
    }
    
    
    @end
    //用字典来实现有已下问题
    /*1.存储的地方不一样
    //person2.age = 20; // 20是存储在peron2对象内部
    //person2.weight = 50; // 50是存放在全局的字典对象里面
    
     2.线程安全问题 (可以加锁解决)
    */
    
    #import <Foundation/Foundation.h>
    #import "MJPerson.h"
    #import "MJPerson+Test.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;
            person.weight = 40;
            
            
            MJPerson *person2 = [[MJPerson alloc] init];
            person2.age = 20; // 20是存储在peron2对象内部
            person2.weight = 50; // 50是存放在全局的字典对象里面
            
            NSLog(@"person - age is %d, weight is %d", person.age, person.weight);
            NSLog(@"person2 - age is %d, weight is %d", person2.age, person2.weight);
        }
        return 0;
    }

     

  2. 使用关联对象来实现 
    #import "MJPerson+Test.h"
    #import <objc/runtime.h>
    
    @implementation MJPerson (Test)
    
    - (void)setName:(NSString *)name
    {
    //   关联对象 就是将传进来的name 和 person对象(self) 关联起来 ,达到 一个person对象对应一个name  就不用使用字典的方式来做了
        
    /*    参数讲解
        <#id  _Nonnull object#>  你要给哪一个对象添加关联对象 person对象(self)
     <#const void * _Nonnull key#> 关联的key 取的时候需要用相当于内部应该有个字典
     <#id  _Nullable value#>  关联对象是什么(关联的值) name
     <#objc_AssociationPolicy policy#> 关联策略
     objc_AssociationPolicy                   对应的修饰符
     OBJC_ASSOCIATION_ASSIGN                 assign
     OBJC_ASSOCIATION_RETAIN_NONATOMIC    strong, nonatomic
     OBJC_ASSOCIATION_COPY_NONATOMIC      copy, nonatomic
     OBJC_ASSOCIATION_RETAIN              strong, atomic
     OBJC_ASSOCIATION_COPY                copy, atomic
     
    //    objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
     */
        objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name
    {
        // 隐式参数
        // _cmd == @selector(name)
        return objc_getAssociatedObject(self, _cmd);
    }
    
    - (void)setWeight:(int)weight
    {
        objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (int)weight
    {
        // _cmd == @selector(weight)
        return [objc_getAssociatedObject(self, _cmd) intValue];
    }
    
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, @selector(name));
    //}
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, @selector(weight)) intValue];
    //}
    
    //#define MJNameKey @"name"
    //#define MJWeightKey @"weight"
    //- (void)setName:(NSString *)name
    //{
    //    objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //}
    //
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, MJNameKey);
    //}
    //
    //- (void)setWeight:(int)weight
    //{
    //    objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //}
    //
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, MJWeightKey) intValue];
    //}
    
    //static const void *MJNameKey = &MJNameKey;
    //static const void *MJWeightKey = &MJWeightKey;
    //- (void)setName:(NSString *)name
    //{
    //    objc_setAssociatedObject(self, MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //}
    //
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, MJNameKey);
    //}
    //
    //- (void)setWeight:(int)weight
    //{
    //    objc_setAssociatedObject(self, MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //}
    //
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, MJWeightKey) intValue];
    //}
    
    //static const char MJNameKey;
    //static const char MJWeightKey;
    //- (void)setName:(NSString *)name
    //{
    //    objc_setAssociatedObject(self, &MJNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //}
    //
    //- (NSString *)name
    //{
    //    return objc_getAssociatedObject(self, &MJNameKey);
    //}
    //
    //- (void)setWeight:(int)weight
    //{
    //    objc_setAssociatedObject(self, &MJWeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //}
    //
    //- (int)weight
    //{
    //    return [objc_getAssociatedObject(self, &MJWeightKey) intValue];
    //}
    
    @end
    #import <Foundation/Foundation.h>
    #import "MJPerson.h"
    #import "MJPerson+Test.h"
    #import <objc/runtime.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;
            person.name = @"jack";
            person.weight = 30;
            
            
            MJPerson *person2 = [[MJPerson alloc] init];
            person2.age = 20;
            person2.name = @"rose";
            person2.name = nil;
            person2.weight = 50;
            
            NSLog(@"person - age is %d, name is %@, weight is %d", person.age, person.name, person.weight);
            NSLog(@"person2 - age is %d, name is %@, weight is %d", person2.age, person2.name, person2.weight);
        }
        return 0;
    }

     

关联对象的原理

 

 

 

 

 

Category的底层结构

分类的本质(实现原理):

  • Category编译之后的底层结构是 struct category_t (结构体),里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

分类底层结构定义的如下: 

//objc-runtime-new.h

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);
};

 

项目中每定义一个分类,底层都会增加一个category_t对象。

Category源码阅读顺序:

  1. objc-os.mm (runtime入口)
    • _objc_init (runtime初始化)
    • map_images
    • map_images_nolock
  2. objc-runtime-new.mm
    • _read_images
    • remethodizeClass
    • attachCategories
    • attachLists
    • realloc、memmove、 memcpy

category的加载过程:

  1. 通过runtime加载类的所有分类数据
  2. 将所有分类的方法,属性,协议数据分别合并到一个数组 (后面参与编译的Category数据,会在数组的前面)
  3. 将合并后的分类数据(方法,属性,协议)插入到类原来到数据之前

由源码可见,对同名方法而言,会优先调用分类中的方法。如果多个分类中包含同名方法,则会调用最后参与编译的分类中的方法。

摘录源码中核心的attachCategories实现如下(objc4-756.2):

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    //方法二维数组
    //[[method_t,method_t],[method_t,method_t,method_t]]
    //二维数组中的一个元素(数组)存放一个分类中的方法列表
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //属性二维数组
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //协议二维数组
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        //取出分类
        auto& entry = cats->list[i];

        //取出分类中的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    //取出(元)类对象中的数据(class_rw_t)
    auto rw = cls->data();
    
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //将所有分类的方法添加到(元)类对象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //将所有分类的属性添加到(元)类对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //将所有分类的协议添加到(元)类对象的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

 

Category和Extension的区别

区别一

  • Category
    • 专门用来给类添加新的方法
    • 不能给类添加成员属性(其实是可以通过runtime给分类添加属性)
    • 分类中用@property定义变量,只会生成变量的getter、setter方法的声明,不能生成方法实现和带下划线的成员变量。
  • Extension
    • 可以说是特殊的分类,也称作匿名分类
    • 可以给类添加成员属性,但是是私有变量
    • 可以给类添加方法,也是私有方法

区别二

  虽然有人说Extension是一个特殊的Category,也有人将Extension成为匿名分类,但是两者的区别很大。

  • Category

    • 是运行期决定的
    • 类扩展可以添加实例变量,分类不能添加实例变量(原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。)
  • Extension

    • 在编译器决定,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
    • 伴随着类的产生而产生,也随着类的消失而消失。
    • Extension一般用来隐藏类的私有消息,必须有一个类的源码才能添加一个类的Extension,所以对于系统的一个类,比如NSString,就无法添加类扩展。
 

1.Category 基本使用场合

  经常用的是给类添加方法,协议、属性,编写的分类里面的方法, 最终是在运行过程中的时候合并到类对象(对象方法)/元类对象(类方法)里面 (不是编译的时候合并的)

 

posted @ 2021-01-19 18:44  俊华的博客  阅读(821)  评论(0编辑  收藏  举报