iOS底层原理(三)Category

Category的本质

Category的底层结构

1.我们先给Person增加一个Person+Eat的分类

@interface Person (Eat) <NSCopying, NSCoding>

- (void)eat;

@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end

@implementation Person (Eat)

- (void)eat
{
    NSLog(@"eat");
}

- (void)eat1
{
    NSLog(@"eat1");
}

+ (void)eat2
{
    
}

+ (void)eat3
{
    
}

@end

2.然后通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m转换成Person+Eat.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; // 属性列表
};

3.我们还发现会生成一个_category_t结构体类型的变量,这个变量对应着该分类文件是Person+Eat,并且里面记录着所有的分类信息

// 变量名对应着分类文件名
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person", // 类名
	0, // cls
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat, // 对象方法
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat, // 类方法
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat, // 协议列表
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat, // 属性列表
};

4._OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat这个变量里面记录着分类的对象方法eateat1

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Eat_eat},
	{(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_Person_Eat_eat1}}
};

5._OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat这个变量里面记录着分类的类方法eat2eat3

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_Person_Eat_eat2},
	{(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_Person_Eat_eat3}}
};

6. _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat这个变量里面记录着NSCopyingNSCoding两个协议

static struct /*_protocol_list_t*/ {
	long protocol_count;  // Note, this is 32/64 bit
	struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	2,
	&_OBJC_PROTOCOL_NSCopying,
	&_OBJC_PROTOCOL_NSCoding
};

7._OBJC_$_PROP_LIST_Person_$_Eat这个变量里面记录着属性weightheight

static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"weight","Ti,N"}, 
	{"height","Td,N"}}
	// Ti,N和Td,N对应着int和double两个类型
};

Category的加载处理过程

1.通过分析查找到objc-rumtime-new.mm文件里的attachCategories函数,将分类文件里的数据信息都附加到对应的类对象或者元类对象里,详细代码如下

// 附加上分类的核心操作
// cls:类对象或者元类对象,cats_list:分类列表
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)" : "");
    }

    // 先分配固定内存空间来存放方法列表、属性列表和协议列表
    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];

        // entry.cat就是category_t *cat
        // 根据isMeta属性取出每一个分类的类方法列表或者对象方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        
        // 如果有方法则添加mlist数组到mlists这个大的方法数组中
        // mlists是一个二维数组:[[method_t, method_t, ....], [method_t, method_t, ....]]
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
			// 将分类列表里先取出来的分类方法列表放到大数组mlists的最后面(ATTACH_BUFSIZ - ++mcount),所以最后编译的分类方法列表会放在整个方法列表大数组的最前面            
			mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 同上面一样取出的是分类中的属性列表proplist加到大数组proplists中
        // proplists是一个二维数组:[[property_t, property_t, ....], [property_t, property_t, ....]]
        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;
        }

        // 同上面一样取出的是分类中的协议列表protolist加到大数组protolists中
        // protolists是一个二维数组:[[protocol_ref_t, protocol_ref_t, ....], [protocol_ref_t, protocol_ref_t, ....]]
        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, __func__);
        
        // 将分类的所有对象方法或者类方法,都附加到类对象或者元类对象的方法列表中
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    // 将分类的所有属性附加到类对象的属性列表中
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    // 将分类的所有协议附加到类对象的协议列表中
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

2.上述的每步操作都会调用attachLists方法来进行元素分配,详细代码如下

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

   if (hasArray()) {
       // 获取原本的个数
       uint32_t oldCount = array()->count;
       // 最新的个数 = 原本的个数 + 新添加的个数
       uint32_t newCount = oldCount + addedCount;
       
       // 重新分配内存
       array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
       // 将新数组的个数改为最新的个数
       newArray->count = newCount;
       // 将旧数组的个数改为最新的个数
       array()->count = newCount;
       
       // 递减遍历,将旧数组里的元素从后往前的依次放到新数组里
       for (int i = oldCount - 1; i >= 0; i--)
           newArray->lists[i + addedCount] = array()->lists[i];
       
       // 将新增加的元素从前往后的依次放到新数组里
       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 { .... }
}

