类的加载(二)---分类的加载

在上一篇文章中我们基本了解了类是如何加载到内存的,但是我们仅仅探究了本类的方法加载,而对于分类的情况仍然没有探究,本文就对分类的方法时如何加载到内存的进行探究,另外也探究懒加载和非懒加载分类的加载做分开探究。

分类中有实现load方法即非懒加载分类

分类的本质

我们使用clang命令编译分类,看看他经过编译后变成什么类型。

#import "Animal+Cate1.h"

@implementation Animal (Cate1)

- (void)instanceMethod1 {
    NSLog(@"%s", __func__);
}

- (void)instanceMethod2 {
    NSLog(@"%s", __func__);
}

- (void)instanceMethod3 {
    NSLog(@"%s", __func__);
}

- (void)instanceMethod4 {
    NSLog(@"%s", __func__);
}

@end

经过clang命令

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.6.sdk Animal+Cate1.m -o Animal+Cate1.cpp 

找到有关分类的代码:

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;
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
#pragma warning(disable:4273)

static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[4];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Cate1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    4,
    {{(struct objc_selector *)"instanceMethod1", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod1},
    {(struct objc_selector *)"instanceMethod2", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod2},
    {(struct objc_selector *)"instanceMethod3", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod3},
    {(struct objc_selector *)"instanceMethod4", "v16@0:8", (void *)_I_Animal_Cate1_instanceMethod4}}
};

extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Animal;

static struct _category_t _OBJC_$_CATEGORY_Animal_$_Cate1 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Animal",
    0, // &OBJC_CLASS_$_Animal,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Cate1,
    0,
    0,
    0,
};

可以看到,分类会被编译成一个_category_t类型的结构体,这个结构体包含了分类实例方法、分类类方法、协议、属性等,所以分类本质是一个结构体。

接下来我们就通过探究分类的方法是如何加载到内存中来探究分类的加载。为了可以通过断点调试探究类的加载,我们仍然需要一份可以运行的runtime源码(objc4-7.8.1源码)进行探究。

非懒加载类和非懒加载分类

分类何时加载内存?

我们创建一个Animal类,并且为该类添加两个分类,添加一些方法。在主类和分类都实现load方法。

主类:

@implementation Animal

+ (void)load {
}
- (void)fun1 {
    NSLog(@"%s", __func__);
}
- (void)fun2 {
    NSLog(@"%s", __func__);
}
- (void)fun3 {
    NSLog(@"%s", __func__);
}
- (void)fun4 {
    NSLog(@"%s", __func__);
}
@end

分类1:

@implementation Animal (Cate1)
+ (void)load {
}
- (void)fun1 {
    NSLog(@"%s", __func__);
}
- (void)fun2 {
    NSLog(@"%s", __func__);
}
- (void)cate_func1 {
    NSLog(@"%s", __func__);
}
- (void)cate_func2 {
    NSLog(@"%s", __func__);
}
@end

分类2:

@implementation Animal (Cate2)
+ (void)load {
}
- (void)fun1 {
    NSLog(@"%s", __func__);
}
- (void)fun2 {
    NSLog(@"%s", __func__);
}
- (void)cate_func1 {
    NSLog(@"%s", __func__);
}
- (void)cate_func2 {
    NSLog(@"%s", __func__);
}
@end

在上一次探究过程中,我们探究到methodizeClass的时候,苹果给他的注释是Attach categories,而在methodizeClass方法中,可以看到有调用一个和分类的有关的方法objc::unattachedCategories.attachToClass

void attachToClass(Class cls, Class previously, int flags)
{
    runtimeLock.assertLocked();
    ASSERT((flags & ATTACH_CLASS) ||
           (flags & ATTACH_METACLASS) ||
           (flags & ATTACH_CLASS_AND_METACLASS));

    auto &map = get();
    auto it = map.find(previously);

    if (it != map.end()) {
        category_list &list = it->second;
        if (flags & ATTACH_CLASS_AND_METACLASS) {
            int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
            attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
            attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
        } else {
            attachCategories(cls, list.array(), list.count(), flags);
        }
        map.erase(it);
    }
}

这里有一个关键的方法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();

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

