面试题答案
一键面试内存管理方面的差异
- 类别(Category)
- 方法存储:类别在运行时将新方法添加到类的方法列表中。它不会为类添加新的实例变量,所以不存在因类别新增实例变量带来的内存管理问题。
- 内存影响:类别中的方法是与类本身的方法共享内存空间的,加载类别时,其方法被动态添加到类的方法列表,不会额外增加实例的内存开销。
- 扩展(Extension)
- 方法存储:扩展本质上是一个匿名类别,在编译期就将方法声明添加到类的接口中,和类的其他方法一样处理。
- 实例变量:扩展可以声明实例变量(虽然在实现中通常通过关联对象实现实例变量的效果)。如果扩展中声明并使用了实例变量,那么就需要开发者负责这些实例变量的内存管理,比如在对象销毁时释放相关内存。
程序加载机制方面的差异
- 类别(Category)
- 加载时机:类别在运行时加载。当程序运行到需要使用类别中的方法时,才会将类别中的方法动态添加到类的方法列表中。这种延迟加载机制使得程序在启动时不需要加载所有类别方法,从而提高了程序的启动速度。
- 动态性:类别允许在运行时为已有的类添加新方法,这为运行时动态扩展类的功能提供了很大的灵活性。
- 扩展(Extension)
- 加载时机:扩展在编译期就被处理。编译器会将扩展中的声明合并到类的接口中,所以在编译后的二进制文件中,扩展的方法和类本身的方法没有区别,在程序启动时一起加载。
- 静态性:由于在编译期处理,扩展不能像类别那样在运行时动态地为类添加方法,它主要用于为类提供一些私有的方法和属性声明,增强类的封装性。
差异原因
- 设计目的不同
- 类别:主要目的是在不修改类的源代码的情况下,为已有类添加新方法,侧重于运行时的动态扩展。所以它采用运行时加载方法的机制,并且不允许添加实例变量,以保持轻量级的扩展特性。
- 扩展:目的是为类提供私有方法和属性声明,增强类的封装性。因此在编译期处理,和类的其他部分一起构建,同时允许声明实例变量(虽然实现方式较特殊)。
- 实现方式不同
- 类别:通过运行时的动态方法解析机制,在运行时将类别方法添加到类的方法列表。
- 扩展:在编译期直接将声明合并到类的接口,其实现和类的普通方法一样在编译期处理。
避免潜在问题的方法
- 内存管理问题
- 类别:由于类别不能添加实例变量,不会因实例变量内存管理带来问题。但在使用类别方法时,要注意方法中涉及的对象的内存管理,比如如果方法返回一个对象,要遵循内存管理规则(ARC 下自动管理,MRC 下手动释放)。
- 扩展:如果在扩展中使用关联对象实现实例变量效果,在对象销毁时要确保关联对象被正确移除(在 ARC 下也要注意关联对象的内存管理,比如使用
OBJC_ASSOCIATION_RETAIN_NONATOMIC
等策略)。在 MRC 下更要手动管理关联对象的内存。
- 程序加载性能问题
- 类别:避免在类别中定义过多复杂的方法,尤其是在程序启动时不需要立即使用的方法,防止延迟加载时影响性能。同时,要注意类别方法的命名,避免不同类别中方法名冲突。
- 扩展:由于扩展在编译期加载,如果扩展中方法过多,会增加程序启动时的加载时间。所以对于一些不常用的方法,可以考虑使用类别来实现,以减少启动时的加载负担。此外,扩展中声明的私有方法要做好封装,防止在类外部被错误调用。