news 2026/10/10 1:51:03

Objective-C面向对象基础:类、消息传递与属性机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Objective-C面向对象基础:类、消息传递与属性机制详解

聊到 OC(Objective-C),很多人的第一反应是“这不是一门老语言了吗”。确实,苹果生态里 Swift 已经唱了主角,但存量代码、历史项目、跨平台库、以及不少经典架构设计里,Objective-C 的身影依然无处不在。尤其是那些搞 iOS 底层、逆向、或者要接老 SDK 的工程师,绕不开这门语言。更关键的是,OC 的“面向对象”设计和我们现在用的多数语言都不太一样:它没有 JavaScript 那种原型链,却也和 Java、Python 的 class 体系有明显的差异,一批从其他语言转过来的朋友第一次写时,多少都会被[obj doSomething]这种中括号调用搞懵。

这篇文章写给那些已经知道“对象”大概是什么概念、但想系统把 OC 面向对象基础吃透的人。我会从类与对象的关系、消息传递机制、属性与协议这些核心知识点讲起,再给一套能直接跑的示例代码,最后把日常最容易踩的坑翻出来一个一个说。内容定位是“上篇”,核心聚焦在类、对象、方法、属性、初始化这些最基础的骨架,够你把手里的 OC 代码看懂、能自己动手写一个像模像样的类。至于更进阶的动态特性、内存管理细节、Category 和关联对象那些,留给下篇展开。

1. 项目概述:OC 的出身,以及它的面向对象到底在解决什么

1.1 为什么有了 C 语言,还要再整一个 OC 出来

OC 本质上是在 C 语言之上加了一层“对象消息”语法。C 语言能写操作系统、能写驱动、能写高性能后端,但它有一个非常实际的问题:当你做一个大工程时,代码的复用、模块的边界、团队之间的分工,全靠自觉。你可以在 C 里用结构体加函数指针模拟出“对象”,比如定义struct Person再写一堆person_set_name(p, "tom")这样的函数,但这种模拟需要大量约定的规矩,而且一不留神就把数据暴露得满世界都是。

面向对象要解决的核心问题,说白了就是三件事:把数据和操作它的方法绑在一起、把内部的实现细节藏起来、让不同的对象能按统一的接口被调用。OC 把这三件事用语法层面的机制固定下来了,而不是靠程序员自觉。类(Class)就是蓝图,对象(Object)就是根据蓝图造出来的实际东西,消息(Message)就是让对象执行动作的方式。这个组合虽然不如 C++ 那样讲究虚函数表,也不如 Java 那样强制统一继承树,但胜在动态、灵活、贴近运行时行为。

1.2 这篇“上篇”具体会覆盖哪些内容

我说实话,OC 的面向对象如果一口气全讲完,读者消化不了,我自己也容易写成一锅粥。所以“上篇”我严格控制范围,只聊最核心的四块:首先是最基本的类和对象怎么定义、怎么创建;其次是 OC 最有辨识度的消息调用机制,也就是为什么方法调用要用中括号;然后是属性@property的底层逻辑,它背后其实是自动生成的 setter/getter;最后是协议(Protocol),这个词对应很多语言里的“接口”。每个部分我都会给你看代码、讲原理、再附带上我在工程里实际遇到过的问题。

这四块内容消化完之后,你已经能读懂绝大多数 OC 类的结构了。像@interface、@implementation、@property、init这种高频词,不会再让你犯怵。至于更深的内容——比如消息转发、Method Swizzling、KVO 的实现原理、ARC 下的引用计数细节——那是下篇的活,现在不要急着去碰。

2. 核心细节解析:类、对象与消息机制

2.1 类与对象的关系:先有图纸,再造房子

OC 里的类和对象,用“图纸和房子”来类比是最舒服的。类就是图纸,它约定了这个对象有哪些属性、能响应哪些方法;对象才是真正住人的房子,它占据内存、有实际的数据值。同一个类可以创建出无数个对象,每个对象的属性值不同,但它们遵循的结构是一样的。

在实际代码里,OC 的类通常分成两个文件写:头文件.h和实现文件.m。头文件里放对外暴露的接口声明,相当于“你可以这么用我”;实现文件里放真正的内部细节,相当于“我到底是怎么干的”。这种拆分开方式,一方面把公开接口和内部实现解耦了,另一方面也让#import的时候不会把一堆实现细节暴露给外部。

