分類與擴充

weixin_33895657發表於2018-12-06

分類(Category)

Category是Objective-C 2.0之後新增的語言特性,分類、類別其實都是指的Category。Category的主要作用是為已經存在的類新增方法。

我們用分類都做了什麼事

  • 宣告私有方法(我們定義一個分類,只把標頭檔案放在對應宿主檔案的.m當中,滿足私有方法的宣告和使用,對外不暴露)
  • 分解體積龐大的類檔案(比如一個類的功能特別複雜,那麼我們可以按照功能來對類當中的方法進行分類,把同一個功能的方法放在一個分類檔案當中)
  • 把Framewrok的私有方法公開化

特點

  • 執行時決議(我們在編寫好分類的檔案後,它並沒有把分類的內容新增到宿主類上面,這個時候宿主類中還沒有分類中的方法,而是在執行時通過runtime把分類中新增的內容真實的新增到宿主類上面)
  • 可以為系統類新增分類(比如我們經常會用到獲取UIView座標的分類方法,就是在對UIView新增了分類)

分類中可以新增哪些內容

  • 例項方法
  • 類方法
  • 協議
  • 屬性(其實只是宣告瞭get、set方法,並沒有新增例項變數,可以通過關聯物件來新增例項變數)

我們來看看分類的底層結構

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;//例項屬性列表
   
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

我們通過分類的結構就可以更好的印證我們能夠為分類新增哪些內容,這裡並沒有例項變數的成員結構.

原始碼分析

objc-runtime-new.mm
這裡主要分析例項方法的新增邏輯.
我們從 remethodizeClass 這個方法開始分析

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;
    runtimeLock.assertWriting();
//判斷當前類是否是元類物件,也就是判斷新增的方法是例項方法還是類方法
    isMeta = cls->isMetaClass();
    // 獲取cls中未完成整合的所有分類
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
      //將cats拼接到cls上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

來看看attachCategories這個拼接分類到宿主類的方法

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;//如果當前類沒有分類的話直接返回
    if (PrintReplacedMethods) printReplacements(cls, cats);
    //判斷當前類是否是元類物件
    bool isMeta = cls->isMetaClass();

    /*
二維陣列 [[method_t,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));//協議列表

    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);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
          //協議列表新增規則,同方法列表新增規則
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    // 獲取宿主類當中的rw資料,其中包含宿主類的方法列表資訊
    auto rw = cls->data();
    // 主要是針對 分類中有關於記憶體管理相關方法情況下的一些特殊處理
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    /*
    methods代表類的方法列表,attachLists方法表示將mcount個元素的mlists(方法列表)拼接到rw(宿主類)的methods(方法列表)上
    */
    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);
}

演示圖例

1915113-815e61f7dd872b25.png
分類方法新增到物件方法列表中
新增分類的流程
  • 獲取分類列表的count,然後原來的類方法列表記憶體移動count
  • 分類列表記憶體拷貝到原來的類方法列表的前方
  • 同樣的方法,優先呼叫分類的方法
  • 分類具有同樣的方法,根據編譯順序決定,取最後編譯分類的方法列表

擴充(Extension)

我們用擴充都做了什麼事

  • 宣告私有屬性
  • 宣告私有方法
  • 宣告私有成員變數

擴充的特定

  • 編譯時決議
  • 只以宣告存在,沒有具體實現,寄生於宿主類.m中
  • 不能為系統類新增擴充套件

分類、擴充套件區別

  • 分類中原則上只能增加方法(能新增屬性的的原因只是通過runtime解決無setter/getter的問題而已)
  • 類擴充套件不僅可以增加方法,還可以增加例項變數(或者屬性),只是該例項變數預設是@private型別的(用範圍只能在自身類,而不是子類或其他地方)
  • 類擴充套件中宣告的方法沒被實現,編譯器會報警,但是類別中的方法沒被實現編譯器是不會有任何警告的。這是因為類擴充套件是在編譯階段被新增到類中,而類別是在執行時新增到類中。
  • 類擴充套件不能像分類那樣擁有獨立的實現部分(@implementation部分),也就是說,類擴充套件所宣告的方法必須依託對應類的實現部分來實現。

相關文章