23

Category、load、initialize 源码讲解

 4 years ago
source link: http://www.cnblogs.com/guohai-stronger/p/12763944.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今天深圳天气有暴风雨,没有事情干,趁着周末和平常晚上写一篇关于Category知识的梳理!可能针对平常只会知道些category基本结论知道的人有些帮助,写这篇博客会按照下面的目录结合实例以及Category的源码进行一一讲解!!!

  1. Category的实现原理?
  2. Category中有load方法吗?load方法是什么时候调用的?
  3. load、initialize方法的区别是什么?它们在Category中的调用的顺序?以及出现继承时调用过程发生怎样的变化?
  4. Category能否添加成员变量?如果可以,如何给Category添加成员变量?

一、Category的实现原理

1. 前沿Category讲解-所有知识在这

在Xcode中使用Category,可以在里面添加方法以及遵守相应的协议。下面以实例讲解,首先创建ZXYPerson类,类中有对象方法run方法,创建两个分类ZXYPerson+Test和ZXYPerson+Eat,方法分别为test和eat方法。,结构代码如下:

7F7rQfm.jpg!web

View Code

运行代码执行结果如下:

Ufi2aiE.png!web

上面person是实例对象,所以run实例方法是放在ZXYPerson的实例对象方法中,而对于ZXYPerson(Test)和ZXYPerson(Eat)中的test和eat方法放在哪里了呢?

拓展: isa指针方法

  •  instance的isa指向class

当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用

  • class的isa指向meta-class

当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

FVnq6zE.png!web

答:Category的方法并不是在编译的时候将方法加入到ZXYPerson的实例对象中的,而是在运行时通过Runtime运行时机制将分类的方法合并到到实例对象中的!

将ZXYPerson+Eat.m clang编译成.cpp文件,查看编译之后的代码(xcrun -sdk iphonesimulator clang -rewrite-objc ZXYPerson+Eat.m)

查看一下协议Category的结构体如下:

6vQjiiI.png!web

上面协议ZXYPerson+Test以及ZXYPerson+Eat经过编译会编译成两个_category_t结构体,在运行时会把它加入到ZXYPerson的实例对象中(也有可能是类对象中,或者元类对象)

JbiiIrb.jpg!web

rYviqif.jpg!web

如果换成ZXYPerson+Test协议变为.cpp文件,_category_t这个结构体不会发生改变,但是后面的传参会发生改变如下:

vINn63M.jpg!web

fYzuqie.jpg!web

2. 源码讲解

下面来验证一下为什么Category的优先级高于主类?原理是什么?

2.1 结论

View Code

上面的run方法到底会调用哪一个呢,这个 编译顺序有关,最后参与编译的分类会优先调用

抽出上面的main.m代码,ZXYPerson、ZXYPerson+Eat以及ZXYPerson+Test都具有run方法(看上面收起来的代码)

#import <Foundation/Foundation.h>
#import "ZXYPerson.h"
#import "ZXYPerson+Eat.h"
#import "ZXYPerson+Test.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZXYPerson *person = [[ZXYPerson alloc]init];
        [person run];
    }
    return 0;
}

看下编译顺序和运行结果

6FbyIvM.jpg!web

如果将编译顺序改变下

7nE3me7.jpg!web

2.2 原因

查看的OC的源码是objc4-723版本 查看Category的加载处理过程,过程可以为以下步骤:

  1. objc-os.m 
  • _objc_init
  • map_images
  • map_images_nolock

2. objc-runtime-new.mm

  • _read_images(images是镜像,逆向开发博客有)
  • remethodizeClass
  • attachCategories
  • attachLists
  • realloc、memmove、memcpy

直接看Category加载逻辑,搜索attachCategories,找到对应的实现

mUvaiqb.jpg!web

下面针对attachCategories的实现开始讲解,大家认认真真看下代码

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    // cls: [ZXYPerson class]
    // cats - category: 代表[ZXYPerson+Eat,ZXYPerson+Test]
    
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();
    
    /**方法列表 **mlists  二维数组
     *[
     * [method_t,method_t],
     * [method_t,method_t]
     * ]
     */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    
    /**属性列表 **proplists 二维数组
     *[
     * [property_t,property_t],
     * [property_t,property_t]
     * ]
     */
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    
    /**协议列表 **protolists 二维数组
    *[
    * [protol_t,protol_t],
    * [protol_t,protol_t]
    * ]
    */
    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];
        //将category里面的方法列表组都加入到一个数组中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        //将category里面的属性列表组都加入到一个数组中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        //将category里面的协议列表组都加入到一个数组中
        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);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    // 将所有分类的属性,附加到类(对象)对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    //将所有分类的协议,附加到类(对象)对象的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

