MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C类别与扩展在运行时中的实现原理

2021-10-043.2k 阅读

一、Objective-C 类别(Category)概述

在Objective-C编程中,类别(Category)是一种强大的特性,它允许我们在不继承类或修改原始类代码的情况下,为现有的类添加新的方法。类别在实际开发中应用广泛,比如在为系统类添加自定义功能,或者将一个庞大的类的方法进行模块化拆分等场景。

举例来说,如果我们有一个NSString类,想要为它添加一个判断字符串是否为邮箱格式的方法。通常情况下,我们不能直接修改NSString的源代码来添加这个方法,但使用类别就可以轻松实现。

@interface NSString (EmailValidation)
- (BOOL)isValidEmail;
@end

@implementation NSString (EmailValidation)
- (BOOL)isValidEmail {
    NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}";
    NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex];
    return [emailTest evaluateWithObject:self];
}
@end

在其他地方使用时,就像这样:

NSString *testEmail = @"test@example.com";
BOOL isValid = [testEmail isValidEmail];

二、Objective-C 类别在运行时的实现原理

  1. 类别在编译期的处理 在编译阶段,编译器会将类别中的方法声明收集起来。类别中的方法声明和类的普通方法声明类似,都会被记录在类的方法列表相关的数据结构中。不过,类别方法并不会像类的原生方法那样,在编译时就与类紧密绑定。

  2. 运行时的类结构与方法列表 在Objective-C运行时,类是一个复杂的数据结构,其中包含了类的元数据、属性列表、方法列表等重要信息。类的方法列表存储在method_list_t结构体中,每个方法在这个列表中有一个method_t结构体表示。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

当一个类加载到运行时环境中时,它的原始方法列表已经确定。而类别在运行时,会在类的方法列表中进行动态插入。

  1. 类别方法的插入过程 当程序运行到加载类及其相关类别时,运行时系统会将类别中的方法插入到类的方法列表的前面。这就意味着,如果类别和原始类中存在同名方法,类别中的方法会优先被调用。这种插入机制的实现,主要依赖于运行时库中的相关函数。

例如,在objc-runtime-new.mm文件中,attachCategories函数负责将类别中的方法、属性和协议等信息合并到类中。它会遍历类的所有类别列表,将每个类别中的方法逐一插入到类的方法列表中。

static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

三、Objective-C 扩展(Extension)概述

扩展(Extension)是类别(Category)的一种特殊形式,也被称为匿名类别。与普通类别不同的是,扩展通常定义在类的实现文件(.m)中,并且可以声明实例变量和属性。

例如,对于一个Person类,我们可以在其实现文件中定义扩展:

@interface Person ()
@property (nonatomic, strong) NSString *secretInfo;
- (void)privateMethod;
@end

@implementation Person
- (void)privateMethod {
    NSLog(@"This is a private method.");
}
@end

这里扩展中声明的secretInfo属性和privateMethod方法,对于外部代码来说是不可见的,只有Person类的实现文件内部可以使用。

四、Objective-C 扩展在运行时的实现原理

  1. 扩展与类的绑定 扩展在编译时,其声明的属性、方法和实例变量会被编译器当作类的一部分来处理。与类别不同,扩展的方法、属性等信息在编译期就会与类紧密绑定,而不是在运行时动态插入。

  2. 运行时对扩展信息的处理 在运行时,扩展中声明的属性会和类的其他属性一样,存储在类的属性列表中。方法也会被编译成与类的原生方法相同的形式,存储在类的方法列表中。

例如,扩展中声明的属性会在运行时通过objc_property_t结构体来表示,和类的其他属性一起构成类的属性列表。

typedef struct objc_property *objc_property_t;

扩展中声明的方法同样会以method_t结构体的形式存在于类的方法列表中,与类的原生方法并无本质区别。

