分类(Category)的本质 及其与类扩展(Extension) /继承(Inherit)的区别

一、分类的概念

分类是为了扩展系统类的方法而产生的一种方式,其做用就是在不修改原有类的基础上,为一个类扩展方法,最主要的是能够给系统类扩展咱们本身定义的方法。面试

如何建立一个分类?↓↓数组

1)Cmd+N,iOS-->Objective-C File,Next;
(2)File Type选择category,class选择须要的类,分类名,Next。

好比咱们为Person建立了一个Student的分类:安全

 

其实分类的做用仍是挺大的,好比咱们有一个类的功能很复杂  若是只在这个类中实现的话不够清晰,这个时候咱们能够给这个类按照功能多建几个分类,能够条理清晰的完成相应功能,好比person类,老师/学生/工人等等都有本身的特性,能够经过分类来合理管理。app

二、分类的底层实现

咱们能够经过一个例子来引出分类的本质,好比如今有一个person类,而person类如今又有两个分类,分别是PersonClass+Kid和PersonClass+sutdent,若是这三个类中都有一个test的对象方法(方法中打印对应的文件名),那么当我建立person对象后调用test方法,究竟会是个什么结果呢?出现这个结果的缘由又是什么?函数

咱们经过打印发现,调用set的方法后控制台上打印的是PersonClass-kid,也就是其实是调用PersonClass-kid分类的test方法atom

这个时候咱们可能就有疑惑了,咱们在以前讲到对象的本质的时候说当调用方法时,其底层都是经过消息机制来实现的  也就是objc_msgSend(objc,selector(msg)) spa

消息机制会经过isa找对应的对象找到对应的方法 好比实例对象调用对象方法  就会根据实例对象的isa去类对象中遍历对象方法列表 找到合适的方法就return 没有的话就根据supperclass去父类中查找  一级级查找线程

按理说应该是person调用test方法,person实例对象根据其isa指针跑到person的类对象中找到对象方法列表,也就是person类的test方法进行调用。3d

但实际并不是如此,咱们都知道一个类只有一个类对象 只有一个元类对象,因此出现这个结果的缘由只多是分类的方法被添加到了类对象的方法列表中,并处在主类自身方法的前面。指针

那么分类的方法是何时被添加到类对象的方法中去的呢?是编译的时候仍是运行的时候呢?

答案是在运行时经过runtime动态添加到类对象中去的

首先在编译阶段,系统是先将各个分类转换为category_t结构体,这个结构体里面存储着分类中的各类信息(类方法/对象方法/属性/协议等等)

咱们能够在源码中找到这个结构↓↓↓

而后在运行时经过runtime加载各个category_t结构体(PersonClass+Kid和PersonClass+sutdent),经过while循环【①】遍历全部的分类,把每个Category的方法添加到一个新的方法大数组中,属性添加到一个新的方法大数组中,协议数据添加到一个新的方法大数组中;

最后,将合并后的分类数据(方法、属性、协议),插入到类原来数据(也就是主类的数据)的前面,咱们再调用方法时,经过消息机制遍历方法列表,优先找到了分类方法

这个流程咱们能够在阅读源码中找到依据↓↓

上面的流程能够解释为何调用同名方法时有限调用了分类中的实现方法,可是咱们这里有两个分类方法,那为何是调用的PersonClass-kid的方法呢?分类间的优先级又是什么?

分类的优先级其实咱们在上面的流程中有提到,也就是①的位置,就是经过while循环遍历全部的分类,添加到数组中,也就是优先调用哪一个分类取决于哪一个分类被添加到数组的前面,

由于是while循环,因此越先编译的反却是放到了数组后面,后面参与编译的Category数据,会在数组的前面

这个编译顺序咱们能够在这个位置查看↓↓哪一个文件排名靠前就先编译哪一个文件

 咱们看到PersonClass-kid在最后,也就是最晚编译的,根据while的取值规则,反倒被添加到了数组的最前面,消息机制在方法列表中找到了对应方法后就直接teturn了,因此调用了了PersonClass-kid的方法,当咱们手动调整编译顺序后,好比把PersonClass-student.m调到了最后,发现最终打印的结果是:PersonClass-sutdent

 

若是当出现继承关系呢?方法又会怎么调用呢?

