美文网首页
iOS底层原理 - 探寻Category本质(二)

iOS底层原理 - 探寻Category本质(二)

作者: hazydream | 来源:发表于2017-11-20 13:57 被阅读34次

面试题引发的思考:

Q: Category中load方法是什么时候调用的?load方法能继承吗?

  • load方法在runtime加载类、分类的时候调用;
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用。

Q: loadinitialize方法的区别什么?它们在Category中的调用的顺序?以及出现继承时两者之间的调用过程?

  • 答案见文章分析。

1. Category中load方法探究:

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
+ (void)test;
@end

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

// TODO: -----------------  Person+Category1类  -----------------
@implementation Person (Category1)
+ (void)load {
    NSLog(@"Person (Category1) + load");
}
+ (void)test {
    NSLog(@"Person (Category1) + test");
}
@end

// TODO: -----------------  Person+Category2类  -----------------
@implementation Person (Category2)
+ (void)load {
    NSLog(@"Person (Category2) + load");
}
+ (void)test {
    NSLog(@"Person (Category2) + test");
}
@end

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    // [Person test];
}

代码如上:
定义一个Person类、定义两个Person的分类,然后分别实现load方法、test方法,不做任何操作,直接运行程序打印结果:

打印结果

发现类和分类的load方法被调用,这是因为:
load方法在runtime加载类、分类的时候调用

接着放开[Person test]语句,调用test方法,运行程序打印结果:

打印结果

查看编译顺序:

编译顺序

iOS底层原理 - 探寻Category本质(一)可知:

  • 分类重写类的方法会优先调用,类的方法存在最后的内存中,所以调用顺序优先按照分类的编译顺序逆序排列。结论跟打印结果一致。

Q: 那么load方法的是如何实现调用的呢?调用顺序又是怎样的呢?

(1) load方法调用原理:

接下来根据OC源码进行分析,首先找到runtime初始化函数_objc_init

runtime初始化函数

然后通过load_images读取模块找到load_images函数:

load_images函数

然后通过call_load_methods读找到call_load_methods函数:

call_load_methods函数

源码显示:
优先调用类的load方法,再调用分类的load方法。

然后通过call_class_loads找到call_class_loads函数:

call_class_loads函数

源码显示:
call_class_loads函数直接取出类里面的方法,返回方法的地址,通过地址直接调用load方法。

同理通过call_category_loads找到call_category_loads函数:

call_category_loads函数

源码显示:
call_category_loads函数直接取出分类里面的方法,返回方法的地址,通过地址直接调用load方法;
分类中load方法的调用顺序是按照编译顺序正序调用的。

Q: test方法调用方式与load方法调用方式有何不同?

  • load方法调用方式是通过地址直接调用;
  • test方法调用方式是通过消息发送机制进行调用。

(2) load方法调用顺序原理:

以上分析可知:
load_images函数在执行call_load_methods();语句之前,有一个prepare_load_methods函数为load函数处理:

load_images函数

然后通过prepare_load_methods找到prepare_load_methods函数:

prepare_load_methods函数

发现优先对类进行处理,分类直接按顺序存储。

然后通过schedule_class_load找到schedule_class_load函数,看看需要对类做什么样的处理:

schedule_class_load函数

发现首先对schedule_class_load函数进行递归操作,先获取父类存储,然后获取子类存储。

通过以上分析可知:

  • load方法会在runtime加载类、分类时调用;
  • 每个类、分类的load方法在程序运行过程中只调用一次

load方法的调用顺序:

  1. 先调用类的load方法
    a> 按照编译的先后顺序调用
    b> 调用子类的load方法之前会先调用父类的load方法
  2. 再调用分类的load方法
    a> 按照编译的先后顺序调用

接下来通过代码验证一下:

// TODO: -----------------  Person类  -----------------
@implementation Person
+ (void)load {
    NSLog(@"Person + load");
}
@end

// TODO: -----------------  Person (Category1)类  -----------------
@implementation Person (Category1)
+ (void)load {
    NSLog(@"Person (Category1) + load");
}
@end

// TODO: -----------------  Person (Category2)类  -----------------
@implementation Person (Category2)
+ (void)load {
    NSLog(@"Person (Category2) + load");
}
@end

// TODO: -----------------  Student类,继承Person类  -----------------
@implementation Student
+ (void)load {
    NSLog(@"Student + load");
}
@end

// TODO: -----------------  Student (Category1)类  -----------------
@implementation Student (Category1)
+ (void)load {
    NSLog(@"Student (Category1) + load");
}
@end

// TODO: -----------------  Student (Category2)类  -----------------
@implementation Student (Category2)
+ (void)load {
    NSLog(@"Student (Category2) + load");
}
@end

// TODO: -----------------  Animal类,独立的类  -----------------
@implementation Animal
+ (void)load {
    NSLog(@"Animal + load");
}
@end
编译顺序

分析以上:

  1. 首先调用类的load方法,按照编译的先后顺序调用,则先调用的是Student类,但是调用子类的load方法之前会先调用父类的load方法,所以先调用Person类,再调用Student类,最后调用Animal类;
  2. 再调用分类的load方法,按照编译的先后顺序调用,所以调用顺序为:Student (Category1)Student (Category2)Person (Category1)Person (Category2)

