iOS-类的加载(下)

2020-10-28  本文已影响0人  Y丶舜禹

前言

在之前的文章iOS-类的加载(上),我们探究了是如何加载到内存中以及懒加载类非懒加载类,这篇文章下我们将探寻一下分类的加载情况。

分类的本质

在main文件中新建一个ZGPerson分类,

@interface ZGPerson (ZG)

@property (nonatomic, strong) NSString *cate_name;
@property (nonatomic, assign) int cate_age;

- (void)cateA_instanceMethod1;

- (void)cateA_instanceMethod3;

- (void)cateA_instanceMethod2;

- (void)cateA_classMethod3;

@end

@implementation ZGPerson (ZG)


+ (void)load{
    
}

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

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

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

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

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...

        ZGPerson *person = [ZGPerson alloc];
        NSLog(@"%p",person);
    }
    return 0;
}

我们可以通过下面几种方式探寻分类本质

  • 通过clang
  • 通过Xcode文档搜索Category
  • 通过objc源码搜索 category_t
通过clang

通过终端命令clang -rewrite-objc main.m -o main.cpp 查看底层编译,即 生成main.cpp,打开。

ZGPerson分类
_category_t

可见,分类的本质是_category_t

通过Xcode文档搜索Category

可以通过Xcode文档搜索Category文档

Category
通过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;
    }
};

其中

  • name:分类的名字
  • cls:对应的原类
  • instanceMethods:实例方法列表
  • classMethods:类方法列表
  • protocols:协议列表
  • instanceProperties:实例属性列表

分类加载流程

首先,创建两个分类ZGPerson + ZGAZGPerson + ZGB

@interface ZGPerson (ZGA)


@property (nonatomic, strong) NSString *cate_name;
@property (nonatomic, assign) int cate_age;

- (void)cateA_instanceMethod1;

- (void)cateA_instanceMethod3;

- (void)cateA_instanceMethod2;

- (void)cateA_classMethod3;

@end

@implementation ZGPerson (ZGA)

+ (void)load{
    
}

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

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

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

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

@end

ZGPerson (ZGB)

@interface ZGPerson (ZGB)


@property (nonatomic, strong) NSString *cate_name;
@property (nonatomic, assign) int cate_age;

- (void)cateA_instanceMethod1;

- (void)cateA_instanceMethod3;

- (void)cateA_instanceMethod2;

- (void)cateA_classMethod3;

@end

@implementation ZGPerson (ZGB)

+ (void)load{
    
}

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

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

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

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

@end

在上一篇iOS-类的加载(上)文章中分析了类的加载流程:realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加载,其中分析了分类的data数据 时如何 加载到中的,分类的加载顺序是:是编译时加载进内存的顺序决定,并且越晚加进来,越在前面

methodizeClass的源码实现中,我们发现类的数据分类的数据是分开处理的

static void methodizeClass(Class cls, Class previously)
{
    .....省略一些代码

    // Attach categories. 链接分类
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

#if DEBUG
    // Debug: sanity-check all SELs; log method list contents
    for (const auto& meth : rw->methods()) {
        if (PrintConnecting) {
            _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(meth.name));
        }
        ASSERT(sel_registerName(sel_getName(meth.name)) == meth.name); 
    }
#endif
}

那么为什么是这样处理的呢?原来是因为在编译阶段,类就已经被确认好了内存位置,是clean memory(即实例方法存储在中,类方法存储在元类中),而分类是后面才加进来的

分类通过attatchToClass添加到本类,下面我们查看attatchToClass源码

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

        
        const char *mangledName  = cls->mangledName();
        const char *ZGPersonName = "ZGPerson";

        if (strcmp(mangledName, ZGPersonName) == 0) {
            bool kc_isMeta = cls->isMetaClass();
            auto kc_rw = cls->data();
            auto kc_ro = kc_rw->ro();
            printf("%s: 这个是我要研究的 %s \n",__func__, ZGPersonName);

        }
        
        
        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 方法(分类链接方法),这是为什么呢?

attachCategories源码
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
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();// rwe 初始化 --> 要对copy出的干净内存进行方法插入操作
    // 自己加的调试代码
    const char *mangledName  = cls->mangledName();
    const char *ZGPersonName = "ZGPerson";

    if (strcmp(mangledName, ZGPersonName) == 0) {
        bool kc_isMeta = cls->isMetaClass();
        auto kc_rw = cls->data();
        auto kc_ro = kc_rw->ro();
        if (!kc_isMeta) {
            printf("%s: 这个是我要研究的 %s \n",__func__,ZGPersonName);
        }
    }

    // 遍历 分类数据的准备(method property protocol)
    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);
}

可以看出,这确实是链接分类的方法,但是为什么没调用呢?这个我们稍后再说,这里我们先分析一下这个方法做了什么?其中我们主要分析一下排序方法prepareMethodLists和插入方法attachLists