咱们继续建立一个teacher类,继承自person类,同事teacher类有两个分类,分别是teacher+chinese和teacher+english,结构以下↓↓

一样在teacher类及其分类中实现test方法,打印本身的文件名,

而后建立一个teacher类,调用teacher实例对象的对象方法,打印结果是 teacher-chinese

这个流程和刚才说到的同样,teacher实例对象调用方法,首先根据isa去teacher的类对象中查找方法,而分类中的方法在运行时也被添加到了方法列表,且在主类本身的方法以前,因此会调用分类的方法,而究竟先调用哪一个分类的方法取决于编译顺序,又由于teacher-chinese是teacher分类中最晚被编译的,因此结果是 teacher-chinese

假如teacher及其分类没有实现test方法呢?

打印结果是PersonClass-sutdent

这是由于teacher实例变量根绝isa去类对象方法列表中没有找到对应的方法(即分类和主类都没实现此方法)那么类对象将根据本身的superclass指针去父类(person)中去寻找对应的方法,而上面也分析到了,person的分类方法加载到方法列表且处在主类方法前面,因此调用的是最晚编译的分类的方法,即PersonClass-sutdent

 

因此当调用某个方法时,流程应该是这样的

1.先去该类的分类中查看有无此方法,有的话调用分类的方法(多个分类都有此方法的话就调用最晚编译的分类的方法);

2.没有分类的话或者分类中没有此方法的话,就查看主类中有无实现此方法,有的话调用;

3.主类在也没有实现对应方法的话就根据superclass指针去父类中查找,一级级查找,找到调用

4.找到最顶部的基类也没找到对应方法的话,报方法找不到的错误,项目crash

 

三、分类的load方法和initialize方法

在面试过程当中涉及到分类时常常会问道,category有load方法吗?loda方法何时加载?load方法与initialize方法有什么区别?再出现继承与分类状况时,各个load方法或者initialize方法是按什么顺序调用的?

咱们在查看苹果官方关于load方法的介绍文档中,能够看出:

当类被引用进项目的时候就会执行load函数(在main函数开始执行以前),与这个类是否被用到无关,每一个类的load函数只会自动调用一次.也就是load函数是系统自动加载的,load方法会在runtime加载类、分类时调用。

好比咱们在项目中建立了几个类及分类,发现没有作任何处理运行项目,发现load方法被自动调用了:

 一个项目中有不少类,那么这些类的调用顺序是什么?

先调用类的+load
  1.按照编译前后顺序调用(先编译,先调用)
  2.用子类的+load以前会先调用父类的+load

再调用分类的+load
  1.按照编译前后顺序调用(先编译,先调用)

主要流程就是这样↓↓

这个顺序在源码中有体现:

源码阅读指引↓↓

objc4源码解读过程:objc-os.mm
_objc_init

load_images

prepare_load_methods
schedule_class_load
add_class_to_loadable_list
add_category_to_loadable_list

call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)

 

好比,如今有一个person类,person类有两个子类student和teacher,
编译顺序是student/person/teacher  那么load调用顺序应该是这样的:
1.系统按照编译顺序,先找到student类,而后查看student有没有父类且这个父类没有执行过
    loda方法,发现有(pserson类),而后再查看person类有没有没调用过load方法的父类,
    发现有一个NSObject,在遍历NSObject有没有没调用过load方法的父类,发现其是基类,
    没有父类了因此,就先调用NSObject的load方法,而后接下来调用person的load方法,而后
    再调用student的load方法

2.接下来找到person类,发现其不存在没有调用过load方法的父类且其本身的load方法也被调用
    过了,因此直接跳过了,没有调用任何的load方法

3.最后来到了teacher类,查找其父类时,发现父类及更高级别的父类都实现了load方法,而自
   己的load方法尚未调用过,因此调用了teacher的load方法

因此调用顺序是:NSObject的load方法->Person的load方法->sudent的loda方法->techer的load方法
由于咱们没法修改NSObject的load方法实现,因此没法查看到它的方法打印

当全部类的都调用完load方法后,接下来开始调用分类的load方法↓↓