为了验证我的猜测,我在这段代码中添加了一个判断并打上断点,运行代码,看看有没有执行。


58E905F4-A157-4EF8-8E70-D6C14E8CC87B.png

程序成功执行到断点,为了进一步验证,往下执行到method_list_t *mlist = entry.cat->methodsForMeta(isMeta);处,继续执行,查看mlist内容。

(lldb) p *mlist
(method_list_t) $10 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "fun1"
      types = 0x0000000100000ea0 "v16@0:8"
      imp = 0x0000000100000b90 (KCObjc`-[Animal(Cate1) fun1] at Animal+Cate1.m:16)
    }
  }
}

mlist是个method_list_t的数组,此时mlist有4个元素,lldb查看内容。

(lldb) p $10.get(0)
(method_t) $11 = {
  name = "fun1"
  types = 0x0000000100000ea0 "v16@0:8"
  imp = 0x0000000100000b90 (KCObjc`-[Animal(Cate1) fun1] at Animal+Cate1.m:16)
}
(lldb) p $10.get(1)
(method_t) $12 = {
  name = "fun2"
  types = 0x0000000100000ea0 "v16@0:8"
  imp = 0x0000000100000bc0 (KCObjc`-[Animal(Cate1) fun2] at Animal+Cate1.m:20)
}
(lldb) p $10.get(2)
(method_t) $13 = {
  name = "cate_func1"
  types = 0x0000000100000ea0 "v16@0:8"
  imp = 0x0000000100000bf0 (KCObjc`-[Animal(Cate1) cate_func1] at Animal+Cate1.m:24)
}
(lldb) p $10.get(3)
(method_t) $14 = {
  name = "cate_func2"
  types = 0x0000000100000ea0 "v16@0:8"
  imp = 0x0000000100000c20 (KCObjc`-[Animal(Cate1) cate_func2] at Animal+Cate1.m:28)
}

显然,这是Cate1的方法列表,可以确定,此处便是分类添加的地方。

那么这个方法时何时调用的呢,我们可以通过bt查看调用堆栈。

* thread #1, queue = 'com.apple.main-thread', stop reason = step over
  * frame #0: 0x00000001002f61da libobjc.A.dylib`attachCategories(cls=Animal, cats_list=0x00007ffeefbf6b40, cats_count=1, flags=8) at objc-runtime-new.mm:1355:13
    frame #1: 0x00000001002f86f3 libobjc.A.dylib`load_categories_nolock(this=0x00007ffeefbf6bb0, catlist=0x0000000100001038)::$_4::operator()(category_t* const*) const at objc-runtime-new.mm:3095:25
    frame #2: 0x00000001002e0982 libobjc.A.dylib`load_categories_nolock(hi=0x000000010070c2f0) at objc-runtime-new.mm:3114:5
    frame #3: 0x00000001002df89a libobjc.A.dylib`loadAllCategories() at objc-runtime-new.mm:3122:9
    frame #4: 0x00000001002c01b6 libobjc.A.dylib`load_images(path="/private/tmp/objc.dst/usr/lib/libobjc.A.dylib", mh=0x00000001002bb000) at objc-runtime-new.mm:3140:9
    frame #5: 0x000000010000b26c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 418
    frame #6: 0x000000010001efe9 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 475
    frame #7: 0x000000010001d0b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #8: 0x000000010001d0de dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 230
    frame #9: 0x000000010001d154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #10: 0x000000010000b662 dyld`dyld::initializeMainExecutable() + 129
    frame #11: 0x0000000100010bba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
    frame #12: 0x000000010000a227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #13: 0x000000010000a025 dyld`_dyld_start + 37

可以看到,attachCategoriesdyldnotifySingle通知runtime执行load_images时,load_images便会通过loadAllCategories->load_categories_nolock->attachCategories,将分类加载到内存中。

类是如何加载到内存的?

了解了分类是何时开始加载到内存后,那分类的方法mlist是如何加载到内存的呢?

继续往下执行断点,来到mlists[ATTACH_BUFSIZ - ++mcount] = mlist;,ATTACH_BUFSIZ等于64,分类方法被添加到一个数组mlists中,mlists此时是一个二位数组,mlist被放在最后一个。

断点来到prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);,在上次探究已经了解到,这个方法内部会调用fixupMethodList对分类方法进行排序(
mlists + ATTACH_BUFSIZ - mcount内存平移至当前的方法列表mlist

断点往下,来到rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);,这个方法显示时间分类方法添加到rwemethods中,而在上次研究类的加载的时候,在没有分类的情况下,rwe是一个空值,此时我们添加了两个分类,rwe显然不再为空。

可以用lldb输出查看

(lldb) p rwe
(class_rw_ext_t *) $23 = 0x0000000100639a10

那个rwe是如何创建的呢,往上查找可以看到auto rwe = cls->data()->extAllocIfNeeded();,我们进入到extAllocIfNeeded看看它是如何创建的。

class_rw_ext_t *extAllocIfNeeded() {
    auto v = get_ro_or_rwe();
    if (fastpath(v.is<class_rw_ext_t *>())) {
        return v.get<class_rw_ext_t *>();
    } else {
        return extAlloc(v.get<const class_ro_t *>());
    }
}

显然,如果当前datarwe数据,那么就直接返回,如果没有,则通过extAlloc方法,传入ro,创建rwe

查看extAlloc

class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
    runtimeLock.assertLocked();

    auto rwe = objc::zalloc<class_rw_ext_t>();

    rwe->version = (ro->flags & RO_META) ? 7 : 0;

    method_list_t *list = ro->baseMethods();
    if (list) {
        if (deepCopy) list = list->duplicate();
        rwe->methods.attachLists(&list, 1);
    }

    // See comments in objc_duplicateClass
    // property lists and protocol lists historically
    // have not been deep-copied
    //
    // This is probably wrong and ought to be fixed some day
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }

    set_ro_or_rwe(rwe, ro);
    return rwe;
}