prepareMethodLists
static void 
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                   bool baseMethods, bool methodsFromBundle)
{
    runtimeLock.assertLocked();
    
    const char *mangledName  = cls->mangledName();
    const char *ZGPersonName = "ZGPerson";

    if (strcmp(mangledName, ZGPersonName) == 0) {
        bool kc_isMeta = cls->isMetaClass();
        auto kc_rw = cls->data();
        auto kc_ro = kc_rw->ro();
        printf("%s: 这个是我要研究的 %s \n",__func__,ZGPersonName);
    }
    

    if (addedCount == 0) return;

    // There exist RR/AWZ/Core special cases for some class's base methods.
    // But this code should never need to scan base methods for RR/AWZ/Core:
    // default RR/AWZ/Core cannot be set before setInitialized().
    // Therefore we need not handle any special cases here.
    if (baseMethods) {
        ASSERT(cls->hasCustomAWZ() && cls->hasCustomRR() && cls->hasCustomCore());
    }

    // Add method lists to array.
    // Reallocate un-fixed method lists.
    // The new methods are PREPENDED to the method list array.

    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[I];
        ASSERT(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }

    // If the class is initialized, then scan for method implementations
    // tracked by the class's flags. If it's not initialized yet,
    // then objc_class::setInitialized() will take care of it.
    if (cls->isInitialized()) {
        objc::AWZScanner::scanAddedMethodLists(cls, addedLists, addedCount);
        objc::RRScanner::scanAddedMethodLists(cls, addedLists, addedCount);
        objc::CoreScanner::scanAddedMethodLists(cls, addedLists, addedCount);
    }
}

从其中的排序方法fixupMethodList中可以看出,排序是通过方法的内存地址排序的即SEL name

static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    runtimeLock.assertLocked();
    ASSERT(!mlist->isFixedUp());

    // fixme lock less in attachMethodLists ?
    // dyld3 may have already uniqued, but not sorted, the list
    if (!mlist->isUniqued()) {
        mutex_locker_t lock(selLock);
    
        // Unique selectors in list.
        for (auto& meth : *mlist) {
            const char *name = sel_cname(meth.name);
            meth.name = sel_registerNameNoLock(name, bundleCopy);
        }
    }
    // sel - imp
    // Sort by selector address.//通过内存地址排序
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
    
    // Mark method list as uniqued and sorted
    mlist->setFixedUp();
}
struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
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]));// cpy 新的在前面
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            // 0到1 - list 没有数据第一次 - 第0个元素给list,此时 list 是一维的
            list = addedLists[0];
        }
        else {
            // 1 list -> many lists
            // 1+many - 举例 many lists 是3个
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;// 容量计算,旧+新的和 1+3=4
            setArray((array_t *)malloc(array_t::byteSize(newCount)));// 开辟总大小的空间 newCount
            array()->count = newCount;// array 的数量:是新的添加 manylists 后的数量
            if (oldList) array()->lists[addedCount] = oldList;// 旧的list 放最后面 第3个位置
            // memcpy(位置, 放谁, 大小)
            // 把新的 lists 从起始位置0开始放
            memcpy(array()->lists, addedLists,
                   addedCount * sizeof(array()->lists[0]));
        }
    }

这里加了一些注释,可以看出加入的array(方法列表,属性列表,协议列表)总是会加在的前面,这也就解释了为什么分类的方法会在本类之前调用

attachLists
attachCategories分析

刚才我们一直没调用到attachCategories方法,那我们只能想一下其他的办法,在源码中搜索attachCategories,看看都会在哪里调用,发现只有两处调用

  • attachToClass()
  • load_categories_nolock()

经过探索,如果想要调用attachCategories方法,我们只需要在分类里面加一个+ load方法,变会调用attachCategories方法

attachCategories

其中的函数调用栈如下


load_categories_nolock

而在 attachToClass方法中,这里经过调试发现,基本不会进到if流程调用attachCategories,除非加载两次,一般的类一般只会加载一次

所以接下来我们只需要研究load_categories_nolock()的调用,我们全局搜索load_categories_nolock的调用的地方,发现只有两次调用

static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);

    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        load_categories_nolock(hi);
    }
}
if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

经过我们调试发现,只有loadAllCategories方法才会调用load_categories_nolock,另外一个基本不会调用。

继续全局搜索loadAllCategories,发现只在load_images中调用

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

所以综上所述,该情况下的分类的数据加载时机的反推路径为:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images

而我们的分类加载正常的流程的路径为:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories

分类加载

分类和类的各种加载时机

我们可以大致将类 和 分类 是否实现+load的情况分为4种

类+分类
分类实现+load 分类未实现+load
类实现+load 非懒加载类+非懒加载分类 非懒加载类+懒加载分类
类未实现+load 懒加载类+非懒加载分类 懒加载类+懒加载分类

非懒加载类 + 非懒加载分类

调用路径为

非懒加载类 + 非懒加载分类

非懒加载类 + 懒加载分类

非懒加载类 + 懒加载分类

懒加载类 + 非懒加载分类

懒加载类 + 非懒加载分类

懒加载类 + 懒加载分类

懒加载类 + 懒加载分类

总结

  • 非懒加载类 + 非懒加载分类,其数据的加载在load_images 方法中,首先对类进行加载,然后把分类的信息贴到类中。
  • 非懒加载类 + 懒加载分类,其数据加载在read_image 就加载数据,数据来自datadata在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起。
  • 懒加载类 + 懒加载分类,其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成。
  • 懒加载类 + 非懒加载分类,只要分类实现了load,会迫使主类提前加载,即在_read_images 中不会对类做实现操作,需要在 load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据。
上一篇下一篇

猜你喜欢

热点阅读