iOS底層原理總結 -- 利用Runtime原始碼 分析Category的底層實現

小李小李一路有你發表於2019-11-12

窺探iOS底層實現--OC物件的本質(一)

窺探iOS底層實現--OC物件的本質(二)

窺探iOS底層實現--OC物件的分類:instance、class、meta-calss物件的isa和superclass

窺探iOS底層實現-- KVO/KVC的本質

iOS底層原理總結 -- 利用Runtime原始碼 分析Category的底層實現 ...

前言:

本文總結了一下Category中的內部去實現部分,程式碼部分較多,新增了註釋,閱讀起來可能比較枯燥。但是請大家務必堅持讀完。會有更多的收貨。

本文中涉及到Class類物件的內部結構的知識,請參考文章 窺探iOS底層實現--OC物件的分類:instance、class、meta-calss物件的isa和superclass

思考:

  1. Category的實現原理?
  2. 為什麼Category的中的方法會優先呼叫?
  3. 延伸問題 - 如果多個分類中都實現了同一個方法,那麼在呼叫該方法的時候會優先呼叫哪一個方法?
  4. 擴充套件和分類的區別?

Category 基本實現

首先 看一下分類程式碼程式碼的實現 可選擇性跳過

///> main.h
int main(int argc, const char *argv[]){
	@autoreleasepool{
	  Person *person = [[Person alloc] init]
  	[person run];
	  [person test];
  	[person eat];
	}
	return 0
}

///> person
@interface Person: NSObject
@end
@implementation Person
- (void)run{
  Nslog(@"run")
}
@end

///> person+test
@interface Person(test)
- (void)test;
@end
@implementation Person(test)
- (void)test{
  Nslog(@"test")
}
@end
  
///> person+Eat
@interface Person(eat)
- (void)eat;
@end
@implementation Person(eat)
- (void)eat{
  Nslog(@"eat")
}
@end
複製程式碼

分類的底層結構體 編譯完畢之後

編譯完畢的時候 一開始程式執行的時候 所有分類的方法 一開始都存放在 結構體中(每一個分類都有一個新的結構體物件),

編譯完畢之後 category存放在 結構體category_t中 並沒有合併到 原始類中 每一個分類都會生成catrgory_t的結構體, 在執行時的時候才會將分類中的方法、協議、屬性等 合併到原始的類中去。

下面是原始碼觀看的過程在每一步都給出了註釋, 有點枯燥,但是看完之後會很受益。

分類程式碼 C\C++原始碼分析

利用:

xcrun -sdk iphoneos clang -arch arm64 -  OC原始檔 -o 輸出的CPP檔案
複製程式碼

命令可以檢視轉化為C\C++程式碼。會有生成一個xxx.cpp的檔案就是我們想要的檔案

可以將其拖入到xcode中, 方便搜尋

接下來直接搜尋 category_t 得出如下結構體 我已經將註釋放在後面了

struct _category_t{
  const char *name;   ///> 分類的名字
  struct _class_t *cls;   ///> class
  const struct _method_list_t *instance_methods;   ///> 例項方法列表
	const struct _method_list_t *class_methods;   ///>  類方法列表
	const struct _protocol_list_t *protocols;    ///>  協議
	const struct _prop_list_t *properties;   ///> 屬性
}
複製程式碼

每建立一個類都會 根會根據如下方法建立一個category_t的結構體

static struct _category_t OBJC_$CATEGORY_PERSON_$_Test __attribute__ ((userd, section("__DATA,__objc_const")))={
  ///>  屬於那個類的分類
  "Person",  
  ///> class
  0,
  ///>  物件方法列表
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$Test,
  ///>  類方法列表
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$Test,
  ///>  協議列表 
  0, // (const _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_PERSON_$_Test,	
  ///>  屬性列表
  0, // (const _prop_list_t *)&_OBJC_$_PROP_LIST_PERSON_$_Test,
}
複製程式碼

接下來檢視一下 Runtime的原始碼是怎麼將分類合併的,

Runtime原始碼分析