对于方法attachCategories的参数cls:在本次例子中代表的是[ZXYPerson class]; cats代表的是分类列表[ZXYPerson+Eat,ZXYPerson+Test]

然后紧接着分配了三个数组空间,三个二维数组mlists,proplists以及protolists分别放着方法、属性和协议列表,然后看一个附加加载过程的原理attachLists()方法

    /**方法列表 **mlists  二维数组
    *[
    * [method_t,method_t],协议ZXYPerson+Test的方法test
    * [method_t,method_t] 协议ZXYPerson+Eat的方法Eat
    * ]
    *  addedCount = 2
    */
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            //因为ZXYPerson有一个方法run,所以oldCount为1 ,addCount两个分类共有两个方法,所以newCount = 3
            uint32_t newCount = oldCount + addedCount;
            
            //通过realloc方法重新分配内存,并分配newCount的空间大小
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            
            //array()->lists:返回原来的方法列表
            //void    *memmove(void *__dst, const void *__src, size_t __len);将src的array()->lists移到array()->lists + addedCount,因为addedCount = 2,相当于将array()->lists向后移动2位
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            
            //addedLists:所有分类的方法列表
            //void    *memcpy(void *__dst, const void *__src, size_t __n); 将src的addedLists拷贝到dst的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]));
        }
    }

上面源码对应方法内容也做了比较详细的解释,大家可以细细体会看下!下面memcpy(拷贝)和memmove(移动)函数

2eMfMbf.png!web

现在就来回答第一个问题: Category的实现原理?

1. 通过Runtime加载某个类的所有Category数据

2. 把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会放在数组的签名

3. 将合并后的分类数据(方法、属性、协议)插入到类原来数据的前面

写到上面了,大家可能有人有疑问,那Category和 Objective-C 的class Extension有什么关系区别嘛?

  1. Objective-C 的class Extension 是在编译的时候,它的数据已经包含在类信息中
  2. Category在运行时才会将数据合并到类信息中

二、Category中有load方法嘛?load方法什么时候调用?

创建ZXYPerson以及ZXYPerson的分类ZXYPerson+Test和ZXYPerson+Eat,在代码中加入load代码,下面是源代码


#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
}


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ZXYPerson : NSObject

@end

NS_ASSUME_NONNULL_END



#import "ZXYPerson.h"

@implementation ZXYPerson
 
+(void)load {
    NSLog(@"ZXYPerson的load方法");
}

@end



#import <Foundation/Foundation.h>

#import "ZXYPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface ZXYPerson (Test)

@end



#import "ZXYPerson+Test.h"
#import <Foundation/Foundation.h>

@implementation ZXYPerson (Test)

+(void)load {
    NSLog(@"ZXYPerson (Test)的load方法");
}

@end



#import <Foundation/Foundation.h>

#import "ZXYPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface ZXYPerson (Eat)

@end

NS_ASSUME_NONNULL_END



#import "ZXYPerson+Eat.h"

@implementation ZXYPerson (Eat)

+(void)load {
    NSLog(@"ZXYPerson (Test)的load方法");
}

@end

View Code

FbyuUnY.jpg!web

刚刚代码的编译顺序如下:

YFNrYbB.jpg!web

下面通过源码分析一下load的调用顺序! objc4源码解读过程:objc-os.mm

1.  _objc_init,点击load_images

V7F7baN.png!web

2. 进入load_images,看到prepare_load_methods和call_load_methods,首先看call_load_methods

eQRVzqZ.png!web

3. 查看call_load_methods

qUnEriU.jpg!web

上面就验证了一个观点: 先调用类的+load,然后再调用分类的+load

紧接着分别查看call_class_loads方法和call_category_loads方法

4. call_class_loads 加载类的load方法

假如项目中有100个类, 到底先调用哪一个类呢?

Nf26rqE.png!web

回到上面2中的load_images中,发现调用 call_classes_loads 之前也做了调用 prepare_load_methods, 再次进入了prepare_load_method中,看看有没有做一些准备工作:

5. prepare_load_method的实现

Nf2Ubmz.png!web

