
聊到 OCObjective-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, Im %, %ld years old., self.name, (long)self.age); } endinterface下面的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 的interfaceOC 的 Protocol 是类似的东西。它声明了一组方法但不提供实现由遵守这个协议的类来实现。这样做的好处是调用方只需要面向协议编程不需要关心具体对象是谁。protocol Greeting NSObject required - (void)sayHello; optional - (void)sayBye; endrequired表示遵守协议必须实现optional表示可选实现。这在 iOS 里的委托模式中用得特别多比如UITableViewDataSource和UITableViewDelegate那一大堆方法就大量用了optional。你在写自己的组件时想让某个类把一部分工作“外包”出去定义一个协议让外部对象来当 delegate这就是典型的面向接口编程。协议还有一个细节就是它可以让一个类同时遵守多个协议interface MyViewController : UIViewController UITableViewDataSource, UITableViewDelegate这种多协议组合方式在单继承的语言里是替代多继承的主流方案既拿到了多态和接口抽象的好处又不会陷入菱形继承的问题。3. 实操过程从零搭一个能跑的 OC 面向对象 Demo3.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, Im % and Im %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(fHello, Im {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, Im %, %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 内存管理的经典考点你最好在一开始就养成习惯——普通的对象属性用strongdelegate 用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 之后再来看它会忍不住感叹一句原来面向对象还能这么做。下篇我们再往深处走把消息转发、运行时动态性、以及真正的内存管理机制一个个拆开讲清楚。