Objective-C 类的加载原理(下)

根据上篇文章的分析,分类的加载有两条线路:

  1. methodizeClass -> attachToClass -> attachCategories
  2. load_images -> loadAllCategories -> load_categories_nolock -> attachCategories

attachCategories最终调用到了attachList

一、attachList方法列表处理

既然最终分类的处理调用到了attachList,那么先看下它的实现逻辑。

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        //之前的oldCount
        uint32_t oldCount = array()->count;
        //newCount 为新加的与之前的和。
        uint32_t newCount = oldCount + addedCount;
        //开辟空间
        array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
        //设置新数组容量
        newArray->count = newCount;
        array()->count = newCount;
        //将旧的数组插入新的数组中。从index addedCount ~ oldcount-1。相当于插入到后面
        for (int i = oldCount - 1; i >= 0; i--)
            newArray->lists[i + addedCount] = array()->lists[i];
        //将新加入的addedLists依次加入新数组,index从0 ~ addedCount-1。
        for (unsigned i = 0; i < addedCount; i++)
            newArray->lists[i] = addedLists[i];
        //执行完上面的操作相当于将新插入的数组放入旧的数组前面。
        //释放旧数组
        free(array());
        //设置新数组
        setArray(newArray);
        validate();
    }
    else if (!list  &&  addedCount == 1) {//本类没有方法的时候走这个逻辑
        // 0 lists -> 1 list
        //一维数组
        list = addedLists[0];
        validate();
    } 
    else {
        // 1 list -> many lists
        Ptr oldList = list;
        //有旧列表,oldCount为1,否则为0。
        uint32_t oldCount = oldList ? 1 : 0;
        //新count为oldCount + addedCount
        uint32_t newCount = oldCount + addedCount;
        //开辟新空间,设置新数组
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        //设置容量
        array()->count = newCount;
        //如果有旧列表,array[endIndex] 最后一个元素为 oldList指针。
        if (oldList) array()->lists[addedCount] = oldList;
        //循环将新加入的放到list前面。从前往后一个一个放。由于addedLists为**类型,所以这里也是地址。
        for (unsigned i = 0; i < addedCount; i++)
            array()->lists[i] = addedLists[i];
        validate();
    }
}
  • 0 lists ->1 list:相当于直接赋值给了list,在本类没有方法并且只有一个分类的时候。内部存储的相当于是元素。(分类多个,但是单个加载的时候第一个也会进入。)
  • 1 list -> many lists:相当于两层结构,新加入的addedLists是一个**结构,分别加入新的数组的前面,如果有旧的列表(主类的方法列表),旧列表作为一个指针放在最后面。这个数组全部都是指针。
  • 2 many lists -> many lists:首先将旧的数组加入新数组的末尾,接着将新加入的addedLists,放在新数组的前面。整个数组存储的是指针。

假设oldList5addedLists2。那么在进行第01步合并后内存中布局如下:

image.png

如果这个时候再继续加入addedLists这次addedCount3,则有以下布局:

image.png

二、分类与类搭配加载情况

由于类的加载与load方法有关,那么分类的加载与load是否有关系呢?那么有4种方式:

  1. 类和分类都实现load方法。
  2. 类实现load,分类不实现。
  3. 类不实现,分类实现load
  4. 类和分类都不实现。

为了方便跟踪,对文中开始说的两条线路关键方法否打上调试断点。

2.1 类和分类都实现load

调用流程:map_images -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_images -> loadAllCategories-> load_categories_nolock -> attachCategories
这个时候类为非懒加载类,在realizeClassWithoutSwift中查看ro的数据,这个时候还没有分类的方法:

image.png

load_images的最开始的地方直接调用了loadAllCategories

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    //didInitialAttachCategories 控制只来一次。  didCallDyldNotifyRegister 在 _objc_init 中赋值
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //加载所有分类
        loadAllCategories();
    }
……
}
  • 控制条件是didInitialAttachCategories只执行一次(由于load_images会执行多次),didCallDyldNotifyRegister_objc_init中注册完回调后设置。

2.1.1 loadAllCategories

loadAllCategories中根据header_info循环调用了load_categories_nolock,核心实现如下:

static void load_categories_nolock(header_info *hi) {
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;
    // processCatlist 是函数的实现
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {//分类数量
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            //将cat和hi包装成 locstamped_category_t
            locstamped_category_t lc{cat, hi};
   ……
            // Process this category.
            if (cls->isStubClass()) {
         ……
            } else {
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {//非懒加载类  实例方法
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {//懒加载类
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {//类方法
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };
    //调用 processCatlist
    //加载分类 __objc_catlist,count从macho中读取。
    processCatlist(hi->catlist(&count));
    //__objc_catlist2
    processCatlist(hi->catlist2(&count));
}
  • processCatlist是函数的实现,最后对processCatlist调用了两次。读取的是__objc_catlist__objc_catlist2,也就是分类数据的获取。暂不清楚__objc_catlist2会在什么情况下生成。
  • count是分类的个数。
  • attachCategories通过clsflags参数区分类和元类。cats_count参数写死的是1locstamped_category_tlc{cat, hi}分类和header_info组成。

断点确定分类信息:


image.png

2.1.2 attachCategories

为了方便分析,去掉了属性和协议相关内容,只保留方法:

//cls :类/元类 cats_list:分类与header_info  cats_count:1 flags: ATTACH_EXISTING | ATTACH_METACLASS
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
……
    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);
    //创建rwe
    auto rwe = cls->data()->extAllocIfNeeded();

    //cats_count 分类数量,这里写死的是1。
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        //分类中方法,通过 isMeta 控制是否类方法。
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {//最大值为64,也就是说64个分类。64个分类后直接存储,之后count从0重新开始计数。
                //排序
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                //为64的时候 mcount 初始化为0。
                mcount = 0;
            }
            //mcount在这里变化 mlists中从后往前存分类方法列表,也就是后加载的分类在前面。
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

            ……
    }

    if (mcount > 0) {
        //排序
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        //将所有分类数据存储。超过64个后会清0。相当于再多了一次结构。二层结构了。由于是从后往前存的,所以将前面空白的区域剔除。
        //mlists + ATTACH_BUFSIZ - mcount 是一个二维指针
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
……
}
  • 首先通过extAllocIfNeeded创建了rwe数据。
  • 由于cats_count传值为1所以这里相当于没有循环。通过methodsForMeta获取分类方法列表。
  • mcount64的时候,重新开始计数。也就是说当cats_count > 64的时候会重新进行计数。但是目前loadAllCategories传递的是1所以不会进入这里的逻辑。那么只有attachToClass会进入这个逻辑了。根据源码也就是attachLists一次性做多传入64个指针数据,多于64个会进行多次赋值,直接走1 list -> many lists逻辑。(待后续研究这个。)
  • 之后调用prepareMethodLists进行排序,然后会调用attachLists将分类数据加入rwe中。

mlists赋值前后做对比:

image.png

2.1.3 extAllocIfNeeded

class_rw_ext_t *extAllocIfNeeded() {
    //获取rwe
    auto v = get_ro_or_rwe();
    if (fastpath(v.is())) {
        return v.get(&ro_or_rw_ext);
    } else {
        //创建rwe
        return extAlloc(v.get(&ro_or_rw_ext));
    }
}

extAllocIfNeeded内部调用是extAlloc创建rwe

class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
    runtimeLock.assertLocked();
    //调用alloc创建空间
    auto rwe = objc::zalloc();
    //设置版本,元类为7,非元类为0。
    rwe->version = (ro->flags & RO_META) ? 7 : 0;
    //获取ro方法列表
    method_list_t *list = ro->baseMethods();
    if (list) {
        //是否深拷贝,跟踪的流程中 deepCopy 为false
        if (deepCopy) list = list->duplicate();
        //将ro的方法列表放入rwe中。
        rwe->methods.attachLists(&list, 1);
    }
    //属性
    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);
    }
    //设置rwe,rwe-ro = ro
    set_ro_or_rwe(rwe, ro);
    return rwe;
}
  • 通过alloc创建rwe
  • romethods数据拷贝到rwe中(这里没有深拷贝,其实也就是链接了个地址而已)。
  • 链接属性和协议。
  • 设置rwerwe中的ro指向ro

接着就进入了开始分析的attachLists方法。

2.1.4 methodsForMeta

method_list_t *methodsForMeta(bool isMeta) {
    if (isMeta) return classMethods;
    else return instanceMethods;
}

判断是否元类,元类返回classMethods,类返回instanceMethods

2.1.5 attachLists 流程

实现多个分类,跟踪调用流程attachLists中对methods变化。当类没有方法的时候会进入0 lists -> 1 list分支:

image.png

接着第二个分类会进入1 list -> many lists分支:

image.png

加载第三个以及更多分类会进入many lists -> many lists分支:

image.png

此时方法列表分布如下:


image.png

在这个流程中attachLists整个流程与内存分布如下:

分类方法合并流程

0 lists -> 1 list的进入逻辑,首先是主类没有方法,分为两种情况
1.分类是在load_categories_nolock过程中加载的(第一个分类会进入)。
2.分类在prepare_load_methods的时候加载,类只有一个分类。

2.1.6 方法存储列表探究

根据上面的分析,当只有主类或者主类没有方法仅有一个分类的情况下,方法列表是存储在list中的,否则存储在array()中。(这里不讨论ro合并的情况,合并了都是存储在ro中也就相当于是在list中)。

2.1.6.1 方法列表的存储逻辑

listarray()声明如下:

typename List

const Ptr *lists;
union {
    Ptr list;
    uintptr_t arrayAndFlag;
};

//通过最后一位进行标记是否有存储指针。
bool hasArray() const {
    return arrayAndFlag & 1;
}
//读取指针地址,最后一位去除
array_t *array() const {
    return (array_t *)(arrayAndFlag & ~1);
}
//最后一位标记
void setArray(array_t *array) {
    arrayAndFlag = (uintptr_t)array | 1;
}

//存储赋值
array()->lists[i] = addedLists[i];
  • listarrayAndFlag只能存在一个。
  • 方法指针列表存储在lists中,arrayAndFlag指向listsarrayAndFlag & 1只是为了标记是否存储了指针数组地址,通过最后一位进行标记。
  • 调用方是rwe->methods.attachLists

2.1.6.2 方法列表的读取逻辑

上面分析清楚了方法列表的存储逻辑,但是显然读取方法列表的时候都是先外层再内层进行二分查找的。那么这其中肯定做了区分或者包装:

  //获取methods
  auto const methods = cls->data()->methods();
  for (auto mlists = methods.beginLists(),
            end = methods.endLists();
       mlists != end;
       ++mlists)
  {
      method_t *m = search_method_list_inline(*mlists, sel);
      if (m) return m;
  }

image.png

可以看到这里的mlistsarrayAndFlag中的元素内容结构是相同的。
那么核心就是在beginLists()endLists做的区别了:

const Ptr* beginLists() const {
    if (hasArray()) {
        //这里不是返回的array,直接返回的lists数组首地址。
        return array()->lists;
    } else {
        //这里进行了&地址操作,相当于包装了一层。
        return &list;
    }
}

const Ptr* endLists() const {
    if (hasArray()) {
        return array()->lists + array()->count;
    } else if (list) {
        return &list + 1;
    } else {
        return &list;
    }
}
  • 直接判断是否存在hasArray从而返回array()->lists&list
  • &list相当于直接包装了一层,所以在for循环中就是同一个数据类型的调用了。
    image.png

2.2 类实现load,分类不实现

调用流程:map_images -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
这个时候没有走attachCategories的逻辑。
realizeClassWithoutSwift中断点验证ro

image.png

这个时候ro中已经有分类中的方法了,也就是说编译阶段就已经将分类的方法合并进类的ro中了。

2.3 类不实现,分类实现load(只一个分类情况)

调用流程:map_images -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
这个时候也没有走attachCategories的逻辑。这里逻辑与类实现load,分类不实现相同。

2.4 类和分类都不实现load

调用流程:没有走入断点流程。这个也能讲的通,此时类为懒加载类。调用类的一个方法继续断点会进入realizeClassWithoutSwift中(慢速消息查找流程进入):

image.png

此时依然是合并到ro中了。

根据以上的验证有如下结论:

  • 类和分类都实现了load方法,在load_images的时候会进入attachCategories流程生成rwe,将分类方法列表拼接在主类之前(都是指针,存储在指针数组中)。
  • 类和分类有任一一个实现load方法,直接将分类的方法列表合并到了类的ro数据中。
  • 类和分类都没有实现load方法,直接将分类的方法列表合并到了类的ro数据中,并且将类的实例化推迟到了第一次发送消息的时候。

2.5 多分类情况

上面分析了类和分类的四种组合情况,那么如果类有多个分类呢?

2.5.1 类实现load,分类部分实现load

