iOS 分类的加载原理

一、分类的本质

1、我们先准备一个.m文件包含主类和分类

#import 

//类
@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具体实现,名字解析

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:分类中添加的协议列表
  • properties:分类中添加的属性列表

3、接下分类实现的具体结构


image.png

其中的实例方法和类方法我也明确标出,发现

  • 3.1 分类的属性不会自动实现setter/getter方法,但属性列表中存在
  • 3.2 从cate_instanceMethod3可以看出,分类中申明但未实现的方法不存在于方法列表中

二、分类的加载

1、分类绑定到类当中

我们知道objc源码中初始化函数_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 里面就是分类给主类添加属性、方法、协议的具体实现

其实这里是根据主类+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 -> 二维数组
    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))); // 创建一个数组
            array()->count = newCount; // 4
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

我们在这里我可以发现

  • 1.1、 category方法的添加不是覆盖,而是将老方法平移到数组的后方,将新方法插入到前面
  • 1.2、category方法越后加入,就会排到前面去,而方法的查找是顺序的,所以后加入的方法先找到.

2、分类加载到内存

经过上面的分析我们已经知道分类中的方法、属性、协议如何绑定到类,但我们还不知道分类是如何加载到内存的,不过在上面的过程中我们知道分类需要在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 
  std::pair 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(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

这个创建一个存储桶的结构(这是一个键值对形式的结构),向里面存内容,结合上面的函数调用get().try_emplace(cls, lc)得知,以clskeylcvalue进行存储。现在就分析到这,再走下去就跟我们本来的研究方向走偏了。

我们现在看下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

你可能感兴趣的:(iOS 分类的加载原理)