☰
OC面向对象(下):协议、分类、消息机制与KVC/KVO实战指南
2026/10/15 6:02:12 网站建设 项目流程

把眼光从“单个对象”挪到“对象之间的协作”上,这才是**OC面向对象(下)**真正要解决的问题。上篇把类、对象、封装、继承、多态讲清楚,你确实能写一个完整的单页面Demo了;可一旦进入真实项目,跨页面传值、给系统类扩展能力、监听某个属性变化、让控制器之间解耦通信,就会发现光靠上篇那点语法完全不够用。

协议(Protocol)、分类(Category)、消息传递、KVC/KVO,这些才是OC面向对象里的“协作层”内容。这一篇我会直接从实际需求出发,把每个特性背后“为什么这样设计”的逻辑拆开讲,而不是罗列API。适合已经写过完整Demo、对类和方法有基本概念的开发者,看完你能真正理解系统框架里那些 delegate 和数据源协议是怎么跑起来的,也明白分类和继承该怎么选、KVO 什么时候会失灵。

为了让你学的时候有地图,先说我理解的“上篇与下篇”的分界:上篇是“单个对象长什么样”,下篇是“对象之间怎么打交道”。协议是打交道的规则,分类是给别人添砖加瓦的方式,消息机制是OC一切方法调用的底层真相,KVC/KVO则是运行时在背后替你干活的两个代表。

1. 整体思路:为什么下篇要学这些,而不是继续堆语法

1.1 从“会写类”到“会设计协作关系”

很多人学OC面向对象会卡在一种状态:类也会写,方法也会调,但看到系统的头文件里到处都是@protocol和<UIScrollViewDelegate>就开始犯怵。原因就是上篇学的是“语法形状”,下篇需要的其实是“协作思维”。

我举个直观例子。你做一个小应用,页面上有一个按钮,点击之后要弹窗确认,确认完要执行某个操作。如果你在上篇的思维框架里,会本能地想到“在控制器里写一个方法,在视图里写一个方法,然后直接调用”。可真实的项目里,视图层往往不知道逻辑层的存在,控制器也会因为多个页面共用而变得臃肿。这时候系统的做法是:视图只负责“我发出一个事件”,具体谁来处理、怎么处理,通过协议告诉外界。这就是协作设计。

所以下篇的每个知识点,本质上都在回答同一个问题:**多个对象在同一个程序里,如何既保持独立性,又能可靠地互相调用。**协议是定义规则,分类是扩展能力而不破坏原有结构,消息机制是让OC能在运行时动态决定调用谁,KVC/KVO是让对象之间的通知变得更智能化。

1.2 学习路径该怎样安排

我建议你不要按文档顺序平铺着学,而是按“先模仿系统、再自己设计”的顺序走。

第一步,去看你天天在用的UITableView的协议写法。它里面有@required的方法,也有@optional的方法,为什么这样拆?因为有些信息是表格必须拿到的(比如有多少行),有些只是锦上添花(比如某个高度定制)。这种“必须与可选”的拆分思想,正是协议设计里最值得学的部分。

第二步,把分类当成“整理代码”的工具来学,而不是“炫技”。很多人在工作一年后才意识到,一个类动辄上千行,其实可以通过分类按照业务模块拆开,每个分类只做一件事。

第三步,再啃消息机制与运行时。这个阶段不建议一上来就去读源码,而是先理解“[obj method]”只是消息发送的语法糖,再理解 OC 在找不到方法时的三级补救流程(动态方法解析、快速转发、完整转发),你自然就明白为什么 OC 能如此灵活。

第四步,才是KVC和KVO。这两个东西是“做应用层功能”时最常用的,比如 Model 转字典、属性监听。把它们放在最后学,是因为底层原理牵扯到运行时的方法替换,前面有了基础才好理解。

1.3 这些内容在项目里到底解决什么问题

我直接说一下实际价值,避免你觉得学了一堆用不上。

