iOS Category底層原理詳細研究流程

本文探索源碼 objc4 ( git開源地址 )c++

什麼是 Category ?

CategoryObject-C 2.0 以後添加的語言特性, Category 的主要做用是爲已經存在的類添加方法.git


什麼是 Extension ?

extension 一般咱們稱之爲擴展、延展、匿名分類。extension 看起來很像一個匿名的 category ,可是 extensioncategory 幾乎徹底是兩個東西。和 category 不一樣的是 extension 不但能夠聲明方法,還能夠聲明屬性、成員變量。extension 通常用於聲明私有方法,私有屬性,私有成員變量。github


CategoryExtension 有什麼區別 ?

讓咱們從多個方面來回答這個問題。數組

  1. 表現形式
  • Category 是一個 .h 和一個 .m.bash

  • Extension 是一個 .h . (固然,也能夠在一個類的 .m 中伴生, 寫法就是 @interface *** /.../ @end 就很少說了.) 數據結構

    那麼一樣, 建立時, 選擇對應的類型便可.

  1. 功能機制
1️⃣ : Extension
  • Extension 是一個類的一部分, 它在 編譯期 和頭文件中 @interface , 實現文件中的 @implement 一塊兒造成一個完整類 , Extension 伴隨類的產生而產生 , 亦隨之一塊兒消亡.
  • Extension 能夠添加實例變量.
  • Extension 通常用來隱藏類的私有信息 , 它沒法直接爲系統類提供擴展 , 但能夠縣建立系統類的子類 , 而後添加擴展.

舉個🌰 app

p.objExtension = 28;
NSLog(@"%d",p.objExtension);
複製代碼

如上使用, 發生崩潰.ide

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[LBPerson setObjExtension:]: unrecognized selector sent to instance 0x6000008ed4e0'
複製代碼

NSObject 修改成 LBPerson , 結果正常.函數

2️⃣ : Category
  • Category 是在運行時期決議. 這個時期對象的內存佈局已經肯定了 , 若是再添加實例變量會破壞類的內部佈局 , 這在編譯型語言是災難性的.佈局

  • Category 能夠給系統類添加分類.

  • Category 能夠添加屬性 , 可是並不會生成成員變量和對應的 getter 以及 setter 方法 .

一樣 , 咱們實驗一下 : 🌰

// LBPerson+Category.h

#import "LBPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface LBPerson (Category)
@property (nonatomic,assign) int ageCategory;
@end
複製代碼

使用:

LBPerson * p = [[LBPerson alloc] init];
p.ageCategory = 25;   //分類添加屬性錯誤
NSLog(@"%d",p.ageCategory);
複製代碼

打印結果: 閃退.

-[LBPerson setAgeCategory:]: unrecognized selector sent to instance 0x6000021a72a0

固然, 咱們能夠經過 runtime 設置關聯對象 來解決這個問題 , 下面會仔細闡述.


Category 有什麼用 ?

  • 減小單個文件的體積 . 抽取 , 分離
  • 把不一樣的功能組織到不一樣的 Category
  • 能夠隨意按需加載
  • 聲明私有方法
  • framework 的私有方法公開 ( 在子類中經過引用 , 聲明父類類別後 , 便可調用其未公開的私有方法)

Tips:

請不要亂來:蘋果官方會拒絕使用系統私有API的應用上架,所以即便學會了如何調用私有方法,在遇到調用其它類的私有方法時,要謹慎處理,儘可能用其它方法替代。


Category 底層原理解析

1. 編譯時

說了這麼多 , 終於要開始看看它究竟是個啥了. 打開終端/iterm2 , 編譯轉換成 c++.

clang -rewrite-objc LBPerson+Category.m
複製代碼

找到編譯後的文件, 打開, 搜索 _category_t , 找到結構體定義.

struct _category_t {
	const char *name;  // 類名稱
	struct _class_t *cls;  // 類對象, 在編譯時沒有值
	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; // 分類屬性列表
};
複製代碼

繼續搜索 , 便可找到編譯時 , 這個結構體存放的內容

static struct _category_t _OBJC_$_CATEGORY_LBPerson_$_Category __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"LBPerson",
	0, // &OBJC_CLASS_$_LBPerson,
	0,
	0,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_LBPerson_$_Category,
};
複製代碼

搜索一下 _PROP_LIST_LBPerson_ 會發現咱們定義的屬性. 固然, 歸納一下: 定義的方法, 屬性, 等都會在編譯時存放在對應的字段中 , 編譯後經過 section 段區分標識存放到生成的 Mach-O 可執行文件中.

同時 , 再往下看

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_LBPerson_$_Category,
};
複製代碼

也就是說當在編譯時 , 工程中全部的分類會被存儲到 __DATA 數據段中的 __objc_catlist 這個 section 段中.

以上就是編譯時分類所作的事情 , 分類結構體, 分類的方法 , 以及聲明的屬性 , 和存放 已經完成.

2. 運行時

想要了解分類在運行時是如何加載和處理的. 咱們須要先知道一個概念.

2.1 dyld

什麼是 dyld?

  • dyld 是蘋果的動態加載器 , 用來加載 image ( 注意: 這裏的 image 不是指圖片 , 而是 Mach-O 格式的二進制文件 )

  • 當程序啓動時 , 系統內核會首先加載 dyld , 而 dyld 會將咱們的 APP 所依賴的各類庫加載到內存中 , 其中就包括 libobjc ( OC 和 runtime ) , 這些工做是在 APP 的 main 函數執行以前完成的.

  • _objc_initObject-C - runtime 庫的入口函數 , 在這裏主要是讀取 Mach-O 文件 OC 對應的 Segment section , 並根據其中的數據代碼信息 , 完成爲 OC 的內存佈局 , 以及初始化 runtime 相關的數據結構.

那麼也就是說 , 咱們要去 _objc_init 中一探究竟, 看看分類究竟是怎麼加載 , 如何讀取 , 又是如何釋放的呢 ?

2.2 查看源碼

步驟 1️⃣: 直接打開 objc4 源碼. 找到 _objc_init 入口函數 , 也能夠經過添加符號斷點找進來.

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
複製代碼

前面一些初始化操做就很少說了 . 來看看這個 , 順便提一句.

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
複製代碼
  • map_images : dyldimage 加載進內存時 , 會觸發該函數.
  • load_images : dyld 初始化 image 會觸發該方法. ( 咱們所熟知的 load 方法也是在此處調用 )
  • unmap_image : dyldimage 移除時 , 會觸發該函數 .

那麼咱們就去研究研究 map_images 他加載時 , 咱們的分類到底處理了什麼 .

步驟 2️⃣ : 點擊進去 , 中間過渡方法我就省略了. 直接來到 _read_images 的方法實現.

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
    size_t resolvedFutureClassCount = 0;
    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);

    runtimeLock.assertLocked();

    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) {
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

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

因爲方法過長 , 我就把一些讀取其餘的數據刪除掉了, 只保留了讀取分類部分 , 感興趣的同窗能夠去研究下其餘數據的讀取邏輯.

點擊進去 _getObjc2ClassList

GETSECT(_getObjc2ClassList,           classref_t,      "__objc_classlist");
複製代碼

這個就是咱們剛剛提到的 , 在編譯時 , 分類被加載到這個 section 段中 , 咱們看到讀取是這麼讀的 . 也順便驗證了咱們編譯時的流程 .

那麼也就是說 遍歷全部的分類 , 而後一一添加設置 . 接下來 , 注意到在遍歷中有一個方法

addUnattachedCategoryForClass(cat, cls->ISA(), hi);
複製代碼

看名字大概也猜獲得, 往原類中添加 ( 注意是原類, 不是元類. 也就是原先的類 ).

步驟 3️⃣: 直接進去查看這個方法.

static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader) {
    runtimeLock.assertLocked();

    // DO NOT use cat->cls! cls may be cat->cls->isa instead
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}
複製代碼

這裏主要是 NXMapInsert(cats, cls, list); 這個方法, 其實簡單說一下也就是把當前分類 , 和原類 創建起一個綁定關係 ( 其實就是經過數據結構映射起來 ). 爲下面的事情作準備.

步驟 4️⃣ : 回到遍歷方法中 , 創建起了綁定關係以後 , 下面還有一個方法 , 這個方法是遍歷中最後的方法了 , 那麼它必然是要將分類中的方法添加到原類中的 . 固然這是咱們的猜測 , 咱們帶着這個目的去分析代碼 .

remethodizeClass(cls->ISA());
複製代碼

點擊進去 , 前面開闢空間 , 讀取分類數據和其餘操做就不細說了 , 圖上我都寫進去了 . 其實簡單概括一下就是 開闢了一個空間 , 而後在下面的 prepareMethodLists 中 , 把分類等要添加要元類的數據放進去.

步驟 5️⃣ : 重點 attachCategories 方法前半段咱們已經簡單概述了一下 , 那麼接下來來到

rw->methods.attachLists(mlists, mcount);
複製代碼

點擊進去 :

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;
        /** 拷貝的操做 void *memmove(void *__dst, const void *__src, size_t __len); */
        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]));
    }
}
複製代碼

以上方法就是分類添加方法的核心邏輯 . 簡單來講就是經過 memmovememcpy 這兩個函數 , 以及本來類的方法列表和新分類的方法列表合併 , 添加到原方法的方法列表中.

並且要注意的是:

分類的方法是添加在方法列表數組前面的位置的 .

運行時讀取分類設置總結 :

  • ① : 在運行時, 讀取 Mach-O 可執行文件 , 加載其中的 image 資源時 ( 也就是 map_images 方法中的 _read_images ) , 去讀取編譯時所存儲到 __objc_catlistsection 段中的數據結構 , 並存儲到 category_t 類型的一個臨時變量中.
  • ② : 遍歷這個臨時變量數組, 依次讀取
  • ③ : 將 catlist 和原先類 cls 進行映射
  • ④ : 調用 remethodizeClass 修改 method_list 結構 , 將分類的內容添加到本來類中.