类实现了load方法,分类部分实现呢?
按照猜想,没有实现load方法的分类应该直接合并到类的ro中,实现了load方法的分类应该在load_imagesattachCategories流程中与类中的方法放在同一个指针数组中。
创建4个分类,其中1、2个不实现load3、4实现load进行验证,类的ro数据如下:

image.png

这个时候发现ro中只有主类的方法(这个时候事情就不简单了)。跟进attachLists
image.png

image.png

可以看到实现了load与没有实现load方法的分类都会走到attachCategories逻辑。经过验证只要主类实现了load,分类至少实现一个load,则所有分类都不合并进主类ro

结论:类实现了load,分类只要有一个实现load方法,所有分类都不合并进类的ro数据,在load_images的时候在attachCategories中合并放入rwe中。

2.5.2 类不实现load,分类部分实现

前面讨论了类不实现,分类只有一个实现load的情况。那么有多个分类呢?

主类不实现load4个分类中的1个实现load

image.png

会直接进行合并。

主类不实现load4个分类中的2个(更多个逻辑一样)实现load

image.png

这个时候已经没有合并了。还是在load_images的时候加入rwe中(不过走的是load_images->prepare_load_methods->realizeClassWithoutSwift->methodizeClass->attachToClass逻辑)。

那么多添加几个分类只有两个分类实现load的情况:

image.png

同样会走prepare_load_methods的逻辑。

结论:主类不实现load,分类至少2个实现load,则在load_images的时候分类加入rwe中,走的是prepare_load_methodswhy?因为这个时候类无法处理了,所以不能合并。

2.5.2.1 实现两个分类load探究

为什么分类实现至少2load没有走loadAllCategories进行加载,而是走了prepare_load_methods。这和主类实现load有什么不同呢?
这两者的区别就是主类有没有实现load方法。
在主类实现了load方法的时候macho中能找到__objc_nlclslist

image.png

主类没有实现load,则macho中没有__objc_nlclslist(⚠️没有合并ro的情况下则没有,合并了的情况下肯定就有了)。

对于load_images

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    //didInitialAttachCategories 控制只来一次。  didCallDyldNotifyRegister 在 _objc_init 中赋值
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //加载所有分类
        loadAllCategories();
    }

    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        //准备所有load方法
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    //调用 + load方法
    call_load_methods();
}

对于主类没有实现load方法,跟踪流程到load_categories_nolock

image.png

最终会走objc::unattachedCategories.addForClass的逻辑。没有走attachCategories逻辑。

addForClass

void addForClass(locstamped_category_t lc, Class cls)
{
    runtimeLock.assertLocked();
    //没有缓存,进行拼接。
    auto result = get().try_emplace(cls, lc);
    if (!result.second) {
        result.first->second.append(lc);
    }
}

这里相当于将分类相关信息进行了缓存。最终会来到prepare_load_methods的逻辑。

prepare_load_methods

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //添加主类的load方法
        schedule_class_load(remapClass(classlist[i]));
    }

    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
        //实现类
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        //添加分类的load方法。
        add_category_to_loadable_list(cat);
    }
}
  • 通过判断是否有非懒加载的分类从而决定是否调用realizeClassWithoutSwift,在realizeClassWithoutSwift中如果已经实例化过了类,则不会再继续执行后面的逻辑。

realizeClassWithoutSwift此时会来到attachToClass中进入attachCategories的逻辑:

image.png

能不能进入这个流程是it != map.end()控制的,也就是上面addForClass做的处理。这里没有走if分支,因为在realizeClassWithoutSwift中会调用元类的realizeClassWithoutSwift

那么什么情况下会进入if分支?
搜索发现只有在methodizeClasspreviously分支才有调用,但是这个值一直传递的nil,猜测是内部条件控制代码(有可能是切换测试什么的),正常不会进入这个逻辑:

image.png

load_categories_nolock调用attachCategories的过程中cats_count写死的1,而在这里传递的是所有的分类数量。也就与2.1.1中的分析逻辑自洽了。

对于2.1.1中的逻辑,超过64个会从0开始计数,那么底层是怎么存储的呢?创建68个分类验证下逻辑:

image.png

验证结果,第一次进入:
image.png

第二次进入(这里因为HP66在最后一个):
image.png

结论:

  • loadAllCategories中加载分类是一个一个加载,attachCategories传递的是1
  • prepare_load_methodsattachCategories会将分类全部加载。
    这也能理解,因为prepare_load_methods中的调用逻辑是针对单个类的,loadAllCategories是针对所有分类的。