打印结果如下:

打印结果

打印结果与分析结果一致,结论可证。

2. Category中initialize方法探究:

// TODO: -----------------  Person类  -----------------
@implementation Person
+ (void)initialize {
    NSLog(@"Person + initialize");
}
@end

// TODO: -----------------  Person+Category1类  -----------------
@implementation Person (Category1)
+ (void)initialize {
    NSLog(@"Person (Category1) + initialize");
}
@end

// TODO: -----------------  Person+Category2类  -----------------
@implementation Person (Category2)
+ (void)initialize {
    NSLog(@"Person (Category2) + initialize");
}
@end

// TODO: -----------------  Student类,继承Person类  -----------------
@implementation Student
+ (void)initialize {
    NSLog(@"Student + initialize");
}
@end

// TODO: -----------------  Student (Category1)类  -----------------
@implementation Student (Category1)
+ (void)initialize {
    NSLog(@"Student (Category1) + initialize");
}
@end

// TODO: -----------------  Student (Category2)类  -----------------
@implementation Student (Category2)
+ (void)initialize {
    NSLog(@"Student (Category2) + initialize");
}
@end

// TODO: -----------------  Teacher类,继承Person类  -----------------
@implementation Teacher

@end

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [Teacher alloc];
    // objc_msgSend([Person class], @selector(alloc));
    // objc_msgSend([Teacher class], @selector(alloc));
    [Student alloc];
    // objc_msgSend([Person class], @selector(alloc));
    // objc_msgSend([Student class], @selector(alloc));
    [Person alloc];
    // objc_msgSend([Person class], @selector(alloc));
}

代码如上:
定义一个Person类、两个Person的分类,再定义Person的子类Student、两个Student的分类,然后分别实现initialize方法,最后定义Person的子类Teacher;不做任何操作,直接运行程序则没有打印。

接着放开[Teacher alloc];[Student alloc];[Person alloc];语句,运行程序打印结果:

打印结果

查看编译顺序:

编译顺序

通过以上代码分析即编译顺序可知:

  1. 第一句打印的是Person (Category2)类的initialize方法,代码执行的是[Teacher alloc];语句。
    说明[Teacher alloc];语句会先调用父类Personinitialize方法;

  2. 第二句打印的是Person (Category2)类的initialize方法,代码执行的是[Student alloc];语句。
    说明[Student alloc];语句会先调用父类Personinitialize方法;

  3. 第三句打印的是Student (Category2)类的initialize方法。
    说明[Student alloc];语句会先调用父类Personinitialize方法,再调用自己Studentinitialize方法;

  4. 第四句没有打印。
    说明[Person alloc];语句调用自己的initialize方法时,前面已经初始化过了;说明每个类只会初始化一次。

通过以上分析可得:

  • initialize方法在类第一次接收消息时调用;
  • 优先调用父类的initialize方法,再调用子类的initialize方法;
    (先初始化父类,再初始化子类,每个类只初始化一次)

    如果分类没有实现initialize方法,则会调用父类的initialize方法
    如果分类实现了initialize方法,就会覆盖类本身的initialize方法

(1) initialize方法调用原理:

根据OC源码进行分析,首先找到runtime函数class_getClassMethod

class_getClassMethod函数

然后通过class_getInstanceMethod读找到class_getInstanceMethod函数:

class_getInstanceMethod函数

然后通过lookUpImpOrNil读找到lookUpImpOrNil函数:

lookUpImpOrNil函数

然后通过lookUpImpOrForward读找到lookUpImpOrForward函数:

lookUpImpOrForward函数

源码显示:

  • 传入的类需要初始化且没有初始化,进行初始化操作,否则跳过;
  • 只有在第一次接收消息的时候调用initialize方法。

然后通过_class_initialize读找到_class_initialize函数:

_class_initialize函数

源码显示:

  • 如果传入的类存在父类且父类没有初始化,那么先初始化父类;
  • 然后再初始化自己,实现initialize方法。

3. Category中load方法、initialize方法比较:

Q: load方法、initialize方法的区别是什么?以及出现继承时两者之间的调用过程?

  1. 调用方式:
  • load是根据函数地址直接调用;
  • initialize是通过objc_msgSend调用。
  1. 调用时刻:
  • load 是runtime加载类、分类的时候调用(只会调用一次);
  • initialize是类第一次接收到消息的时候调用,每个类只会调用initialize一次(父类的initialize可能调用多次)。
  1. 调用顺序:
  • load
    1. 先调用类的load方法;
      a> 按照编译的先后顺序调用;
      b> 调用子类的load方法之前会先调用父类的load方法;
    2. 再调用分类的load方法。
      a> 按照编译的先后顺序调用
  • initialize
    1. 先初始化父类;
    2. 再初始化子类(可能最终调用的是父类的initialize 方法)。

相关文章

网友评论

      本文标题:iOS底层原理 - 探寻Category本质(二)

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