【iOS】有關(guān)Category我的理解

一、Category(類別,分類)的使用場景

  • 為已經(jīng)存在的類添加方法和屬性
  • 把不同的功能組織到不同的category
  • 模擬多繼承(另外模擬多繼承的還有protocol

1)給類擴展:類別或者繼承

category相比繼承的缺點

1.新的擴展方法還需要父類的實現(xiàn),與原方法重名。若使用類別則會覆蓋。所謂覆蓋其實也不是很準確,具體解釋見原理。
2.category不能添加實例變量,具體解釋見原理。
3.如果一個類有多個category,不同的類別同時定義同一個方法,則調(diào)用哪個不一定,具體解釋見原理。

category相比inherit的優(yōu)點

1.category可以修改原類基礎(chǔ)上擴展類的方法,實現(xiàn)私有類 方法擴展以及一些不能修改源碼類的擴展,也就是說不會入侵原類的代碼結(jié)構(gòu),比繼承更為輕量級。
2.category可以將類的實現(xiàn)分散到多個文件。
3.category可以創(chuàng)建私有方法的前向引用:在類別里聲明一個類的私有方法,不需要實現(xiàn),編譯不會報錯,運行時會調(diào)用私有方法。繼承不能繼承私有方法。
4.category可以向類添加非正式協(xié)議:因為Objective-C里面的類大都是NSObject的子類,所以在NSObject的類別里定義函數(shù),所有對象都能使用

2)添加屬性:runtime

直接在category.h中添加這時候會有warning

?? Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category

類別不會自動幫屬性生成getter/setter方法,也不能生成帶下劃線的實例變量。我們可以用runtime關(guān)聯(lián)對象】去實現(xiàn)category唯一有的類添加新的屬性并生成getter/settet方法。

runtime我們提供了以下方法
參數(shù):  id object:被關(guān)聯(lián)的對象  
          const void *key:關(guān)聯(lián)的key,要求唯一
          id value:關(guān)聯(lián)的對象(如dog)
          objc_AssociationPolicy policy:內(nèi)存管理的策略

//關(guān)聯(lián)對象 
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
 
//獲取關(guān)聯(lián)的對象
id objc_getAssociatedObject(id object, const void *key)
 
//移除關(guān)聯(lián)的對象
void objc_removeAssociatedObjects(id object)

當對象被釋放時,會根據(jù)這個策略來決定是否釋放關(guān)聯(lián)的對象,當策略是RETAIN/COPY時,會釋放(release)關(guān)聯(lián)的對象,當是ASSIGN,將不會釋放。
值得注意的是,我們不需要主動調(diào)用removeAssociated來接觸關(guān)聯(lián)的對象,如果需要解除指定的對象,可以使用setAssociatedObjectnil來實現(xiàn)。

舉個??

我為Student+teach.h類添加了個name屬性,.h文件如下

#import "Student.h"

@interface Student (teach)

//不會生成添加屬性的getter和setter方法,必須我們手動生成
@property (nonatomic, copy) NSString * name;

@end

.m文件如下

#import "Student+teach.h"
//導(dǎo)入頭文件
#import <objc/runtime.h>

@implementation Student (teach)
//定義關(guān)聯(lián)的key
static const char *key = "name";

//getter方法
- (NSString *)name {
    //根據(jù)key獲取關(guān)聯(lián)的值
    return objc_getAssociatedObject(self, key);
}