macho中可以看到对应的section信息。即使主类合并了ro,对应分类也会导致出现对应段,不过内容为空:

image.png

2.6 LLVM探究 load 处理

为什么2个以上分类实现load方法,即使主类不实现load方法也能加入rwe中?这块逻辑是在哪里处理的呢? 核心显示是有没有合并ro数据。

因为两个以上load方法类无法处理,load方法在load_imags中都是要调用的,合并后只能调用一个。原理很明显,但是底层是怎么判断处理的呢?

既然合并ro了,那么可以分别对合并与不合并的case进行编译生成macho文件,然后class-dump头文件查看:

image.png

确认是在生成macho文件的时候就已经合并了。显示是llvm阶段处理的事情。(⚠️:目前没有探索出来具体操作步骤在LLVM哪一部分)

那么在llvm源码中核心点肯定在load的处理上。既然是对load的处理,那么在源码中搜索下"load",在RewriteModernObjC中找到了RewriteModernObjC::RewriteObjCCategoryImplDecl->ImplementationIsNonLazy
同理在CGObjCMac.cppCGObjCNonFragileABIMac::GenerateClass->DefinedNonLazyClasses
以及CGObjCNonFragileABIMac::GenerateCategory-> DefinedNonLazyCategories
搜索DefinedNonLazyCategories以及DefinedNonLazyClasses都是对生成macho文件的处理。
class_ro_t的构建过程了,搜索到了class_ro_t的定义,其中有m_baseMethods_ptr,最终通过搜索m_baseMethods_ptr定位到了对方法的操作:

image.png

但是很遗憾并没有找到合并分类与主类方法的逻辑。目前暂不清楚这块逻辑是怎么处理的。⚠️:待后续再详细研究。

至此已经理解清楚了分类中的方法加载逻辑。

小结:

  • 类实现load,分类至少一个实现load,会在load_images过程中通过loadAllCategories将所有分类数据加入rwe中,不会合并进主类ro(类本身是非懒加载类)
  • 类实现load,所有分类不实现load会将分类的方法合并到类的ro中。(类本身是非懒加载类)
  • 类不实现load
    • 分类只有一个并且实现load,分类方法会被合并到类的ro中。(由于合并了ro,类本身也变成非懒加载类)
    • 分类有多个,分类中至少2个实现load。分类方法不会被合并进主类ro中,在load_images的过程中会走prepare_load_methods逻辑将分类方法加入rwe中。(由于没有合并ro,类本身是懒加载类。分类导致它被加载。为什么不合并?因为两个以上load方法类无法处理,load方法在load_imags中都是要调用的,合并后只能调用一个。
  • 类和分类都不实现load,所有分类方法会被合并进类的ro中。(类是懒加载类)
  • 类本身是非懒加载类或者子类是非懒加载类是在map_images过程中实例化的。
  • 类本身是懒加载类,由于自身或者子类的非懒加载分类导致的类被实例化是在load_images过程中的。(ro合并的情况如果分类有load方法会导致类变为非懒加载类)
  • 本质上只有类自身实现load才是非懒加载类。其它情况都是被迫,本质上不属于非懒加载类。
  • 类与分类方法列表会不会合并,取决于load方法的总个数。只有一个或者没有则会合并,否则不合并。
  • 空的分类不会被加载。

既然load影响类和分类的合并,那么直接验证下initialize(注意这里要查看元类的ro数据):

image.png

initialize并不影响分类的合并。

三、类中同名方法的查找

3.1 方法查找逻辑再次分析

在进行慢速消息查找流程的时候会有多层次以及二分查找逻辑,逻辑如下。
getMethodNoSuper_nolock

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
……    
    //获取methods
    auto const methods = cls->data()->methods();
    //循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        //查找方法
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

按照刚才合并分类的逻辑,这里相当于是对外层指针数组的遍历,从而找到方法列表的指针。从beginLists开始遍历,相当于从数组最开始遍历。也就是后加载的分类会被先查找。具体数据存储这里需要区分数组与数组指针(也就是一维数组与二维数组),具体参考2.1.6中的分析。

findMethodInSortedMethodList

template
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    //method list count
    uint32_t count;
     //count >>= 1 相当于除以2。加入count为8
    for (count = list->count; count != 0; count >>= 1) {//7 >> 1 = 3,前一个已经比较了4,这里就完美的避开了4。
        //base是为了配合少查找
        //probe中间元素,第一次 probe = 0 + 8 >> 1 = 4
        probe = base + (count >> 1);
        //sel
        uintptr_t probeValue = (uintptr_t)getName(probe);
        //与要查找的sel是否匹配
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            //查找分类同名sel。
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            //匹配则返回。
            return &*probe;
        }
        //没有匹配
        if (keyValue > probeValue) {//大于的情况下,在后半部分
            //没有匹配的情况下,如果sel在后半部分,这里base指向了上次查找的后面一个位置。
            base = probe + 1;//5
            //count对应减1
            count--;//7 -- 操作为了少做比较,因为已经比较过了
        }
        //在前半部分不进行额外操作。
    }
    
    return nil;
}

