对象的本质
在 Objective-C 中,对象实际上是一个结构体指针,称为 isa 指针。这个指针指向一个存储在内存中的对象实例。对象实例实际上是一个连续的内存块,这个内存块包括了对象的实例变量、对象的类信息和其他的一些内部信息。
isa 指针是一个指向该对象所属的类的指针,通过 isa 指针,对象能够调用自己类的方法。isa 指针是一个 C 语言结构体指针,这个结构体中保存了该对象的类型信息以及对应的方法列表。在对象创建时,分配一个内存空间用于存储对象数据和 isa 指针,isa 指针指向该对象所属的类。
Objective-C 的对象都是在堆上分配内存空间的,而且其大小是可变的,由系统动态分配和管理。对象的实例变量会存储在对象的内存空间中,而方法则存储在该对象所属的类的方法列表中,方法列表是一个 C 语言的结构体,其中包含了所有该类中定义的方法的信息,如方法名、方法实现的地址等。对象的方法调用过程实际上就是通过 isa 指针查找到对象所属类的方法列表,并在该列表中查找需要调用的方法实现的地址,然后执行该方法。
在对象中,实例变量存储对象的状态和数据,类信息存储对象所属的类的信息,这包括方法列表、属性列表等信息。当对象调用一个方法时,实际上是向对象所属的类发送了一个消息,类会根据方法名在方法列表中查找对应的方法,并执行它。这也是 Objective-C 运行时的一个重要特性。
在 Objective-C 中,对象通常可以分为以下两类:
静态对象
静态对象是指在编译时就已经确定了内存空间的对象,通常是全局变量、静态变量或常量等。静态对象在程序运行期间不会发生变化,因此不需要进行引用计数和内存管理。动态对象
动态对象是指在运行时动态创建和销毁的对象,通常使用 alloc、new、copy 或 mutableCopy 等方法创建。动态对象在程序运行期间可以动态地增加或减少,因此需要进行引用计数和内存管理。
在 Objective-C 中,动态对象通常由类对象、实例对象和元类对象组成,它们分别代表了不同的对象类型。其中,类对象代表了一个类的定义,实例对象代表了类的一个实例,元类对象则代表了一个类对象的定义。
类对象和元类对象都是特殊的实例对象,它们的内部结构与普通实例对象不同。类对象通常存储了类的属性、方法列表和父类等信息,元类对象则存储了类方法列表和父类等信息。在 Objective-C 中,所有的类都是通过类对象来创建和初始化的。
Protocol和Extension
协议(Protocol)定义了一组方法,通常用于声明一个类应该实现哪些方法。类可以遵循一个或多个协议,实现协议中定义的方法。在 Objective-C 中,协议的声明类似于接口的概念,它只定义方法而不实现。
扩展(Extension)是一个类的匿名分类,它在不改变原有类接口的情况下,可以增加实例变量和方法。通常扩展用于声明私有方法、私有变量和类的匿名分类。
Objective-C 中的协议和扩展有以下特点:
- 协议和扩展都使用 @protocol 关键字来声明。
- 协议中只包含方法的声明,不包含属性和实现;扩展中可以包含方法的实现、属性和实例变量的声明。
- 协议可以被任何遵循它的类实现;扩展只能被原类实现。
- 协议中的方法可以被实现为必需方法或可选方法,必需方法必须被实现,而可选方法可以选择性实现;扩展中的方法都是必需方法,必须被实现。
- 一个类可以遵循多个协议,但只能有一个扩展。
在实际应用中,协议和扩展经常被用于解耦和添加私有方法。协议可以将类的实现和接口分离,让类具有更好的可复用性;扩展可以为一个类添加私有方法,这些方法不需要在公共接口中暴露,避免了其他类不必要的依赖。
Category
Objective-C 中的 category 实际上是一种给现有类添加方法的机制,它允许在不修改原始类的情况下添加方法。Category 的实现原理是在运行时动态修改类的方法列表,将分类中的方法添加到类的方法列表中。
当程序启动时,runtime 会读取所有的分类信息并创建一个运行时的方法列表,这个方法列表中存储了所有类的实例方法和类方法,分类中定义的方法就会被添加到这个方法列表中。在调用方法时,会在运行时的方法列表中寻找相应的方法实现。
因此,可以通过 Category 动态添加方法来扩展现有类的功能,而不需要改变原始类的实现。同时,由于 Objective-C 中所有的方法都是动态绑定的,所以可以保证在运行时即使给一个对象添加了一个方法,该对象也能够正确的响应该方法的调用。
category 通常用于以下几个方面:
给已有的类添加方法:通过给已有的类添加方法,可以为其添加新的行为或扩展现有行为,使其满足业务需求。比如可以通过 category 给 NSString 类添加一些常用的方法,方便日常开发中使用。
将类的实现分散到多个不同的文件中:当一个类的实现过于复杂时,可以通过 category 将其实现分散到多个文件中,从而提高代码的可维护性和可读性。
实现协议:category 可以实现协议,这在一些场景下非常有用。比如在一个大型项目中,可以通过 category 将某个协议的实现分散到多个不同的文件中,便于管理和维护。
需要注意的是,category 不能添加新的实例变量。如果要添加实例变量,可以通过关联对象的方式实现。另外,当多个 category 实现了同一个方法时,编译器会按照一定的顺序将其合并。这个顺序是不确定的,因此如果多个 category 实现了同一个方法,就需要保证它们之间没有冲突。
category添加实例变量
如果要为类添加属性或成员变量,可以使用关联对象(Associated Object)。所谓关联对象就是将一个对象与另外一个对象建立一种关联关系,从而使得这个对象具备某些特定的属性或行为。
具体实现方式是使用 Objective-C 的运行时机制,通过调用 objc_setAssociatedObject 函数或者 objc_getAssociatedObject 函数为对象关联属性或成员变量。关联对象的实现方式很灵活,可以用于给分类添加属性,也可以用于给任意对象添加属性。
下面是一个示例代码,演示如何通过关联对象在 category 中添加属性:
#import <objc/runtime.h>
@interface NSObject (MyCategory)
@property (nonatomic, strong) id myProperty;
@end
@implementation NSObject (MyCategory)
- (id)myProperty {
return objc_getAssociatedObject(self, @selector(myProperty));
}
- (void)setMyProperty:(id)myProperty {
objc_setAssociatedObject(self, @selector(myProperty), myProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
上面的代码中,我们为 NSObject 添加了一个名为 myProperty 的属性。在实现中,使用 objc_setAssociatedObject 函数和 objc_getAssociatedObject 函数来为对象关联属性,并在 get 和 set 方法中进行属性的读写操作。需要注意的是,由于是给对象关联属性,所以需要使用 objc_setAssociatedObject 函数的 OBJC_ASSOCIATION_RETAIN_NONATOMIC 参数来保证属性的内存安全。
category使用注意事项
不要重写原有的方法,这可能导致意想不到的后果。
当同一方法在多个 category 中实现时,无法预测哪个实现会被调用。
如果你要在 category 中添加新的属性,你需要实现一个关联对象的 setter 和 getter 方法,因为 category 不能添加实例变量。
在使用 category 时,应该选择一个合适的命名方式,以免与其他类库中的 category 重名。通常,可以在类名后面添加一个前缀。
避免在 category 中定义与主类相同名称的方法或属性,这样会导致覆盖原始实现。
当使用 category 时,应该非常小心地选择命名,尤其是在使用常见的名字时。为了避免冲突,可以在命名时加入特殊前缀或后缀,以确保 category 名称唯一。
不要在 category 中定义与主类相同名称的方法或属性,因为这样会导致覆盖原始实现。
方法的本质
在 Objective-C 中,方法本质上是一个 C 函数,它的第一个参数是接收消息的对象(也称为消息接收者,receiver),后面的参数则是方法的参数。
在运行时,Objective-C 方法的调用是通过向接收者发送消息来完成的。当向一个对象发送消息时,Objective-C 运行时会根据这个对象的类和消息的名称找到对应的方法实现。如果找到了,则会调用该方法;如果没有找到,则会转发该消息。
Objective-C 方法的实现可以是实例方法,也可以是类方法。实例方法是在对象实例上执行的方法,而类方法则是在类对象上执行的方法。
方法的选择器(selector)是用来标识一个方法的名称和参数类型的。在 Objective-C 中,每个方法都有一个唯一的选择器,它由方法名和参数类型编码组成。选择器是通过编译器在编译时生成的,可以通过 @selector() 关键字来获取。当向一个对象发送消息时,Objective-C 运行时会使用选择器来查找方法的实现。
总的来说,Objective-C 中的方法本质上就是一组 C 函数,它们的名称和参数类型由选择器标识,通过向对象发送消息来调用。方法的实现可以是实例方法或类方法。
Block
block本质上也是一个OC对象,它内部也有个isa指针
block是封装了函数调用以及函数调用环境的OC对象
在底层实现上,Block在编译时被转换成了一个结构体,包含了函数指针和捕获变量。当Block被执行时,它会创建一个闭包,保存捕获的变量的值和执行的代码。在Block内部使用的局部变量,在Block执行时被捕获,Block执行完毕后,这些变量依然存在于内存中,直到Block本身被销毁。
在实际应用中,Block可以作为一种轻量级的回调方式,方便在不同代码块间传递数据和代码逻辑,以便实现异步执行、并行处理和代码重用。常见的使用场景包括网络请求、动画效果、定时器、事件响应等。同时,需要注意Block的内存管理,避免出现循环引用等问题。
block的存储
block 在创建时会在栈上分配内存,如果在方法或函数内部创建 block 并返回,那么这个 block 就被分配在栈上,栈在方法或函数结束时被销毁,这时 block 中的变量将被销毁,如果再次访问 block 中的变量就会出现野指针。
为了解决这个问题,block 支持三种类型:全局块、栈块和堆块。全局块是编译时就分配好的,不会捕获任何外部变量,可以在任何时候使用,而栈块和堆块是在运行时分配的。
如果 block 捕获的变量是通过 __block 关键字声明的,那么就会在堆上分配内存,否则就会在栈上分配内存。当 block 被复制到堆上时,栈上的 block 会被销毁,而复制到堆上的 block 可以在其它方法或函数中使用,直到被释放为止。因此,如果需要在 block 中持有外部对象时,应该使用弱引用或者复制一份对象到堆中。
block的弱引用方式
- 使用 __weak 弱引用
可以使用 __weak 关键字来创建一个指向对象的弱引用,从而避免循环引用。在 block 中引用 self 时,使用 __weak 来避免 strong reference 循环引用,例如:
__weak typeof(self) weakSelf = self;
[self someMethodWithCompletionHandler:^{
[weakSelf doSomething];
}];
- 使用 __block 局部变量
另一种方式是使用 __block 修饰符来创建一个可变的局部变量,从而避免循环引用。在 block 中引用 self 时,使用 __block 来避免 strong reference 循环引用,例如:
__block typeof(self) blockSelf = self;
[self someMethodWithCompletionHandler:^{
[blockSelf doSomething];
}];
需要注意的是,使用 __block 修饰符时需要注意变量的生命周期,因为 block 会对变量进行复制,如果变量在 block 执行完毕后被释放,那么 block 引用的变量就是一个野指针,会导致程序崩溃。
- 使用弱化的 self
还有一种方式是使用一个弱化的 self 变量来避免循环引用。这个变量只需要在 block 中使用,不需要在其他地方使用。例如:
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething];
}
});
需要注意的是,在外部使用__weak修饰后,在block内部访问该对象,需要使用strongSelf来避免引起循环引用。如果不使用strongSelf,由于外部对象已经被__weak修饰,如果在block内部直接使用该对象,可能会因为在执行block的过程中对象被释放而导致程序崩溃。
使用strongSelf可以将外部对象在block内部强引用一次,使其在block执行期间不被释放,从而避免程序崩溃。
使用block需要注意的问题
循环引用问题:在 block 中捕获外部对象时,需要注意避免循环引用问题。可以使用 __weak 来修饰外部对象,也可以使用 __block 和弱引用来避免循环引用。
block 的生命周期:需要注意 block 的生命周期,特别是在异步线程中执行 block 时,需要确保 block 执行期间相关对象不会被释放。
block 的线程安全性:需要注意多线程环境下 block 的线程安全性,特别是当多个线程访问同一个 block 时,需要避免竞争条件。
block 的参数和返回值类型:需要注意 block 的参数和返回值类型,特别是当 block 作为参数传递时,需要确保参数和返回值类型与接收方的要求一致。
block 的代码风格:需要注意代码风格,特别是当多个 block 嵌套使用时,需要遵循一定的代码风格规范,以提高代码的可读性和可维护性。