协议解决的是“传值”和“回调”问题。比如一个自定义的弹窗组件,点击确定按钮后要让你外面的控制器知道。你当然可以给弹窗类加一个 block 属性,但协议的方式更规范,而且一个对象可以同时遵守多个协议,把所有事件处理逻辑都聚合在控制器里,方便排查问题。

分类解决的是“既想用又不能改源码”的困境。比如你要给 UIColor 加一个从十六进制字符串生成颜色的方法,你不可能去改 UIKit 的源码,分类就是为这个场景设计的。它还能把一个大类拆成多个文件,多人协作时不至于总在同一个文件上冲突。

消息机制和运行时,解决的是“动态到最后一刻”的问题。有些调用在你写代码时根本不知道目标是谁,比如事件响应链里,点击一个事件要在整个视图树里找谁能处理,这种“不确定”用硬编码的继承是无法优雅实现的,需要运行时的转发机制来兜底。

KVC和KVO解决的是“用一个字符串名字去操作属性、在属性改变时收到通知”的问题。前者在做数据模型和字典互转时特别管用,后者是实现界面自动刷新的最朴素方案。

2. 协议与代理:OC里最优雅的“接口”协作方式

2.1 协议的本质与适用场景

协议在OC里,你可以把它理解成一张“能力清单”。一个类声明自己遵守某个协议,等于对外承诺:“我一定实现这个清单里必须实现的方法,可选实现的方法我也会尽力提供。”系统框架里经常见到的<UITableViewDataSource>、<UICollectionViewDelegate>,其实都是这种能力约定。

从实现细节上说,协议里的方法可以分成两类:@required和@optional。@required是编译器会检查的,如果你没有实现,编译器会给出警告。@optional则完全看你心情,系统在调用前会先用respondsToSelector:判断一下你有没有实现,没有实现就不调用。

我当初学协议的时候最困惑的一点是:它和继承到底有什么区别。后来想明白一个场景就通了。列表的滚动事件可以通知给很多方,点击事件可以通知给很多方,但OC是单继承的,一个视图控制器不可能同时继承自多个基类。协议是一种比继承更轻量的“横向能力组合”,一个类可以同时遵守好几个协议,所有协议都是“插拔式”的增加能力,不用动继承体系。

协议最经典的应用场景就是代理模式。代理这个词听起来玄乎,翻译成人话就是“把某件事的决策权或处理权交给别人”。视图层不关心谁来处理事件,它只负责发现事件,然后通过协议转发出去。

2.2 代理模式实操:从定义协议到建立连接

我拿一个实际需求来演示:自定义一个“购物车商品行”视图,用户点击行上的删除按钮,需要把“我是第几行要删除”这个消息告诉外层控制器。

第一步是定义协议,放在这个视图的头文件里:

@protocol CartItemViewDelegate <NSObject> @required - (void)cartItemViewDidTapDelete:(CartItemView *)itemView; @optional - (void)cartItemViewDidTapCheckbox:(CartItemView *)itemView; @end

这里有个细节:代理方法里一定要把触发者本身传过去,也就是itemView这个参数。因为如果页面里有多个商品行,每个行都遵守同一个代理,控制器收到回调后要靠这个参数来区分到底是谁发出的,否则回调之后不知道改哪一行数据,就乱套了。

第二步是在视图类里加一个 delegate 属性:

@interface CartItemView : UIView @property (nonatomic, weak) id<CartItemViewDelegate> delegate; @property (nonatomic, assign) NSInteger index; @end

weak这个修饰是这里的精髓。如果不加,控制器持有视图,视图又强引用代理(控制器),就会形成循环引用,退出页面后双双无法释放,内存泄漏就这么产生了。代理用弱引用,持有关系变成单向的:控制器强引用视图,视图弱引用控制器。这是所有代理模式里都必须遵守的规则。

第三步是在按钮的点击事件里转发消息:

- (void)onDeleteButtonTapped:(id)sender { if (self.delegate && [self.delegate respondsToSelector:@selector(cartItemViewDidTapDelete:)]) { [self.delegate cartItemViewDidTapDelete:self]; } }

