category的本质

261 阅读5分钟

1、原理

分类的实例方法最终会合并存在类对象中。
分类的类方法最终会合并在元类中

  • 底层实现

合并方法列表的时间是在运行时。通过runtime动态将分类合并入对应的地方

通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 方法可以看到源码,分类

#import "HXHPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface HXHPerson (Test)
- (void)test;
+ (void)test2;
@end

NS_ASSUME_NONNULL_END

的结构如下

///可以看到里边可以放属性,协议,实例方法,类方法。
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;
};

下边是怎么赋值

static struct _category_t _OBJC_$_CATEGORY_HXHPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"HXHPerson",
	0, // &OBJC_CLASS_$_HXHPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_HXHPerson_$_Test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_HXHPerson_$_Test,
	0,
	0,
};
///这是类方法列表的赋值
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_HXHPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_HXHPerson_Test_test2}}
};

这里可看到,它把值赋给了结构体对应的元素(这里边我没有写属性和协议,所以下边是两个0)

把分类合并入原来类的核心实现为(代码可在官网的runtime找到)

/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

上述方法的调用时机为调用 _read_images 读取镜像时。

合并所有分类之后
最终方法列表存储为了二元数组(下边源码为对应实现)

///addedLists:分类列表
///addedCount:分类数量
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;
            
            //首先扩容
            ///array()->lists原来的方法列表
            ///将原来的数据,往后挪动addedCount这么多位。
            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]));
        }
    }

总结:加载过程/实现原理
pre、首先,编译时会再入内存(这个时候load方法已经执行)。
1、通过runtime加载某个类的所有category数据;
2、把所有category的方法、属性、协议数据,合并到一个大数组中;(后面参与编译的category数据回在数组的前面)
3、将合并后的分类数据(方法、属性、协议)插入到类原来数据的前面。

要关注的runtime源码方法:

类:objc-os.mm

  • _objc_init ///类载入的入口方法
  • map_images
  • map_images_nolock

类:objc-runtime-new.mm

  • _read_images
  • remethodizeClass
  • attachCategories
  • attachLists

源码写的很清楚,是怎么把分类数组添加到本来的类对象中。
需要思考的是为什么要这么做?

下边是一些思考题

1、为什么要用二维数组存这些方法列表?

2、经过这么操作之后,分类中的方法会优先调用,因为他的地址更靠前。那如果两个分类都有呢,谁更靠前?
这个取决去编译顺序。最后面参与编译的,会被放在前边。

3、分类和类扩展(有人会叫他匿名分类)的区别?
category分类编译之后的底层结构是struct category_t, 里面存储着分类的对象方法,类方法,属性,协议信息,在程序运行的时候,runtime会将category数据合并到类信息中。 class extension类拓展在编译的时候它的数据就已经包含在类信息中。

4、category能否添加成员变量?

5、category中有load方法嘛?load方法是什么时候调用的?load方法能继承么?
有。
runtime加载类和分类时调用。不管你用不用,载入内存的时候就已经调用。
可以继承。但一般不会去主动调用load,如果调用的话,机制就变成了消息发送机制(可以去复习消息发送机制的调用顺序)。不再是load_images载入镜像时那种调用所有load。

源码如下

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
const char *
load_images(enum dyld_image_states state, uint32_t infoCount,
            const struct dyld_image_info infoList[])
{
    bool found;

    // Return without taking locks if there are no +load methods here.
    found = false;
    for (uint32_t i = 0; i < infoCount; i++) {
        if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
            found = true;
            break;
        }
    }
    if (!found) return nil;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        found = load_images_nolock(state, infoCount, infoList);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    if (found) {
        call_load_methods();
    }

    return nil;
}

此方法为载入镜像,它是objc_init 类初始化其中的一步,load_images最后一步为调用load方法 call_load_methods() ,found是判断是否已经载入过方法列表。

2、load方法

  • +load方法会在runtime加载类,分类时调用
  • 每个类,分类的+load方法,在程序运行过程中只调用一次
  • 调用顺序

1、先调用类的load

按照编译先后顺序调用(先编译,先调用)
调用子类的load之前会先调用父类的+load

2、再调用分类的load

按照编译先后顺序调用。

///载入镜像时调用,准备方法列表
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **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
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
///递归方法会先去调用父类的方法存到数组,所以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;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

问题1:为何还能正常调用原来的方法?别的普通方法都是覆盖

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 *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        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;
}

可以看到。load方法的调用不是通过寻找类对象中的方法列表,而是直接for循环分类的所有load方法的来调用。
普通方法是消息机制调用。

3、initialize

3.1、如果分类实现了initialize,就会覆盖分类的initialize;
3.2、如果子类没有实现initialize,就会调用父类的initialize;

问题1、initialize和load的区别?

方向1:调用方式和时机

load首次调用是遍历所有类和分类的load方法地址,直接调用。

initialize是类第一次接收到消息时调用 走的是消息机制,会先调用父类initialize,再调用子类initialize。

方向2:次数 初始化的时候,每一个类只会initialize一次,父类的initialize方法可能调用多次。

方向3:调用顺序
load是先调用类的load(按编译顺序),再去调用分类的load
调用子类load之前会先调用父类的load

initialize
1> 先初始化父类
2> 再初始化子类(可能最终调用的是父类的initialize)