OC类的扩展
什么是对于OC类的扩展?
OC类是什么?
学习过Objective-C(简称OC)语言的同学都知道,OC中绝大部分类都是继承自NSObject
,而什么是NSObject
呢?
我们看NSObject.h
,会发现NSObject
实际上是一个遵循了NSObject Protocol
,包含一个Class
属性并提供了若干方法的类。
- 首先它遵循了
NSObject
协议,这使得一个NSObject
对象可以响应NSObject Protocol
方法的调用; - 其次它包含了一个类型为
Class
,变量名为isa
的成员变量,但是这个isa
被OBJC_ISA_AVAILABILITY
修饰了; - 然后它定义了一些基础的方法,有些可以供开发者调用,有些只在特定编译环境下可以调用。
OBJC_ISA_AVAILABILITY
先来看看isa
,这个OBJC_ISA_AVAILABILITY
是啥意思?通过一番查找,我们在objc-api.h
中发现了它的定义:
/* OBJC_ISA_AVAILABILITY: `isa` will be deprecated or unavailable
* in the future */
#if !defined(OBJC_ISA_AVAILABILITY)
# if __OBJC2__
# define OBJC_ISA_AVAILABILITY __attribute__((deprecated))
# else
# define OBJC_ISA_AVAILABILITY /* still available */
# endif
#endif
宏的意思是,如果没有定义OBJC_ISA_AVAILABILITY
,那么就定义一个OBJC_ISA_AVAILABILITY
的宏,但是这个宏的实现是根据__OBJC2__
来确定的.
在__OBJC2__
时,OBJC_ISA_AVAILABILITY
代表__attribute__((deprecated))
,也就是我们熟知的弃用标记;而在非__OBJC2__
时,是空定义(/* still available */
)。
那么再看OBJC_ISA_AVAILABILITY
,按照它的定义,现在实际上是deprecated
(被弃用的),我们可以试一下用这个宏来修饰成员变量,编译器确实会报警告。也就是说isa
这个属性已经是弃用状态了,但是我们知道至少它目前还是在使用的···
Class
又是什么
来到runtime.h
,可以看到Class
的定义。
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
所谓的Class
类型,实际上是一个objc_class
的结构体,这个结构体包含了一些成员,其中有一个OBJC_ISA_AVAILABILITY
宏修饰的Class
类型的isa
,还有一些被!__OBJC2__
包围的成员。但是,这个结构体被标记了OBJC2_UNAVAILABLE
,什么意思,不能用了么?
OBJC2_UNAVAILABLE
我们在objc-api.h
里,可以找到这个宏的定义
/* OBJC2_UNAVAILABLE: unavailable in objc 2.0, deprecated in Leopard */
#if !defined(OBJC2_UNAVAILABLE)
# if __OBJC2__
# define OBJC2_UNAVAILABLE UNAVAILABLE_ATTRIBUTE
# else
/* plain C code also falls here, but this is close enough */
# define OBJC2_UNAVAILABLE \
__OSX_DEPRECATED(10.5, 10.5, "not available in __OBJC2__") \
__IOS_DEPRECATED(2.0, 2.0, "not available in __OBJC2__") \
__TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE __BRIDGEOS_UNAVAILABLE
# endif
#endif
这个宏的实现是根据__OBJC2__
这个条件来确定的。所谓的__OBJC2__
,其实就是代表OC的版本2.0,是苹果在2006年发布的编程语言更新。可想而知,现在十几年过去了,标记了!__OBJC2__
的代码都可以说是无效的代码,因为根本不会被编译。
那真正的“Class
”去哪儿了?
objc_class
通过翻找objc runtime
的源码,我们可以找到有一个objc-runtime-new.h
的文件,在__OBJC2__
条件下被#include
到项目的objc-private.h
中,而这个文件里正有我们想要的"Class"
的定义。
typedef struct objc_class *Class;
也就是说,所谓的Class
,依然是一个objc_class
结构体。只是,它的定义在objc-runtime-new.h
中,如下(由于定义太长了,只截取一部分,感兴趣的同学可以自行翻阅源码):
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
}
至此,我们才算只找到了Class
的真身。
(Tips: 为什么苹果要这样欺骗我们?我也看到了一些讨论,诸如why-objc-class-has-different-definition-between-runtime-h-and-objc-runtime-new-h)
光从objc_class
结构中,我们几乎无法看出来它到底是个啥,它由3个成员组成:superclass、cache和bits
。
superclass
是指向父类,cache
是一些缓存,而bits
就是我们今天的主角——“可扩展项”。
扩展什么?
上面说到class_data_bits
是可扩展项,那么我们能扩展什么呢?
继续翻看class_data_bits
的定义,我们找到了class_rw_t
,然后我们可以找到两个结构class_ro_t
和class_rw_ext_t
,ro
其实就是read only
,而rw
就是read write
, ext
则代表extension
。
通过梳理这些类型的结构,我们找到了以下关键词:
class_ro_t
中有
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
class_rw_ext_t
中有
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
至此,我们得出结论。我们可以扩展的项有:
1、methods
2、protocols
3、ivars
4、properties
而这些扩展项,有的是只读的,有的是可写的,下面我们来探讨一下该如何进行。
如何扩展OC类?
methods
methods在ro
和rw
中的存储方式,分别为method_list_t
和method_array_t
,首先我们分析一下这两个结构。
method_list_t和method_array_t
// Two bits of entsize are used for fixup markers.
// Reserve the top half of entsize for more flags. We never
// need entry sizes anywhere close to 64kB.
//
// Currently there is one flag defined: the small method list flag,
// method_t::smallMethodListFlag. Other flags are currently ignored.
// (NOTE: these bits are only ignored on runtimes that support small
// method lists. Older runtimes will treat them as part of the entry
// size!)
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
bool isUniqued() const;
bool isFixedUp() const;
void setFixedUp();
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
ASSERT(i < count);
return i;
}
bool isSmallList() const {
return flags() & method_t::smallMethodListFlag;
}
bool isExpectedSize() const {
if (isSmallList())
return entsize() == method_t::smallSize;
else
return entsize() == method_t::bigSize;
}
method_list_t *duplicate() const {
method_list_t *dup;
if (isSmallList()) {
dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
dup->entsizeAndFlags = method_t::bigSize;
} else {
dup = (method_list_t *)calloc(this->byteSize(), 1);
dup->entsizeAndFlags = this->entsizeAndFlags;
}
dup->count = this->count;
std::copy(begin(), end(), dup->begin());
return dup;
}
};
说白了,method_list_t
就是一个存放method_t
的list。
class method_array_t :
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;
public:
method_array_t() : Super() { }
method_array_t(method_list_t *l) : Super(l) { }
const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
return beginLists();
}
const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};
也是一个存放method_t
的list。
那么就是说,如果我们想要扩展方法,就必须得往这两个list中插入我们想要增加的方法,或者把list中的方法实现替换成我们想要的实现。
如何扩充method list
首先class_ro_t
中的baseMethodList
,即基本方法列表,几乎是定死的。它只在编译器设置了ptrauth
(pointer_authentication)时是动态生成的,但是依然是不可修改
的。
通过搜索源码,我们发现除了类本身的baseMethodList
之外,method_list_t
只出现在protocol_t
和category_t
这两个结构中,那是不是通过添加protocol
和category
,就能够修改method_list_t
的值呢?
Category
我们先看category
:
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;
}
};
category
结构提供了两个method_list_t
,一个是instanceMethods
,另一个是classMethods
。字面意思理解,就是一个实例方法列表和一个类方法列表。
另外,我们可以发现method_list_t *methodsForMeta(bool isMeta)
这个方法,将这两个成员返回,而这个函数,在attachCategories
(realizeClassWithoutSwift
和_read_images
)时被调用。
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
在attachCategories
时,category
中定义的方法,会以一个method_list_t
的二维数组的形式,通过prepareMethodLists
重命名和排序(即fixupMethodList
,具体作用就是一些编译优化,可以类似参考what-is-objc-msgsend-fixup-exactly ),然后添加到class
的bits
中,并且是顺序添加。
也就是说,category
确实提供了扩展method
的可行性,并且是直接扩展方法列表。
Protocol
继续看protocol
:
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
protocol
中确实定义了实例方法和类方法,并且区分了optional
和required
。但是通过查看源码,并没有发现protocol
方法添加到类上的时机。
我们可以写一段程序验证一下我们的想法:
@protocol MyProtocol <NSObject>
- (void)add;
+ (void)minus;
@end
@interface TestProtocolMethodList : NSObject <MyProtocol>
@end
@implementation TestProtocolMethodList
- (void)addTest {}
//- (void)add {}
+ (void)minusTest {}
//+ (void)minus {}
@end
unsigned int iCount = 0;
Method *iMethods = class_copyMethodList([[TestProtocolMethodList class] class], &iCount);
for (int n = 0; n < iCount; n++) {
Method m = *(iMethods + n);
SEL sel = method_getName(m);
NSLog(@"%@", NSStringFromSelector(sel));
}
unsigned int cCount = 0;
Method *cMethods = class_copyMethodList(objc_getMetaClass([NSStringFromClass([TestProtocolMethodList class]) UTF8String]), &cCount);
for (int n = 0; n < cCount; n++) {
Method m = *(cMethods + n);
SEL sel = method_getName(m);
NSLog(@"%@", NSStringFromSelector(sel));
}
通过打印,我们会发现:
- 在没有实现
MyProtocol
定义的方法时,日志输出的只有TestProtocolMethodList
本身自带的方法(addTest
和minusTest
) - 在实现了
MyProtocol
定义的方法后,日志输出TestProtocolMethodList
的方法列表里,包含了所有的方法(貌似是废话)
这样印证了,只是遵循protocol
是不会默认添加方法到方法列表的。
addMethod
除了Category
和Protocol
之外,我们还在addMethod
函数中找到了method_list_t
的身影。而addMethod
函数除了在MethodizeClass
(realizeClassWithoutSwift
)时会被调用之外,还在class_addMethod
和class_replaceMethod
是被调用,即我们熟知的“Runtime API”。
也就是说,使用class_addMethod
和class_replaceMethod
这两个“Runtime API”,也可以实现对类方法的扩展。
Note:
- 经过查询,`method_array_t`并没有被修改,而只是用于一些值的传递。
protocols
回过头来看protocol_list_t
和protocol_array_t
,首先我们发现与method_array_t
相似,protocol_array_t
也是只读的,所以我们主要看下protocol_list_t
。
经过搜索,可以得知分别在attachCategories
、methodizeClass
、protocol_addProtocol
和class_addProtocol
时,protocol_list_t
被合并到类中。此时,Protocol
中定义的方法和属性,被一并合并到类的protocol_list_t
中。
ivars
接着来说ivars
,也就是“成员变量”。
ivar_list_t
和ivar_t
class_ro_t
中定义的变量列表ivar_list_t
其实就是ivar_t
链表,我们看下ivar_t
是啥:
struct ivar_t {
#if __x86_64__
// *offset was originally 64-bit on some x86_64 platforms.
// We read and write only 32 bits of it.
// Some metadata provides all 64 bits. This is harmless for unsigned
// little-endian values.
// Some code uses all 64 bits. class_addIvar() over-allocates the
// offset for their benefit.
#endif
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};
可以很清楚的看到,ivar_t
其实就是定义了 变量名、类型、对齐方式、内存大小和偏移量的结构体。系统在分配类实例的内存空间时,会通过这些定义来分配内存。
扩展成员变量
想要搞清楚怎么扩展成员变量,我们就得看看ivar_list_t
在哪些时候被改变了,还是通过搜索大法来查一查···
通过搜索,我们可以看到只有两处,ivar_list_t
被改变了,分别是class_addIvar
和free_class
,free_class
很明显是在类释放的时候调用的,那么可以把目光聚焦在class_addIvar
上。
源码如下:
ivar_list_t *oldlist, *newlist;
if ((oldlist = (ivar_list_t *)cls->data()->ro()->ivars)) {
size_t oldsize = oldlist->byteSize();
newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
memcpy(newlist, oldlist, oldsize);
free(oldlist);
} else {
newlist = (ivar_list_t *)calloc(ivar_list_t::byteSize(sizeof(ivar_t), 1), 1);
newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
}
uint32_t offset = cls->unalignedInstanceSize();
uint32_t alignMask = (1<<alignment)-1;
offset = (offset + alignMask) & ~alignMask;
ivar_t& ivar = newlist->get(newlist->count++);
#if __x86_64__
// Deliberately over-allocate the ivar offset variable.
// Use calloc() to clear all 64 bits. See the note in struct ivar_t.
ivar.offset = (int32_t *)(int64_t *)calloc(sizeof(int64_t), 1);
#else
ivar.offset = (int32_t *)malloc(sizeof(int32_t));
#endif
*ivar.offset = offset;
ivar.name = name ? strdupIfMutable(name) : nil;
ivar.type = strdupIfMutable(type);
ivar.alignment_raw = alignment;
ivar.size = (uint32_t)size;
ro_w->ivars = newlist;
大意就是,在存在老的成员变量时合并,不存在老的成员变量时直接创建,然后赋值给类的成员变量链表。
综上所述,想要扩展类的成员变量,除了在定义interface
或者extesion
外,只能通过“Runtime API”class_addIvar
来额外添加。
properties
剩下最后一个可扩展项Property
,字面意思“财产”,我们在日常工作中,一般会称呼它为“属性”。
在类的结构中,统一有property_list_t
和property_array_t
两种类型的“属性列表”定义,没有猜错的话property_array_t
应该还是不会被改动而只作为值传递,事实也确实如此···
property_list_t
和property_t
property_list_t
是property_t
的链表结构,property_t
定义如下:
struct property_t {
const char *name;
const char *attributes;
};
它定义了一个“名称”和一个“属性”,并且通过attributes
这个命名,可以猜测这可能是一个“属性列表”。
我们继续查看attributes
,通过copyPropertyAttributeList
方法,我们可以得知这个变量实际上是objc_property_attribute_t *
类型。
也就是一个数组,内部包含了若干个objc_property_attribute_t
结构:
/// Defines a property attribute
typedef struct {
const char * _Nonnull name; /**< The name of the attribute */
const char * _Nonnull value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
那这里的“属性名称”和“属性值”分别是什么意思呢?我没有找到确切的文档描述,但是通过以下的代码可以打印出来看看,有兴趣的同学可以试试看。
int pCount = 0;
objc_property_t *properties = class_copyPropertyList([[TestClass class] class], &pCount);
for (int n = 0; n < pCount; n++) {
objc_property_t p = *(properties + n);
NSLog(@"%s --- %s", property_getName(p), property_getAttributes(p));
}
最后打印出来的是当前类所有的properties
的名称和属性,名称就是我们定义的名称,属性是一些奇怪的字符,这些字符遵循如下规则:
/*
Property attribute string format:
- Comma-separated name-value pairs.
- Name and value may not contain ,
- Name may not contain "
- Value may be empty
- Name is single char, value follows
- OR Name is double-quoted string of 2+ chars, value follows
Grammar:
attribute-string: \0
attribute-string: name-value-pair (',' name-value-pair)*
name-value-pair: unquoted-name optional-value
name-value-pair: quoted-name optional-value
unquoted-name: [^",]
quoted-name: '"' [^",]{2,} '"'
optional-value: [^,]*
*/
关于属性的定义,具体可以参考:Property Type String
扩展Property
扩展Property
,其实就是修改property_list_t
。
property_list_t
分别在attachCategories
、methodizeClass
、class_addProperty
、class_replaceProperty
和protocol_addProperty
时被合并到类的properties中。
也就是说,我们可以通过Category
、Runtime API
或者Protocol
来额外添加Property
。
Something else
- 我们都知道OC类也是C++的结构体,因此OC类也可以通过继承来扩展以上的四种扩展项;
- 另外,OC的类定义时,支持使用
extension
,这也为其提供了扩展能力。
综上所述,我们已知可以提供的扩展方案包括:
- Category
- Protocol
- Runtime API
- Inherit
- Extension
我们需要注意些什么?
虽然OC为我们提供了诸多的手段,来给一个类提供扩展,但是实际操作时,我们还是有很多需要注意的点,下面我们就分别来介绍一下。
Category
通过之前的描述,我们知道添加Category
,可以为一个类提供除了ivars
以外的所有扩展,可谓非常强大。
但是由于一些编译限制,以及运行时逻辑,导致我们使用这个方案时,需要注意以下几个问题:
- 方法重名
- 我们在定义
Category
时,可以任意声明方法,因此难免会出现重名现象。出现重名后,由于OC的方法调用机制,会导致我们的代码可能无法按照预期执行。所以,我们必须尽量避免重名方法的出现。 - 如果定义了重名方法,那么后定义的方法会被执行(如果是在不同文件中,则后编译的方法会被执行);原类中的重名方法始终不会被调用到,哪怕它是最后参与编译的。
- 我们在定义
- 定义
Property
- 我们定义的
property
会被正确加到类的properties
中,但是由于category无法提供ivars
扩展,导致编译器无法为类创建对应的成员变量,所以我们需要为我们定义的property
实现getter
和setter
方法。 - 我们在定义
property
时,可以通过objc_setAssociatedObject
添加关联属性。关联属性需要设置objc_AssociationPolicy
,以确定property
的引用类型,但是其中不包括weak
。
- 我们定义的
- 导入
- 在
.m
(或.mm
)文件中使用Category
定义的方法时,我们需要先import
对应的头文件 - 如果
Category
被打包到.a
或者.framework
中,默认该Category
并不会被编译,所以需要一些额外处理(参考让iOS静态库中的category变得可用)
- 在
Protocol
我们知道Protocol
不会自动添加方法或者Property
到类中,所以我们更多需要注意的,是如何定义和使用一个Protocol
。
- 定义
Property
Protocol
中声明的Property
,是在由遵循它的类定义时才生效,所以如果是Category
遵循了该Protocol
,需要自己实现getter
和setter
方法
- 使用
- 防止出现循环引用
- 由于
@optional
的声明在未实现时,不会引起编译错误,所以我们调用Protocol
声明的方法
(或property
)时,需要先使用respondsToSelector
判断调用方是否实现了该方法
(或property
的getter
和setter
)
Runtime API
Runtime API
为我们提供了直接操作类内部结构的手段,所以我们在使用的时候,必须要非常小心。一些常见的问题,比如:
- 方法类型
- OC方法分为“类方法”和“实例方法”,我们在添加时需要明确该方法是哪种类型。如果是“类方法”,我们添加的对象是元类;如果是“实例方法”,我们添加的对象是类本身
- 我们需要通过不同的方法来获取元类和类本身,使用
objc_getMetaClass
获取元类,使用objc_getClass
获取类本身
- 方法名
- 通常我们使用
SEL
来标记一个OC方法,所以要保证我们调用的SEL
和添加的SEL
是同一个,否则会报unrecognized selector
- 如果添加了同名方法,会导致只有其中一个会被调用到,与Catrgory不同的是,先添加的总是会被调用到,而后添加的不会被调用到
- 通常我们使用
- 参数
- 除了需要提供IMP实现,我们在添加方法时,还可能需要提供参数类型,它是
const char * _Nullable
类型,具体定义规则要参考Type Encodings
- 除了需要提供IMP实现,我们在添加方法时,还可能需要提供参数类型,它是
由于使用`Runtime API`添加方法是在运行时完成的操作,所以是一个非常危险的行为,一旦失误就会导致程序崩溃,所以一定要非常谨慎,并且经过细致的测试。
Inherit
继承的方式,可以完成几乎所有的扩展,但是继承之后就是一个新的类了,它会拥有一份新的结构。 继承时需要注意的,主要是变量权限和方法重写问题。
Extension
Extension
可以理解为在原类上继续做扩展,所以没有太多需要注意的,但是苹果还是列出了一些可能出现的情况,希望我们关注:
- 权限
- 我们可以在
原类
和Extension
中对于同一个Property
定义不同的读写权限,所以如果我们在原类中没有定义write
权限,那么我们必须要自己实现该Property
的setter
方法。 - 原类的方法和
Property
默认是public
的,而Extension
中定义的可以认为是“private
”的,但是如果你把Extension
定义在独立的.h
文件中,他们也会是public
的。
- 我们可以在
参考资料
- ObjC runtime源码:objc4
- 苹果的官方建议:Customizing Existing Classes