​ 首先下載Runtimed的原始碼。 ------ 這裡用xcode開啟

  1. 搜尋 "catrgory_t {"

    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;
        // Fields below this point are not always present on disk.
        struct property_list_t *_classProperties;
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    };
    複製程式碼

    可以看到 Runtime中的結構和上面的category_t的結構類似。

  2. Runtime的程式入口檔案為objc-os.mm 檔案,

  3. 我這裡直接到 有關Category的程式碼部分 在objc-runtime-new.mm檔案中 搜搜Discover categories. 的註釋程式碼

      // Discover categories. 
       for (EACH_HEADER) {
            /**
             catlist 是一個二維陣列,
             每一個分類都會建立一個category_t的結構體
             這裡的二維陣列放了兩個分類結構體的內容 如程式碼中的 eat和test
             catlist = [[],[]]
             */
            category_t **catlist = 
                _getObjc2CategoryList(hi, &count);
            bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    
            ///> 將每一個陣列中的內容遍歷
            for (i = 0; i < count; i++) {
                ///> 獲取 單獨的category_t結構體
                category_t *cat = catlist[i];
                ///> 重新對映class 取出結構體的class
                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);
                    }
                }
            }
        }
    複製程式碼

    以上程式碼中找到了 核心的方法: remethodizeClass 使用了兩次 , 重新組織類的方法和元類的方法

  4. command+單機,進入

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
        runtimeLock.assertWriting();
        isMeta = cls->isMetaClass();
        // Re-methodizing: check for more categories
        if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
            if (PrintConnecting) {
                _objc_inform("CLASS: attaching categories to class '%s' %s", 
                             cls->nameForLogging(), isMeta ? "(meta)" : "");
            }
            
         		///> 附加分類的程式碼呼叫 , 傳入了 類物件、分類。
    	      ///> cls: [Person class]
    				///> cats: [category_t(test), category_t(eat)]
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    複製程式碼
  5. command 進入 attachCategories(cls, cats, true /flush caches/); 方法

    ///> cls: [Person class]
    ///> cats: [category_t(test), category_t(eat)]
    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches){
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        ///> 是否是元類物件
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations
        ///> malloc 分配記憶體
        ///> 方法陣列  二維陣列  eg:[[method_t,method_t], [method_t,method_t]]
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
          
        ///> 屬性陣列 eg:[[property_t,property_t], [property_t,property_t]]
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
      
        ///> 協議陣列 eg:[[protocol_t,protocol_t], [protocol_t,protocol_t]]
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first
        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, entry.hi);
            ///> 將每一個分類的協議列表陣列放在 上方定義的二維陣列當中!
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            ///> 將每一個分類的屬性列表陣列放在 上方定義的二維陣列當中!
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
    
        ///> 取出類物件中的資料
        auto rw = cls->data();
    
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        /**
         核心程式碼:
         rw: 類物件結構體中 有一個erw的結構,
         這一步驟就是將資料合併到類物件的 rw結構中去  請參照文章:
         將所有的分類的物件方法  附加到類物件中去!
         也就是 在執行d時的時候講 分類的資料合併到了原始的類物件中!!
         */
        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);
    }
    複製程式碼
  6. command 進入 rw->methods.attachLists(mlists, mcount); 方法中

        /**
         addedLists: [[method_t, method_t],[method_t, method_t]]
         addedCount: 2      s二維陣列的數量
         */
        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;
                ///> realloc 重新分配記憶體   newCont
                ///> 為了合併分類中的陣列 擴充原來陣列的大小
                ///> 需要重新分配記憶體
                setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
                array()->count = newCount;
                
                /*
                 記憶體移動
                    array()->lists 原來的方法列表
                    addedCount 分類的陣列的count
                 
                    將原來的方法列表挪動到新的位置,
                    (array()->lists + addedCount  addedCount是挪動的位數
                    相當有將原來的方法放到了最後!
                 */
                memmove(array()->lists + addedCount, array()->lists, 
                        oldCount * sizeof(array()->lists[0]));
                
                /*
                 記憶體挪動 拷貝
                 array()->lists 原來的方法列表
                 addedLists 傳進來的分類list
                 
                 上面的方法已經將 類的方法列表做到了擴充  並且類原始帶的方法列表向後挪動的 addedCount的位數
                 為的就是 將傳入的分類的方法列表 拷貝到array()->lists(原始方法列表)的最前面,
                 
                 所以 這就是分類的資料會優先呼叫的 原因
                 */
                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]));
            }
        }
    複製程式碼

總結分類的一些問題

由原始碼分析我們可以得知,

  1. Category的實現原理?

    原理:底層結構是結構體 categoty_t 建立好分類之後分兩個階段:

    1. 編譯階段:

      將每一個分類都生成所對應的 category_t結構體, 結構體中存放 分類的所屬類name、class、物件方法列表、類方法列表、協議列表、屬性列表。

    2. Runtime執行時階段:

      將生成的分類資料合併到原始的類中去,某個類的分類資料會在合併到一個大的陣列當中(後參與編譯的分類會在陣列的前面),分類的方法列表,屬性列表,協議列表等都放在二維陣列當中,然後重新組織類中的方法,將每一個分類對應的列表的合併到原始類的列表中。(合併前會根據二維陣列的數量擴充原始類的列表,然後將分類的列表放入前面)

  2. 為什麼Category的中的方法會優先呼叫?

    如上所述, 在擴充陣列的時候 會將原始類中擁有的方法列表移動到後面, 將分類的方法列表資料放在前面,所以分類的資料會優先呼叫

  3. 延伸問題 - 如果多個分類中都實現了同一個方法,那麼在呼叫該方法的時候會優先呼叫哪一個方法?

    在多個分類中擁有相同的方法的時候, 會根據編譯的先後順序 來新增分類方法列表, 後編譯的分類方法在最前面,所以要看 Build Phases --> compile Sources中的順序。 後參加編譯的在前面。

  4. 擴充套件和分類的區別

    擴充套件@interface 是匿名分類, 不是分類。 就是屬性新增 在編譯的時候就加入到了類中

    category在runtime中才合併的。


參考:

  1. Runtime原始碼地址:Source Browser:OBJective-c原始碼找到objc4,下載版本號最大是最新的原始碼
  2. MJ老師底層相關視訊

再次感謝!!


如有錯誤之處還請各位大神指出!!

相關文章