iOS底層原理 15 :類的加載(下)

上一篇中,我們介紹了類是如何從mach-o中加載到內存的,分析了read_images方法,readClass方法, realizeClassWithoutSwift方法,methodizeClass以及attachLists方法

接下來我們探索分類的加載,在探索之前我們需要知道分類的結構

category_t結構

1、探索category_t的底層結構
  • 首先我們在mian.m函數里面去定義一個分類
@interface NSObject (LGB)
@property (nonatomic,strong) NSString * cate_name;

- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
- (void)cate_instanceMethod3;
- (void)cate_instanceMethod4;
+ (void)cate_instanceMethod;
@end

@implementation NSObject (LGB)
- (void)cate_instanceMethod1{
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2{
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod3{
    NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod4{
    NSLog(@"%s",__func__);
}
+ (void)cate_instanceMethod{
    NSLog(@"%s",__func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *obj = [[NSObject alloc]init];
        
        LGPerson *person = [[LGPerson alloc] init];
        
        NSLog(@"%ld - %ld",sizeof(person),class_getInstanceSize(person.class));
    }
    return 0;
}
  • 使用 clang rewrite-objc main.m -o mian.cpp 終端命令去編譯mian.m得到mian.cpp文件

  • mian.cpp里面,我們得到了分類的底層結構, instance_methods表示實例方法列表,class_methods表示類方法列表

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;
};
  • 其中instance_methods表示實例方法列表class_methods表示類方法列表
  • 我們發現了一個問題:查看看_prop_list_t,明明分類中定義了屬性,但是在底層編譯中并沒有看到屬性,如下圖所示,這是因為分類中定義的屬性沒有相應的setget方法,我們可以通過關聯對象來設置
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_NSObject_$_LGB __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_prop_t),
    1,
    {{"cate_name","T@\"NSString\",&,N"}}
};

當然我們通過objc源碼搜索 category_t,我們也能得到分類category_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);
    
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};
2、分類與類拓展的區別

category : 分類,類別

  • 專門用來給類添加新的方法
  • 不能給類添加成員變量
  • 可以添加屬性, 但是只會生成變量的 getter setter 方法的聲明不會生成方法的實現和帶下劃線的成員變量

extension : 類拓展

  • 可以說是特殊的分類,也稱為匿名分類
  • 可以給類添加成員變量和屬性,但是是私有變量
  • 可以給類添加方法,但也是私有方法
3、關聯對象

如果想要給分類有效的添加屬性,需要在重寫的 getter setter方法里面去關聯對象

#import <Foundation/Foundation.h>
@interface NSObject (LGA)
@property (nonatomic,strong) NSString * lga_name;
@end

#import "NSObject+LGA.h"
#import <objc/runtime.h>
@implementation NSObject (LGA)
- (void)setLga_name:(NSString *)lga_name{
    objc_setAssociatedObject(self, "lga_name", lga_name, OBJC_ASSOCIATION_RETAIN);
}

- (NSString *)lga_name{
    return  objc_getAssociatedObject(self, "lga_name");
}
@end

分類的加載

準備: 創建LGPerson的兩個分類:LGA LGB



在分析realizeClassWithoutSwift時,realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加載,中分析了分類的data數據 是如何 加載到類中的,且分類的加載順序是:LGA -> LGB的順序加載到類中,即越晚加進來,越在前面

其中查看methodizeClass的源碼實現,可以發現類的數據分類的數據是分開處理的,主要是因為類在編譯階段,就已經確定好了方法的歸屬位置(即實例方法存儲在類中,類方法存儲在元類中),而分類是后面才加進來的


其中分類需要通過attatchToClass添加到類,然后才能在外界進行使用,在此過程,我們已經知道了分類加載三步驟的后面兩個步驟,分類的加載主要分為3步:

  • 【第一步】分類數據加載時機:根據類和分類是否實現load方法來區分不同的時機
  • 【第二步】attachCategories準備分類數據
  • 【第三步】attachLists將分類數據添加到主類中
【第一步】分類加載的時機

以主類LGPerson + 分類LGA、LGB 均實現+load方法為例

  • load_images
  • loadAllCategories
  • load_categories_nolock
  • attachCategories

拓展:只要有一個分類是非懶加載分類,那么所有的分類都會被標記位非懶加載分類

分類與類的搭配下的加載時機

【情況1】非懶加載類 + 非懶加載分類

  • 類的數據加載是通過_getObjc2NonlazyClassList加載,即ro、rw的操作,對rwe賦值初始化,是在extAlloc方法中
  • 分類的數據加載是通過load_images加載到類中的

其調用路徑為:

  • 非懶加載類路徑:map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,此時的mlists是一維數組,然后走到load_images部分

  • 非懶加載分類路徑:load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此時的mlists是二維數組

【情況2】非懶加載類 + 懶加載分類
非懶加載類 與 懶加載分類的數據加載,有如下結論:

  • 類 和 分類的加載是在read_images就加載數據
  • 其中data數據在編譯時期就已經完成了

【情況3】懶加載類 + 懶加載分類
懶加載類 與 懶加載分類的數據加載是在消息第一次調用時加載的

【情況4】懶加載類 + 非懶加載分類
只要分類實現了load,會迫使主類提前加載,即 主類 強行轉換為 非懶加載類樣式, 加載流程就和情況1是一致的

分類與類的搭配下的加載時機.png

關聯對象的原理

關聯對象設置值流程