至於屬性和協議, 跟方法的流程是同樣的, 只是存放在不一樣的 section 段中. 這裏就很少贅述了 , 參考如下 :

疑問:

最後有一個疑問: ❓

首先咱們知道 OC 查找方法流程中 , 當查找類方法的方法列表時 , 是採用了一個二分查找的方式的 . 那麼咱們類方法的擴展方法是添加到了原類的方法列表中前面位置的 . 那麼它如何保證外部調用方法時 , 是必定會調用到類方法中的呢 ?

答:

看以下圖:

當方法遍歷二分查找時 , 後面的方法查找到 , 一樣會往前查找一遍看看有沒有同名 ( 方法編號 ) 方法 , 若是有 , 則返回的是前面的方法 . 以此來保證了其優先級順序 , 也就是說 方法列表中前面的方法會有高優先級執行權限 .

從而也就保證了分類實現的目的.


Category 關聯屬性

衆所周知 , Category 中聲明屬性 , 但並不會在 method_list 中生成對應的 settergetter 方法以及對應的實例變量 , 編譯時會有警告.

那麼解決方法你們也都知道 , 就是手動設置關聯屬性 , 能夠理解成手動補上 settergetter 方法 . 具體寫法以下:

#import "LBPerson+Category.h"
#import <objc/runtime.h>

static NSString * ageCategoryKey = @"ageCategoryKey";
@implementation LBPerson (Category)
- (NSString *)ageCategory {
    return objc_getAssociatedObject(self, &ageCategoryKey);
}

- (void)setAgeCategory:(NSString *)age {
    objc_setAssociatedObject(self, &ageCategoryKey, age, OBJC_ASSOCIATION_COPY);
}
複製代碼

爲何分類並不會爲其屬性自動生成對應方法?

看了上面分類結構體的源碼以後 , 其實咱們就很清楚了 . 由於咱們並無看到像類的結構體中的實例變量列表 , 也就是咱們所說的 ivar_list , 所以也就不會有編譯器像類中自動幫咱們作 @synthesize 生成實例變量 ivar 和自動生成settergetter 方法了

那麼咱們就來深刻探討一下 , 關聯屬性究竟是如何實現屬性以及究竟是如何存儲 , 又是如何銷燬的呢 ?

注意 : 一樣是剛剛的代碼 objc4

步驟 1. 直接點擊進入 objc_setAssociatedObject 方法. 過渡方法跳過.

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    //進行內存管理!!!
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        //初始化 HashMap
        AssociationsHashMap &associations(manager.associations());
        //當前對象的地址按位取反(key)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            //<fisrt : disguised_object , second : ObjectAssociationMap>
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                 //<fisrt : 標識(自定義的) , second : ObjcAssociation>
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    //ObjcAssociation
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
複製代碼

具體總結一下這個方法: ( 其實咱們也能大概猜出來這個方法要作什麼 , 無非是和自動生成的 setter 方法相似的操做 )

  • 先根據內存管理語義作對應的引用計數等其餘的操做. ( acquireValue(value, policy) 方法 )

  • 建立了一個管理者 AssociationsManager , 如何建立了一個 AssociationsHashMap , 給這個 map 對象賦值 : ( 將當前對象的地址 按位取反做爲 key , 建立 ObjectAssociationMap 對象做爲 value)

  • ObjectAssociationMap 賦值 : ( 將用戶指定 , 傳進來的 key 做爲 key , 建立 ObjcAssociation 對象做爲 value)

  • ObjcAssociation 對象中存放了用戶指定的值 以及內存管理策略語義 . ( ObjcAssociation(policy, new_value) )

  • 給當前原類的 isa 添加標識 , 以便銷燬時識別是否須要釋放關聯對象.( object->setHasAssociatedObjects() )

步驟 2. 搜索 delloc , 點擊依次進入, 找到:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
複製代碼

那咱們看到析構函數中根據 isa 判斷了當有關聯對象或者其餘標識時 , 會調用 object_dispose 方法. 再點擊 , 依次進入

void *objc_destructInstance(id obj) {
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
複製代碼

依次釋放關聯屬性等等. 那麼也就是說, 咱們的關聯屬性的生命週期 是跟隨原對象走的.

至此 , 關聯對象的原理咱們已經解析完畢 , 總結一下:

Category 關聯屬性總結:

  • 關聯屬性經過本身定義一個新的數據結構 ObjcAssociation 容器來保存用戶設置的內容 以及讀取用戶設置的內容 . 以此達到屬性那種經過方法訪問實例變量的效果.

  • 分類關聯屬性的生命週期同原先類 . 經過在 isa 中標識是否有關聯對象來在 dealloc 中實現銷燬操做.


下篇會繼續 load 方法的探索. 歡迎關注交流.

簡書地址

相關文章
相關標籤/搜索