这份代码完美验证了苹果在WWDC2020上关于内存优化的介绍,这个方法中,objc::zalloc开辟了一个rwe脏内存,并将传入的ro拿到本类的方法、属性、协议列表,通过attachLists方法添加到rwe中,然后返回rwe

回到分类方法添加的部分,rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);,我们通过rwe的创建知道知道此时rwe是本类的方法的,那么分类的方法是如何添加的呢,本类和分类的方法是如何保存到rwe的方法列表的呢。我们通过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 {
        // 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. 当前list没有元素

这份代码是

else if (!list  &&  addedCount == 1) {
    // 0 lists -> 1 list
    list = addedLists[0];
} 

显然这个时rwe创建时本类方法添加会走的流程,addedLists此时是一个数组指针地址,addedLists[0]便是将本类的方法列表直接赋值给list

listlist_array_tt的成员

class list_array_tt {
...
 private:
    union {
        List* list;
        uintptr_t arrayAndFlag;
    };
 ...
}

2. 当前list有一个元素

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

符合这个情况的便是Animal的第一个分类Cate1的方法列表添加进来,这里重新开辟了一个大小为原有方法列表数量(此时只有本类,数据为1)+新添加方法列表数量(此时只有一个分类添加进来,数据为1)的空间,并将原有的方法列表放在最后的位置,将新添加的方法列表整段复制到新的空间中。

3. 当前list有多个元素

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

符合这种情况的是Animal第二个分类Cate2方法添加,和第二种情况类似,这里也是重新开辟了一个大小为原有方法列表数量(此时为2)+ 新添加方法列表数量(此时为1)的空间,并将旧的方法列表数组平移到新创建数组的末尾,将新添加的方法添加添加到新数组的最前面。

这份代码也完美解释了为什么分类和本类有同名方法的情况下,会优先执行分类的同名方法。

贴上消息慢速查找的一个关键方法,是不是茅塞顿开。

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

小结

对于非懒加载类和非懒加载分类,主类的方法加载流程为:

map_images->_read_images->_read_images->realizeClassWithoutSwift->methodizeClass->prepareMethodLists

分类方法的加载流程为:

load_images->loadAllCategories->load_categories_nolock->attachCategories->prepareMethodLists->attachLists

懒加载类和非懒加载类

我们知道懒加载的类不会在map_images的时候开始加载,只有当给类第一次发送消息时,类才会开始加载到内存中,那如果主类没有实现load方法,而分类中实现了load方法呢?

我们同样用Aniamal类做研究,并在类加载的关键方法realizeClassWithoutSwift和分类的加载关键方法load_categories_nolock加入针对性研究代码打上断点。

6C541811-0A43-4C58-9AFE-2348B8501E37.png
94CCFBC9-3E4B-401F-B1F0-9DE9E6F8CC35.png

运行程序,程序先来到了load_categories_nolock加载分类,往下走断点,来到addForClass

E232907D-D501-41B7-80B3-72DEA2DB8F34.png

查看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的实现:

  // Inserts key,value pair into the map if the key isn't already in the map.
  // The value is constructed in-place if the key is not in the map, otherwise
  // it is not moved.
  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);
  }