五、类别与扩展的区别总结

  1. 声明位置与可见性

    • 类别:可以在任何地方声明,通常用于为系统类或其他外部类添加方法。类别中的方法对类的所有实例都是可见的。
    • 扩展:一般在类的实现文件中声明,主要用于为类添加私有的属性、方法和实例变量。扩展中的内容对外部代码不可见,只有类的实现文件内部可以访问。
  2. 实例变量声明

    • 类别:不能声明实例变量。虽然类别可以通过关联对象(Associated Objects)来模拟添加实例变量的效果,但本质上并不是真正的实例变量。
    • 扩展:可以声明实例变量,这些实例变量会成为类的一部分,在类的实例创建时会分配内存空间。
  3. 方法冲突处理

    • 类别:如果类别和类中存在同名方法,类别中的方法会优先被调用。在运行时,类别方法通过动态插入到类的方法列表前面来实现这种优先级。
    • 扩展:由于扩展的方法在编译期就与类绑定,不存在运行时动态插入的过程。如果扩展和类中存在同名方法,编译器会报错,因为这被认为是重复定义。
  4. 编译与运行时处理

    • 类别:编译期收集方法声明,运行时动态将方法插入到类的方法列表中。
    • 扩展:编译期将属性、方法和实例变量当作类的一部分进行处理,运行时与类的原生部分一样存储和访问。

六、类别与扩展在实际开发中的应用场景

  1. 类别在实际开发中的应用场景
    • 为系统类添加功能:比如为UIView类添加方便的截图方法。
@interface UIView (Screenshot)
- (UIImage *)takeScreenshot;
@end

@implementation UIView (Screenshot)
- (UIImage *)takeScreenshot {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0);
    [self.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return screenshot;
}
@end
- **将大型类的方法模块化**:当一个类的方法过多时,可以将相关方法分组到不同的类别中,提高代码的可读性和可维护性。例如,一个`User`类有很多与用户信息处理、用户界面展示相关的方法,可以将用户信息处理的方法放在`User (Info)`类别中,用户界面展示的方法放在`User (UI)`类别中。

2. 扩展在实际开发中的应用场景 - 添加私有属性和方法:在开发一个类时,有些属性和方法只希望在类的内部使用,不对外暴露。这时可以使用扩展来声明这些私有内容。比如一个NetworkManager类,有一些用于内部数据处理的私有方法和属性,可以通过扩展来定义。

@interface NetworkManager ()
@property (nonatomic, strong) NSMutableDictionary *cache;
- (void)processResponseData:(NSData *)data;
@end

@implementation NetworkManager
- (void)processResponseData:(NSData *)data {
    // 内部数据处理逻辑
}
@end
- **对类进行功能增强但不希望影响外部接口**:有时候我们需要在类的实现文件内部对类进行一些额外的功能扩展,但又不想改变类的公开接口。扩展就可以很好地满足这个需求,因为外部代码无法访问扩展中的内容。

七、类别与扩展在运行时实现的注意事项

  1. 类别方法冲突的风险 由于类别方法会在运行时插入到类的方法列表前面,如果多个类别中定义了同名方法,可能会导致难以预料的行为。例如,在一个项目中,如果两个不同的类别都为NSString类添加了customMethod方法,那么运行时到底调用哪个类别中的customMethod方法,取决于最后加载的类别。为了避免这种情况,在命名类别方法时应尽量保证唯一性,或者在使用多个类别时,仔细规划方法的命名空间。

  2. 扩展中实例变量的访问控制 虽然扩展可以声明实例变量,但这些实例变量的访问控制需要注意。由于扩展通常定义在类的实现文件中,外部代码无法直接访问扩展中声明的实例变量。然而,如果不小心在类的公开方法中暴露了对这些实例变量的访问,可能会破坏类的封装性。因此,在使用扩展声明实例变量时,要确保对这些实例变量的访问仅限于类的内部实现。

  3. 运行时性能影响 类别在运行时动态插入方法的过程,虽然带来了灵活性,但也会对性能有一定的影响。每次加载类别并插入方法时,运行时系统需要进行内存分配、列表插入等操作。特别是在应用启动时,如果有大量的类别需要加载和处理,可能会导致启动时间变长。因此,在使用类别时,要权衡功能需求和性能影响,避免过度使用类别导致性能问题。

  4. 扩展与类的继承关系 扩展中声明的属性和方法会成为类的一部分,当类被继承时,子类也会继承扩展中的内容。这就需要在设计扩展时,考虑到子类可能的使用情况。如果扩展中的某些方法或属性不希望被子类继承,应该谨慎设计,或者通过其他方式实现相关功能,以保证类的继承体系的合理性和稳定性。

八、类别与扩展在不同版本运行时的变化

  1. 早期版本与现代版本运行时的差异 在早期的Objective-C运行时版本中,类别和扩展的实现机制相对简单。随着运行时库的不断发展和优化,现代版本在处理类别和扩展时更加高效和灵活。

