美文网首页
Category 面试题总结

Category 面试题总结

作者: 相瑾瑜 | 来源:发表于2018-11-19 16:04 被阅读6次

    Category(分类)这一Object-C 2.0之后添加的语言特性,在日常开发中使用频率非常高。而且面试时Category基本上是都会涉及到的一个知识点。下面罗列一下面试中经常会提出的问题,基本上涵盖了这个知识点:

    1. Category和Extension的区别。
    2. Category底层实现原理
    3. Category的加载处理过程
    4. Category中 + load方法的调用
    5. Category中 + initialize方法的调用
    6. Category中load和initialize方法的区别
    7. Category中添加成员变量的实现

    1. Category和Extension的区别。

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

    2. Category底层实现原理

    Category编译之后的底层结构是 struct category_t ,里面存储着分类的对象方法,类方法,属性,协议信息。


    3. Category的加载处理过程

    下面创建了4个类,一个People类和3个People类的分类(Run、Jump、Eat)。
    这4个类都实现了 - instanceMethod这个实例方法。
    调用People的这个实例方法,查看打印结果。

    @interface People : NSObject
    - (void)instanceMethod;
    @end
    
    @implementation People
    - (void)instanceMethod
    {
        NSLog(@"people instanceMethod");
    }
    @end
    
    @interface People (Run)
    - (void)instanceMethod;
    @end
    
    @implementation People (Run)
    - (void)instanceMethod
    {
        NSLog(@"people run instanceMethod");
    }
    @end
    
    @interface People (Jump)
    - (void)instanceMethod;
    @end
    
    @implementation People (Jump)
    - (void)instanceMethod
    {
        NSLog(@"people jump instanceMethod");
    }
    @end
    
    @interface People (Eat)
    - (void)instanceMethod;
    @end
    
    @implementation People (Eat)
    - (void)instanceMethod
    {
        NSLog(@"people eat instanceMethod");
    }
    @end
    

    查看People类中的方法列表:

    #import "People.h"
    #import <objc/runtime.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            unsigned int count;
            Method *methodList = class_copyMethodList([People class], &count);
            for (int i = 0; i < count; i ++) {
                Method method = methodList[I];
                NSLog(@"%@",NSStringFromSelector(method_getName(method)));
            }
            free(methodList);
        }
        return 0;
    }
    
    People class method list

    发现People类中有4个instanceMethod方法,分类中的instanceMethod也在People类中。而且这时没有调用People的实例方法,是在runtime运行中加载了People类之后,Category的所有数据插入到了People类中。

    下面调用一下People类的实例方法:

    #import <Foundation/Foundation.h>
    #import "People.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            People *people = [[People alloc] init];
            [people instanceMethod];   // 打印结果为:people run instanceMethod
            
        }
        return 0;
    

    打印结果为 people run instanceMethod
    从结果来看,调用People的实例方法时调用了分类的方法,也就是所有分类的方法都合并到一个数组中,然后插入到原有类的前面,但是为什么是People (Run)分类覆盖了实例方法,而不是其他两个?

    在TARGETS中查看一下编译文件排序:

    TARGETS - Compile Sources.png

    发现 People+Run.m是最后编译的。也就是说编译顺序在最后的方法会排在方法列表的最前面。

    所以Category的加载处理过程是:
    1. 通过runtime加载某个类的所有的Category数据。
    2. 将所有的Category数据(方法、属性、协议)合并成到一个大数组中。这些数据后面参与编译的Category数据,会保存在数组的前面。
    3. 将合并后的分类数据(方法、属性、协议)插入到类的原来的数据的前面。


    4. Category中 + load方法的调用

    - Category有load方法。
    - load方法在Runtime加载类、分类时就会调用。
    - 每个类、分类在程序运行过程中,只调用一次load方法。

    创建6个类,之间的关系是:
    Animal : NSObject
    People : NSObject
    Student : People
    People Category : People+Run , People+Jump , People+Eat

    Animal 、 People 继承自 NSObject;
    Student 继承自People
    People+Run , People+Jump , People+Eat 是People的分类

    分别实现一下load方法:

    @implementation Animal
    + (void)load
    {
        NSLog(@"animal load method");
    }
    @end
    
    @implementation People
    + (void)load
    {
        NSLog(@"people load method");
    }
    @end
    
    @interface Student : People
    
    @end
    
    @implementation Student
    + (void)load
    {
        NSLog(@"student load method");
    }
    @end
    
    @implementation People (Run)
    + (void)load
    {
        NSLog(@"people run load method");
    }
    @end
    
    @implementation People (Jump)
    + (void)load
    {
        NSLog(@"people jump load method");
    }
    @end
    
    @implementation People (Eat)
    + (void)load
    {
        NSLog(@"people eat load method");
    }
    @end
    

    然后在main.m中不引入类的头文件:

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
        }
        return 0;
    }
    

    类的编译顺序是:

    编译顺序

    按照之前的思路,打印的顺序应该是:
    student、jump、animal、eat、People、run
    或者是:
    run、People、eat、animal、jump、student

    但是打印结果不是这样,打印出结果:

    + load method result

    原因是调用+load 方法不是通过消息发送机制(objc_msgSend),而是根据内存中函数地址直接调用。而且是在runtime加载类、分类时调用。

    +load方法调用顺序总结如下:

    • +load方法时在runtime加载类、分类的时候调用。
    • 每个类、分类的+load方法在程序运行中只调用一次
    1. 先调用类的+load方法
      1.1 调用类的+load方法时,按照编译先后顺序调用(先调用Student再调用Animal)
      1.2 调用子类的+load方法时,先调用父类的+load方法(调用Student时,先调用People,再调用Student)
      于是调用顺序是:People、Student、Animal
    2. 再调用分类的+load方法
      2.1 调用分类+load方法时,按照编译先后顺序调用

    PS. 如果是手动调用 load方法,则会触发消息机制(objc_msgSend)调用。按照消息机制调用顺序执行。但是一般不会手动调用load方法。


    5. Category中+ initialize方法的调用

    +initialize是在类第一次接收消息时调用的。

    创建几个类,他们之间的关系是:
    People : NSObject
    Student : People
    People Category : People+Run , People+Jump , People+Eat

    People 继承自 NSObject;
    Student 继承自People
    People+Run , People+Jump , People+Eat 是People的分类

    分别实现 + initialize 方法:

    @interface People : NSObject
    @end
    
    @implementation People
    +(void)initialize
    {
        NSLog(@"people initialize");
    }
    @end
    
    @interface Student : People
    @end
    
    @implementation Student
    +(void)initialize
    {
        NSLog(@"student initialize");
    }
    @end
    
    @implementation People (Run)
    +(void)initialize
    {
        NSLog(@"people run initialize");
    }
    @end
    
    @implementation People (Jump)
    +(void)initialize
    {
        NSLog(@"people jump initialize");
    }
    @end
    
    @implementation People (Eat)
    +(void)initialize
    {
        NSLog(@"people eat initialize");
    }
    @end
    

    分别调用People的alloc方法和Student的alloc方法:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [People alloc];
        }
        return 0;
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [Student alloc];
        }
        return 0;
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // 分别调用People和Student的alloc
            [People alloc]; 
            [Student alloc];
        }
        return 0;
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // 调用一次People allocation,三次Student allocation
            [People alloc];
            [Student alloc];
            [Student alloc];
            [Student alloc];
        }
        return 0;
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // 调用三次Student allocation
            [Student alloc];
            [Student alloc];
            [Student alloc];
        }
        return 0;
    }
    
    

    编译的顺序是:

    initialize类编译顺序
    // 打印结果
    [People alloc];
     --> people run initialize
    
    [Student alloc]; 
    --> people run initialize 
    --> student initialize
    
    [People alloc];
    [Student alloc]; 
    --> people run initialize 
    --> student initialize
    
    [People alloc];
    [Student alloc]; 
    [Student alloc]; 
    [Student alloc]; 
    --> people run initialize 
    --> student initialize
    
    [Student alloc]; 
    [Student alloc]; 
    [Student alloc]; 
    --> people run initialize 
    --> student initialize
    

    发现有几个现象:

    • 调用People alloc时打印的是People分类Run的 initialize方法
    • 调用Student alloc时打印的是People分类Run的initialize方法和Student initialize方法
    • 调用People 和 Student的alloc时打印的还是和调用Student alloc一样的结果
    • 多次调用Student alloc时打印的结果和调用一次Student alloc的一样

    所以得出以下几个结论:

    • +initialize是类第一次接收消息的时候调用
    • +initialize是通过objc_msgSend(消息机制)调用,所以分类方法会覆盖类方法
    • 调用子类(Student)的+initialize方法时底层会先调用父类(People)的+initialize方法,再调用子类的方法
      objc_msgSend([People class], @selector(initialize));
      objc_msgSend([People class], @selector(initialize));
    • 每个类只会初始化一次(只调用一次initialize),多次接收消息只调用一次+initialize方法

    因为+ initialize是通过objc_msgSend调用的,所以会有以下特点:

    • 如果子类没有实现 + initialize方法,会调用父类的 + initialize方法。所以当多个子类都没有实现 + initialize方法的话,会多次调用父类 + initialize方法。

    • 当分类实现了 + initialize方法,会覆盖类本身的 + initialize方法调用。因为Category的加载过程是将所有的Category的方法、属性、协议信息合成一个大数组,再将这个大数组插入到类信息的前面。Category中编译越靠后越优先调用。


    6. Category中load和initialize方法的区别

    Category 中 + load 和 + initialize 方法的区别总结如下:

    调用方式
    1. +load是根据方法函数的内存地址直接调用
    2. +initialize是通过objc_msgSend调用
    调用时刻
    1. +load是runtime加载类、分类时调用(只会调用一次)
    2. +initialize是类第一次接收消息时调用,每一个类只会初始化(initialize)一次,但是父类的+ initialize方法可能会调用多次。
    调用顺序
    1. +load
      1.1 先调用类的+load方法
      编译越早,调用越早
      调用子类的+load方法时,先调用父类的+load方法
      1.2 再调用分类的+load方法
      编译越早,调用越早

    2. +initialize
      2.1 先初始化父类
      2.2 再初始化子类,若子类没有实现+initialize方法,最终还是会调用父类的+initialize方法
      2.3 如果分类实现了+initialize方法,会覆盖类的+initialize方法。编译越晚,调用越早。


    7. Category中添加成员变量的实现

    一个类中如果写一个属性的话,编译器会自动做3件事情:

    1. 生成一个成员变量
    2. 生成成员变量的getter、setter声明
    3. 生成getter和setter的实现

    但是如果在一个分类中写一个属性,编译器只会做1件事情:

    1. 生成getter和setter的声明

    根据分类的结构,不能直接给分类添加一个成员变量,但是可以间接实现分类有成员变量的效果:使用关联对象(Association Object)。

    关联对象是runtime中的方法,使用时需要引入<objc/runtime.h>

    关联对象主要的方法有3个:

    1. 设置关联对象
      OBJC_EXPORT void
      objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)

    返回类型为 void,其中有4个参数:
    id _Nonnull object : 给哪一个对象添加关联对象
    const void * _Nonnull key :传入一个指针进去,接收的是地址值
    id _Nullable value :关联什么值
    objc_AssociationPolicy policy :关联的策略

    关联策略:

    objc_AssociationPolicy :
    
    // 给关联对象指向一个弱引用
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    // 给关联对象指向一个强引用,这个关联对象是非原子性
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                                *   The association is not made atomically. */
    // 给关联对象指向copy,这个关联对象是非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                                *   The association is not made atomically. */
    // 给关联对象指向一个强引用,这个关联对象是原子性
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                *   The association is made atomically. */
    // 给关联对象指向copy,这个关联对象是非原子性
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                *   The association is made atomically. */
    };
    
    // 关联对象策略对应的修饰符:
    // 关联对象策略中没有weak修饰符,没有弱引用这种效果
    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
    
    1. 获取关联对象
      OBJC_EXPORT id _Nullable
      objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

    返回类型为 id,其中有2个参数:
    id _Nonnull object : 获取哪一个对象的关联对象
    const void * _Nonnull key :传入一个指针进去,接收的是地址值

    1. 移除关联对象
      OBJC_EXPORT void
      objc_removeAssociatedObjects(id _Nonnull object)

    返回类型为 void,其中有1个参数:
    id _Nonnull object : 移除哪一个对象的所有关联对象

    其他3个参数比较明了,说一下key这个参数的用法,一般key的常见用法有4种:

    1. static void *myKey = &myKey;
    - (void)setAge:(int)age
    {
        objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
        objc_setAssociatedObject(self, myKey, @(age), policy);
    }
    
    - (int)age
    {
        return [objc_getAssociatedObject(self, myKey) intValue];
    }
    
    1. static char myKey;
    - (void)setAge:(int)age
    {
        objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
        objc_setAssociatedObject(self, &myKey, @(age), policy);
    }
    
    - (int)age
    {
        return [objc_getAssociatedObject(self, &myKey) intValue];
    }
    
    1. 直接使用属性名作为key
      使用属性名可以防止名称冲突,而且每一个不同的字符串的地址不一样
    - (void)setAge:(int)age
    {
        objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
        objc_setAssociatedObject(self, @"age", @(age), policy);
    }
    
    - (int)age
    {
        return [objc_getAssociatedObject(self, @"age") intValue];
    }
    
    1. 使用get方法的@selector作为key
    - (void)setAge:(int)age
    {
        objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
        objc_setAssociatedObject(self, @selector(age), @(age), policy);
    }
    
    - (int)age
    {
        return [objc_getAssociatedObject(self, @selector(age)) intValue];
    }
    
    // 在getter中可以使用隐式参数_cmd,_cmd对应当前方法的selector
    - (void)setAge:(int)age
    {
        objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
        objc_setAssociatedObject(self, @selector(age), @(age), policy);
    }
    
    - (int)age
    {
        return [objc_getAssociatedObject(self, _cmd) intValue];
    }
    

    这样就可以在分类中实现有成员变量的效果:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            People *people  = [[People alloc] init];
            people.age = 10;
            
            NSLog(@"age = %d",people.age); // age = 10
        }
        return 0;
    }
    

    相关文章

      网友评论

          本文标题:Category 面试题总结

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