You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Category中实现基于集合的KVO兼容只读依赖属性?

为FinalClass通过Category实现KVO兼容的过滤集合属性

需求概述

无法修改或子类化FinalClass,仅能通过Category扩展一个只读属性someCollectionSubset——该属性是原类中someCollection集合的过滤子集,要求具备KVO兼容性以支持绑定。

现有方案的局限

对于非集合属性依赖场景,可通过实现keyPathsForValuesAffectingValueFor<Property>来关联依赖属性,但苹果文档明确指出,该方法不支持包含多对多关系的键路径,因此无法直接用于集合依赖场景。

苹果官方给出的两种解决方案在Category中难以落地:

  • 无法安全地在原类的init/dealloc中注册/注销观察者
  • 无法通过子类扩展生命周期方法

最优实现:方法混写(Method Swizzling)

通过Objective-C运行时的方法混写技术,拦截原类中所有修改someCollection的操作,在合适时机为someCollectionSubset发送KVO变更通知,覆盖集合替换和集合内容修改两种场景。

完整代码实现

FinalClass+Category.m

#import "FinalClass+Category.h"
#import <objc/runtime.h>

@implementation FinalClass (Category)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Hook 集合替换方法
        SEL originalSetSel = @selector(setSomeCollection:);
        SEL swizzledSetSel = @selector(swizzled_setSomeCollection:);
        Method originalSetMethod = class_getInstanceMethod(self, originalSetSel);
        Method swizzledSetMethod = class_getInstanceMethod(self, swizzledSetSel);
        
        if (!class_addMethod(self, originalSetSel, method_getImplementation(swizzledSetMethod), method_getTypeEncoding(swizzledSetMethod))) {
            method_exchangeImplementations(originalSetMethod, swizzledSetMethod);
        }
        
        // Hook 可变集合获取方法
        SEL originalMutableSetSel = @selector(mutableSetValueForKey:);
        SEL swizzledMutableSetSel = @selector(swizzled_mutableSetValueForKey:);
        Method originalMutableMethod = class_getInstanceMethod(self, originalMutableSetSel);
        Method swizzledMutableMethod = class_getInstanceMethod(self, swizzledMutableSetSel);
        
        if (!class_addMethod(self, originalMutableSetSel, method_getImplementation(swizzledMutableMethod), method_getTypeEncoding(swizzledMutableMethod))) {
            method_exchangeImplementations(originalMutableMethod, swizzledMutableMethod);
        }
    });
}

- (void)swizzled_setSomeCollection:(NSSet *)newCollection {
    // 发送子集属性的KVO变更通知
    [self willChangeValueForKey:@"someCollectionSubset"];
    [self swizzled_setSomeCollection:newCollection]; // 调用原方法
    [self didChangeValueForKey:@"someCollectionSubset"];
}

- (NSMutableSet *)swizzled_mutableSetValueForKey:(NSString *)key {
    NSMutableSet *mutableSet = [self swizzled_mutableSetValueForKey:key];
    if ([key isEqualToString:@"someCollection"]) {
        // 返回代理集合拦截修改操作
        return [[MutableSetProxy alloc] initWithTarget:mutableSet owner:self key:@"someCollectionSubset"];
    }
    return mutableSet;
}

- (NSSet *)someCollectionSubset {
    // 实现集合过滤逻辑
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"..."]; // 替换为你的过滤条件
    return [self.someCollection filteredSetUsingPredicate:predicate];
}

@end

#pragma mark - 可变集合代理类
@interface MutableSetProxy : NSProxy
- (instancetype)initWithTarget:(NSMutableSet *)target owner:(id)owner key:(NSString *)key;
@end

@implementation MutableSetProxy {
    NSMutableSet *_targetSet;
    id _ownerObject;
    NSString *_observedKey;
}

- (instancetype)initWithTarget:(NSMutableSet *)target owner:(id)owner key:(NSString *)key {
    _targetSet = target;
    _ownerObject = owner;
    _observedKey = key;
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [_targetSet methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    NSString *selName = NSStringFromSelector(invocation.selector);
    // 判断是否为集合修改操作
    BOOL isMutation = [selName hasPrefix:@"add"] || [selName hasPrefix:@"remove"] ||
                      [selName isEqualToString:@"setSet:"] || [selName isEqualToString:@"intersectSet:"] ||
                      [selName isEqualToString:@"minusSet:"] || [selName isEqualToString:@"unionSet:"];
    
    if (isMutation) {
        [_ownerObject willChangeValueForKey:_observedKey];
        [invocation invokeWithTarget:_targetSet];
        [_ownerObject didChangeValueForKey:_observedKey];
    } else {
        [invocation invokeWithTarget:_targetSet];
    }
}

@end

方案说明

  1. 集合替换场景处理:
    混写setSomeCollection:方法,在调用原方法前后,为someCollectionSubset发送KVO的willChange和didChange通知,覆盖直接替换整个集合的操作。

  2. 集合内容修改场景处理:
    混写mutableSetValueForKey:方法,当请求someCollection的可变集合时,返回一个NSProxy代理对象。代理拦截所有修改集合的操作(增删、集合运算等),在执行前后发送子集属性的KVO通知。

  3. 线程安全保障:
    在load方法中使用dispatch_once确保方法混写仅执行一次,避免重复混写引发的逻辑异常。

注意事项

  • 若原类存在自定义的集合修改方法(非通过mutableSetValueForKey:的操作),需额外混写这些方法以保证KVO通知的完整性。
  • NSProxy的方法拦截逻辑可根据需求扩展,覆盖所有可能修改集合的方法。

内容的提问来源于stack exchange,提问作者Jan Z.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 14:16:47