分类Category
分类是运行时动态
决议的,扩展是编译期
决议的。
通过clang -rewrite-objc main.m -o main.cpp
查看底层编译可以了解代码在C++
是怎么处理的。
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
从底层查看分类的结构体,会发现其有实例/类方法列表,还有属性列表,但属性不会自动生成get set方法。
在面向对象(oop)的编程语言中,每一个对象都是某个类的实例。在 Objective-C
中,所有对象的本质都是一个 objc_object
结构体,且每个实例对象的第一个成员变量都是 Class isa
,指向该对象对应的类,每一个类描述了一系列它的实例对象的信息,包括对象占用内存大小、成员变量列表、该对象能执行的函数列表
...等。
为什么分类不能添加属性?其实可以添加,只是不会生成get、set方法
从 category
定义中可以看出 category
可以添加实例方法、类方法甚至可以实现协议、添加属性,同时也看到不能添加成员变量。 那为什么说不能添加属性呢?实际上,category
允许添加属性,可以使用 @property
添加,但是能添加 @property 不代表可以添加 “完整版的” 属性,通常我们说的添加属性是指编译器为我们生成了对应的成员变量和对应的 setter 和 getter 方法来存取属性
。在 category
中虽说可以书写 @property
,但是不会生成 _成员变量
,也不会
生成所添加
属性的 getter
和 setter
方法的实现,所以尽管添加了属性,也无法使用点语法调用 setter 和 getter 方法。(实际上,点语法可以写,只不过在运行时调用到这个方法时会报找不到方法的错误: unrecognized selector sent to instance ....
)。我们此时可以通过 associated object 来为属性手动实现 setter 和 getter 存取方法。
关键原因
:分类中声明的属性默认
为@dynamic
修饰。@dynamic
意思是告诉系统set get
方法由用户实现,即动态
实现。如果在分类的.m里手动添加set
get
方法。class_copyPropertyList
可以拿到分类
的属性名
,即可以生成属性名
,但不会生成ivar
成员变量,因为ivar
是生成set
和get
方法的关键
,所以也没有setget方法
,所以相当于
不能添加属性。实例变量是成员变量的一种特殊情况。
广义原因
:类的成员变量布局
以及其实例对象大小在编译时
就已确定,设想一下,如果 Objective-C
中允许给一个类动态添加成员变量,会带来一个问题:为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用
。 我们所说的 “类的实例”(对象),指的是一块内存区域,里面存储了 isa
指针和所有的成员变量,是类alloc
出来,并且已经分配好了内存布局(内存对齐等一系列优化)且和isa关联后的产物。所以假如允许动态修改类已固定的成员变量的布局,那么那些已经创建出的对象就不符合类的定义了,就变成无效对象了
。而方法的定义都是在类对象或元类对象中的
,不管如何增删方法,都不会影响对象的内存布局
,已经创建出的对象仍然可以正常使用。
解决方案:可以用关联
来给分类动态实现属性效果。
在Runtime
中存在一个类型为AssociationHashMap
的哈希映射表保存着对象动态添加的属性,每个对象以自身地址为key
维护着一个绑定属性表,我们动态添加的属性就都存储在这个表里,这也是动态添加property能成功的基础
。
- AssociationsHashMap(key,value),其中key是类,value是对应类的关联属性表,存储的是ObjectAssociationMap(key,value),其实这个key从底层这个方法_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)的参数就可以看出,key是我们关联属性时设置的字符串,value存储的(set方法参数,策略即属性修饰符)。
- 关联对象不用手动移除,因为在dealloc析构函数中会自动判断当前类是否有associate并自动移除。
load_images阶段系统调用
+load方法时:分类中load方法不会覆盖本类的load方法,且是主类先执行load。
用户主动调用
+load方法时:优先执行分类的load。
所以+load方法可能会执行多次。那么在+load的逻辑如果严谨点的话,需要dispatch_once保证执行一次。
initialize是类第一次接收到消息的时候调用
扩展Extension
-
extension
看起来很像一个匿名的 category
,但是 extension 和有名字的 category 几乎完全是两个东西
。extension 在编译期
决议,它就是类的一部分,在编译期和头文件里的 @interface 以及实现文件里的 @implement 一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡
。extension 一般用来隐藏类的私有
信息,你必须有一个类的源码才能为一个类添加 extension,所以你无法为系统的类比如NSString 添加 extension. -
但是 category 则完全不一样,它是在
运行期
决议的。就 category 和 extension 的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而 category 是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的) -
swift
里,extension其实就是分类,属性只能添加计算属性
和静态属性
,不能添加存储属性
,其本质没变,也是符合分类原则的,因为分类是可以添加属性的,但是不能生成set、get方法。扩展 最大的优势是可以向无权访问源代码的类添加新功能,能够更好地组织代码。
网友评论