例如,在早期版本中,类别方法的插入可能没有现在这么完善的机制,可能会在方法冲突处理、内存管理等方面存在一些不足。而现代运行时通过更加精细的方法列表管理和冲突检测机制,提高了类别使用的稳定性和可靠性。

  1. 运行时版本更新对开发的影响 随着运行时版本的更新,开发者在使用类别和扩展时可能需要注意一些兼容性问题。例如,某些在旧版本运行时中可行的技巧或行为,在新版本中可能不再适用。同时,新版本运行时可能引入了一些新的特性或优化,开发者可以利用这些改进来提升代码的性能和质量。

例如,在某些运行时版本更新后,对类别方法插入的顺序进行了调整,这可能会影响到一些依赖于方法调用顺序的代码逻辑。开发者需要根据新的运行时特性,对相关代码进行检查和调整,以确保应用的正常运行。

九、类别与扩展在内存管理方面的考量

  1. 类别中关联对象的内存管理 由于类别不能直接声明实例变量,开发者常常使用关联对象来为类别添加类似实例变量的功能。在使用关联对象时,需要注意内存管理。

例如,当使用objc_setAssociatedObject函数为对象设置关联对象时,需要指定关联策略。不同的关联策略决定了关联对象的内存管理方式。

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

如果选择了OBJC_ASSOCIATION_RETAIN_NONATOMIC策略,当设置关联对象时,系统会对关联对象进行retain操作,在对象释放时会自动release关联对象。如果选择了OBJC_ASSOCIATION_ASSIGN策略,关联对象不会被retain,这可能会导致野指针问题,需要开发者自行管理关联对象的生命周期。

  1. 扩展中实例变量与属性的内存管理 扩展中声明的实例变量和属性,其内存管理遵循Objective-C的内存管理规则。对于强引用类型的属性,在对象创建时会对属性值进行retain(ARC下为强引用),在对象释放时会自动release(ARC下自动释放)。

例如,对于扩展中声明的NSString *secretInfo属性,如果在ARC环境下,当对象释放时,secretInfo所指向的字符串对象会根据其引用计数情况自动释放。在MRC环境下,开发者需要在对象的dealloc方法中手动release该属性。

// MRC环境下
@implementation Person
- (void)dealloc {
    [_secretInfo release];
    [super dealloc];
}
@end

十、类别与扩展在多线程环境下的注意事项

  1. 类别方法的线程安全性 在多线程环境下,类别方法的调用可能会引发线程安全问题。由于类别方法是动态插入到类的方法列表中的,多个线程同时访问和调用类别方法时,如果方法内部涉及共享资源的操作,可能会导致数据竞争和不一致。

例如,如果一个类别为NSMutableArray类添加了一个addObjectAndSort方法,该方法在添加对象后对数组进行排序。如果多个线程同时调用这个方法,可能会导致数组数据的不一致。为了避免这种情况,可以使用锁机制(如@synchronized块)来保证方法在同一时间只有一个线程可以执行。

@interface NSMutableArray (ThreadSafeAddAndSort)
- (void)addObjectAndSort:(id)object;
@end

@implementation NSMutableArray (ThreadSafeAddAndSort)
- (void)addObjectAndSort:(id)object {
    @synchronized(self) {
        [self addObject:object];
        [self sortUsingSelector:@selector(compare:)];
    }
}
@end
  1. 扩展中属性和方法的线程安全性 扩展中声明的属性和方法同样需要考虑线程安全问题。如果扩展中的属性是共享资源,多个线程同时访问和修改该属性可能会导致数据错误。对于扩展中的方法,如果涉及对共享资源的操作,也需要进行适当的同步处理。

例如,扩展中声明的NSMutableDictionary *cache属性,如果多个线程同时对这个缓存字典进行读写操作,可能会导致数据不一致。可以通过使用线程安全的集合类(如NSCache)或者加锁的方式来保证线程安全。

@interface NetworkManager ()
@property (nonatomic, strong) NSCache *cache;
@end

@implementation NetworkManager
// 初始化cache等相关操作
@end

通过对Objective-C类别与扩展在运行时实现原理的深入剖析,以及对它们在实际开发中的应用场景、注意事项等方面的探讨,开发者可以更加准确和灵活地使用这两个强大的特性,编写出高质量、稳定且高效的Objective-C代码。无论是为系统类添加功能,还是对自定义类进行功能扩展和模块化管理,都能得心应手地运用类别和扩展,提升开发效率和代码的可维护性。同时,深入理解其运行时原理,也有助于在面对复杂问题和性能优化时,能够从本质上找到解决方案。