• iOS底层原理探索分类Category的本质


    前言

    首先,这里有几个与Category相关的面试题,大家可以看一下
    1、Category如何使用?
    2、Category的原理是什么?
    3、Category与类扩展的区别?
    4、Category中load方法是什么时候调用的?load方法能被继承吗?
    5、load和initialize的区别是什么?他们在category中的调用顺序是怎样的?出现继承的时候他们之间的调用过程是什么?
    6、Category是否可以添加成员变量?如果可以,如何添加?

    这几个面试题你能答出几个呢?如果有不会的地方,那咱们一起来学习下吧

    Category

    Category分类的作用:在不改变原有的类的前提下,可以为类单独添加一些方法、协议、属性。

    首先,我们创建一个类YZPerson,其里面有一个对象方法-(void)run;然后分别新建两个分类:YZPerson+Eat、YZPerson+Drink。里面分别有四个方法:

    - (void)eat1
    {
        NSLog(@"YZPerson+Eat-eat1");
    }
    
    - (void)eat2
    {
        NSLog(@"YZPerson+Eat-eat2");
    }
    
    + (void)eat3
    {
        NSLog(@"YZPerson+Eat-eat3");
    }
    
    + (void)eat4
    {
        NSLog(@"YZPerson+Eat-eat4");
    }
    

    使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YZPerson+Eat.m命令行指令,可以将YZPerson+Eat.m转化为C语言源码YZPerson+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和第4的赋值是如下两个图

    在这里插入图片描述

    在这里插入图片描述

    从源码可以看出,分类在经历过编译后,将分类里面的内容:对象方法、类方法、协议、属性都转化为类型为_category_t的结构体变量。

    对分类的源码分析:

    1.运行时的初始化:

    在这里插入图片描述

    2.调用_dyld_objc_notify_register方法,传入map_images地址(方法地址或者函数地址):

    在这里插入图片描述

    3.调用map_images_nolock方法,在map_images_nolock方法中调用_read_images方法(镜像,加载一些模块):

    在这里插入图片描述

    4.加载分类信息(分类信息是个二维数组):

    在这里插入图片描述

    5.找到remethodizeClass(cls)核心方法的实现(给类对象和原类对象重新组织方法):

    static void 
    // cls 类对象
    // cats 分类列表
    attachCategories(Class cls, category_list *cats, bool flush_caches) 
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations
        // 分配存储空间
        // 方法列表
        method_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        // 属性数组
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        // 协议数组
        protocol_list_t **protolists = (protocol_list_t **)
            malloc(cats->count * sizeof(*protolists));
    
        // Count backwards through cats to get newest categories first
        int mcount = 0;
        int propcount = 0;
        int protocount = 0;
        int i = cats->count;
        bool fromBundle = NO;
        while (i--) {		
        		//取出某个分类,i--,先取的最后编译的那一个
            auto& entry = cats->list[i];
    				//对方法列表的操作
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;//mcount++,对第一个取出的进行操作
                fromBundle |= entry.hi->isBundle();
            }
    				//对属性列表的操作
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    				//对协议列表的操作
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
    		
    		// 类对象里边的数据
        auto rw = cls->data();
    
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        //将所有分类的对象(类)方法列表附加到原来类的对象(类)方法列表里面
        rw->methods.attachLists(mlists, mcount);//mcount个数
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
        rw->properties.attachLists(proplists, propcount);
        free(proplists);
    
        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }
    

    其中,attachLists方法的实现:

    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 = array()->lists
                memmove(array()->lists + addedCount, array()->lists, 
                        oldCount * sizeof(array()->lists[0]));
                        
    			//addedLists分类数据
    			//addedLists覆盖array()->lists数据
                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]));
            }
        }
    

    通过查阅以上源码,可以得到:

    在运行时,通过runtime机制,将多个分类里面的【方法列表(包括:对象方法列表和类方法列表)、协议列表和属性列表】分别集合成数组,然后将新的数组添加到【原来类对象里面的方法列表、元类里面的类方法列表、类对象里面的协议列表、属性列表】的最前面,也就是将分类里面的内容动态的添加到了类对象和元对象里面。
    同时,由于是添加在最前面,所以当分类、原类、父类里面都有同一个方法时(例如:-(void)run;方法),优先执行分类里面的方法,如果没有再执行原类里面的方法,如果再没有才会去父类里面找该方法。 需要注意的是,是优先调用,并没有覆盖原类中的方法。
    有多个分类同时有某一个方法的时候,由于遍历是i- -,然后做的mcount++操作,因此,最后编译的分类文件,第一个被查找。

    问:什么时候决定分类文件是最后被编译的呢?

    在这里插入图片描述

    在下面的文件,最后一个被编译。

    总结:Category的加载过程

    通过Runtime加载某个类的所有

    数据
    把所有Category的方法、属性、协议数据合并到一个大数组中(最后面参与编译的Category数据会在数组前面)
    将合并后的分类数据(包括方法、属性、协议),插入到原来数据的前面
    以上就是Category被加载的过程,也是Categorey的原理。

    分原子父
    分类在前,原类在后(分类添加到原类的前面)
    原类在前,父类在后(消息发送机制)
    在这里插入图片描述

    +(void)load;方法

    下面介绍一下有关load相关的知识

    在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法。也就是不管程序在运行过程中是否调用过该类,在程序初始化的时候,都会调用+load方法且只会调用一次。
    先加载父类,再加载子类
    先加载原始类,再加载分类
    初始化load调用顺序:父子原分

    有一点需要说明的是,+(void)load;方法跟分类中自定义方法不一样。因为,如果是自定义方法,原类跟分类方法一样的话,只会调用分类的方法。而+(void)load;方法会把所有的原来、分类里面的+(void)load;都会调用一遍。同样是原类和分类里面一样的方法,为什么会出现不一样的结果呢?

    我们继续查看源码
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    自定义方法的调用[YZPerson test];是消息传递机制,因此会通过isa指针在元类中查找类方法,如果有分类+test方法,则优先调用分类的+test方法。
    而+load方法,是根据直接在内存中找到+load的内存地址,通过load_method方法调用的。

    先调用原类的load方法,再调用分类的load方法;
    先调用父类的load方法,再调用子类的load方法;
    没有继承关系的多个原类,按编译顺序调用(先编译,先调用);
    多个分类只按编译顺序调用(先编译,先调用);

    +initialize方法

    下面介绍一下有关initialize相关的知识
    +initialize方法会在类第一次接收到消息时调用。

    在第一次使用某个类时(比如创建对象等),就会调用一次+initialize方法
    一个类只会调用一次+initialize方法
    调用顺序:先调用父类的,再调用子类的
    初始化initialize调用顺序:父子分原
    我们查看相关源码:

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    通过源码分析,不难看出上面的知识点。
    由于是基于isa指针机制,+initialize方法有以下特点:

    如果分类实现了+initialize,就调用分类的+initialize,不会再调用类本身的+initialize调用(网上有说是覆盖原类中的+initialize方法,其实并不是真正的覆盖,而是没有调用原类中的+initialize方法)

    父类
    @implementation YZPerson
    + (void)initialize
    {
        NSLog(@"YZPerson-initialize");
    }
    @end
    
    父类的分类
    @implementation YZPerson (Eat)
    + (void)initialize
    {
        NSLog(@"YZPerson(Eat)-initialize");
    }
    @end
    
    子类(原类)
    @implementation YZStudent
    //+(void)load
    //{
    //    NSLog(@"YZStudent-load");
    //}
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [YZStudent alloc];
        }
        return 0;
    }
    

    神奇的一幕出现了:

    2020-02-26 17:02:11.224559+0800 Category[75206:2732274] YZPerson(Eat)-initialize
    2020-02-26 17:02:11.224800+0800 Category[75206:2732274] YZPerson(Eat)-initialize
    

    不是说好的initialize只调用一次吗?怎么调用了两次?为什么呢?

    首先,打印出来的是分类,这个没有问题,因为分类方法在父类的方法前面,优先显示分类的。
    [YZStudent alloc];会先去找父类的,父类YZPerson并没有实现initialize方法,因此,第一次打印是父类的initialize;
    父类调用完毕后,并没有结束,而是去调用其本身的initialize方法,其本身没有initialize方法,由于继承关系,就去父类里面找initialize,最后调父类的initialize。
    伪代码:

    if (原类没有初始化)
    {
        if (父类没有初始化)
        {
            objc_msgSend([YZPerson alloc], @selector(initialize));
        }
        objc_msgSend([YZStudent alloc], @selector(initialize));
    }
    

    因此,会出现调用两次。其实每个类的初始化还是只有一次。第一次是父类Person的初始化,第二次是子类Student的初始化,由于子类没有+initialize,所以调用父类的+initialize方法,也就是:如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能被调用多次)

    问:分类可以添加属性吗?

    我们知道,分类只能添加方法,不能添加属性。这句话其实不严谨,应该说:
    分类只能添加方法,不能直接添加属性,可以间接添加属性。

    在普通类中,@property (assign, nonatomic) int age;
    会做三件事:

    生成age的成员变量
    生成age的get、set方法的声明
    生成age的get、set方法的实现
    而在分类中,@property (assign, nonatomic) int weight;可以写,但是它的作用只有一个:
    生成weight的get、set方法的声明

    如何实现为分类间接添加属性呢?

    我们可以通过runtime中的关联对象的方法(objc_setAssociatedObject)实现分类中属性的get、set方法的实现,具体实现如下:

    @interface YZPerson : NSObject
    @property (assign, nonatomic) int age;
    @end
    
    @interface YZPerson (Eat)
    @property (copy, nonatomic) NSString *name;
    @end
    
    #import <objc/runtime.h>
    @implementation YZPerson (Eat)
    - (void)setName:(NSString *)name
    {
        objc_setAssociatedObject(self, @selector(setName:), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name
    {
        return objc_getAssociatedObject(self, @selector(setName:));
    }
    @end
    
    YZPerson *person1 = [[YZPerson alloc] init];
    person1.age = 10;
    person1.name = @"zhangSan";
            
    YZPerson *person2 = [[YZPerson alloc] init];
    person2.age = 20;
    person2.name = @"liSi";
            
    NSLog(@"person1.age = %d, person2.age = %d", person1.age, person2.age);
    NSLog(@"person1.name = %@, person2.name = %@", person1.name, person2.name);
    
    结果:
    2020-02-27 16:26:56.015710+0800 Category[6423:189583] person1.age = 10, person2.age = 20
    2020-02-27 16:26:56.015980+0800 Category[6423:189583] person1.name = zhangSan, person2.name = liSi
    

    面试题解答:

    调用顺序
    categrory:分原子父
    load:父子原分
    initialize:父子分原

    categrory方法,完全遵守消息发送机制,因此是分子父
    load和initialize方法,都是代码中明确写到的:递归调用父类,因此是 父子
    load方法代码中明确写的,先调用原类再调用分类,因此是 原分
    initialize方法中,没有明确写原类、分类的调用关系,因此,遵循消息发送机制,因此是分原
    1、Category如何使用
    分类可以在不修改原来类模型的基础上拓充方法;
    2、Category的原理是什么?
    在编译的时候,转化为category_t类型的结构体类型。
    在运行时将所有Category的方法、属性、协议数据合并到一个大数组中(最后面参与编译的Category数据会在数组前面),将合并后的分类数据(包括方法、属性、协议),插入到原来数据的前面;
    3、Category与类扩展的区别?
    分类可以在不修改原来类模型的基础上拓充方法
    • 分类只能扩充方法、不能扩充成员变量;
    • 继承可以扩充方法和成员变量,继承会产生新的类;
    • 分类是有名称的,类扩展没有名称;
    • 分类只能扩充方法、不能扩充成员变量;类扩展可以扩充方法和成员变量;
    • 类扩展一般就写在.m文件中,用来扩充私有的方法和成员变量(属性);
    • 分类是在运行时将数据合并在类信息中,类扩展是编译的时候它的数据就已经包含在类信息中;
    4、Category中load方法是什么时候调用的?load方法能被继承吗?
    在程序启动的时候会加载所有的类和分类,并调用所有类和分类的+load方法。也就是不管程序在运行过程中是否调用过该类,在程序初始化的时候,都会调用+load方法且只会调用一次。

    @implementation YZPerson
    +(void)load
    {
        NSLog(@"YZPerson-load");
    }
    @end
    
    @implementation YZStudent
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSLog(@"----");
            [YZStudent load];
            NSLog(@"----");
        }
        return 0;
    }
    
    结果:
    2020-02-26 15:23:20.747580+0800 Category[74061:2678272] YZPerson-load
    2020-02-26 15:23:20.747839+0800 Category[74061:2678272] ----
    2020-02-26 15:23:20.747862+0800 Category[74061:2678272] YZPerson-load
    2020-02-26 15:23:20.747871+0800 Category[74061:2678272] ----
    

    load方法可以被继承
    但,[YZStudent load];这种调用方法相当于消息发送机制,走的是isa指针那一套,并不是原有系统调用load方法。

    5、load和initialize的区别是什么?他们在category中的调用顺序是怎样的?出现继承的时候他们之间的调用过程是什么?

    1.调用方式的不同:
    load是通过找到函数地址直接调用的;
    initialize是通过消息机制objc_msgSend调用的;

    2.调用时刻的不同
    load是程序运行的时候,通过runtime加载类、分类的时候调用(只会调用一次)
    initialize是类第一次使用的时候调用的;(如果子类没有+initialize方法,父类可能会被调用多次)

    load在分类中,按编译顺序调用
    initialize在分类中,按编译顺序调用

    load在继承中调用是按isa指针调用
    initialize在继承中调用是按isa指针调用

    6、Category是否可以添加成员变量?如果可以,如何添加?
    分类不可以直接添加属性,可以间接通过runtime中的关联方式进行添加属性。

    扩展知识点:

    在这里插入图片描述

    更多学习

    iOS分类(category),类扩展(extension)—史上最全攻略
    iOS底层原理总结 - Category的本质

    {
        "_track_id" = 3492084489;
        "anonymous_id" = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
        "distinct_id" = newId;
        event = "$AppPageLeave";
        identities =     {
            "$identity_idfv" = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
            "$identity_login_id" = newId;
        };
        lib =     {
            "$app_version" = "1.4.1";
            "$lib" = iOS;
            "$lib_method" = code;
            "$lib_version" = "4.1.3";
        };
        "login_id" = newId;
        properties =     {
            "$app_id" = "cn.sensorsdata.SensorsData";
            "$app_name" = SensorsData;
            "$app_version" = "1.4.1";
            "$device_id" = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
            "$is_first_day" = 0;
            "$lib" = iOS;
            "$lib_method" = code;
            "$lib_version" = "4.1.3";
            "$manufacturer" = Apple;
            "$model" = "x86_64";
            "$network_type" = WIFI;
            "$os" = iOS;
            "$os_version" = "15.2";
            "$screen_height" = 896;
            "$screen_name" = DemoController;
            "$screen_width" = 414;
            "$timezone_offset" = "-480";
            "$title" = "SensorsAnalytics iOS Demo";
            "$url" = WoShiYiGeURL;
            "$wifi" = 1;
            AAA = "2AADC4B8-CE6C-4BE2-BEBC-4DA23CEC7A74";
            "__APPState__" = 0;
            "event_duration" = "17.352";
        };
        time = 1640921936297;
        type = track;
    }
    
  • 相关阅读:
    php日常日志写入格式记录
    ssh 配置config 别名
    win10 使用docker
    gulp watch error ENOSPC
    log4net各种Filter使用【转】
    【转】Controllers and Routers in ASP.NET MVC 3
    【转】ASP.NET MVC学习笔记-Controller的ActionResult
    JavaScript 面向对象程序设计(下)——继承与多态 【转】
    Ajax– 刷新页面 【转】
    [webgrid] – selecterow
  • 原文地址:https://www.cnblogs.com/r360/p/15766711.html
Copyright © 2020-2023  润新知