分类的load方法调用顺序和分类的主类没有任何关系,分类的调用顺序很简单:
就是彻底按照编译顺序调用load方法,好比A有两个分类a1,a2,B有两个分类b1,b2,
分类的编译顺序是b1,a2,b2,a1,那么分类的load方法调用顺序就是:
b1的load方法->a2的load方法->b2的load方法->a1的load方法

 

这个时候咱们又会产生一个新的困惑?咱们以前在调用方法时,好比咱们调用一个对象的test方法,是根据isa指针去方法列表中查找,找到后就return不在向上或者向下继续查找执行了,可是为何load方法却不这样呢?为何load方法在执行完父类的load方法后还继续向下执行子类的load方法?

这是由于load方法并非经过消息机制实现的,也就是否是经过objc_msgSend(obj,@selector(方法))来实现的,消息机制是找到对应的方法就return,而load方法是直接经过方法地址直接调用

 以上就是有继承和分类状况下类的load方法调用顺序问题。

 

接下来来看initialize方法:

initialize方法是在一个类或其子类第一次接收到消息以前进行调用的,用来初始化,一个类只会被初始化一次

initialize在类或者其子类的第一个方法被调用前调用。即便类文件被引用进项目,可是没有使用,initialize不会被调用

load方法是不管类有没有被用到,只要添加被引入到项目就会被调用,而initialize则是在这个类或者其子类第一次调用方法的时候就会进行调用。

某个类调用方法时,是经过消息机制,也就是runtime的objc_msgSend方法实现的,因此initialize方法实际上是在objc_msgSend进行判断调用的

也就是当咱们调用[teacher alloc],其实是转化为了objc_msgSend([teacher class],@selector(alloc))方法,而objc_msgSend([teacher class],@selector(alloc))的内部结构有对teacher的initialize进行了判断,内部结构以下

objc_msgSend([teacher class],@selector(alloc)){
  if([teacher class]没有初始化){
      //对teacher进行初始化  固然初始化并无这么简单还涉及到了父类的初始化
        objc_msgSend([teacher class],@selector(initialize));
  }  
        objc_msgSend([teacher class],@selector(alloc))
}    

 

一样在上面的项目中,咱们重写每一个类及分类的initialize方法,调用teacher的alloc方法, 

 

 咱们发现是先调用父类Person分类的initialize方法  而后在调用本身分类的initialize方法,

 上面提到了,objc_msgSend方法会判断类是否进行了初始化,没有的话就进行初始化,

而对类的初始化过程,是优先对类的父类进行初始化的,也就是以下的结构

objc_msgSend([teacher class],@selector(initialize)){
  if(teacher有父类 && teacher的父类没有初始化){
      //递归 有限初始化最顶级的父类
        objc_msgSend([teacher父类 class],@selector(initialize));
  }  
    //标记
    类已初始化 = yes;
}   

又由于initialize不一样于load经过地址调用方法 ,而是经过消息机制来进行调用的,因此会遍历类对象的方法列表,找到对应的方法就return了,而分类的方法位于主类方法前,后编译的分类排序更靠前,因此先调用了父类person分类Kid的方法,而后调用了teacher分类english的方法

 上面流程咱们能够在源码中找到依据:

首先调用方法是查看有没有初始化,没有的话就调用初始化操做

而初始化操做中先初始化父类

 

 

由于initialize是经过消息机制来实现的,因此当子类没事实现initialize方法是,会根据supertclass指针去调用父类中的同名方法(对象本质中有讲到)

也就是当咱们注释掉teacher类及其分类中initialize方法的实现再调用[teacher alloc]方法时发现 调用了两次person分类的initialize方法

2019-04-15 13:44:26.323779+0800 test[68408:9131102] PersonClass-Kid
2019-04-15 13:44:26.324083+0800 test[68408:9131102] PersonClass-Kid

第一次打印是由于初始化teacher时会先初始化父类person,第二次打印是由于初始化teacher时没有找到它的initialize方法,因此去父类中查找了

虽然调用了两次person的initialize方法,但person只初始化了一次,第二次是初始化teacher

因此,initialize是当类第一次用到时就对调用,先调用父类的+initialize,再调用子类的initialize。

 

load方法和initialize方法均可以用来作什么操做?

首先 load方法和initialize方法有几个相同点:

1>在不考虑开发者主动调用的状况下,系统最多会调用一次

