分類(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有很棒的幫助(分爲三篇文章介紹的)

參考資料
相關文章
相關標籤/搜索