//setter方法
- (void)setName:(NSString *)name {
    //參數(shù)設(shè)置
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

運行測試代碼

   Student *xiaoming = [[Student alloc] init];
    xiaoming.name = @"xiaoming";
    NSLog(@"%@",xiaoming.name);

打印結(jié)果

2017-05-23 13:49:52.166 RuntimeTest[3009:178563] xiaoming
注:這里區(qū)分一下屬性和成員變量的區(qū)別
1.我聲明了一個屬性name
@property (nonatomic, copy) NSString * name;

會為我生成一個以下劃線為開頭的實例變量_name,也不需要寫@synthesize name;,會自動還是你歌城getter/setter方法。那么在.m文件中可以直接使用_name實例變量,也可以self.name通過屬性的getter/setter設(shè)置。

2.大括號
@interface MyViewController:UIViewController
{
 NSString *name;
}
@end

此時用點語法會提示,編輯器會提示讓你把.改為->。因為點語法是調(diào)用方法,為代碼里沒有g(shù)etter/setter方法。

3.點語法的說明

如果點表達式出現(xiàn)在=左邊,則該屬性的setter的方法將會被調(diào)用。右邊則為getter方法。

4.synthesize的用法

@synthesize name = myname;
會自動生成實例變量myname而不是默認的_name,自動生成getter/setter方法,所以它的作用是指定實例變量名稱。

3)模擬多繼承:消息轉(zhuǎn)發(fā)、delegate和protocol、類別

oc不支持多線程,由于消息機制消息名字查找發(fā)生在運行時而非編譯時,多個基類可能導(dǎo)致二義性問題。模擬多線程有以下幾個方法: 消息轉(zhuǎn)發(fā),delegateprotocol,類別。

類別:

如上??。

delegate和protocol:

委托協(xié)助主體完成操作任務(wù),將需要定制化的操作預(yù)留給委托對象來自定義實現(xiàn),類似子類化主體。
除此之外,可以用作事件監(jiān)聽。

消息轉(zhuǎn)發(fā):

當向某對象發(fā)送消息但是runtime system在當前類和父類中都找不到該方法的實現(xiàn)時,runtime不會立即報錯,而是有以下步驟,

模擬多線程.png

簡述流程:
1.動態(tài)方法解析:向當前類發(fā)送resolveInstanceMethod:的信號,檢查是否像這個類動態(tài)添加了方法。
相關(guān)鏈接:http://blog.csdn.net/haishu_zheng/article/details/12873151
2.快速消息發(fā)送:檢查這個類是否實現(xiàn)了forwardingTargetForSelector:方法,是吸納了就調(diào)用。若返回值對象非空nil或者非self,則向該返回對象重新發(fā)送信息。
利用快速消息轉(zhuǎn)發(fā)forwardingTargetForSelector:方法重寫,結(jié)合【類別】或者【類型強轉(zhuǎn)】可以實現(xiàn)多繼承方法

