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

窺探iOS底層實現--OC對象的本質(一)c++

窺探iOS底層實現--OC對象的本質(二)objective-c

窺探iOS底層實現--OC對象的分類:instance、class、meta-calss對象的isa和superclassshell

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

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

前言:

本文總結了一下Category中的內部去實現部分,代碼部分較多,添加了註釋,閱讀起來可能比較枯燥。可是請你們務必堅持讀完。會有更多的收貨。app

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

思考:

  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
複製代碼

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

編譯完畢的時候 一開始程序運行的時候 全部分類的方法 一開始都存放在 結構體中(每個分類都有一個新的結構體對象),ui

編譯完畢以後 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老師底層相關視頻

再次感謝!!


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

相關文章
相關標籤/搜索