Objective-C 中的对象、类、协议和扩展、方法和Block

对象的本质

在 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 嵌套使用时,需要遵循一定的代码风格规范,以提高代码的可读性和可维护性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容