首先我們先來了解一下objc_setAssociatedObject方法的四個參數:

  • 參數1:要關聯的對象,即給誰添加關聯屬性
  • 參數2:標識符,方便下次查找
  • 參數3:value
  • 參數4:屬性的策略,即retain,copy,


objc_setAssociatedObject源碼實現:
SetAssocHook.get()是一種接口模式的設計思想,對外的接口不變,內部的邏輯變化不影響外部的調用


進入SetAssocHook,其底層實現是_base_objc_setAssociatedObject,類型是ChainedHookFunction

所以可以理解為SetAssocHook.get()等價于_base_objc_setAssociatedObject

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);//接口模式,對外接口始終不變
}

??等價于

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _base_objc_setAssociatedObject(object, key, value, policy);//接口模式,對外接口始終不變
}

進入_base_objc_setAssociatedObject源碼實現:_base_objc_setAssociatedObject -> _object_set_associative_reference
進入_object_set_associative_reference源碼實現

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    //object封裝成一個數組結構類型,類型為DisguisedPtr
    DisguisedPtr<objc_object> disguised{(objc_object *)object};//相當于包裝了一下 對象object,便于使用
    // 包裝一下 policy - value
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();//根據策略類型進行處理
    //局部作用域空間
    {
        //初始化manager變量,相當于自動調用AssociationsManager的析構函數進行初始化
        AssociationsManager manager;//并不是全場唯一,構造函數中加鎖只是為了避免重復創建,在這里是可以初始化多個AssociationsManager變量的
    
        AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全場唯一

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回的結果是一個類對
            if (refs_result.second) {//判斷第二個存不存在,即bool值是否為true
                /* it's the first association we make 第一次建立關聯*/
                object->setHasAssociatedObjects();//nonpointerIsa ,標記位true
            }

            /* establish or replace the association 建立或者替換關聯*/
            auto &refs = refs_result.first->second; //得到一個空的桶子,找到引用對象類型,即第一個元素的second值
            auto result = refs.try_emplace(key, std::move(association));//查找當前的key是否有association關聯對象
            if (!result.second) {//如果結果不存在
                association.swap(result.first->second);
            }
        } else {//如果傳的是空值,則移除關聯,相當于移除
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();//釋放
}

通過源碼可知,我們總結一下關聯對象的設置流程:

  • 1.創建一個 AssociationsManager 管理類
  • 2.獲取唯一的全局靜態哈希Map:AssociationsHashMap
  • 3.判斷是否插入的關聯值value是否存在
    - 3.1 如果存在走第4步
    - 3.2 如果不存在,關聯對象 -插入空 的流程
  • 4.通過try_emplace方法,并創建一個空的 ObjectAssociationMap 去取查詢的鍵值對
  • 5.如果發現沒有這個 key 就插入一個 空的 BucketT進去并返回true
  • 6.通過setHasAssociatedObjects方法標記對象存在關聯對象即置isa指針的has_assoc屬性為true
  • 7.用當前 policy 和 value 組成了一個 ObjcAssociation 替換原來 BucketT 中的空
  • 8.標記一下 ObjectAssociationMap 的第一次為 false
關聯對象取值流程

我們通過源碼 objc_getAssociatedObject() --> _object_get_ associative_reference(object,key)

_object_get_associative_reference的源碼實現:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};//創建空的關聯對象

    {
        AssociationsManager manager;//創建一個AssociationsManager管理類
        AssociationsHashMap &associations(manager.get());//獲取全局唯一的靜態哈希map
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即獲取buckets
        if (i != associations.end()) {//如果這個迭代查詢器不是最后一個 獲取
            ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查詢器獲取一個經過屬性修飾符修飾的value
            ObjectAssociationMap::iterator j = refs.find(key);//根據key查找ObjectAssociationMap,即獲取bucket
            if (j != refs.end()) {
                association = j->second;//獲取ObjcAssociation
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();//返回value
}

通過源碼可知,主要分為以下部分:

  • 創建一個 AssociationManager管理類
  • 獲取唯一的全局靜態哈希Map:AssociationMap
  • 通過find方法根據 DisguisedPtr找到 AssociationsHashMap 中的 iterator 迭代查詢器
  • 如果這個迭代查詢器不是最后一個 獲取 : ObjectAssociationMap (policy和value)
  • 通過find方法找到ObjectAssociationMap的迭代查詢器獲取一個經過屬性修飾符修飾的value
  • 返回value

關聯對象涉及的哈希Map結構

  • AssociationHashMap里面存放的是ObjectAssociationMap
  • ObjectAssociationMap存放的是ObjectAssociation
  • ObjectAssociation是一種類似字典一樣結構,存放{policy ,value}結構

load_images

load_image的源碼分析:

  • 1.首先找到所有懶加載類的load方法:prepare_load_methods()
  • 2.然后進行調用:call_load_methods()
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        //
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

prepare_load_methods()源碼分析:

  • 獲取非懶加載類,以及繼承鏈上實現了load方法的類
  • 獲取非懶加載分類上實現了load方法的類
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    //_getObjc2NonlazyClassList 獲取非懶加載類的列表
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 遞歸獲取繼承鏈上的類
        schedule_class_load(remapClass(classlist[i]));
    }

    // 獲取非懶加載分類上的list
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

call_load_methods()源碼分析:

  • call_class_loads() 執行類的load方法
  • call_category_loads() 執行分類的load方法
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

load_images流程分析圖:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380