总结

  • 编译时
    • 每一个Category都会生成一个_category_t结构体对象,记录着所有的属性、方法和协议信息
  • 运行时
    • 通过Runtime加载某个类的所有Category数据
    • 把所有Category的方法、属性、协议数据,合并到一个大数组中,并且后面参与编译的Category数据,会在数组的前面
    • 将合并后的Category数据(方法、属性、协议),插入到类原来数据的前面

面试题

1.如果几个分类中都有同样的方法,会调用哪个,调用顺序是什么

  • 有分类会先调用分类的方法,如果多个分类都有相同的方法,那么会根据编译顺序来决定执行哪个分类的方法,后参与编译的分类方法会放到整个方法列表数组的最前面,到时调用会遍历所有的方法列表数组,先找到的分类方法列表先执行。
  • 在Xcode中查看编译顺序:Build Phases->Compile Sources
  • 分类里面相同的方法会覆盖类原本的方法这种说法是错误的,根本就没有覆盖,只是最先遍历找到哪个就执行哪个,不存在覆盖的概念

2. Class Extension和Category的实现是一样的吗

  • 不一样。
  • 类扩展只是将.h文件中的声明放到.m中作为私有来使用,编译时就已经合并到该类中了。
  • 分类中的声明都是公开的,而且是利用运行时机制在程序运行时将分类里的数据合并到类中

3.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

  • 有load方法
  • load方法在Runtime加载类、分类的时候调用,而且只调用一次
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

4.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

load方法

  • load方法会在Runtime加载类、分类时调用- 每个类、分类的load,在程序运行过程中只调用一次
源码分析

1.在objc-runtime-new.mmload_images方法可以发现准备处理load方法和调用load方法的函数

void load_images(const char *path __unused, const struct mach_header *mh) {
   	....
    
    {
        mutex_locker_t lock2(runtimeLock);
        // 准备load方法
        prepare_load_methods((const headerType *)mh);
    }

   	// 调用load方法
    call_load_methods();
}

2.在prepare_load_methods中发现,调用load方法前的类的处理和分类的处理

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

    runtimeLock.assertLocked();
    
	// 按编译顺序拿到所有的类的list
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
        
    // 按添加进数组的顺序遍历处理类的列表
    for (i = 0; i < count; i++) {
        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);
        
      	....
 
	   // 按顺序直接添加
       add_category_to_loadable_list(cat);
    }
}

3.递归调用schedule_class_load方法来优先添加父类放到列表中,然后再添加当前类,所以执行调用时肯定先执行父类的load方法

static void schedule_class_load(Class cls) {
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // 递归调用,找到传进来的类的父类添加到列表中
    schedule_class_load(cls->getSuperclass());

   // 然后再调用当前传进来的类添加到列表中,所以父类肯定是在前面
   add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

4.在objc-loadmethod.mmcall_load_methods方法可以发现,程序运行时会先调用类的load方法,然后调用分类的load方法,详细代码如下

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *po ol = objc_autoreleasePoolPush();

    do {
        // 1. 优先调用类的load方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 只调用一次分类的load方法
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

5.找到每个类的load方法的内存地址,然后直接调用

static void call_class_loads(void) {
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        
        // 取出类里面的load方法
        // load_method_t:指向函数地址的指针
        // 这里的method对应的结构体为loadable_class
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        
        // 直接调用load方法
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

// 找到的method对应的结构体
struct loadable_class {
    Class cls;  // may be nil
    IMP method; // 这个就是load方法
};

6.找到每个分类的load方法的内存地址,然后直接调用

static bool call_category_loads(void) {
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        
        // 取出每一个分类里的load方法
        // 这里的method对应的结构体为loadable_category
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            
            // 直接调用load方法
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }

   ....
 }
 
// 找到的method对应的结构体
struct loadable_category {
    Category cat;  // may be nil
    IMP method; // 这个方法就是load方法
};
调用顺序
  • 先调用类的load
    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的load之前会先调用父类的load
  • 再调用分类的load
    • 按照编译先后顺序调用(先编译,先调用)
load方法系统调用和主动调用的区别
  • 系统调用load方法调用是直接找到类和分类中的方法的内存地址直接调用
  • 主动调用load方法是通过消息机制来发送消息的,会在对应的消息列表里按顺序遍历一层层查找,找到就调用

initialize方法

initialize方法会在类第一次接收到消息时调用

源码分析

1.由于在调用到这个类的时候才会执行initialize方法,那么说明是在发消息过程中来执行的,我们在objc-runtime-new.mm中调用class_getInstanceMethod或者class_getClassMethod方法,详细代码如下

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

    return _class_getMethod(cls, sel);
}

2.一步步调用最后找到initializeNonMetaClass函数,先递归找到父类调用initialize方法,然后当前类调用initialize方法。可以在callInitialize里发现本质都是通过Runtime的消息机制进行的发送

void initializeNonMetaClass(Class cls) {
	ASSERT(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // 如果存在父类,并且没有初始化父类,就去初始化父类
    supercls = cls->getSuperclass();
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
  
    SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
        
        // 未初始化的类通过setInitializing做了标记,下次就不会再调用了
            cls->setInitializing();
            reallyInitialize = YES;

            // Grab a copy of the will-initialize funcs with the lock held.
            localWillInitializeFuncs.initFrom(willInitializeFuncs);
        }
    }
    
    ....
    
    {
       // 调用初始化
       callInitialize(cls);
       ....
       
   	}
   	
   	....
}