while (probe的逻辑应该是为了分类在编译阶段合并ro导致的主类有同名方法而做的处理。

3.2 逻辑验证

由于分类合并进ro与加载时产生rwe是互斥的,所以分为两个逻辑验证。

3.2.1 分类同名方法不合并验证

主类和4个分类都实现instanceMethod方法,主类与任意一个分类实现load方法,并且调用instanceMethod进行验证。

image.png

可以看到开始查找是从分类HP4开始查找的,findMethodInSortedMethodList中断点验证:

(lldb) p probe
(entsize_list_tt::iterator) $15 = (entsize = 24, index = 1, element = 0x0000000100008270)
(lldb) p &*probe
(method_t *) $16 = 0x0000000100008270
(lldb) p $16->name()
(SEL) $17 = "instanceMethod"
(lldb) p $16->imp()

HP4中查找到instanceMethod就返回了。

3.2.2 分类同名合并情况验证

去掉上面验证中主类的load方法的实现,首先在类加载的过程中验证ro数据(需要注意分类load实现最多只能1个,否则不会合并,具体看之前的分析):
首先在类加载的过程realizeClassWithoutSwift中验证ro数据:

image.png

这个时候合并在了一起。但是在方法查找的时候同名方法应该是在一起的,这个时候还不在一起。那么就查看下排序前后。

SEL修正前后:

image.png

方法排序前后:


image.png

可以看到确实对同名方法进行了排序,这也就是为什么findMethodInSortedMethodList内部会在找到方法后继续往前找的原因。
那么按照猜想刚才方法查找的时候HP4应该在数组的最前面。

验证:


image.png

验证符合预期。

四、总结

  • 类与分类的合并:取决于load方法的实现总个数是否存在多个(initialize不影响)。(因为两个以上load方法类无法处理,load方法在load_imags中都是要调用的,合并后只能调用一个。)
    • 合并:0/1load实现,分类方法列表会被合并进主类ro中,后编译的分类同名方法在前。(排序后)
      • 0load实现,类为懒加载类。
      • 1load实现,类为非懒加载类(由于合并,谁实现load已经无所谓了)
    • 不合并:2个及以上load。分类的方法列表会被加载到rwe中。
      • 主类实现loadload_images过程中通过loadAllCategories将分类数据加载到rwe中。
      • 主类没有实现loadload_images过程中通过prepare_load_methods流程最终实例化类和加载分类方法到rwe中。
  • 类的实例化
    • 分类或者子类的分类(load)导致类被实例化是在load_images过程中。类本身是懒加载类,被迫实例化。
    • 子类或者类的load方法导致类被实例化是在map_images中。
    • 其它情况类为懒加载类,在慢速消息查找lookUpImpOrForward过程中实例化。
  • 类的懒加载&非懒加载
    • 懒加载:类、子类、分类、子类分类没有实现load方法的情况,类为懒加载类。
    • 非懒加载
      • 完全非懒加载:类实现load方法(包括分类有load合并ro的情况),此时类为非懒加载类
      • 依赖非懒加载:类本身没有实现load方法
        • 子类实现load方法:由于递归实例化导致父类被实例化,父类父类本质上还是懒加载类,在这里相当于非懒加载类。
        • 分类/子类分类实现load方法:在prepare_load_methods中由于分类是非懒加载分类导致类被初始化,也相当于类变成了非懒加载类。

类和分类加载流程(只包含了方法加载的逻辑):


类的加载流程

你可能感兴趣的:(Objective-C 类的加载原理(下))