iOS Category Load Initialize

一:Category 类内部原理

一个类永远只有一个类对象。那分类的方法存放在哪里。运行起来后,最后对象方法统一都会放在类对象中。如果存在类方法,那么统一都会放在元类方法中。分类的合并是 运行时通过runtime动态的讲分类的方法合并到类方法和元类方法中。

那编译时刻 分类的最终数据结构就是下面这个结构体,下面这个样子

这里需要注意一下,有几个分类,就会有几个这样的_category_t 结构体。

由上图可以看出,分类中可以有实例方法、类方法、协议、属性。

这些文件,实例方法在运行时加入到类的实例方法中,类方法在运行时加入到元类的类方法中。

objc_os.mm :运行时的入口(images :镜像、模块)

下面讲解一下分类运行时刻如何把方法和属性和协议放在类中的。

1 创建容器。这里有Person类和分类:Person(Test) 和 Person(Eat)

2:这里取出来所有的分类的方法 放在一个数组中,同理协议和属性。注意这里是while i--  这个分类是按照--的顺序来加入的,跟编译顺序有关系,最后面编译的分类 首先加入方法中(),那么在查找方法的时候就会先找到这个方法并直接返回。

3:这里开始合并,把所有分类合并好的数组 合并到类对象的方法中(协议、属性)。这里的rw要注意。可以向下看,有具体解释,类中也有一个rw的属性,是关于所有的实例方法列表、属性列表和协议列表还有其他的一个结构体,命名的意义性。

下面是attachLists()方法的内部实现,如何合并分类的方法到类中的源代码。

详解合并类方法到类中的方法的过程

这个是形象化的过程

这里也就知道原来类的方法列表在3的位置,也就只有一个红色的1的区域,因为有2个分类,所以扩张了两个区域,并且把原来的方法列表memmove在了最后一个区域处,并把分类的方法分别memcopy在前两个区域中,所以也就能证明 类和分类 同时实现一个方法,会优先实现分类的,这并不是覆盖,多个分类同时实现一个方法,则会首先实现编译在最后面的文件的方法。

事实用代码验证:


#import <Foundation/Foundation.h>

@interface Person : NSObject

- (void)run;

@end

#import "Person+Test.h"

@implementation Person (Test)
- (void)run
{
    NSLog(@"Test run");
}

@end

#import "Person+Eat.h"

@implementation Person (Eat)

- (void)run
{
    NSLog(@"Eat run");
}

@end
//调用:
   Person *person = [[Person alloc] init];
   [person run];
// 结果
2018-07-25 22:30:18.882 newxc[1373:101454] Eat run

那么编译的顺序看哪里呢,看下图:

所以最后面参与编译的分类,优先放在前面,所以就会优先调用与分类与类重复的方法。

memcopy:不会做判断,直接copy,

memove:内部会做判断往右边还是左边挪动,会将后面的数据依次往后挪,根据传进来的数据的地址大小,就自动知道从左挪到右,还是从右挪到左。保证数据的完整性(完整的挪到想要到达的位置)。

比方说 3 4 1 1  copy 3 4 到 4 1的位置的话,先copy3 到4的位置,然后再copy原来4的位置的数据也就是现在的3 到1的位置,这也就是说挪动数据不完整。但是move却不会这样, 先挪动4 到1的位置,然后再挪动3 到4的位置,这样就保证了数据的完整性。

这是一个类的结构,rw_t 这个 在上面有提到,可以认真观察下。

ro的意思是只读,ro里面的baseMethodList跟class_rw_t这个里面的methods是一样的,只是ro是拷贝了一份,但是一般不用。

也可以认为是之前的方法列表,也就是没有添加分类之前的最基本的分类列表。而class_tw_t中的mehods是随意可改的。

1.2:关联对象

分类不可以直接添加成员变量,可以间接的方式。看下面几种类比实现方式

#import "Person.h"

@interface Person (Test)

@property (nonatomic, assign) int weight;
@property (nonatomic, assign) int name;

@end

@implementation Person (Test)

// 方案一
const void *weightKey = &weightKey;