当你做真实项目时,我强烈建议你把respondsToSelector:的判断写成习惯。因为代理方法不一定是必实现的,尤其是@optional方法,不判断直接调用,一旦对方没有实现,程序当场崩溃。

第四步是在控制器里遵守协议、设置代理、实现方法:

// 控制器头文件或者类扩展里 @interface CartViewController () <CartItemViewDelegate> @end @implementation CartViewController - (void)viewDidLoad { // ... CartItemView *itemView = [[CartItemView alloc] init]; itemView.delegate = self; itemView.index = 2; } - (void)cartItemViewDidTapDelete:(CartItemView *)itemView { NSInteger targetIndex = itemView.index; // 删除数据源中第 targetIndex 条数据,然后刷新列表 } @end

这个模式在你日后写组件化页面时会常遇到。只要两个类之间有“一个产生了事件,另一个要处理事件”的关系,都可以用代理。协议本身不关心对象是谁,只关心对象是否具备这个能力,所以它天然具有解耦的能力。

2.3 协议与系统框架:从使用到模仿

看系统头文件是理解协议设计的最佳教材。以UITableView为例,UITableViewDataSource负责“喂数据”,UITableViewDelegate负责“管交互”。为什么拆成两个协议而不是一个?因为数据的提供者通常是你业务模型所在的控制器,而交互行为的决策者可能还牵扯到导航逻辑,分开之后可以灵活组合。如果你的数据源对象和交互代理对象是同一个控制器,两个都遵守就行;如果想拆开,也完全支持。

我自己在项目里模仿过这个思路,把业务部分拆成“数据提供”和“事件回调”两个协议,在后续更换数据来源时,只改数据源,代理逻辑完全不用动。调试问题的时候也方便:数据不对,就去查数据源相关方法;交互不生效,就去查代理相关方法。这种分离的思维其实比语法本身更有价值。

写协议时我给新手一个建议:方法名一定要自带上下文。比如cartItemViewDidTapDelete:比didTapDelete:好在哪?好在你看到这个名字就知道是谁被点了,省去在多个代理回调里猜对象的烦恼。系统的tableView:didSelectRowAtIndexPath:也是这种风格,第一个参数永远是触发者,后面跟着具体信息。

3. 分类与扩展:不改源码也能给类“加能力”

3.1 分类的真实用途:扩展、拆分、模拟默认实现

分类(Category)是OC里非常独特的一个特性。它允许你在不修改一个类源码的情况下,给这个类增加方法。我在项目里用得最多的场景有三个,每个都跟写业务直接相关。

第一个场景是给系统类加工具方法。比如 UIColor 从十六进制转颜色、UIImage 做圆角裁剪、NSString 做空字符串判断。这些方法你几乎每个项目都要用,但它们不属于UIKit的设计,所以用分类挂在系统类上最合适。调用的感觉就像系统原生方法一样自然:

UIColor *brandColor = [UIColor colorWithHexString:@"#4A90E2"];

第二个场景是把一个臃肿的大类拆到多个文件中。我有一个个人中心的控制器,字典里塞了签名、订单、设置三种业务,总共一千多行。我建了三个分类文件,把不同业务的逻辑和代理方法全部“扔”进对应分类,主类文件里只留公共属性和生命周期方法。编译结果没有任何区别,但代码阅读效率翻倍。

第三个场景是给已有的类“补全能力”。比如上线后发现某个模型需要一个拼接字符串的方法,但这个模型属于底层公共库,改动它会引发一大片回归测试,这个时候分类就派上用场了:在业务模块里给它加一个分类方法,只在业务层生效。

3.2 分类与扩展的差别,以及与继承的取舍

分类和扩展(Extension)是两个容易混淆的东西。扩展的语法是:

@interface SomeClass () // 在这里面声明的属性、方法,都只在当前实现文件里可见 @property (nonatomic, assign) NSInteger privateCount; @end

它看起来像“匿名分类”,但是和分类有三个本质区别:扩展里可以添加实例变量,分类里不行;扩展是编译时确定的方法,分类是运行时合并进去的;扩展里的方法名和@property会直接编译进类的数据结构,分类则会走运行时动态合并。

我的建议是:凡是“私有成员、私有属性”,用扩展;凡是“外部可见的扩展能力、给系统类加方法”,用分类。两者搭配使用的频率很高,控制器里基本都会写一个扩展来存放内部属性,同时用分类组织业务逻辑。

那遇到问题该用继承还是分类?我总结了一个判断标准:如果你需要为类添加新的存储属性,用继承;如果只是添加行为方法,优先考虑分类。比如“需要一个是否已读状态的列表项模型”,这是存储需求,继承更合适。比如“给所有字符串增加一个URL编码方法”,这是行为需求,分类足够。分类无法直接添加属性的限制,可以用关联对象解决,但关联对象会增加运行时的负担,非必要不推荐。

3.3 关联对象的实操与坑

分类里直接写@property只会生成 getter/setter 的声明,不会生成实例变量和方法实现。如果你非要在分类里持久化数据,就要用运行时关联对象 API:

#import <objc/runtime.h> static const char kBadgeKey; @implementation UIButton (Badge) - (void)setBadgeNumber:(NSInteger)badgeNumber { NSNumber *value = @(badgeNumber); objc_setAssociatedObject(self, &kBadgeKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSInteger)badgeNumber { NSNumber *value = objc_getAssociatedObject(self, &kBadgeKey); return value ? value.integerValue : 0; } @end

这段代码里objc_setAssociatedObject的第三个参数是关联策略,它有点像属性修饰符的映射,OBJC_ASSOCIATION_RETAIN_NONATOMIC对应strong, nonatomic,OBJC_ASSOCIATION_ASSIGN对应assign。关联对象生命周期跟随着被关联对象,被关联对象释放时,关联对象也会自动释放。

这个方案我在真实项目里踩过坑。给 UIButton 加了一个点击事件的关联 block,后来发现同一按钮在不同页面显示的内容不一样,数据串了。排查后发现是 key 用了同一个静态值,导致多个按钮实例共享了同一个关联入口。修正方式是把 key 从全局静态改成基于当前对象内存地址的关联方式,或者直接在 key 里加上对象唯一标识。如果你只是给分类加一些小标记值,建议不要让每个对象都使用同一个 key 来存储不同含义的数据,尽量为不同对象单独设计关联字段。

还有一个性能相关的点:关联对象不走正常的引用计数管理路径,虽然有释放机制,但在容器类里频繁操作关联对象会增加额外的查找开销。我做过一次简单测试,在千万级循环里使用关联对象比直接使用实例变量慢一个数量级,所以高频场景不要用。

3.4 分类方法覆盖的隐患:不要和主类同名

分类一个隐蔽的坑是方法覆盖。如果主类本身实现了viewDidAppear:,你在分类里再写一个同样的viewDidAppear:,系统在运行时只会调用分类版本,而且如果多个分类都定义了同名方法,调用哪个完全取决于加载顺序,而不是你的书写顺序。

这就导致一个非常诡异的现象:自己的分类方法“悄悄把系统或主类方法覆盖了”,且没有任何编译警告。我在项目中曾给一个工具类加了一个叫reloadData的分类方法,结果运行时报方法未定义的错误,排查很久才发现是这个类和系统某方法同名了。

规避方法:给分类方法加统一前缀,比如xxb_。这在团队协作里尤其重要,一个团队规定“所有分类方法必须以模块前缀开头”,不仅能避免覆盖问题,在代码搜索时也能一眼看出哪里是原始类、哪里是扩展的。

4. 消息传递与动态绑定:OC方法调用的底层真相

4.1 消息发送机制:[obj method]不是直接调用函数

OC的一个经典论点是:[obj method]并不是直接跳到函数代码地址去执行,而是被编译器转换成objc_msgSend(obj, @selector(method))这个函数调用。这个函数会在运行时根据对象的 isa 指针去查找方法实现,找到就跳转,找不到就走消息转发流程。