// 调用initialize的函数
void callInitialize(Class cls) {
    // 通过runtime消息机制发送initialize消息
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}
调用顺序
  • 先调用父类的initialize,再调用子类的initialize
  • (先初始化父类,再初始化子类,每个类只会初始化1次)
initialize和load的区别
  • initialize是通过objc_msgSend进行调用的,而load是找到函数地址直接调用的
  • 如果子类没有实现initialize,会调用父类的initialize(所以父类的initialize可能会被调用多次)
  • 如果分类实现了initialize,就覆盖类本身的initialize调用

5.Category能否添加成员变量?如果可以,如何给Category添加成员变量?

不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果

实现方案

通过objc_setAssociatedObject设置关联对象来实现

@interface Person (Test)

@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) int weight;
@end

@implementation Person (Test)

- (void)setName:(NSString *)name
{
    /*
     * object:关联的对象
     * value:关联的值
     * objc_AssociationPolicy:关联策略
     */
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name
{
    // 隐式参数
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setWeight:(int)weight
{
    objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)weight
{
    // _cmd == @selector(weight)
    return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
实现源码分析

1.在objc-references.mm中我们可以看到objc_setAssociatedObject的实现代码如下

void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) {
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    
    // 通过传进来的object生成一个key
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;
        // 取出AssociationsManager里的AssociationsHashMap这个属性
        AssociationsHashMap &associations(manager.get());

        // 如果value有值
        if (value) {
            // 根据传进来的object key的值disguised取出ObjectAssociationMap
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            // 根据refs_result的key存放进association
            // association就是ObjcAssociation
            // 总之就是对应的每一个key一层层的赋值
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else { // 如果value为空
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        // 也会找到associations进行擦除
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

	....
}

2.关联对象的取值函数objc_getAssociatedObject的实现代码如下

void _object_get_associative_reference(id object, const void *key) {
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        
        // associations是manager里面所有的AssociationsHashMap的list
        AssociationsHashMap &associations(manager.get());
        // 根据object在list里找到对应的那个AssociationsHashMap类型的i
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            
            // 再取出所有的ObjectAssociationMap的list
            ObjectAssociationMap &refs = i->second;
            // 根据key在list里找到对应的ObjectAssociationMap类型的j
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                // 取出ObjectAssociation类型的值association
                association = j->second;
                // 取出association里的value和策略
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

3.关联对象的几个类的实现关系可以用下图表示

iOS底层原理(三)Category

总结:
  • 关联对象并不是存储在被关联对象本身内存中
  • 关联对象存储在全局的统一的一个AssociationsManager
  • 设置关联对象为nil,就相当于是移除关联对象
  • 当object对象被释放,关联对象的值也会对应的从内存中移除(内存管理自动做了处理)
上一篇:复习——数据库


下一篇:[极客大挑战 2019]BabySQL