- (void)setWeight:(int)weight
{
    objc_setAssociatedObject(self, weightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)weight
{
    return [objc_getAssociatedObject(self, weightKey) intValue];
}



 // 方案二
 static const char nameKey;
 
 
 - (void)setName:(int)name
 {
 objc_setAssociatedObject(self, &nameKey, @(name), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
 - (int)name
 {
 return [objc_getAssociatedObject(self, &nameKey) intValue];
 }



 // 方案三
 #define nameKey @"name"
 
 - (void)setName:(int)name
 {
 // 这其实传进去的是字符串地址 : NSString *str = @"name";  @"name"放在数据常量区
 objc_setAssociatedObject(self, nameKey, @(name), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
 - (int)name
 {
 return [objc_getAssociatedObject(self, nameKey) intValue];
 }





// 方案四
- (void)setName:(int)name
{
    //_cmd == @selector(name)
    //@selector(name) 相当于返回某个结构体的指针
    NSLog(@"%p %p %p", @selector(name), @selector(name), @selector(name));
    // 这其实传进去的是字符串地址 : NSString *str = @"name";  @"name"放在数据常量区
    objc_setAssociatedObject(self, @selector(name), @(name), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 上面第一个self只要是对象就行 因为接收的是id类型 并且这个方法不会影响person这类原来的内存结构,name也不会存进到perosn的ivar列表里
}

- (int)name
{
    // 这个也可以这么写    return [objc_getAssociatedObject(self, _cmd) intValue]; 因为前面已经设置了set方法,所以现在才可以用_cmd
    return [objc_getAssociatedObject(self, @selector(name)) intValue];
}


@end

下面讲内部原理:

1.3:关联对象

map:可以理解为就是字典

给person分类添加属性的set和get方法,


#import "Person+Eat.h"
#import <objc/runtime.h>

@implementation Person (Eat)

// 方案四
- (void)setName:(int)name
{
    //_cmd == @selector(name)
    //@selector(name) 相当于返回某个结构体的指针
    NSLog(@"%p %p %p", @selector(name), @selector(name), @selector(name));
    // 这其实传进去的是字符串地址 : NSString *str = @"name";  @"name"放在数据常量区
    objc_setAssociatedObject(self, @selector(name), @(name), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 上面第一个self只要是对象就行 因为接收的是id类型 并且这个方法不会影响person这类原来的内存结构,name也不会存进到perosn的ivar列表里
}

- (int)name
{
    // 这个也可以这么写    return [objc_getAssociatedObject(self, _cmd) intValue]; 因为前面已经设置了set方法,所以现在才可以用_cmd
    return [objc_getAssociatedObject(self, @selector(name)) intValue];
}

/*
 
 // 方案三
 #define nameKey @"name"
 
 - (void)setName:(int)name
 {
 // 这其实传进去的是字符串地址 : NSString *str = @"name";  @"name"放在数据常量区
 objc_setAssociatedObject(self, nameKey, @(name), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
 - (int)name
 {
 return [objc_getAssociatedObject(self, nameKey) intValue];
 }

 
 
 // 方案二
 static const char nameKey;
 
 
 - (void)setName:(int)name
 {
 objc_setAssociatedObject(self, &nameKey, @(name), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
 - (int)name
 {
 return [objc_getAssociatedObject(self, &nameKey) intValue];
 }
 
 // 方案一
 const void *weightKey = &weightKey;
 
 
 - (void)setWeight:(int)weight
 {
 objc_setAssociatedObject(self, weightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
 - (int)weight
 {
 return [objc_getAssociatedObject(self, weightKey) intValue];
 }


 */

objc_setAssociatedObject的内部原理

总结就是

所以 关联对象不是把key放在类中,而是自己维护了一个全局的map,是runtime自己维护的。

1.1.0:没有weak。它并不是属性。

关联对象没有若引用效果的。

// 原因:
     
        Person *person = [[Person alloc] init];
        
        
        {
           Person *temp = [[Person alloc] init];
            
            objc_setAssociatedObject(person, @"temp", temp, OBJC_ASSOCIATION_ASSIGN);
        }
        
        NSLog(@"%@",objc_getAssociatedObject(person, @"temp"));

这个会报错,bad_access错误。因为出了作用域,如果它是weak的,那么就会置为nil,则不会报野指针错误。

对object不存在引用,value在内部是存在引用的。

一旦person对象销毁,那么所有key对应的map会自动移除,

消除关联对象的两种方式:

1.1.1:局部的,哪个属性关联了,消除哪个,比方说

person.name = @"";

这样既可把name这个属性的关联对象置为空了,。

解读:相当于是

 objc_setAssociatedObject(person, @"temp", nil, OBJC_ASSOCIATION_ASSIGN);

这个相当于擦出的是AssociationMap里面的key和value。

1.1.2:移除所有关联对象:

objc_removeAssociatedObjects(<#id object#>)

这个相当于擦除的是AssociationHashMap里面的key和value,相当于是这个对象的所有关联对象都擦除。

二:load

不管用不用得到建立在项目中的类,都会被加载入内存.

#import "Person.h"

@implementation Person
+ (void)load
{
    NSLog(@"person-load");
}
@end

#import "Person+Test.h"

@implementation Person (Test)

+ (void)load
{
    NSLog(@"person-test-load");
}
@end


#import "Person.h"

@interface Student : Person

@end

#import "Student.h"

@implementation Student

+ (void)load
{
    NSLog(@"Student-load");
}
@end

#import "Student+Test1.h"

@implementation Student (Test1)

+ (void)load
{
    NSLog(@"Student-Test1-load");
}

@end
// 在完全没有调用的情况下 打印
2018-07-26 20:20:44.519 newxc[1827:174524] person-load
2018-07-26 20:20:44.520 newxc[1827:174524] Student-load
2018-07-26 20:20:44.520 newxc[1827:174524] person-test-load
2018-07-26 20:20:44.521 newxc[1827:174524] Student-Test1-load

这个load方法 跟 分类的方法不同。

load方法调用的时机:runtime在加载这个类 分类 就用调用对应的+load方法。

疑问:为什么分类已经实现了load,类的load依然被调用了?

先看一段代码

#import "Person.h"

@implementation Person

+ (void)load
{
    NSLog(@"person-load");
}

+ (void)test
{
    
}
@end
#import "Person+Test.h"

@implementation Person (Test)

+ (void)load
{
    NSLog(@"person-test-load");

}
+ (void)test
{
    
}

@end

#import "Person+Eat.h"

@implementation Person (Eat)

+ (void)load
{
    NSLog(@"person-Eat-load");

}
+ (void)test
{
    
}
@end
// c语言函数
void printMethodNameOfClass(Class clas)
{
    unsigned int cout;
    // 获得方法数组
    Method *methodlist = class_copyMethodList(clas, &cout);
    
    // 储存方法名
    NSMutableString *muString = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < cout; i ++) {
        // 获得方法
        Method methd = methodlist[i];
        
        // 获得方法名
        NSString *methodStr = NSStringFromSelector(method_getName(methd));
        
        // 拼接方法名
        [muString appendString:methodStr];
        [muString appendString:@", "];
        
    }
    // 释放
    free(methodlist);
    
    // 打印一下类和方法名
    NSLog(@"%@ %@",clas, muString);
    
}
int main(int argc, char * argv[]) {
    @autoreleasepool {
        printMethodNameOfClass(object_getClass([Person class]));
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
2018-07-26 20:33:24.577 newxc[1974:197984] person-load
2018-07-26 20:33:24.579 newxc[1974:197984] person-test-load
2018-07-26 20:33:24.580 newxc[1974:197984] person-Eat-load
2018-07-26 20:33:24.581 newxc[1974:197984] Person test, load, test, load, test, load, 

一:为什么类的load方法被调用,为什么类比分类load方法先调用?

可以看到这里是先看看有没有load方法:hasLoadMethods,这个跟普通的类的发送机制不同,这里是通过另外一套:调用一套load的实现方法。具体看下面内容。

看到这里,补充一下,可能会有异议,是否跟编译顺序有关,所以调用load的顺序,但是下面的结果可以证明跟编译顺序无关。

这里调用类的 load方法

这里是调用分类的load方法 

由上可知,load方法内部是重调用了load方法,并不是去调用的原来的类中直接调用的,所以会load方法不覆盖。

那为什么test是直接调用的呢:因为test方法是objc_msgSend() 方法来转发方法的(这就是通过消息机制,就是通过isa来 superclass等来寻找)。而load方法是直接通过load方法在内存中的地址 直接找到调用的。

二:多个类先调用load的顺序是怎样的?

那如果是多个类的情况下,类先调用的顺序是怎样的呢?比方说person类 student类 ,Student继承自Person。

下面是内部源码, 可以看到先把所有的编译好的类的一个数组,按照这个数组里的顺序 开始遍历,在遍历的过程中 先找到类的父类加入  然后再是子类。这个prepare_load_methods方法在讲load时第一张图片中有,那是在调用call_load_methods(内部将调用类和分类的顺序)方法之前

这个是证明类的加载顺序,即上面数组中类的顺序 就是这个编译的顺序,即使打印的顺序。

这是分类的

所以下面是总结,可以详细看一下。

so:总结:

+load方法会在runtime加载类、分类时调用。

每个类、分类的+load,在程序运行过程中只调用一次。

调用顺序:

1:先调用类的+load;

a:按照编译先后顺序调用(先编译、先调用)

b:调用子类的+load之前会先调用父类的+load



2:再调用分类的+load;

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

分类 是后编译先调用。
 

三  +initialize

+initialize方法会在类第一次接收到消息时调用,走的是objc_sendMsg() (消息机制)

调用顺序:

先调用父类的+initialize,再调用子类的+initialize方法(子类的可能不会调用)。

先初始化父类,后初始化子类,每个类只会初始化一次。

3.1首先用代码证实一下

#import "Person.h"

@implementation Person

+ (void)initialize
{
    NSLog(@"person-initialize");

}
@end


#import "Person+Test.h"

@implementation Person (Test)
+ (void)initialize
{
    NSLog(@"person-test-initialize");

}
@end
// 调用
  [Person alloc];
// 结果
2018-07-30 19:20:52.022 newxc[5252:1290176] person-test-initialize

证明有父类的 接上

#import "Person.h"

@interface Student : Person

@end

#import "Student.h"

@implementation Student
+ (void)initialize
{
    NSLog(@"Student-initialize");
 
}

@end

#import "Student+Test2.h"

@implementation Student (Test2)

+ (void)initialize
{
    NSLog(@"Student-test2-initialize");
    
}
@end

// 调用
[Student alloc];
// 打印
2018-07-30 19:24:39.544 newxc[5287:1296984] person-test-initialize
2018-07-30 19:24:39.545 newxc[5287:1296984] Student-test2-initialize

下面通过底层源码分析原因

首先[Student alloc]; 转成c语言就是objc_sendMsg([Student class],@selector(alloc));

objc_sendMsg 底层实现是半开源 是汇编实现的 isa-》类对象/元类,寻找方法-》superclass-》类对象/元类对象,寻找方法(msg_lookup)。

class_getInstanceMehod

首先 查找方法

下面这个跟上面那个是一个方法

上面调用的_class_initialize方法就是下面的具体实现 在此方法中会首先查看有没有父类,如果有则递归调用父类的_class_initialize方法,然后调用完父类的之后,看下面的图,会调用子类自己的callInitialize方法 在此方法中调用objc_msgSend()

上面就是源码实现原理

3.2:子类没有实现,父类实现了+initialized方法,调用子类的initialized方法,会发生什么?

#import "Person.h"

@implementation Person

+ (void)initialize
{
    NSLog(@"person-initialize");

}
@end

#import "Student.h"

@implementation Student
@end
// 调用
[Student alloc]
2018-07-30 22:31:59.896 newxc[5535:1415054] person-test-initialize
2018-07-30 22:31:59.897 newxc[5535:1415054] person-test-initialize

可以看到父类调用了两次

如果在此基础上再添加teacher类继承自person 不实现initialized方法

#import "Person.h"

@interface Teacher : Person

@end
#import "Teacher.h"

@implementation Teacher

@end
// 调用
[Student alloc];
[Teacher alloc];
// 结果
2018-07-30 22:34:35.784 newxc[5565:1419550] person-test-initialize
2018-07-30 22:34:35.785 newxc[5565:1419550] person-test-initialize
2018-07-30 22:34:35.786 newxc[5565:1419550] person-test-initialize

原因:

上图中是对上面的例子进行解读,伪代码示例。

        // 这个第一次是person确实没有initalize,,所以就初始化一次,调用此方法
        objc_msgSend([Person class], @selector(alloc));
        // 通过isa没找到,通过superclass找到父类的元类 找到了initialize方法, 打印person-test-initialize
        objc_msgSend([Student class], @selector(alloc));
        // 同上,通过superclass找到父类的元类,找到了initialize方法, 打印person-test-initialize
        objc_msgSend([Teacher class], @selector(alloc));

结论解释:

a:看student类有没有初始化,没有->找到父类Person,没有初始化->初始化父类,打印第一次person-test-initalize。

b:调用父类的 继续往下走,会调用student的初始化方法,也就是发送objc_msgSend方法,通过isa发现Student类里面没有,通过superclass找到父类Person中发现有initalize方法,然后直接调用,注意:这个跟上面找initalize方法不一样,这个是直接调用,走的是普通类调用的正常流程,所以打印第二次person-test-initalize。

c:看Teacher有没有初始化,没有-> 找到父类Person,有初始化,略过,初始化自己,跟上面一样,跟objc_msgSend方法 通过isa 找到teacher中没有initalize方法,然后通过superclass找父类中有initalize方法,然后,直接调用。so,打印第三次person-test-initalize。

这里注意:父类的initalize方法调用了三次,不代表父类初始化了三次。第一次调用是在当时的类中 判断父类时调用的,后面的两次 是给student和teacher发消息时候调用的

再给一个例子,以此证明:


#import "Person.h"

@implementation Person

+ (void)initialize
{
    NSLog(@"person-initialize");
}

@end

#import "Student.h"

@implementation Student
+ (void)initialize
{
    NSLog(@"student-initialize");
}

@end

#import "StudentA.h"

@implementation StudentA

@end

调用

[StudentA alloc];
2018-07-31 16:05:24.673882+0800 HXNewOC[1979:331349] person-initialize
2018-07-31 16:05:24.674129+0800 HXNewOC[1979:331349] student-initialize
2018-07-31 16:05:24.674244+0800 HXNewOC[1979:331349] student-initialize

好啦 这下就明白啦。

另外,这个分类的调用顺序,就跟正常类的调用顺序一样,分类和类同时实现,会优先调用分类的,子类的不会调用,因为这里没有特殊实现,

举个例子 接上面

#import "Student+AA.h"

@implementation Student (AA)
+ (void)initialize
{
    NSLog(@"student(AA)-initialize");
}

@end

#import "StudentA+Afenlei.h"

@implementation StudentA (Afenlei)
+ (void)initialize
{
    NSLog(@"studentA (Afenlei)-initialize");
}
@end

实现了student的分类AA 和studentA的分类Afenlei 调用[StudentA alloc];

2018-07-31 16:29:27.141243+0800 HXNewOC[2255:372318] person-initialize
2018-07-31 16:29:27.141321+0800 HXNewOC[2255:372318] student(AA)-initialize
2018-07-31 16:29:27.141402+0800 HXNewOC[2255:372318] studentA (Afenlei)-initialize

哈哈 是不是不出意外。当然需要额外注意的就是load的类和分类的调用比较特殊。

想在类加载进内存的时候调用,就用load方法,如果想在第一次加载类的时候调用,就用initialized方法。

 

面试相关题:

1:category的使用场合是什么?

2:category的实现原理是什么?

Categoty编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

3:category和extention的区别是什么?

Class Extension在编译的时候,它的数据就已经包含在类信息中

Category是在运行时,才会将数据合并到类信息中。

4:category中有load方法么?load方法是什么时候调用的?load方法能继承么?

有load方法。rutime在加载类和分类的时候调用load方法。

load方法可以继承,但是一般情况下不会主动调用load方法,都是让系统自动调用。([Student load]不实现子类的load方法,实现父类的load方法,然后开始调用子类的load方法,肯定会调用父类的load方法,实现了继承,因为isa和superclass,这个就是发送了一个方法objc_sendMsg() 方法,实质就是调用isa 找到方法中有的方法(这个方法先放进来的是分类的、其次是类的),如果没有再通过superclass调用父类的,跟子类的原理相同。而分类相同类相同方法的调用顺序是,编译在后面的,先调用。)

5:load、initialize方法的区别是什么?他们在category中的调用顺序?以及他们出现继承时的调用顺序?

区别:

a:调用方式区别:load是根据函数地址直接调用,initialize是通过objc_msgSend调用。

b:调用时刻的区别:load是在runtime加载类、分类时候调用,只会调用一次。initialize是类第一次接收到消息时调用,每一个类只会initialize一次,但是父类的initialize方法可能会被调用多次。

load、initialize调用顺序:

a:load先调用类的load(先编译的类,优先调用,调用子类的load之前,会先调用父类的load),其次再去调用分类的load(先编译的分类,先调用)。

b:initialize 先初始化父类,后初始化子类(要第一次调用),初始化子类时可能最终调用的是父类的initialize方法。

继承: 上面说过了。

6:category能否添加成员变量?怎么添加?

(分类的结构式不能添加成员变量的。)不能直接添加成员变量,但是可以间接实现Category有成员变量的效果

(在外面吧里面的全局变量的值改掉 加上static 只有当前文件才能用)

猜你喜欢

转载自blog.csdn.net/qq_27909209/article/details/81123765