// Person.h #import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; - (void)sayHello; @end
// Person.m #import "Person.h" @implementation Person - (void)sayHello { NSLog(@"Hello, I'm %@, %ld years old.", self.name, (long)self.age); } @end

@interface下面的Person : NSObject表示这个类继承自NSObject。NSObject是 OC 里几乎所有对象的根类,它负责提供对象创建、内存管理、消息处理这些基础能力。你自定义的类几乎都直接或间接继承它,否则对象最基本的生命周期管理都要自己实现,那就太痛苦了。

2.2 方法调用的本质:中括号背后的消息传递

第一次写 OC 的人几乎都会问:为什么调用方法不是person.sayHello()而是[person sayHello]?这看起来只是语法差异,但背后是两个完全不一样的设计理念。Java 和 Python 的方法调用是“编译器帮你查表”,编译阶段就基本确定了该方法对应的实现;OC 的方法调用是“给对象发一条消息”,对象收到消息后再自己决定怎么响应。

Person *person = [[Person alloc] init]; person.name = @"Tom"; [person sayHello];

这行[person sayHello],在编译后的底层其实是objc_msgSend(person, @selector(sayHello))。你可以理解为 OC 运行时去person这个对象所属的类里查一下:你认识sayHello这个方法吗?认识就执行,不认识就进入消息转发流程。这意味着 OC 的方法调用天然具有动态性,有些在编译期不确定的事情,运行期还能再决定。

这也解释了为什么 OC 里向nil发消息不会崩溃。如果你写:

Person *person = nil; [person sayHello];

运行时会发现person是空指针,直接忽略这条消息,返回一个空值。很多从 Java 转过来的同学刚接触时觉得这反直觉,但实际用起来非常舒服,减少了大量的判空代码。当然,动态性也是一把双刃剑,编译器没法帮你发现所有拼写错误,这也是后面很多疑难 Bug 的来源。

2.3 属性:@property到底帮你做了什么

OC 早期版本里,属性要写一大堆样板代码:声明实例变量、手动写 setter、手动写 getter。后来苹果引入了@property语法,一行声明,编译器自动帮你生成_name实例变量、- (NSString *)name和- (void)setName:(NSString *)name方法。这一切默认在 Xcode 4.4 之后就是自动的,你不需要手写@synthesize。

@property (nonatomic, copy) NSString *name;

这句话可以拆开看。nonatomic表示不保证多线程访问安全,写入和读取不加锁,性能好,绝大多数 UI 相关属性都用它;copy表示 setter 里会对传入值做一次 copy,而不是直接 retain,这样外部如果在赋值后修改了原字符串,不会影响对象内部的属性值;NSString *name则是这个属性的类型和名字。

用.语法访问属性,比如person.name = @"Tom",实际上是调用了[person setName:@"Tom"];读取person.name实际上是调用[person name]。这一点很多人写的时候没感觉,但一旦你 override getter 或者 setter 方法,就会立刻意识到这层关系。理解它对你后来学 KVO、学 Core Data、学 Swift 和 OC 互操作都有帮助。

2.4 协议:OC 里面的“接口”概念

如果你之前学过 Java 的interface,OC 的 Protocol 是类似的东西。它声明了一组方法,但不提供实现,由遵守这个协议的类来实现。这样做的好处是:调用方只需要面向协议编程,不需要关心具体对象是谁。

@protocol Greeting <NSObject> @required - (void)sayHello; @optional - (void)sayBye; @end

@required表示遵守协议必须实现,@optional表示可选实现。这在 iOS 里的委托模式中用得特别多,比如UITableViewDataSource和UITableViewDelegate那一大堆方法,就大量用了@optional。你在写自己的组件时,想让某个类把一部分工作“外包”出去,定义一个协议,让外部对象来当 delegate,这就是典型的面向接口编程。

协议还有一个细节,就是它可以让一个类同时遵守多个协议:

@interface MyViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

这种多协议组合方式,在单继承的语言里是替代多继承的主流方案,既拿到了多态和接口抽象的好处,又不会陷入菱形继承的问题。

3. 实操过程:从零搭一个能跑的 OC 面向对象 Demo

3.1 环境准备:没有 Mac 也能编译 OC 代码

很多人以为写 OC 必须打开 Xcode,实际上不完全是。如果你在 macOS 上,安装 Xcode Command Line Tools 之后,直接用一个文本编辑器加clang命令就能编译 OC 文件。如果你手头是 Linux,也可以装 GNUstep 来模拟 Foundation 框架,不过很多 UIKit 相关的 API 就不适用了,学语法和基础面向对象完全够用。

我推荐的方式很简单:新建一个main.m文件,把类和测试代码写在里面,然后用clang -framework Foundation main.m -o demo编译,再./demo运行。这样能最快看到效果,不用每次都跑到 Xcode 里建一个完整工程。

让我给你看一个完整的例子,注意它把Person类和测试入口都写在了一个文件里,方便你直接复制运行:

#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; - (instancetype)initWithName:(NSString *)name age:(NSInteger)age; - (void)sayHello; @end @implementation Person - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self = [super init]; if (self) { _name = [name copy]; _age = age; } return self; } - (void)sayHello { NSLog(@"Hello, I'm %@ and I'm %ld years old.", _name, (long)_age); } @end int main(int argc, const char * argv[]) { @autoreleasepool { Person *person = [[Person alloc] initWithName:@"Tom" age:18]; [person sayHello]; } return 0; }

你可以看到,initWithName:age:这种带参数的方法名是 OC 的一大特色。方法名里的冒号本身就是参数的一部分,调用的时候initWithName:@"Tom" age:18把每个参数都能“描述”出来。这在阅读代码时特别直观,牺牲了一点语法简洁性,换来的是可读性的巨大提升。

3.2 初始化方法:为什么开头要写self = [super init]

新手看 OC 初始化方法的时候,最容易问的就是那一行self = [super init]是什么意思。说白了,这是先让父类完成它自己的初始化工作,再把返回值交给你继续设置子类特有的属性。有些情况下父类初始化可能返回一个完全不同的对象,所以你必须用self接住返回值,否则后续对self的操作就建立在错误的基础上。

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self = [super init]; if (self) { _name = [name copy]; _age = age; } return self; }

这里我特地说一句,为什么用instancetype而不是id。instancetype是一个类型修饰词,告诉编译器“这个方法返回的是接收者的类型”。当你调用[[Person alloc] initWithName:@"Tom" age:18]时,编译器知道它的类型是Person *,这样后面的属性访问、方法调用都有类型检查。如果你用id,编译器就全都不管了,一个错误的方法拼写可能到运行期才炸出来。

3.3 封装一个完整对象:把数据和行为收进黑盒里

刚才的Person类其实已经展示了最基本的封装。外部使用者只需要知道三件事:可以通过initWithName:age:创建对象、可以通过name和age属性读写数据、可以调用sayHello让对象自我介绍。至于内部是用_name存字符串,还是用了什么别致的数据结构,外部完全不需要关心。

封装的意义在真实项目中特别明显。假设你有一个BankAccount类,里面有个balance属性。如果直接暴露成可读可写,外部代码随时可以把余额改成负数。更好的做法是把balance设为对外只读,对外提供一个withdraw:方法,方法内部做足额校验:

@interface BankAccount : NSObject @property (nonatomic, readonly) double balance; - (instancetype)initWithInitialBalance:(double)balance; - (BOOL)withdraw:(double)amount; - (void)deposit:(double)amount; @end

这种写法把“余额数值”和“修改余额的规则”绑定在了一起。调用方想扣钱,只能调用withdraw:,规则全部在方法内部执行。这就是面向对象里“封装”最直接的落地:你有权调用接口,但无权绕过规则。

3.4 和 JavaScript 互调:JavaScriptCore 桥接一次

看热词里有人搜“OC 和 JavaScript 互相调用”,刚好这个和面向对象的设计有很强的关联。在 iOS 里,苹果提供了 JavaScriptCore 框架,它允许你在 OC 对象和 JavaScript 环境之间搭桥。核心思路是让你的 OC 类实现一个JSExport协议,协议里声明的方法都会被自动暴露给 JavaScript。

#import <JavaScriptCore/JavaScriptCore.h> @protocol PersonJSExport <JSExport> @property (nonatomic, copy) NSString *name; - (NSString *)greeting; @end @interface Person : NSObject <PersonJSExport> @end

然后在 OC 里创建对象,把它注入到 JSContext 里,JavaScript 代码就能直接调用这个对象的方法和属性了。这个桥接机制的本质,仍然是协议在发挥作用——你定义了一个面向接口的边界,JavaScript 侧不需要关心它背后是 OC 还是其他什么语言,只需要按照协议暴露出来的那组方法去调用。理解这一点之后,你再去看那些JSExport、JSContext的用法,就不会觉得那是一堆魔法了。

3.5 和 Python 面向对象的横向对比:语法不同,骨架相似

如果你之前学过 Python 的面向对象,再看 OC 其实能省不少力。Python 的类构造方法是__init__,OC 对应的是init系列方法;Python 用self.name访问属性,OC 更多是用_name加一个@property封装;Python 定义接口经常用 ABC(抽象基类),OC 则用 Protocol。

我用同一份逻辑在两个语言里写一遍,你感受一下:

class Person: def __init__(self, name, age): self._name = name self._age = age def say_hello(self): print(f"Hello, I'm {self._name}, {self._age} years old.")
@implementation Person - (instancetype)initWithName:(NSString *)name age:(NSInteger)age { self = [super init]; if (self) { _name = [name copy]; _age = age; } return self; } - (void)sayHello { NSLog(@"Hello, I'm %@, %ld years old.", _name, (long)_age); } @end

两者在“类、对象、属性、方法”这四大件上的逻辑高度一致。区别在于 OC 的语法更加啰嗦,分区更明确,而 Python 则把一切都压缩得很短。这不是谁更好的问题,而是语言设计哲学不同:OC 作为 C 的扩展,注重明确性和编译期可读性;Python 作为脚本语言,注重表达效率。

4. 常见问题与排查技巧实录

4.1 只声明属性没写@synthesize,会不会出问题

我在早期的教程里看到不少人还在教手动写@synthesize name = _name;,这让新手很困惑:我到底写还是不写?答案很简单:在 Xcode 4.4 之后的编译器里,你完全不用写。编译器会自动帮你生成下划线实例变量和对应的 getter/setter。只有当你在实现里同时手动实现了 getter 和 setter 时,编译器才会停止自动合成属性。这时候如果没有手动声明_name,代码会报编译错误。

我建议你在实现方法里直接用_name来访问实例变量,不要一上来就用self.name。原因很简单:self.name走的是 getter/setter,可能会触发额外逻辑,比如 copy、KVO 通知,而在初始化方法里你往往只想要一次直接的赋值,不想引发一连串的副作用。这个习惯能帮你规避很多隐藏的 Bug。

4.2 消息发送给nil到底发生了什么

前面已经提到,向nil发消息不会崩溃,但有一个容易忽略的陷阱:向nil发消息的返回值是nil或0。假设你写:

