底层三:Category

228 阅读8分钟

category

截屏2021-05-06 下午4.14.16.png

category中的方法是通过runtime动态的将分类的方法合并到类对象、元类对象中

最后面参与编译的分类,方法的实现在最前面

category 的底层CPP结构是 _category_t,一个分类对应一个结构体对象,然后通过runtime将方法合并到类对象中

// .h
#import "MJPerson.h"

@interface MJPerson (Eat) <NSCopying, NSCoding>

- (void)eat;

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

@end

// .m

#import "MJPerson+Eat.h"
@implementation MJPerson (Eat)
- (void)run
{
    NSLog(@"MJPerson (Eat) - run");
}

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

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

+ (void)eat2
{
    
}

+ (void)eat3
{
    
}

@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;   //  属性
};

利用结构体创建出一个对象 OBJC_$_CATEGORY_MJPerson_$_Eat
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"MJPerson",
	0, // &OBJC_CLASS_$_MJPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat,
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,
};

// 这些是不同的结构体,去转换后的代码里看
_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat 
_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat
…… …… …… 

这里要通过转换后的CPP代码去看,编译完成后,类名、对象方法、类方法、协议、属性 存放到了结构体 _category_t 底层结构中,并没有合并到类对象中,在程序运行时,runtime会将category的数据,合并到类对象(元类对象)中

分类的加载处理过程

01-OC语法.png

category

  • 后面参与编译Category数据,会在数组的前面(优先调用)

  • 使用msg_send发送消息的调用顺序1.分类 2.类

+load方法


/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first. 
* Category +load methods are not called until after the parent class's +load.
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
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.重复调用class +loads,直到没有其他类
        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;
}


/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
    ...................
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // typedef void(*load_method_t)(id, SEL); 函数指针
        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_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}


/***********************************************************************
* call_category_loads
* Call some pending category +load methods.
* The parent class of the +load-implementing categories has all of 
*   its categories attached, in case some are lazily waiting for +initalize.
* Don't call +load unless the parent class is connected.
* If new categories become loadable, +load is NOT called, and they 
*   are added to the end of the loadable list, and we return TRUE.
* Return FALSE if no new categories became loadable.
*
* Called only by call_load_methods().
**********************************************************************/
static bool call_category_loads(void)
{
    ...................

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        // 直接找到load函数指针
        // typedef void(*load_method_t)(id, SEL); 函数指针
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
           ...................
            // 调用load函数
            (*load_method)(cls, @selector(load));
        }
    }

     ...................
}




  • call_class_loads();
  • call_category_loads();
  • 从这两处可以看出,先调用类的load方法,再调用分类的load方法
  • load_method_t load_method = (load_method_t)cats[i].method;
  • (*load_method)(cls, @selector(load));
  • 通过这两句代码可以看出,load方法是直接找到load函数(方法) 的指针,通过指针调用的load函数,不是通过OBJC的消息机制
  • +load方法会在runtime加载类、分类时调用,每个类、分类的+load,在程序运行过程中只调用一次
load调用顺序

void call_load_methods(void)
{
    ....................
    void *pool = objc_autoreleasePoolPush();

    do {
        //  1.重复调用class +loads,直到没有其他类
        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;
}


void prepare_load_methods(const headerType *mhdr)
{
    .........
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 按照编译顺序,将类的数据加进去
        schedule_class_load(remapClass(classlist[i]));
    }

    ...........
}


/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed 
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
   ..........

    // Ensure superclass-first ordering(递归调用,始终先添加父类方法,父类都放入后,开始放入子类对象)
    schedule_class_load(cls->getSuperclass());
    // 将cls添加到loadable_classes数组的最后面
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • schedule_class_load(cls->getSuperclass()); 通过这里可以看出是先调用父类的load方法,再调用子类的load方法
  • 先调用类的+load

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

    • 按照编译先后顺序调用(先编译,先调用)

子类不实现load的,不会去重复调用父类的load方法,因为不是按照objc_msgSend方式寻址的,不会通过superClass指针向上寻找

+ initialize (初始化)