这个方法中,通过get()获取一张map,以cls为key,将传入的分类lc封装为BucketT作为vlue保存起来,此时类还没有完成加载。

查看lc的内容

(lldb) p lc
(locstamped_category_t) $0 = {
  cat = 0x00000001000020a0
  hi = 0x0000000100738000
}
(lldb) p $0->cat
(category_t *) $1 = 0x00000001000020a0
  Fix-it applied, fixed expression was: 
    $0.cat
(lldb) p *$1
(category_t) $2 = {
  name = 0x0000000100000e5e "Cate1"
  cls = 0x00000001000022c8
  instanceMethods = 0x0000000100002018
  classMethods = 0x0000000100002080
  protocols = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  _classProperties = 0x0000000000000000
}

Animal类的load_categories_nolock执行次数和非懒加载分类的个数有关。Animal类有两个非懒加载分类,所以load_categories_nolock会进来两次。

执行下个断点,程序来到了realizeClassWithoutSwift,这是加载类的方法,当时不同的是,之前realizeClassWithoutSwiftmap_images一步步调用的,但map_images已经执行完成,此时realizeClassWithoutSwift是由load调起的。

07677C10-F5C4-43A2-A9CB-B19AC78A332F.png

关于realizeClassWithoutSwift加载类的过程我们在上一篇文章已经谈论,我们直接来到methodizeClass看看分类是如何加载到类的。

因为rwe从始至终没有创建,所以methodizeClass流程大体和没有分类的类加载的情况一致,直到objc::unattachedCategories.attachToClass

F5B5C201-3281-407E-BC84-1C64177B9198.png

进入attachToClass的实现,

void attachToClass(Class cls, Class previously, int flags)
{
    runtimeLock.assertLocked();
    ASSERT((flags & ATTACH_CLASS) ||
           (flags & ATTACH_METACLASS) ||
           (flags & ATTACH_CLASS_AND_METACLASS));
    
    auto &map = get();
    auto it = map.find(previously);

    if (it != map.end()) {
        category_list &list = it->second;
        if (flags & ATTACH_CLASS_AND_METACLASS) {
            int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
            attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
            attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
        } else {
            attachCategories(cls, list.array(), list.count(), flags);
        }
        map.erase(it);
    }
}

同样打上断点


D80BAB89-C84A-4B10-85A9-683F21F06B3E.png

在这个方法里,通过get()拿出了我们在load_categories_nolock阶段存放分类的map,并通过类(previously此时为Animal)拿到了对应的BucketT类型的it,并通过it->second拿到存放在it第二个位置的category_list,也就是分类列表,通过attachCategories方法将category_list添加到类的rwe中。之后的流程,便和非懒加载类和非懒加载分类一致。

我们可以通过lldb验证。

(lldb) p list.array()
(const locstamped_category_t *) $15 = 0x0000000100738c70
(lldb) p *$15
(const locstamped_category_t) $16 = {
  cat = 0x00000001000020a0
  hi = 0x0000000100738000
}
(lldb) p $16->cat
(category_t *const) $17 = 0x00000001000020a0
  Fix-it applied, fixed expression was: 
    $16.cat
(lldb) p *$17
(category_t) $18 = {
  name = 0x0000000100000e5e "Cate1"
  cls = 0x00000001000022c8
  instanceMethods = 0x0000000100002018
  classMethods = 0x0000000100002080
  protocols = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  _classProperties = 0x0000000000000000
}

