美文网首页
Load方法 && Category/Extension/Pro

Load方法 && Category/Extension/Pro

作者: 奚山遇白 | 来源:发表于2020-03-26 10:39 被阅读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的底层结构和运行时操作函数

    相关文章

      网友评论

          本文标题:Load方法 && Category/Extension/Pro

          本文链接:https://www.haomeiwen.com/subject/lppquhtx.html