iOS 分類的加載原理

一、分類的本質

1传惠、我們先準備一個.m文件包含主類和分類

#import <Foundation/Foundation.h>

//類
@interface MCStudyCate : NSObject
@property(nonatomic, strong) NSString* nickName;
@end

@implementation MCStudyCate
@end

//分類
@interface MCStudyCate (KC)
@property(nonatomic, strong) NSString* cate_pro1;
@property(nonatomic, strong) NSString* cate_pro2;
-(void)cate_instanceMethod1;
-(void)cate_instanceMethod2;
-(void)cate_instanceMethod3;
+(void)cate_classMethod;
@end

@implementation MCStudyCate (KC)
-(void)cate_instanceMethod1{
    NSLog(@"%s", __func__);
}
-(void)cate_instanceMethod2{
    NSLog(@"%s", __func__);
}
+(void)cate_classMethod{
    NSLog(@"%s", __func__);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        MCStudyCate *studyCate = [MCStudyCate alloc];
        NSLog(@"%p",studyCate);
    }
    return 0;
}

使用clang工具將.m文件轉化為.cpp,其對應命令行為$ clang -rewrite-objc main.m -o main.cpp
2、在.cpp文件中有_category_t具體實現(xiàn)睦疫,名字解析

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;
};

并且會拿著這個結構體去實例化具體分類的對象皆辽,其每個字段的解釋具體含義如下

  • name:分類的名稱透绩;
  • cls:分類的所屬類
  • instance_methods:分類中添加的實例方法
  • class_methods:分類中添加的類方法
  • protocols:分類中添加的協(xié)議列表
  • properties:分類中添加的屬性列表

3、接下分類實現(xiàn)的具體結構


image.png

其中的實例方法和類方法我也明確標出棋弥,發(fā)現(xiàn)

  • 3.1 分類的屬性不會自動實現(xiàn)setter/getter方法核偿,但屬性列表中存在
  • 3.2 從cate_instanceMethod3可以看出,分類中申明但未實現(xiàn)的方法不存在于方法列表中

二顽染、分類的加載

1漾岳、分類綁定到類當中

我們知道objc源碼中初始化函數(shù)_objc_init,代碼文件存在于objc-os.mm

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();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

而在_dyld_objc_notify_registermap_images存在如下調用邏輯,大家可以根據源碼查看:
map_images -> map_images_nolock -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategoriesattachCategories 里面就是分類給主類添加屬性粉寞、方法尼荆、協(xié)議的具體實現(xiàn)

其實這里是根據主類+load非懶加載去分析的,不過在一般的懶加載情況下其后面的調用邏輯realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories殊途同歸

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();

    // mlists -> 二維數(shù)組
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

我們去看看方法的添加過程methods.attachLists

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;
            
            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 {
            // 算法 list + list - 分類 
            // list + newlist
            // array
            // newlist  list
            // 1 list -> many lists   1  + 3
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;  // 容量 和
            setArray((array_t *)malloc(array_t::byteSize(newCount))); // 創(chuàng)建一個數(shù)組
            array()->count = newCount; // 4
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

我們在這里我可以發(fā)現(xiàn)

  • 1.1唧垦、 category方法的添加不是覆蓋捅儒,而是將老方法平移到數(shù)組的后方,將新方法插入到前面
  • 1.2振亮、category方法越后加入巧还,就會排到前面去,而方法的查找是順序的坊秸,所以后加入的方法先找到.

2麸祷、分類加載到內存

經過上面的分析我們已經知道分類中的方法、屬性褒搔、協(xié)議如何綁定到類阶牍,但我們還不知道分類是如何加載到內存的,不過在上面的過程中我們知道分類需要在UnattachedCategories才能得到站超,其實在UnattachedCategories中除了attachToClass還有addForClass方法

void addForClass(locstamped_category_t lc, Class cls)
    {
        runtimeLock.assertLocked();

        if (slowpath(PrintConnecting)) {
            _objc_inform("CLASS: found category %c%s(%s)",
                         cls->isMetaClass() ? '+' : '-',
                         cls->nameForLogging(), lc.cat->name);
        }

        auto result = get().try_emplace(cls, lc);
        if (!result.second) {
            result.first->second.append(lc);
        }
    }

通過try_emplace方法得知

  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

這個創(chuàng)建一個存儲桶的結構(這是一個鍵值對形式的結構)荸恕,向里面存內容,結合上面的函數(shù)調用get().try_emplace(cls, lc)得知死相,以clskey融求,lcvalue進行存儲。現(xiàn)在就分析到這算撮,再走下去就跟我們本來的研究方向走偏了生宛。

我們現(xiàn)在看下addForClass方法是哪里調用的

image.png

很清晰的看到,調用addForClass 的方法其實都在objc-runtime-new.mm中的
load_categories_nolock內,順藤摸瓜我們很容易找到其調用邏輯為
addForClass <- load_categories_nolock <- loadAllCategories <- load_images,竟然回到了_objc_init中向dyld注冊的load_images方法
總結一下流程圖為
image.png

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末肮柜,一起剝皮案震驚了整個濱河市陷舅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌审洞,老刑警劉巖莱睁,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡仰剿,警方通過查閱死者的電腦和手機创淡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來南吮,“玉大人琳彩,你說我怎么就攤上這事〔看眨” “怎么了露乏?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長涂邀。 經常有香客問我瘟仿,道長,這世上最難降的妖魔是什么必孤? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任猾骡,我火速辦了婚禮,結果婚禮上敷搪,老公的妹妹穿的比我還像新娘兴想。我一直安慰自己,他們只是感情好赡勘,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布嫂便。 她就那樣靜靜地躺著,像睡著了一般闸与。 火紅的嫁衣襯著肌膚如雪毙替。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天践樱,我揣著相機與錄音厂画,去河邊找鬼。 笑死拷邢,一個胖子當著我的面吹牛袱院,可吹牛的內容都是我干的。 我是一名探鬼主播瞭稼,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼忽洛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了环肘?” 一聲冷哼從身側響起欲虚,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎悔雹,沒想到半個月后复哆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欣喧,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年寂恬,在試婚紗的時候發(fā)現(xiàn)自己被綠了续誉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莱没。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡初肉,死狀恐怖,靈堂內的尸體忽然破棺而出饰躲,到底是詐尸還是另有隱情牙咏,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布嘹裂,位于F島的核電站妄壶,受9級特大地震影響,放射性物質發(fā)生泄漏寄狼。R本人自食惡果不足惜丁寄,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望泊愧。 院中可真熱鬧伊磺,春花似錦、人聲如沸删咱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痰滋。三九已至摘能,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間敲街,已是汗流浹背团搞。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留多艇,地道東北人逻恐。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像墩蔓,于是被迫代替她去往敵國和親梢莽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359