动态绑定的意思就藏在这里:编译时你写的名字和运行时真正执行的实现,可以是两回事。这跟你上篇学的“多态”遥相呼应。比如你写一句[animal speak],如果animal实际指向的是一个 Dog 类,speak就执行 Dog 的版本;如果指向的是 Cat 类,就执行 Cat 的版本。编译器不需要知道animal到底是什么类型,它只需要把消息发出去,剩下的事交给运行时来决定。

理解这套机制对排查问题非常有用。比如你用 LLDB 调试时看到objc_msgSend的栈帧,别慌,这只是所有OC方法调用的公共入口。再比如你在load方法里交换了方法实现,它之所以能生效,也是因为方法查找是在运行时进行的。

4.2 消息转发:给找不到实现的方法留一条“生路”

当你发送一个对象根本没有实现的方法时,OC不会马上崩溃,而是会给你三级补救机会。第一级是resolveInstanceMethod:,允许你动态往类里添加方法实现;第二级是forwardingTargetForSelector:,允许你把消息转给另一个对象处理;第三级是forwardInvocation:,允许你通过 NSInvocation 完整地包装并重定向消息。

这三级的逻辑像瀑布式回退,任何一级成功处理了消息,程序就恢复正常;所有级别都失败,才会抛出unrecognized selector sent to instance然后崩溃。日常开发里遇到最多的就是第三级失败后的崩溃,那说明真的没有谁能处理。

我做过一个比较基础的转发例子。有一个专门负责日志上报的单例,我希望不管调用者调用它的任何方法,都统一走日志上报流程,同时又不希望为每个方法都写空实现:

- (void)forwardInvocation:(NSInvocation *)anInvocation { if ([self.logger respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:self.logger]; } else { [super forwardInvocation:anInvocation]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *sig = [super methodSignatureForSelector:aSelector]; if (!sig) { sig = [self.logger methodSignatureForSelector:aSelector]; } return sig; }

methodSignatureForSelector:必须同时实现,因为anInvocation的创建需要完整的方法签名信息。缺了这个,系统在转发前就会因为拿不到签名而直接崩溃。

这个机制的适用场景不要过度滥用。消息转发本身性能开销比正常调用大得多,而且会把程序的调用关系搞得很隐晦,调试时完全看不出来是谁调了谁。我见过有些框架用消息转发把未实现的接口统一做空操作,项目一出问题,log 里全是转发链,还原现场特别困难。我的想法是:用在“兜底”和“动态路由”这两类少数场景,业务代码里尽量别主动触发转发。

4.3 动态绑定在实际项目里的典型应用

动态绑定最直观的应用是选择器(selector)传参。你可以把一个方法名当作参数传递,这样就能做到“同一段代码,运行时决定调用不同方法”:

[self performSelector:NSSelectorFromString(@"doSomething") withObject:nil afterDelay:1.0];

还有一个应用是响应链(Responder Chain)。UIView 把一个点击事件向上传递,依次询问每个响应者能不能处理,这背后大量使用了respondsToSelector:的判断。如果 OC 是静态绑定语言,这种运行时判断就得提前在编译期写死,灵活性大打折扣。

解耦也是动态绑定的价值。设计一个事件总线的思路:对象A不知道对象B存在,只把消息投递到一个统一的地方,对象B在运行时通过selector注册去消费这个消息。实现原理依然离不开objc_msgSend和动态查找。

5. KVC与KVO:运行时替你“偷偷干活”的两员大将

5.1 KVC基础与深度用法

KVC(Key-Value Coding)允许你通过字符串名字访问对象属性。最基础的写法是:

[person setValue:@"John" forKey:@"name"]; NSString *name = [person valueForKey:@"name"];

这套机制的原理是:运行时先查找有没有对应名字的 setter/getter,有就直接调用;没有的话再继续查找_name这类实例变量;都没有就触发setValue:forUndefinedKey:异常。这就产生了一个实用操作:你可以用字符串动态操作对象属性,不用在编译期写死调用关系。

我在项目里最常用的场景是字典转模型。以前手写一堆model.name = dict[@"name"],有了KVC之后可以直接用:

[model setValuesForKeysWithDictionary:dict];

前提是字典的 key 与模型属性名一一对应。如果某个 key 在模型里不存在,就会触发异常,这时候你可以重写setValue:forUndefinedKey:来做兼容:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key { // 忽略不存在的key,不让程序崩溃 }

这个方法在接口字段调整频繁的项目里特别好用。但有一点要提醒:setValuesForKeysWithDictionary:会有性能损耗,涉及反射查找,如果列表数据量非常大(比如一秒刷新几十次),建议批量处理时用一次KVC赋值,而不是逐条属性赋值。

KVC还可以深入键路径,也就是点开成员内部属性。比如[person valueForKeyPath:@"address.city"]。在做复杂的界面数据展示时,直接用键路径取值能省掉很多嵌套的解包逻辑。但键路径对代码可读性有损伤,一旦拼写错误还是运行时报错,没有编译检查,所以给这类代码要配一个集中的常量管理,不要散落各处硬编码。

5.2 KVO原理与使用要点

KVO(Key-Value Observing)是观察者模式的语言级实现:你监听某个对象的某个属性,属性变化时你能收到回调。最基础的用法是:

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

回调里统一处理:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) { // 更新界面 } }

KVO 的实现原理涉及 isa-swizzling:系统在第一次对一个对象添加观察者时,会在运行时动态创建一个该类的子类,并把对象的 isa 指针指向这个新建的子类。这个子类重写了被观察属性的 setter 方法,在 setter 内部会先调用willChangeValueForKey:,再执行原来设置,最后调用didChangeValueForKey:,这样观察者就能在通知中心收到触发。

用一句人话描述:你观察属性的时候,对象的 setter 方法被运行时换了一个带广播功能的版本。这也解释了两个经典问题。为什么KVO能生效要求对象必须走setter方法,直接修改实例变量不会触发通知。为什么用 KVC 的setValue:forKey:也能触发 KVO,因为KVC赋值最终也调用了 setter。

5.3 KVO的坑与手动触发

我在项目里踩过最典型的坑有三个。

第一个是监听对象中途被释放导致崩溃。添加了观察者之后,如果被观察对象已经释放,但观察者没有移除观察者,那么后续任何一次属性变化都可能触发野指针崩溃。解决方式是在观察者的dealloc里移除:

- (void)dealloc { [self.person removeObserver:self forKeyPath:@"name"]; }

第二个坑是重复添加观察者导致cannot remove an observer for the key path的崩溃。有些场景会动态多次注册同一个KVO,移除时不确定到底注册了几次。我建议用一个属性isObserving来标记是否已注册,所有KVO操作都集中到setup和teardown方法中管理。

第三个坑是模型层级嵌套时不触发。比如你监听了一个外部页面的orderModel.status,orderModel本身属性变了会触发,但你内部status字段变化时可能不会传递。正确的做法是在外层的 model 内部主动给status添加KVO,再转发出去。这属于“手动串联通知”的思路,代码看起来绕,但逻辑必须通。

如果需要手动触发KVO通知,可以这样:

[self willChangeValueForKey:@"name"]; // 直接操作实例变量 _name = newValue; [self didChangeValueForKey:@"name"];

这个写法在你需要绕过 setter 去改底层数据、但又不想破坏观察语义时非常有用。我做过一个数据恢复模块,历史记录回放需要直接修改内存中的模型数据,又不能触发新版界面的正常逻辑,就用这个方式精准地只通知最上层。

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

6.1 协议代理方向的两个高频崩溃

代理崩溃最常见的一种是“代理对象已经被释放,但视图还在持有一个野指针”。虽然代理属性已经声明为 weak,但如果代理对象的生命周期比视图短,视图持有的就是一个已释放地址。点击时再调用代理方法,异常信息往往指向EXC_BAD_ACCESS。

我排查这个问题的经验是:先检查持有关系的方向。绝大部分UI层都应该由控制器强引用视图,视图弱引用代理。如果哪个地方为了省事把代理也写成了 strong,或者手动把一个局部变量赋给了代理,都容易中招。在viewDidDisappear里把delegate置 nil 也是一个保护手段。

另一个崩溃是遵守协议却少实现了@required方法。编译器确实会警告,但很多项目的警告被淹没了。如果你发现加入一个事件回调后运行时崩溃,回溯信息里还带着_objc_msgForward,多半是代理方法缺失。可以用respondsToSelector:做一层防御,或者仔细查看编译时列表里的红旗。

6.2 分类方法莫名不生效的排查方向

分类方法写好了,调用时却找不到,或者调到了旧版本,这种问题八成出在“同名方法”和“加载顺序”上。分类方法会合并进类的元数据,合并不了的时候,行为就会变得难以预测。

一个很常见的现场是:你写了一个viewDidLoad的分类方法,想在所有页面布局前做一些公共初始化。可有的页面运行了,有的页面没运行。这是因为多个分类或者主类的加载顺序不固定,同名的viewDidLoad实现的最终胜出者不一定是你的。

我的做法是把这类公共初始化放到基类里,不要在分类里重写控制器的生命周期方法。如果你实在要重写,至少用一个特征明显的方法名,并测试所有用到这个分类的页面。此外,load方法里做的方法交换,和分类同名方法冲突时,加载顺序也会影响最终结果。

6.3 KVO不触发与消息转发失灵的处理清单

我整理了一个速查表,你可以直接对照排查:

症状可能原因处理方式
已赋值却不触发KVO直接修改实例变量,未走setter改为self.name = value
已走setter却不触发对象在添加观察者前已释放调整对象生命周期管理,先注册后赋值
移除观察者崩溃重复移除或未注册过用isObserving标记,集中管理注册移除
分类中新属性总是nil分类不能直接添加实例变量改用关联对象
分类方法没生效与主类或其他分类同名查重、加前缀、避免重写生命周期方法
消息转发没走进来没有实现methodSignatureForSelector:必须同时提供方法签名
didChangeValue触发两次手动触发又调用了setter手动触发时绕过setter操作实例变量

消息转发失灵最常见的原因就是没实现methodSignatureForSelector:。系统找不到签名,就不会给你进入forwardInvocation:的机会。其次就是forwardingTargetForSelector:返回了 nil,但target也没有正确持有,导致后续转发直接失败。如果代码涉及多线程,还要注意代理对象的线程安全,KVO回调经常发生在子线程,UI更新记得切回主线程。

6.4 一个排错的完整思路示例

有一个页面,用户点击收藏后,界面上小红点数字不刷新。初查,发现收藏操作的数据模型属性确实变了,打印值没问题,但UI没动。继续查,看到收藏按钮的回调里直接改的self.model.collectCount,但该属性是在一个底层数据库上下文中通过KVC赋的。这就有问题了,因为KVC赋值虽然会走setter,但这里是在子线程中调用的,KVO回调也落在了子线程,而UI更新的主线程代码被写在了不正确的回调分支里。

正确做法是在子线程中使用dispatch_async切回主线程更新UI。如果你定位到一个情况很复杂的KVO问题,建议先关闭动画和延时,用一个最简单的设置来重复触发,缩小范围。然后再检查调用线程、setter路径、注册顺序,按这三步走,绝大多数问题都能暴露出来。

在我实际开发的经验里,面向对象下篇这些内容不是孤立的,它们往往协同工作。一个完整的业务模块里,控制器用协议接收视图的事件,模型用KVC完成JSON转模型,分类为公共类提供了业务属性的快捷访问,底层的消息机制则静静保证着这一切的动态性。把这几个点串起来去理解,你才算真正掌握了OC面向对象的核心协作层。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询