iOS 知识点 - Category / Extension / Protocol 小合集
2025年12月3日 16:27
谈到 OC 基础,错不开的三种机制:Category / Extension / Protocol。
它们分别解决了:
-
Category: 在 不修改类源代码、不继承的前提下,给已有类 “添加方法”(组织文件、系统类加功能、AOP 风格 hook 等)。 -
Extention: 在实现文件里补充声明 私有 属性、实例变量。 -
Protocol: 只定义 “接口规范”(方法/属性的声明),不提供实现,用于解耦(代码只依赖协议,不依赖具体类)& 多态(不同类实现同一协议,都可赋给协议限定类型id<Protocol>)
Category
-
概念:category 是一种给已有类(包括系统类)增加实例方法/类方法的机制,不需要子类化,也不需要访问原类源码。
-
限制:
- 不能直接增加新实例变量(
ivar),但是可以通过 关联对象 间接添加 “类似属性” 的存储。
#import <objc/runtime.h> static const void *kNameKey = &kNameKey; @implementation NSObject (Name) - (void)setName:(NSString *)name { objc_setAssociatedObject(self, kNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)name { return objc_getAssociatedObject(self, kNameKey); } @end- 不能直接访问原类 pivate 类型的变量/方法(同子类),必须要原类在 .h 中公开声明。
- 不能直接增加新实例变量(
-
编译后的本质:
category在编译后,额外的方法会被编译器 “合并” 到原类的 method_list 中,runtime加载类时一起注册。-
简化过程:
- 编译:每个
.m中的@implementation ClassName (CategoryName)生成一个category_t结构,其中包含:class nameinstance methods listclass methods listprotocol list
- 程序加载:
runtime在load_images时遍历所有category:- 找到对应 class
- 把 category 的 methods_list 插入到 class 的 methods_list 列表前方。(同名方法覆盖原来的实现)
- 编译:每个
-
简化过程:
-
⚠️注意事项:
- 如果分类与原类(或其他分类)有同名方法,后加载的 method 会 覆盖前面的实现(行为依赖于加载顺序)
-
经典用途:
- 给类添加方法(
NSArray+Utils.h等) - 拆分类的实现,按照功能分块(常用于 swift 代码风格)
- 方法交换(日志、埋点、hook 等)
- 给类添加方法(
runtime讲解篇:juejin.cn/post/757172…
延伸名词 runtime 之 load_images(面向切面编程)
-
概念:
load_images是 runtime 的一个内部函数,在dyld把一个新的 Mach-O image (主程序 / 动态库 / 插件) 加载进来时,会回调 runtime 的map_images/load_images这一整套流程。 -
接口作用:
- 注册 image 里的 类列表、分类列表、协议列表、选择子 等;
- 把 category 的 方法/协议 挂载到对应的类上;
- 收集并按照一定顺序调用
+load方法(先类,后分类)。
你也可以简化理解成:每当一块儿新的二进制文件被载入进程,runtime 就用 load_images 把这块儿里的 oc 元数据接入到系统里。
延伸名词 AOP(面向切面编程)
含义:一种编程思想,能够在「不改动原有代码逻辑」的前提下,在指定的“切面点”上插入额外逻辑(如埋点、日志、监控、权限校验等)。
| 核心概念 | 含义 |
|---|---|
| 切点(Pointcut) | 想“切入”的位置,比如方法调用前/后 |
| 通知(Advice) | 在切点执行的额外逻辑(before、after、around) |
| 切面(Aspect) | 切点 + 通知 的组合 |
| 织入(Weaving) | 把这些逻辑动态插入代码执行流程的过程 |
iOS 实现 AOP 的方法
- 方法交换:在交换方法中实现新的逻辑
-
消息转发:利用
forwardInvocation:或resolveInstanceMethod:在运行时拦截消息,再“转发”到自己的处理逻辑。 - 三方库:Aspects
Extension
-
概念: 在类的实现被编译前,给它再“补充”一些
- 方法声明
- 属性声明
- 额外 ivar(实例变量)
-
⚠️注意事项:
-
Extension必须和@implementation MyClass在同一个编译单元或可见范围,这样编译器才会把它当成类定义的一部分,生成ivar和访问器方法 (setter/getter)。
-
Protocol
- 概念:接口规范与解耦,只写签名、不写实现。
-
可以包含:
- 实例/类方法
-
Property声明。
@required 和 @optional 的意义和成本
编译期行为:
-
@required:类遵循了这个协议,但是没实现 required 方法 → 编译器 warning。 -
@optional:完全由你自己决定是否实现,编译器不强制。
运行时行为:
- 协议本身只是一堆元数据(
protocol_t),runtime 存储了:- 协议有哪些
@required和@optional的方法; - 哪些是实例 / 类方法;
- 哪些是 property。
- 协议有哪些
- 但是消息派发是不看协议的:
- 派发时只看这个对象的方法列表里有没有该 selector;
- 至于这个 selector 来自哪个协议、是否声明在协议里,派发阶段都不关系。
/// 因此,在调用时常配合: if ([self.delegate respondsToSelector:@selector(doOptionalThing)]) { [self.delegate doOptionalThing]; }
协议里的 property 到底是什么?
前置知识:协议只写签名,不写实现。
- 实际上在协议中声明的
@property (nonatomic, copy) NSString *name;仅仅等价于 setter、getter 的声明(无实现、无 ivar)。
- (NSString *)name;
- (void)setName:(NSString *)name;
protocol 经典用途
代理模式
- 例如:vc 处理在 view 上的交互事件,view 通过代理调用 vc 实际处理事件的方法。
/// View.h
@interface View : UIView
@property (nonatomic, weak) id<MyViewDelegate> delegate;
@end
/// View.m
- (void)buttonTapped {
if ([self.delegate respondsToSelector:@selector(didClickButton:)]) {
[self.delegate didClickButton:self];
}
}
/// ViewController.m
@interface ViewController () <MyViewDelegate>
@end
@implementation ViewController
#pragma mark - MyViewDelegate
- (void)didClickButton:(MyView *)view {
// 处理事件
}
- 这样就做到了:解耦 + 多态 + 依赖倒置
模块/组件之间的接口抽象 类型约束/API设计
protocol vs 继承
| 协议 | 继承 | |
|---|---|---|
| 本质 | 接口集合,不带实现、不带存储 | 类型扩展机制,带实现、带存储 |
| 语义关系 | 描述 “能做” 某些事情 | 描述 “就是” 某种事物 |
| 数量 | 一个类可实现 多 个协议 | 一个类只能有 一 个直接父类(多继承例外) |
| 主要用途 | 解耦、抽象、多态 | 复用、建立层次结构 |
| 编译期检查 | 检查是否实现 required 方法(warning) | 检查 override 签名、类型转换等 |
| 运行时检查 |
conformsToProtocol:class_conformsToProtocol
|
isKindOfClass:isMemberOfClass:
|
延伸知识点 isKindOfClass: 与 isMemberOfClass:
-
isKindOfClass:: 是不是这个类或它的子类
- (BOOL)isKindOfClass:(Class)aClass {
for (Class c = object_getClass(self); c; c = class_getSuperclass(c)) {
if (c == aClass) return YES;
}
return NO;
}
-
isMemberOfClass:: 是不是这个类本身(不包括子类)
- (BOOL)isMemberOfClass:(Class)aClass {
return object_getClass(self) == aClass;
}
对于实例对象(instance):
@interface Animal : NSObject
@end
@interface Dog : Animal
@end
Animal *a = [Animal new];
Dog *d = [Dog new];
// isKindOfClass:
[a isKindOfClass:[Animal class]]; // YES (Animal 本身)
[a isKindOfClass:[Dog class]]; // NO (Animal 不是 Dog 家族)
[d isKindOfClass:[Animal class]]; // YES (Dog 是 Animal 的子类,被认为是 Animal 家族的一员)
[d isKindOfClass:[Dog class]]; // YES (Dog 本身)
// isMemberOfClass:
[a isMemberOfClass:[Animal class]]; // YES (a 的 class 恰好是 Animal)
[a isMemberOfClass:[Dog class]]; // NO (class 是 Animal,不是 Dog)
[d isMemberOfClass:[Animal class]]; // NO (class 是 Dog,不是 Animal)
[d isMemberOfClass:[Dog class]]; // YES (class 恰好是 Dog)
对于类对象(class):
/// ❌错误的用法
// [Animal isKindOfClass:Animal]; // 左侧的 “Animal” 被 oc 语法解释为消息接受者,右侧的 “Animal” 会被认作 “类型名”,编译器报错参数异常。
//
// ✅正确的用法
[Animal isSubclassOfClass:[Animal class]]; // YES (正经用法)
[Animal isKindOfClass:[Animal class]]; // NO (不正经用法)
-
[Animal isKindOfClass:[Animal class]];为什么输出NO?- 首先,在 runtime 讲解中已知,类本身也是对象,类型为
Class。 - 再结合
isKindOfClass:方法的实现:- 从
object_getClass(self)开始循环向上查询 - self 是类对象本身(class object), 首次查询的结果就是 metaClass object;
- class object != metaClass object,已经错过了类对象本身,因此返回 NO。
- 从
- 首先,在 runtime 讲解中已知,类本身也是对象,类型为