2> 若是父类和子类都被调用,父类的调用必定在子类以前

+load

因为调用load方法时的环境很不安全,咱们应该尽可能减小load方法的逻辑,load很常见的一个使用场景,交换两个方法的实现

//摘自MJRefresh
+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
    [self exchangeInstanceMethod1:@selector(reloadRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_reloadRowsAtIndexPaths:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(deleteRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_deleteRowsAtIndexPaths:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(insertRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_insertRowsAtIndexPaths:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(reloadSections:withRowAnimation:) method2:@selector(mj_reloadSections:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(deleteSections:withRowAnimation:) method2:@selector(mj_deleteSections:withRowAnimation:)];
    [self exchangeInstanceMethod1:@selector(insertSections:withRowAnimation:) method2:@selector(mj_insertSections:withRowAnimation:)];
}

+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}

 

+initialize

initialize方法通常只应该用来设置内部数据,好比,某个全局状态没法在编译期初始化,能够放在initialize里面。好比NSMutableArray这种类型的实例化依赖于runtime的消息发送,因此显然没法在编译器初始化:

// int类型能够在编译期赋值
static int someNumber = 0; 
static NSMutableArray *someArray;
+ (void)initialize {
    if (self == [Person class]) {
        // 不方便编译期复制的对象在这里赋值
        someArray = [[NSMutableArray alloc] init];
    }
}

 

 

还有几个注意点:

1》load调用时机比较早,运行环境不安全,因此在load方法中尽可能不要涉及到其余的类。由于不一样的类加载顺序不一样,当load调用时,其余类可能还没加载完成,可能会致使使用到还没加载的类从而出现问题;

2》load方法是线程安全的,它使用了锁,咱们应该避免线程阻塞在load方法(由于整个应用程序在执行load方法时会阻塞,即,程序会阻塞直到全部类的load方法执行完毕,才会继续);initialize内部也使用了锁,因此是线程安全的(即只有执行initialize的那个线程能够操做类或类实例。其余线程都要先阻塞,等待initialize执行完)。但同时要避免阻塞线程,不要再使用锁。

3》iOS会在应用程序启动的时候调用load方法,在main函数以前调用

4》在首次使用某个类以前,系统会向其发送initialize消息,一般应该在里面判断当前要初始化的类,防止子类未覆写initialize的状况下调用两次

 

 

四、关联对象

分类中是可使用属性的,但不能建立成员变量的,而主类中是可使用属性与成员变量的

缘由咱们能够经过比较类与分类的底层结构能够看出,

分类的结构↓↓

 

类对象的结构↓↓

由于分类的实际结构中并无存放成员变量的数组,因此其是没法建立和使用成员变量的

而当咱们在建立属性时,其实这个属性其实是执行了一下操做:

@property(nonatomic,assign)double height;
/**
   //1.声明成员变量
   {
      double _weight;
   }
   2.实现set方法和get方法
   - (void)setHeight:(double)height{
      _height = height;
   }
 
   - (double)height{
      return _height;
   }
 */

由于分类中没有成员变量,因此分类中的属性也就没有自动去实现set方法和get方法,这也就致使了咱们在使用分类属性时出现crash↓↓

因此咱们若是想让分类中的属性或成员变量能跟主类中同样使用的话,须要经过运行时创建关联引用

使用方法:重写分类属性的set/get方法↓↓

//首先须要导入runtime的头文件 #import <objc/runtime.h>
- (void)setHeight:(double)height{
    /**
     id  _Nonnull object:这个参数是指属性与哪一个对象产生关联?通常写self便可
     const void * _Nonnull key:这个是关联属性名  我通常都是直接写属性名 即@"height"
     id  _Nullable value:关联属性的属性值  也就是height
     objc_AssociationPolicy policy:这个参数通常是值属性的修饰符 好比咱们常常用copy来字符串  assign修饰基本数据类型  还有就是原子锁,咱们经常使用的就是不加锁nonatomic
     */
    //这就至关于把height这个属性与self进行绑定,能够当作是至关于把@{@"height":@(height)}这个键值对存放在全局中的某个位置,能够读取与设置
    //height是基本类型 须要包装成NSNumber
    objc_setAssociatedObject(self, @"height", @(height), OBJC_ASSOCIATION_ASSIGN);
    
}

- (double)height{
    //这个是指根据key去取出对应的属性值  这个须要注意的点事key必定要和set方法中的key一致
   return  [objc_getAssociatedObject(self, @"height") doubleValue];
}

 

关联对象提供了如下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)

关于objc_setAssociatedObject中objc_AssociationPolicy参数的使用:

关联对象的原理咱们能够经过源码来查看 [objc4源码解读:objc-references.mm]

 

其中有几个点须要注意:

1.关联对象并非存储在被关联对象自己内存中 主类的属性是存储到类对象本身的内存中的,可是经过关联方式并不会把属性添加到类对象内存中 而是将关联对象存储在全局的统一的一个AssociationsManager中
2.设置关联对象为nil,就至关因而移除关联对象
3.当关联对象被销毁时,AssociationsManager中存在全部与关联对象绑定的信息都会被释放

按照我的理解的方式应该是这样

这个Map咱们就能够理解为一个字典,里面存放着一个个键值对

 

因此经过上面的分析,咱们能够回答一个常常被问道 的关于category的面试题

//Category可否添加成员变量?若是能够,如何给Category添加成员变量?
答:不能直接给Category添加成员变量,可是能够间接实现Category有成员变量的效果。
咱们可使用runtime的API,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)和objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)这两个来实现。[重写分类属性点的set方法和get方法]

  