(lldb) p $15[1]
(const locstamped_category_t) $20 = {
  cat = 0x0000000100002168
  hi = 0x0000000100738000
}
(lldb) p $20->cat
(category_t *const) $21 = 0x0000000100002168
  Fix-it applied, fixed expression was: 
    $20.cat
(lldb) p *$21
(category_t) $22 = {
  name = 0x0000000100000e64 "Cate2"
  cls = 0x00000001000022c8
  instanceMethods = 0x00000001000020e0
  classMethods = 0x0000000100002148
  protocols = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  _classProperties = 0x0000000000000000
}

推断正确。

小结

对于懒加载类和非懒加载的加载,类不会因为分类实现了load方法而成为非懒加载类,在map_images时候类并没有加载,而是会因为分类中实现load方法提前在load_images时候完成加载并添加分类(正常是第一次发送消息的时候加载)。

整个加载流程为

load_images->(加载分类)loadAllCategories->prepare_load_methods->(主类加载)realizeClassWithoutSwift->methodizeClass->(开始添加分类)objc::unattachedCategories.attachToClass->attachCategories->attachLists

懒加载类和懒加载分类

这是我们最常见的情况,毕竟我们不是每个类都在本类或分类中实现load方法,同样我们在realizeClassWithoutSwiftload_categories_nolock打上断点探究,当然我们得调用一次Animal的对象方法,否则Animal不会加载。

运行程序后发现断点并不会执行到load_categories_nolock,而是在第一次发送消息时执行realizeClassWithoutSwift,这与我们之前探究的类的加载过程一致,那么分类方法加载加载进来了吗?我们来到methodizeClass断点,看看类的方法列表中有哪些方法。

执行断点到list处,用lldb看看list的信息

1340EF1E-90D2-40C2-B7C2-F6703ADE8D7B.png
(lldb) p *list
(method_list_t) $0 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 12
    first = {
      name = "fun1"
      types = 0x0000000100000e9b "v16@0:8"
      imp = 0x0000000100000c80 (KCObjc`-[Animal(Cate2) fun1])
    }
  }
}

list有12个方法,这正好和Animal本类和分类的方法数量总数一致,且list的第一个方法就是分类的方法,显然此时分类1和分类2的方法都已经在类的data中了,所以我们可以下结论:
懒加载类和懒加载分类在编译时期就已经完成分类方法的添加。

此时的方法是没有经过排序的,需要经过prepareMethodLists排序。

对比排序前后的方法列表

排序.png

可以看到经过排序后,同名方法会依次排序,并且后编译的分类方法会排在前面,而我们在消息慢速查找的时候,二分查找到方法名一致的方法后,为往前再查找方法,所以分类的方法仍然是比本类的优先执行,后编译的分类方法比其他同名方法优先执行。

小结

懒加载类和懒加载分类,分类方法在编译时就已经添加到data中,不需要再通过运行时添加到类中,类会在类第一次消息发送时开始加载。

非懒加载类和懒加载分类

同样在realizeClassWithoutSwiftload_categories_nolock打上断点探究。

和懒加载类懒加载分类的情况一样,load_categories_nolock并没有被调用,断点来到realizeClassWithoutSwift,不同的是,这里的realizeClassWithoutSwiftmap_images->map_images_nolock->_read_images->realizeClassWithoutSwift流程调用的,是在main函数之前就开始调用。

同样来到methodizeClass,看看分类方法是否已经添加

(lldb) p *list
(method_list_t) $0 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 12
    first = {
      name = "fun1"
      types = 0x0000000100000ea0 "v16@0:8"
      imp = 0x0000000100000c90 (KCObjc`-[Animal(Cate2) fun1])
    }
  }
}

同样是12个方法,同样在编译时,分类方法就已经添加到data中。

总结

  1. 非懒加载类会在_read_images阶段就完成加载
  2. 懒加载类一般会在第一次方法调用时开始加载,除非他有非懒加载分类
  3. 非懒加载分类会在load_images阶段开始加载,如果主类是懒加载分类会迫使它提前加载
  4. 懒加载分类会在编译时就添加到类的data中,在类加载时再进行排序
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271