iOS 知识点 - Category / Extension / Protocol 小合集

162 阅读6分钟

谈到 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 加载类时一起注册。

    • 简化过程
      1. 编译:每个 .m 中的 @implementation ClassName (CategoryName) 生成一个 category_t 结构,其中包含:
        • class name
        • instance methods list
        • class methods list
        • protocol list
      2. 程序加载:runtimeload_images 时遍历所有 category
        • 找到对应 class
        • 把 category 的 methods_list 插入到 class 的 methods_list 列表前方。(同名方法覆盖原来的实现)
  • ⚠️注意事项

    • 如果分类与原类(或其他分类)有同名方法,后加载的 method 会 覆盖前面的实现(行为依赖于加载顺序)
  • 经典用途

    • 给类添加方法(NSArray+Utils.h 等)
    • 拆分类的实现,按照功能分块(常用于 swift 代码风格)
    • 方法交换(日志、埋点、hook 等)

runtime 讲解篇:juejin.cn/post/757172…

延伸名词 runtimeload_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 的方法

  1. 方法交换:在交换方法中实现新的逻辑
  2. 消息转发:利用 forwardInvocation:resolveInstanceMethod: 在运行时拦截消息,再“转发”到自己的处理逻辑。
  3. 三方库: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: 方法的实现:
      1. object_getClass(self) 开始循环向上查询
      2. self 是类对象本身(class object), 首次查询的结果就是 metaClass object;
      3. class object != metaClass object,已经错过了类对象本身,因此返回 NO。