五、分类(Category)与类扩展(Extension)/继承(Inherit)的区别

不少面试题常常会比较分类和类扩展的区别,首先咱们要看一下什么是分类,什么是类扩展↓↓↓

分类的格式:

@interface待扩展的类(分类的名称)

@end

@implementation待扩展的名称(分类的名称)

@end

分类的建立:

 

类扩展的格式:

@interface XXX()

//属性

//方法(若是不实现,编译时会报警,Method definition for 'XXX' not found)

@end

类扩展的建立:

1.直接在类文件中添加interface代码块

2.

 

关于类扩展和分类的区别:

一、上面提到的分类不能添加成员变量【虽然能够添加属性 可是一旦调用就会报方法找不到的错误】 (能够经过runtime给分类间接添加成员变量)

  而类扩展能够添加成员变量;

二、分类中的属性不会自动实现set方法和get方法,而类扩展中的属性再转为底层时是能够自动实现set、get方法

三、类扩展中添加的新方法,不实现会报警告。categorygory中定义了方法不实现则没有这个问题

四、类扩展能够定义在.m文件中,这种扩展方式中定义的变量都是私有的,也能够定义在.h文件中,这样定义的代码就是共有的,类扩展在.m文件中声明私有方法是很是好的方式。

五、类扩展不能像分类那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。

  

分类(Categories) 和 继承(Inherit) 可能有时候能够实现相同的功能,但其实两个存在较大的差别,简单介绍一下二者的异同。

好比刚才上面的状况,咱们调用一个方法是,系统的查找顺序是先查找分类,分类没有查找主类,主类没有查找父类(分类没有查找主类是由于分类  主类没有查找父类是由于继承)

有人可能会有疑问,既然是先查找分类再查找主类,这不是和继承中的先查找子类方法,没有的话再去父类查找是同样的么,可否用集成来代替分类呢?

实际上是不行的,虽然先查找分类再查找主类这个流程很像继承(看着像是分类是继承自主类的子类),可是二者有很大区别,主要表如今两点:

一、逻辑方面:二者表明的层级关系不同,继承表明父子关系,分类表明同级关系

好比dog与animal是继承关系,dog与cat是同级关系(dog是animal的子类,dog和cat是同级 都是animal的子类)

若是咱们用继承来代替分类,也就是cat继承自dog,那么不管是可读性仍是逻辑表达上都是难以理解的

二、方法调用上:分类这种方式中,主类能够调用分类的方法,分类也能够调用主类的方法,能够相互调用,

        而继承则不行,子类能够调用父类的方法,可是父类却不能调用子类的方法。

 

 

 

关于对category本质的解读,下面这篇文章介绍的更为详细,包括对源码的查找与解读,对理解category有很棒的帮助(分为三篇文章介绍的)

参考资料
相关文章
相关标签/搜索