源码

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
    /****************优先调用父类的initialize方法**********************/
    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->getSuperclass();
    if (supercls  &&  !supercls->isInitialized()) {
        // 递归
        initializeNonMetaClass(supercls);
    }
        @try
        {
        /***********************************************************************
            ...................调用initialize方法.....................
        **********************************************************************/
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             objc_thread_self(), cls->nameForLogging());
            }
        }

}


void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}
  • initializeNonMetaClass(supercls); 先调用父类的initialize方法,同时这里进行了判断

  • if (supercls && !supercls->isInitialized()) {

  • 如果父类已经调用过initialize,不会重复调用

  • 如果两个类,是同一个父类,并且这两个类的都没有实现initialize,父类的initialize会实现两遍(父类的initialize可能会调用多次),因为initialize是通过OBJC的消息机制调用的

  • 如果类A自己没有实现initialize,但是父类实现了initialize,父类的initialize会调用两次,因为initialize是通过OBJC的消息机制调用的

  • ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize)); 说明initialize 是走的OBJC的消息发送机制
  • +initialize方法会在类第一次接收到消息时调用

截屏2021-03-15 上午11.32.56.png

+initialize+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点

  • 如果子类没有实现+initialize,会调用父类的+initialize
  • 如果分类实现了+initialize,就覆盖类本身的+initialize调用

+initialize+load的区别

1.调用方式

  • load是根据函数地址直接调用
  • initialize是通过objc_msgSend调用

2.调用时刻

  • load是runtime加载类、分类的时候调用(只会调用1次)
  • initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

load、initialize的调用顺序?

1.load

1> 先调用类的load

a) 先编译的类,优先调用load

b) 调用子类的load之前,会先调用父类的load

2> 再调用分类的load

a) 先编译的分类,优先调用load

2.initialize

1> 先初始化父类

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

通常情况下两者都是只实现一次,不会重复实现。但是不排除重复实现的可能,譬如误操作直接调用这种,建议使用的时候加上dispatch_once 来使用

关联对象(给分类添加属性)

在类中声明一个属性,会做三件事情

  1. 生成一个成员变量
  2. 生成get和set方法的声明
  3. 实现get和set方法的实现

在分类中写一个属性

只会自动生成getset方法的声明,不会生成方法实现,不会生成成员变量

默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现

对比看的话类的结构中,有成员变量列表ivars(只读),用来存储成员变量,通过关联对象添加的成员变量,并不是放到类的ivars

  • 类的结构: 01-OC语法.png

  • 分类的结构

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;   //  属性
};

关联对象提供了以下API

1. 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

2. 获得关联对象
id objc_getAssociatedObject(id object, const void * key)

3. 移除所有的关联对象
void objc_removeAssociatedObjects(id object)

const修饰的是全局变量,外部可以通过extern关键字进行访问,给全局变量加上static意味着只有当前文件内部可以访问

// .h 
extern const char outKey(编译成功)
extern const char privateKey (编译报错)

// .m
const char outKey (外部可以通过extern访问)
static const char privateKey (因为有static,外部无法访问)

key的常见用法
  • 使用变量地址值,赋值比较啰嗦,切占8个字节(指针变量)
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
  • 还是使用变量地址,但是只占一个字节(char变量)
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
  • 使用属性名作为key(@"property"在常量区,内存地址一致)
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
  • 使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, _cmd) // _cmd == @selector(getter)

API

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object 关联对象(一般是self)
 * @param key 关联的关键词
 * @param value 关联的数值,传nil移除关联对象
 * @param policy 关联策略
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);


/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object 关联对象
 * @param key 关联的关键词
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
    

实际使用

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

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

objc_AssociationPolicy (关联策略)

没有weak效果,使用要注意,离开赋值范围要手动设置nil

关了对象实现weak效果

参考:Weak Associated Object

objc_AssociationPolicy对应的修饰符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong, atomic
OBJC_ASSOCIATION_COPYcopy, atomic
关联对象原理(底层实现)

截屏2021-03-15 下午3.26.30.png

关联对象原理.png

全局有一个AssociationsManager,里面保存一个成员变量

AssociationsHashMap *map{关联对象(object): ObjectAssociationMap}

根据关联对象,可以得到一个

AssociationMap {key:ObjcAssociation}对象

ObjcAssociation有两个属性 {value,policy}

关联对象不会增加成员变量,不能通过person -> age 形式进行访问