思考問題:如果將其類型轉(zhuǎn)成`id ,也可以編譯通過,并實現(xiàn)轉(zhuǎn)發(fā)。可是會帶來什么隱患呢?

3.標準信息發(fā)送:runtime發(fā)送methodSignatureForSelector:消息獲取Selector對應(yīng)的方法簽名。返回值非空則通過forwardInvocation:轉(zhuǎn)發(fā)消息,返回值為空則向當前對象發(fā)送doesNotRecognizeSelector:消息,程序崩潰退出。
標準消息轉(zhuǎn)發(fā)需要重寫 methodSignatureForSelector: 和 forwardInvocation: 兩個方法即可。

二、category的原理和特性

1)原理

runtime的源碼里可以找到_read_images函數(shù):

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
...
    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
...
}

里面提到了兩個比較重要的函數(shù):

  1. addUnattachedCategoryForClass()
  2. remethodizeClass()
  • addUnattachedCategoryForClass()主要是構(gòu)建class和category的哈希表,如果不存在則生成,如果存在則重新構(gòu)建,主要是class到category的一個映射關(guān)系。
  • remethodizeClass調(diào)用了attachCategories方法,這個方法里面把category里的方法,屬性和協(xié)議列表都添加到相應(yīng)的類里面,從本質(zhì)來說,調(diào)用類的方法和分類的方法都是一樣的。

2)category重名函數(shù)覆蓋

在看源碼的attachLists方法

 void attachLists(List* const * addedLists, uint32_t addedCount) {
      if (addedCount == 0) return;

      if (hasArray()) {
          // many lists -> many lists
          uint32_t oldCount = array()->count;
          uint32_t newCount = oldCount + addedCount;
          setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
          array()->count = newCount;
          memmove(array()->lists + addedCount, array()->lists, 
                  oldCount * sizeof(array()->lists[0]));
          memcpy(array()->lists, addedLists, 
                 addedCount * sizeof(array()->lists[0]));
      }
      else if (!list  &&  addedCount == 1) {
          // 0 lists -> 1 list
          list = addedLists[0];
      } 
      else {
          // 1 list -> many lists
          List* oldList = list;
          uint32_t oldCount = oldList ? 1 : 0;
          uint32_t newCount = oldCount + addedCount;
          setArray((array_t *)malloc(array_t::byteSize(newCount)));
          array()->count = newCount;
          if (oldList) array()->lists[addedCount] = oldList;
          memcpy(array()->lists, addedLists, 
                 addedCount * sizeof(array()->lists[0]));
      }
  }

第一個判斷語句中C函數(shù) memmove(dest, src, len)memcpy(des, src, size)完成了將舊函數(shù)數(shù)組后移空出位置,把新的函數(shù)數(shù)組添加到原數(shù)組空位的過程。
下一個判斷語句意思為如果list沒有值,那么直接賦值給它
最后一個判斷語句是找到多個函數(shù)名一樣的,進行插入

綜上所述,category里新增的和原來類重名的函數(shù)將會被添加在原函數(shù)列表的前面,并不是覆蓋。如果想要調(diào)用原函數(shù)的方法,只需要便利函數(shù)列表,調(diào)用最后一個來執(zhí)行。這就是category重名函數(shù)覆蓋,并且同個類多個category重名調(diào)用不確定的原因。(有些文章指出執(zhí)行順序跟編譯順序有關(guān))

3)Category和Extension的區(qū)別

1、Extension在編譯期決議,它就是類的一部分,在編譯期和頭文件里的@interface以及實現(xiàn)文件里的@implement一起形成一個完整的類,它伴隨類的產(chǎn)生而產(chǎn)生,亦隨之一起消亡。Extension一般用來隱藏類的私有信息,你必須有一個類才能為這個類添加Extension,所以你無法為系統(tǒng)的類比如NSString添加Extension
2、Category則完全不一樣,它是在運行期決議的。
3、Extension可以添加屬性、成員變量,而Category一般不可以。

總之,就CategoryExtension的區(qū)別來看,Extension可以添加實例變量,而Category是無法添加實例變量的。因為Category在運行期,對象的內(nèi)存布局已經(jīng)確定,如果添加實例變量就會破壞類的內(nèi)部布局,這對編譯型語言來說是災(zāi)難性的。

4)Category不能添加成員變量

因為方法和屬性并不“屬于”類實例,而成員變量“屬于”類實例。我們所說的“類實例”概念,指的是一塊內(nèi)存區(qū)域,包含了isa指針和所有的成員變量。所以假如允許動態(tài)修改類成員變量布局,已經(jīng)創(chuàng)建出的類實例就不符合類定義了,變成了無效對象。但方法定義是在objc_class中管理的,不管如何增刪類方法,都不影響類實例的內(nèi)存布局,已經(jīng)創(chuàng)建出的類實例仍然可正常使用。

第一次發(fā)簡書文章,如果所述有錯誤或者不明確的地方,望大家不吝賜教?? ~

參考:
美團技術(shù)文檔: http://tech.meituan.com/DiveIntoCategory.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1.項目經(jīng)驗 2.基礎(chǔ)問題 3.指南認識 4.解決思路 ios開發(fā)三大塊: 1.Oc基礎(chǔ) 2.CocoaTouch...
    陽光的大男孩兒閱讀 5,042評論 0 13
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,217評論 30 472
  • 一 類別的簡介 在開發(fā)中有時會用到Category,類別有三個作用: (1)可以將類的實現(xiàn)分散到多個不同文件或多個...
    々莫等閑々閱讀 452評論 0 0
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • 本文目錄一.category簡介二.category和extension三.category真面目四.catego...
    boundlessocean閱讀 1,299評論 1 8