阅读视图

发现新文章,点击刷新页面。

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

谈到 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。
❌