NSString *name = [someObject name]; if ([name length] > 0) { // 这行不会执行,因为 name 为 nil 时 length 返回 0 }

这种行为起初很省心,但如果你把nil当作有效值继续参与计算,就会得到奇怪的结果。例如把一个nil字符串塞进NSArray会导致崩溃,你必须要做判空。我自己的习惯是:接口返回值除非明确设计允许为空,否则在入口就做断言,不要让nil在业务代码里到处流窜。

4.3 方法名带参数有多容易写错

OC 的方法名设计虽然可读性强,但对新手来说非常容易少写冒号或多写冒号。比如- (void)setName:(NSString *)name调用时必须写作[person setName:@"Tom"],漏掉那个冒号就成了另一个方法。方法名其实包含了冒号:setName:和setName是两个完全不同的方法。编译器的提示有时候也让人一头雾水,尤其是当拼错的方法恰好和某个系统方法同名时,报错信息会指向一个奇怪的位置。

我的调试经验是:遇到“找不到方法”或“unrecognized selector”错误时,先用@selector()字符串打印一下,确认编译后运行时到底在找哪个方法。或者直接在实现文件里给方法打个断点,确认有没有走进来。这类问题找起来不难,但需要稳定心态。

4.4 strong 和 weak:属性声明里最不能乱写的东西

既然讲到属性,就不能回避内存管理。ARC 下最常出现的疑难杂症,就是strong和weak用错导致的循环引用。简单说,strong表示持有这个对象,让它活得更久;weak表示只是观察它,不增加引用计数,对象被释放后weak属性会自动变成nil。

最常见的循环引用场景是一个类持有它的 delegate,同时 delegate 又持有这个类的实例,两边都用了strong,谁都不愿意先放手,最后谁也释放不了。解决方式很简单:delegate 属性声明为weak。Delegate 类型通常还是 OC 内存管理的经典考点,你最好在一开始就养成习惯——普通的对象属性用strong,delegate 用weak,字符串和数组这种可变的容器根据情况用copy或strong。这块内容很深,具体细节我会在下篇展开讲,但属性里这几个关键字,你现在至少要看得懂。

4.5 类方法和实例方法为什么不能互相乱调

类方法用+声明,实例方法用-声明。类方法属于“这个类本身”的,比如[Person createDefaultPerson];实例方法属于“具体某个对象”的,比如[person sayHello]。在类方法里面你不能直接用self.name,因为你没有具体的实例对象。同理,实例方法里也不能直接调用类方法——虽然从语法上你可能侥幸没有报错,但逻辑上很容易混乱。

如果你需要在一个类方法里创建出一个实例再调用它的实例方法,这是合理的:

+ (Person *)createDefaultPerson { Person *person = [[Person alloc] init]; person.name = @"Default"; return person; }

createDefaultPerson是类方法,但它内部先创建了一个实例,然后操作这个实例的实例方法,这是合法的。新手容易混淆的是“self”在类方法和实例方法中的含义完全不同,一个指向类本身,一个指向具体对象。这个区分一旦建立起来,你对 OC 的脉络就清楚多了。

5. 几个让我反复吃亏的习惯与建议

5.1 命名规范不是小事,早期偷懒后期加倍还债

OC 的方法名和属性名都特别长,但这恰好是它的特色。比如initWithName:age:一眼看去就知道参数是姓名和年龄。我见过很多从 Python 转来的同学,图省事把方法名写成init1:、doIt:,结果一周后再看自己写的代码,完全不记得doIt干了什么。OC 项目里,方法名就是文档,不要在命名上省字,这是你能给自己和同事省下最多时间的地方。

另一个细节是属性对应的实例变量名默认带下划线前缀:name→_name。这个约定让直接访问实例变量和通过属性访问明显区分开来。看到_name就应该意识到:这是一次内部直接赋值,不经过任何通知和校验。别小看这种视觉提示,关键时刻它就是排查 Bug 的线索。

5.2 不建议跳过基础直接去写 Swift

这两年很多新人一上来就学 Swift,碰到老代码库再回头补 OC 的话,很容易心态失衡。事实上,只有理解 OC 的面向对象和动态运行时,你才能真正理解 Swift 里的很多“怪行为”。比如 Swift 的@objc关键字、与 OC 的互操作桥接、以及 KVC 和 KVO 这些历史遗留机制,源头都在 OC 的运行时设计里。

所以我的建议是,如果你注定要长期在苹果生态里写代码,请给自己留出几周时间把 OC 的老三样重读一遍:消息传递、协议、内存管理。掌握这些东西不会让 Swift 写得更差,反而让你在遇到编译器解决不了的问题时,多一个“底层视角”。这一点,在接入混编项目时体会最深。

5.3 给初学者的一条练习路径

如果你正在为“OC 面向对象怎么学”发愁,我给你一个亲测有效的路径:先照着上面的代码把Person跑起来,改一改属性类型、加一加方法,确保语法熟了;然后自己尝试封装一个BankAccount,给余额做一层校验逻辑;接着定义一个协议,用两个不同类去实现它,感受多态;最后把 JavaScriptCore 那节代码跑通,看看 OC 对象如何暴露给 JS。这几步走完,上篇的基础就算彻底打扎实了。

我个人在实际学习过程中,最大的感受是:OC 就像是 C 和现代 OO 语言之间的桥梁,它的很多设计虽然老了,但自洽得很完整,甚至有一些在今天的 Java、Python 里都找不到的灵巧。尤其是在你写过 C 之后再来看它,会忍不住感叹一句:原来面向对象还能这么做。下篇我们再往深处走,把消息转发、运行时动态性、以及真正的内存管理机制一个个拆开讲清楚。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 1:49:39

主板核心原理:PCB基板、芯片组与供电通路深度解析

1. 这不是教科书里的抽象概念&#xff0c;而是你拆开电脑后真能摸到的“骨架”主板——这个词听起来像电子元件课上的一个术语&#xff0c;但其实它就是你手边那台电脑、那台工控设备、甚至那台智能家电里最核心的“地基”。我干这行十多年&#xff0c;经手过从老式ATX大板到Mi…

作者头像 李华
网站建设 2026/10/10 1:47:03

Outline MCP 服务器详解:Tools 工具集与 Skills 扩展的架构与实践

知识库知识管理协同办公后端前端 【免费下载链接】outline The fastest knowledge base for growing teams. Beautiful, realtime collaborative, feature packed, and markdown compatible. 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/ou/outline 点击查看 免…

作者头像 李华