Load方法 && Category/Extension/Pro

2020-03-26  本文已影响0人  奚山遇白

参考apple开发文档文档地址我们可以看到load方法定义如下:

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
// 当一个类/分类被加入到runtime时调用,另外实现这个方法可以在加载时做一些特殊操作

该方法的具体描述和一些需要特别注意的特性如下:

The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
// load方法在类和分类中都会被动态执行和静态链接,但是只有在类/分类中实现load方法该方法才能被调用
The order of initialization is as follows:
// 具体的执行顺序如下:
All initializers in any framework you link to.
// 调用所有你链接的framework
All +load methods in your image.
// 调用所有load方法
All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
// 调用C++的静态初始化方及C/C++中的构造方法
All initializers in frameworks that link to you.
// 调用所有链接到目标文件的framework中的初始化方法
In addition:
A class’s +load method is called after all of its superclasses’ +load methods.
// 一个类的load方法在父类的load方法调用之后才会被调用
A category +load method is called after the class’s own +load method.
// 一个分类的load方法只有在被分类的原来的类的load方法被调用之后调用
In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.
// 在自定义load方法中,可以安全地向同一二进制包中(其他库中的类)的其它无关的类发送消息,但接收消息的类中的load方法可能尚未被调用。

下面我们就来验证一下上述描述中标注的特性。
我在项目中创建了Catamount类(父类),Cat类(子类1)和Tiger类(子类2)均继承自Catamount类,另外创建了Catamount (Test1),Catamount (Test2),Cat (Test1),Tiger (Test1)四个分类,其每个类的具体实现如下:

@implementation Catamount

- (void)load {
  NSLog(@"Catamount -- load");
  }
@end
  
@implementation Cat
+ (void)load {
    NSLog(@"Cat -- load");
}
@end

@implementation Tiger
@end

@implementation Catamount (Test1)
+ (void)load {
    NSLog(@"Catamount-Test1 -- load");
}
@end

@implementation Catamount (Test2)
+ (void)load {
    NSLog(@"Catamount-Test2 -- load");
}
@end

@implementation Cat (Test1)
@end

@implementation Tiger (Test1)
+ (void)load {
    NSLog(@"Tiger-Test1 -- load");
}
@end

运行项目,执行结果如下:

2018-05-23 11:11:25.820888+0800 debug-objc[1904:186605] Catamount -- load
2018-05-23 11:11:27.116243+0800 debug-objc[1904:186605] Cat -- load
2018-05-23 11:11:28.244515+0800 debug-objc[1904:186605] Catamount-Test2 -- load
2018-05-23 11:11:29.860360+0800 debug-objc[1904:186605] Catamount-Test1 -- load
2018-05-23 11:11:33.083241+0800 debug-objc[1904:186605] Tiger-Test1 -- load

通过上述实践可知:

1.对比cat类和Catamount类中load的执行顺序可知一个类的load方法在父类的load方法调用之后才会被调用

2.对比Tiger类和Cat (Test1)类的实现可知只有在类/分类中实现load方法该方法才能被调用

3.对比Catamount (Test1)类和Catamount (Test2)类 与Catamount类中的load的执行书序可知一个分类的load方法只有在被分类的原来的类的load方法被调用之后调用

4.对比Catamount (Test1)类和Catamount (Test2)类中load的执行顺序可知分类的load的执行顺序不确定(网上有说法是一个类的多个分类的load执行顺序与其在Compile Sources中出现的顺序一致,并为深究,读者可自行验证)

所以回到我们刚开始提出的问题:

【Q1】普通类/Category,load方法执行逻辑。多个分类中有load方法是怎么处理的?

【A1】load方法在类/子类/分类中的执行顺序由上述描述已可知。

而对于Tiger (Test1)类,该类的被分类的原来的类Tiger类并未实现load方法,但是Tiger (Test1)类中的load方法正常执行,所以本文认为一个分类的load方法是否执行,与其原本类是否有实现load方法并无关系,因为load方法的描述本就为:Invoked whenever a class or category is added to the Objective-C runtime,所以也就意味着一个类/分类被链接到项目中的时候,只要该类/分类实现了load方法那么该方法就会被调用(The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.)

【Q2】为什么系统在调用load方法的时候不存在方法覆盖问题

【A2】查阅源码可以发现系统调用load方法的具体方法体如下:

void call_load_methods(void)
{
    loadMethodLock.assertLocked();// 加锁
    ...
    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);
    ...
}

其中主要调用了call_class_loads()和call_category_loads()方法,而这两个方法的执行load方法的核心代码如下:

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue;        
// PrintLoading, OBJC_PRINT_LOAD_METHODS, "log calls to class and category +load methods"
        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }

可以看出call_load_methods里是通过load方法的地址直接调用的load方法,而不是通过消息机制来调用的,所以分类中的load方法并不会覆盖主类以及其他同主类的分类里的load 方法实现。

Tip

【1】当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,所以其运行环境有不确定因素,可能并不能保证所有类都加载完成且可用。然而也正因为load方法调用在main函数之前所以我们不应该在load方法中做耗时操作,以避免程序启动时间过长的情况

【2】load方法使用了锁来保证线程安全,所以应该避免线程阻塞在load方法.

Category/Extension/Protocol的实现原理

Category

Category的主要作用是它可以在不改变原来类的基础上,为类动态的添加方法。分类的使用应该注意以下特点:

1.分类中只能增加方法,不能增加属性

2.在分类方法中可以访问原来类的.h文件中声明的属性和方法

3.分类可以重新实现原来类中的方法,但是会覆盖掉原来方法,所以在开发中尽量避免同名方法覆盖

4.方法调用的优先级:分类>原来的类>父类,如包含多个分类,则调用优先级与编译顺序有关,最后参与编译的分类优先
那么Category具体实现原理是什么呢?其实很简单,就是系统在编译时将Category的方法列表加入到了原有类的方法列表之上。
我们先来看一下Category的结构体声明:

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

// 如果不是元类,则返回实例属性列表;若hi参数有分类类属性,返回类属性列表;否则即为元类返回 nil,因为元类没有属性
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

其中就包括了方法等列表项
在OC代码运行时,系统经历了一下几个方法

objc-os.mm类中的_objc_init方法

——>objc-os.mm类中的map_images方法

————>objc-os.mm类中的map_images_nolock方法

——————>objc-runtime-new.mm类中的_read_images方法

————————>objc-runtime-new.mm类中的remethodizeClass方法

——————————>objc-runtime-new.mm类中的attachCategories方法

然后实现了将所有category的实例方法列表拼成了一个大的实例方法列表,所以最终结果是category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面。需要注意:

1.category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA

2.我们平常所说的category的方法会“覆盖”掉原来类的同名方法,实际上由一可知:这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法即返回imp,也就实现了所谓的“方法覆盖”。

Extension

Extension一般用来隐藏类的私有信息,其定义与Category相似,但是它不仅可以扩展原有类的方法,也可以扩充原有类的属性。此外需注意:定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

Protocol

Protocol是一种特殊的程序设计结构,专门用来声明被别的类实现的方法,也就提供了一个可被其他类实现的通信接口,它具有以下特点:

1.一个类可以同时遵循多个协议

2.协议本身也可以遵循其他协议

3.只要父类遵守了某个协议,那么子类也遵守

需要注意的是delegate一般用weak修饰,而不用strong修饰,是为了避免对象一直被持有导致无法释放,也就是为了防止循环引用。
而实际上Protocol也是一个结构体:struct protocol_t,Runtime提供了Protocol的一系列函数操作,如下所示:

// 返回指定的协议
Protocol * objc_getProtocol ( const char *name );
// 获取运行时所知道的所有协议的数组
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 创建新的协议实例
Protocol * objc_allocateProtocol ( const char *name );
// 在运行时中注册新创建的协议
void objc_registerProtocol ( Protocol *proto ); //创建一个新协议后必须使用这个进行注册这个新协议,但是注册后不能够再修改和添加新方法。
// 为协议添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一个已注册的协议到协议中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 为协议添加属性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回协议名
const char * protocol_getName ( Protocol *p );
// 测试两个协议是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 获取协议中指定条件的方法的方法描述数组
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 获取协议中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 获取协议中的属性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 获取协议的指定属性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 获取协议采用的协议
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看协议是否采用了另一个协议
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );

参考链接:
NSObject的load和initialize方法
iOS类方法load和initialize详解
iOS category内部实现原理
Objc Runtime 深入学习类,对象,Method,消息,Protocol,Category和Block的底层结构和运行时操作函数

上一篇下一篇

猜你喜欢

热点阅读