prepare_load_method 实现分为以下步骤:

  1. 获取所有类,调用schedule_class_load
  2. 关于classlist和categorylist两个数组顺序是根据类、分类被编译的顺序放到了对应的数组中去

6. 类load的调用顺序

确定类load的调用顺序,依赖于schedule_class_load,它的实现如下:

fYRVNfy.jpg!web

上面源码表明首先调用父类,然后子类!

对于重写了+load的类,load方法调用顺序是 先编译的类的父类>先编译的类>后编译类的父类>后编译的类

总结:load调用顺序 ( +load)方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用

1.     先调用类的+load

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

2.  再调用分类的+load方法

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

三、initialize讲解

+initialize的方法在类第一次接受消息时会被调用

1.   例子

首先给ZXYStudent类和Person类覆盖+initialize,主要代码如下

//ZXYPerson
+ (void)initialize{
    
    NSLog(@"Person + initialize");
}

//ZXYPerson +Test
+ (void)initialize{
    
    NSLog(@"Person (Test1) + initialize");
}

//ZXYPerson+Eat
+ (void)initialize{
    
    NSLog(@"Person (Eat) + initialize");
}

//ZXYStudent
+ (void)initialize{
    
    NSLog(@"Student + initialize");
}

//ZXYStudent (Test)
+ (void)initialize{
    
    NSLog(@"Student (Test) + initialize");
}

//ZXYStudent (Eat)
+ (void)initialize{
    
    NSLog(@"Student (Eat) + initialize");
}

此时运行程序,并没有发现打印,说明运行没有调用initialize

假如给ZXYPerson类发送消息,如下,打印出ZXYPerson+Test的initialize方法

qmI73ie.jpg!web

因为initialize是走runtime那套,通过msgSend()方式,所以先打印出ZXYPerson的分类方法,至于先打印出哪一个分类的initialize,看编译顺序(先编译后执行)

A77J7b2.png!web

当使用ZXYPerson的子类ZXYStudent发送消息

UNruEru.png!web

发现先调用ZXYPerson的分类,然后再调用子类的分类!也可以多写几行[ZXYStudent alloc],发现还是一样的打印,说明initialize仅仅在该类第一次收到消息才会调用,下面通过查看源码讲解其调用顺序!

2.  源码讲解

使用objc4查看initialize的源码解读过程主要在 objc-runtime-new.mm中

  1. class_getInstanceMethod
  2. lookUpImpOrNil
  3. lookUpImpOrForward
  4. _class_initialize
  5. callInitialize
  6. objc_msgSend(cls, SEL initialize)

1). 上面[ZXYStudent alloc]相当于objc_msgSend([ZXYStudent class], @selector(alloc))

AZZRZzF.png!web

2). 然后进行点击红色内容

A7Rvm2N.png!web

3). 点进去,然后继续寻找lookUpImpOrForward,源码主要内容

N3eUjer.png!web

这样的源代码说明了 每个类的+initialize方法只会调用一次

4). _class_initialize源代码如下

v2emA3y.png!web

上面的源代码说明首先调用父类的+initialize,然后再调用子类的+initialize,就像上面先调用ZXYPerson的+initialize,然后再调用ZXYStudent的+initialize

5). 最后查看callInitialize

EVBzu2i.png!web

说明+initialize方法是通过msgSend()的方式进行调用

3.  总结

initialize调用顺序

首先调用父类的+initialize方法,然后再调用子类的+initialize

(先初始化父类,然后初始化子类,每个类只会被初始化1次)

通过上面总结下+initialize和+load区别

1.  调用方式

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

2.  调用时刻

  • load是runtime加载类、分类的时候调用(只会被调用1次)
  • initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize可能会调用多次-子类和分类都没有实现+initialize可能存在)

3.  调用顺序

load方法

  • load方法先调用类的load(先编译的类优先调用load,调用子类的load之前,会优先调用父类)
  • 然后调用分类的load,先编译的分类,会优先调用load方法

initialize

  • 先初始化父类
  • 再初始化子类

四.  category能添加成员变量(实例变量)(属性)嘛?

默认情况下: 不能

因为类的内存布局在编译时候就已经确定了,Category是在运行时才加载的,无法更改早已确定的内存布局。

由于分类底层结构的限制,不能添加成员变量到分类中,但是可以通过关联(Associate)方式进行间接实现!

上面就是Category、load和initialize的讲解,希望对大家有所帮助!!!如果觉得写得还不错,给个赞吧!!!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK