类对象的数据结构:
struct objc_class : objc_object {
// 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
}
objc_class结构体中 ISA
占8字节,superclass
占8字节,cache
占16字节。
其中 ISA
与 superclass
分别是 isa指针和父类指针,这个不需要再解释了。有需要的可以查看 OC底层探索 - 类 & 元类 。
cache
cache应该是缓存,那么缓存的是什么呢?根据 cache_t
类型,我们看一下源码
struct cache_t {
private:
// 8字节
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
// 8字节
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4或2字节
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
// 8字节
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
}
/*
因 _bucketsAndMaybeMask 占 8字节;
_originalPreoptCache 指针 8字节, 结构体最大也才8字节,所以联合体占8字节;
所以,cache_t 占16字节
*/
通过源码查看,这里涉及的几个类型都没有像能够存放缓存数据的样子。我们找一下 cache_t
的方法
void insert(SEL sel, IMP imp, id receiver);
insert
方法,很像用来添加缓存数据的,SEL
、IMP
作为参数,看起来像是缓存方法的。
通过 void cache_t::insert(SEL sel, IMP imp, id receiver)
源码查看,与 imp
相关的就是
bucket_t *b = buckets();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
再看一下 bucket_t
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
public:
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
}
果然存放的是方法。但是 cache
如何能获取到 bucket_t
呢?我们再看 cache_t
的源码, 里面有这样一个方法
struct bucket_t *buckets() const;
这样我们的猜测就大概能串联起来了。 cache_t
类型的 cache
中存放的是SEL
和 IMP
,是通过 bucket_t
结构体存储在 bucket_t *buckets()
位置的。
下面来验证一下。
@interface TestClass : NSObject
- (void)func0;
- (void)func1;
- (void)func2;
- (void)func3;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestClass *test = [TestClass new];
[test func0];
[test func1];
[test func2];
}
return 0;
}
在调用 func2
方法后进行lldb调试
(lldb) x/4gx test.class
0x100008168: 0x0000000100008140 0x000000010080e140
0x100008178: 0x0000000100a9fdb0 0x0004801000000007
// 因为 isa 8字节, superclass 8字节,所以 cache_t 从 16字节处开始
(lldb) p/x (cache_t *)0x100008178
(cache_t *) $1 = 0x0000000100008178
(lldb) p *$1
(cache_t) $2 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4306107824
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7
}
}
_flags = 32784
_occupied = 4
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0004801000000007
}
}
}
}
// 成功获取到 bucket_t *
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x0000000100a9fdb0
(lldb) p *$3
(bucket_t) $4 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 8378232
}
}
}
// 第0个位置的sel 是 respondsToSelector
(lldb) p $4.sel()
(SEL) $5 = "respondsToSelector:"
// 第1个位置是 nil
(lldb) p *($3+1)
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
// 第2个位置是 func2
(lldb) p *($3+2)
(bucket_t) $7 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 48744
}
}
}
(lldb) p $7.sel()
(SEL) $8 = "func2"
// 第3个位置是 nil
(lldb) p *($3+3)
(bucket_t) $9 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = nil
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
// 第4个位置是 func1
(lldb) p *($3+4)
(bucket_t) $10 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 49048
}
}
}
(lldb) p $10.sel()
(SEL) $11 = "func1"
// IMP
(lldb) p $10.imp(nil,test.class)
(IMP) $12 = 0x0000000100003ef0 (SXObjcDebug`-[TestClass func1])
通过上面的调试,证明我们的猜测是正确的。但是也发现一个问题,里面缓存的方法好像不是连续存储的。带着这个疑问,我们来查阅源码。
扩容 与 方法缓存添加
// 新的已用容量 = 已用容量 + 1
// occupied() 返回 _occupied ,存放已用容量
mask_t newOccupied = occupied() + 1;
// 旧容积 = _maybeMask ? _maybeMask + 1 : 0, 容积 = 旧容积
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 如果缓存为空,这个判断只会在第一次执行,所以标记slowpath,表示该分支执行概率低
if (slowpath(isConstantEmptyCache())) {
// 容积 = INIT_CACHE_SIZE, 在arm64架构下为2 ,其他架构下是4
if (!capacity) capacity = INIT_CACHE_SIZE;
// 重新分配桶,传入旧容积, 容积, 是否需要释放旧桶
reallocate(oldCapacity, capacity, /* freeOld */false);
/*
reallocate中会调用setBucketsAndMask(newBuckets, newCapacity - 1);来设置桶和掩码,
该方法也是不同架构下不一样,
不过基本都会设置_occupied=0,_maybeMask = newMask
也就是把已用容量置0,并记录新桶容积相关值 _maybeMask = 新桶容积 - 1
*/
}
// 这个判断表示,当新的已用容量 + CACHE_END_MARKER <= 容量的一定比例时不用做任何操作
// CACHE_END_MARKER 在arm64架构下为0,其他为1,
// cache_fill_ratio(capacity)方法在arm64架构下返回7/8,其他返回3/4
// 因为在缓存添加中这个判断分支执行概率最高,所以标记fastpath
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
// CACHE_ALLOW_FULL_UTILIZATION 只有 arm64架构下 = 1,所以这个分支只有arm64架构下才会有
#if CACHE_ALLOW_FULL_UTILIZATION
// 当桶的容积 <= 8,并且新的已用容量 <= 桶的容积时,什么也不做
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
// 除上面情况外,进行扩容
else {
// 桶的容积 变为 2倍
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
// 不能大于给定上限
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 重新分配桶,传入旧容积, 容积, 需要释放旧桶
reallocate(oldCapacity, capacity, true);
}
// 桶指针
bucket_t *b = buckets();
// m = 容积 - 1
mask_t m = capacity - 1;
// 哈希计算
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
// 如果当前i位置sel不存在,存入并返回
if (fastpath(b[i].sel() == 0)) {
// 方法内是 _occupied++;,所以是已用容量 ++
incrementOccupied();
// 存入
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
// 如果当前位置存放的sel与要存的相同,说明已经存储了,直接返回
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
// 如果当前位置存放的sel与要存的不同,继续循环
} while (fastpath((i = cache_next(i, m)) != begin));
// 只要 (i = cache_next(i, m)) != begin 就一直循环,这是为了解决哈希冲突,
// 当发生冲突时,也就是i位置已经存放了其他sel,那么i就通过cache_next(i, m)取一个其他值,再进行循环
通过上面的分析能够解答我们的疑惑了。因为cache中方法是通过哈希存储的,所以不连续。并且缓存方法的桶的大小是慢慢变大的。
但是又产生了一个新的疑惑:为什么扩容之后不把旧桶中的缓存放入新桶?关于这个我们需要看一下哈希函数
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// cache_hash 函数是进行固定的运算,得出一个 mask_t 值,这就是一个哈希函数
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
// 这里进行 & 运算,得到的结果肯定小于等于,其中较小的一个的值
// 所以,最终的结果肯定不会大于mask,而mask是桶的长度-1,
// 那么结果肯定小于桶的长度
return (mask_t)(value & mask);
}
通过对哈希函数的分析,我们能够理解,哈希值的计算结果受桶的容积影响,扩容后的新桶容积与旧桶不同,哈希值无法直接重用。
bits
在源码中定义测试类
@protocol TestProtocol0 <NSObject>
- (void)protocolfunc0;
@end
@protocol TestProtocol1 <NSObject>
- (void)protocolfunc1;
@end
@interface TestClass : NSObject<TestProtocol0, TestProtocol1> {
int _number;
BOOL _isTest;
}
@property (nonatomic, weak) id<TestProtocol0> delegate0;
@property (nonatomic, weak) id<TestProtocol1> delegate1;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)func0;
- (int)func1:(int)a;
+ (NSString *)classFunc0;
@end
@implementation TestClass
- (void)func0 {
}
- (int)func1:(int)a {
return 1;
}
/// MARK: - TestProtocol
- (void)protocolfunc0 {
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
TestClass *obj = [TestClass new];
return obj;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestClass *test = [TestClass new];
}
return 0;
}
获取bits中的 class_rw_t
(lldb) x/6gx test.class
0x100008730: 0x0000000100008758 0x000000010080e140
0x100008740: 0x0000000100a93f70 0x0001803c00000007
0x100008750: 0x0000000100b4c7a4 0x000000010080e0f0
// 因为 ISA 8字节,superclass 8字节, cache 16字节,所以bits 从 32字节位置,即0x100008750开始存储
// 这里根据源码,强转为 class_data_bits_t类型指针 $1
(lldb) p/x (class_data_bits_t *)0x100008750
(class_data_bits_t *) $1 = 0x0000000100008750
// 打印 $1 的值,发现无法看到内部结构
(lldb) p *$1
(class_data_bits_t) $2 = (bits = 4306814884)
// 查找源码,发现class_data_bits_t 中有 data() 方法
(lldb) p $2.data()
(class_rw_t *) $3 = 0x0000000100b4c7a0
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295001776
}
}
firstSubclass = nil
nextSiblingClass = 0x00007ff8568d9ac8
}
获取class_rw_t中的存放的方法( methods )
// 我们在class_rw_t中找到一个methods方法
(lldb) p $4.methods()
(const method_array_t) $5 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008438
}
arrayAndFlag = 4295001144
}
}
}
(lldb) p $5.list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
ptr = 0x0000000100008438
}
(lldb) p $6.ptr
(method_list_t *const) $7 = 0x0000000100008438
(lldb) p *$7
(method_list_t) $8 = {
// entsize_list_tt 是个模板,可以实例化出method_list_t、ivar_list_t、property_list_t三种类型。
// template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
// 这里 entsize_list_tt 内部存放的是 method_t, count = 12, 可能表示是有12个方法,我们打印一下方法
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 12)
}
// entsize_list_tt 中有 get() 方法可以打印内部元素
(lldb) p $8.get(0)
// 可见能够打印method_t,但是没有什么详细信息,我们进method_t源码找一下
(method_t) $9 = {}
// 因为我是用的电脑是x86_64架构的,所以是大端模式,采用big()方法,M1电脑需要使用small()
(lldb) p $8.get(0).big()
(method_t::big) $10 = {
name = "func0"
types = 0x0000000100003f20 "v16@0:8"
imp = 0x00000001000038e0 (SXObjcDebug`-[TestClass func0])
}
(lldb) p $8.get(1).big()
(method_t::big) $11 = {
name = "func1:"
types = 0x0000000100003f28 "i20@0:8i16"
imp = 0x00000001000038f0 (SXObjcDebug`-[TestClass func1:])
}
(lldb) p $8.get(2).big()
(method_t::big) $12 = {
name = "delegate0"
types = 0x0000000100003e73 "@16@0:8"
imp = 0x0000000100003960 (SXObjcDebug`-[TestClass delegate0])
}
(lldb) p $8.get(3).big()
(method_t::big) $13 = {
name = "setDelegate0:"
types = 0x0000000100003f48 "v24@0:8@16"
imp = 0x0000000100003990 (SXObjcDebug`-[TestClass setDelegate0:])
}
(lldb) p $8.get(4).big()
(method_t::big) $14 = {
name = "delegate1"
types = 0x0000000100003e73 "@16@0:8"
imp = 0x00000001000039c0 (SXObjcDebug`-[TestClass delegate1])
}
(lldb) p $8.get(5).big()
(method_t::big) $15 = {
name = "setDelegate1:"
types = 0x0000000100003f48 "v24@0:8@16"
imp = 0x00000001000039f0 (SXObjcDebug`-[TestClass setDelegate1:])
}
(lldb) p $8.get(6).big()
(method_t::big) $16 = {
name = "copyWithZone:"
types = 0x0000000100003f33 "@24@0:8^{_NSZone=}16"
imp = 0x0000000100003910 (SXObjcDebug`-[TestClass copyWithZone:])
}
(lldb) p $8.get(7).big()
(method_t::big) $17 = {
name = "name"
types = 0x0000000100003e73 "@16@0:8"
imp = 0x0000000100003a20 (SXObjcDebug`-[TestClass name])
}
(lldb) p $8.get(8).big()
(method_t::big) $18 = {
name = ".cxx_destruct"
types = 0x0000000100003f20 "v16@0:8"
imp = 0x0000000100003ab0 (SXObjcDebug`-[TestClass .cxx_destruct])
}
(lldb) p $8.get(9).big()
(method_t::big) $19 = {
name = "setName:"
types = 0x0000000100003f48 "v24@0:8@16"
imp = 0x0000000100003a40 (SXObjcDebug`-[TestClass setName:])
}
(lldb) p $8.get(10).big()
(method_t::big) $20 = {
name = "age"
types = 0x0000000100003f53 "i16@0:8"
imp = 0x0000000100003a70 (SXObjcDebug`-[TestClass age])
}
(lldb) p $8.get(11).big()
(method_t::big) $21 = {
name = "setAge:"
types = 0x0000000100003f5b "v20@0:8i16"
imp = 0x0000000100003a90 (SXObjcDebug`-[TestClass setAge:])
}
(lldb) p $8.get(12).big()
// // 可见上面的count正是内部元素数量
Assertion failed: (i < count), function get, file objc-runtime-new.h, line 629.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.
// 方法信息也可以使用 getDescription() 来获取
(lldb) p $8.get(1).getDescription()
(objc_method_description *) $22 = 0x0000000100008458
(lldb) p *$22
(objc_method_description) $23 = (name = "func1:", types = "i20@0:8i16")
可见 methods()
方法获取到的的确是我们定义的方法,不过里面多了一个 .cxx_destruct
方法。
《Effective Objective-C 2.0》中提到的:
When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.
可见 .cxx_destruct
方法是在ARC模式下用于释放成员变量的。只有当前类拥有实例变量时这个方法才会出现,property生成的实例变量也算,且父类的实例变量不会导致子类拥有这个方法。
获取class_rw_t中的属性( properties )
// 这里使用获取属性的方法
(lldb) p $4.properties()
(const property_array_t) $24 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100008628
}
arrayAndFlag = 4295001640
}
}
}
(lldb) p $24.list
(const RawPtr<property_list_t>) $25 = {
ptr = 0x0000000100008628
}
(lldb) p $25.ptr
(property_list_t *const) $26 = 0x0000000100008628
(lldb) p *$26
(property_list_t) $27 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 8)
}
// 由于 property_t 就是一个简单的结构体,所以这里直接能看到信息
(lldb) p $27.get(0)
(property_t) $28 = (name = "delegate0", attributes = "T@"<TestProtocol0>",W,N,V_delegate0")
(lldb) p $27.get(1)
(property_t) $29 = (name = "delegate1", attributes = "T@"<TestProtocol1>",W,N,V_delegate1")
(lldb) p $27.get(2)
(property_t) $30 = (name = "name", attributes = "T@"NSString",C,N,V_name")
(lldb) p $27.get(3)
(property_t) $31 = (name = "age", attributes = "Ti,N,V_age")
(lldb) p $27.get(4)
(property_t) $32 = (name = "hash", attributes = "TQ,R")
(lldb) p $27.get(5)
(property_t) $33 = (name = "superclass", attributes = "T#,R")
(lldb) p $27.get(6)
(property_t) $34 = (name = "description", attributes = "T@"NSString",R,C")
(lldb) p $27.get(7)
(property_t) $35 = (name = "debugDescription", attributes = "T@"NSString",R,C")
获取class_rw_t中的协议( protocols )
// 这里使用获取协议的方法
(lldb) p $4.protocols()
(const protocol_array_t) $36 = {
list_array_tt<unsigned long, protocol_list_t, RawPtr> = {
= {
list = {
ptr = 0x00000001000083d0
}
arrayAndFlag = 4295001040
}
}
}
(lldb) p $36.list
(const RawPtr<protocol_list_t>) $37 = {
ptr = 0x00000001000083d0
}
(lldb) p $37.ptr
(protocol_list_t *const) $38 = 0x00000001000083d0
(lldb) p *$38
(protocol_list_t) $39 = (count = 2, list = protocol_ref_t [] @ 0x00006000037d6fd8)
// 这里是protocol_list_t 结构体类型,根据源码有获取的办法
(lldb) p $39.list[0]
(protocol_ref_t) $40 = 4295002088
// protocol_ref_t 根据源码注释,可能可以强转为 protocol_t*, 尝试发现的确可行
(lldb) p (protocol_t *)$40
(protocol_t *) $41 = 0x00000001000087e8
(lldb) p *$41
(protocol_t) $42 = {
objc_object = {
isa = {
bits = 4303413448
cls = 0x000000010080e0c8
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 537926681
magic = 0
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
}
mangledName = 0x0000000100003be8 "TestProtocol0"
protocols = 0x0000000100008350
instanceMethods = 0x0000000100008368
classMethods = nil
optionalInstanceMethods = nil
optionalClassMethods = nil
instanceProperties = nil
size = 96
flags = 0
_extendedMethodTypes = 0x0000000100008388
_demangledName = 0x0000000000000000
_classProperties = nil
}
(lldb) p $39.list[1]
(protocol_ref_t) $43 = 4295002184
(lldb) p (protocol_t *)$43
(protocol_t *) $44 = 0x0000000100008848
(lldb) p *$44
(protocol_t) $45 = {
objc_object = {
isa = {
bits = 4303413448
cls = 0x000000010080e0c8
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 537926681
magic = 0
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
}
mangledName = 0x0000000100003bff "TestProtocol1"
protocols = 0x0000000100008390
instanceMethods = 0x00000001000083a8
classMethods = nil
optionalInstanceMethods = nil
optionalClassMethods = nil
instanceProperties = nil
size = 96
flags = 0
_extendedMethodTypes = 0x00000001000083c8
_demangledName = 0x0000000000000000
_classProperties = nil
}
获取class_rw_t中的成员变量( ivars )
// 先获取 class_ro_t
(lldb) p $4.ro()
(const class_ro_t *) $46 = 0x00000001000086b0
(lldb) p *$46
(const class_ro_t) $47 = {
flags = 388
instanceStart = 8
instanceSize = 48
reserved = 0
= {
ivarLayout = 0x0000000100003c0d "A"
nonMetaclass = 0x0000000100003c0d
}
name = {
std::__1::atomic<const char *> = "TestClass" {
Value = 0x0000000100003bde "TestClass"
}
}
baseMethods = {
ptr = 0x0000000100008438
}
baseProtocols = 0x00000001000083d0
ivars = 0x0000000100008560
weakIvarLayout = 0x0000000100003c0f ""
baseProperties = 0x0000000100008628
_swiftMetadataInitializer_NEVER_USE = {}
}
// 因为成员变量在底层用ivar 表示,所以ivars可能存放的就是成员变量
(lldb) p $47.ivars
(const ivar_list_t *const) $48 = 0x0000000100008560
(lldb) p *$48
(const ivar_list_t) $49 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 6)
}
(lldb) p $49.get(0)
(ivar_t) $50 = {
offset = 0x0000000100008700
name = 0x0000000100003dd7 "_number"
type = 0x0000000100003f66 "i"
alignment_raw = 2
size = 4
}
(lldb) p $49.get(1)
(ivar_t) $51 = {
offset = 0x0000000100008708
name = 0x0000000100003ddf "_isTest"
type = 0x0000000100003f68 "c"
alignment_raw = 0
size = 1
}
(lldb) p $49.get(2)
(ivar_t) $52 = {
offset = 0x0000000100008710
name = 0x0000000100003de7 "_age"
type = 0x0000000100003f66 "i"
alignment_raw = 2
size = 4
}
(lldb) p $49.get(3)
(ivar_t) $53 = {
offset = 0x0000000100008718
name = 0x0000000100003dec "_delegate0"
type = 0x0000000100003f6a "@"<TestProtocol0>""
alignment_raw = 3
size = 8
}
(lldb) p $49.get(4)
(ivar_t) $54 = {
offset = 0x0000000100008720
name = 0x0000000100003df7 "_delegate1"
type = 0x0000000100003f7d "@"<TestProtocol1>""
alignment_raw = 3
size = 8
}
(lldb) p $49.get(5)
(ivar_t) $55 = {
offset = 0x0000000100008728
name = 0x0000000100003e02 "_name"
type = 0x0000000100003f90 "@"NSString""
alignment_raw = 3
size = 8
}
获取存储在元类中的类方法( methods )
在上面的methods中并没有找到我们定义的类方法,那么类方法存在哪里呢。
先说一下结论,类方法存储在元类的methods中, 下面验证一下。
(lldb) x/1gx test.class
0x100008730: 0x0000000100008758
// 因为测试Mac设备是 _x86_64 的 所以 ISA_MASK 0x00007ffffffffff8ULL
(lldb) p/x 0x0000000100008758 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008758 // 元类地址
(lldb) x/6gx 0x0000000100008758
0x100008758: 0x000000010080e0f0 0x000000010080e0f0
0x100008768: 0x0000000100806240 0x0000e03500000000
0x100008778: 0x0000000100b34bf4 0x0000000000000000
(lldb) p/x (class_data_bits_t *)0x100008778
(class_data_bits_t *) $2 = 0x0000000100008778
(lldb) p *$2
(class_data_bits_t) $3 = (bits = 4306717684)
(lldb) p $3.data()
(class_rw_t *) $4 = 0x0000000100b34bf0
(lldb) p *$4
(class_rw_t) $5 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295001072
}
}
firstSubclass = nil
nextSiblingClass = 0x00007ff8568daef0
}
(lldb) p $5.methods()
(const method_array_t) $6 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008058
}
arrayAndFlag = 4295000152
}
}
}
(lldb) p $6.list
(const method_list_t_authed_ptr<method_list_t>) $7 = {
ptr = 0x0000000100008058
}
(lldb) p $7.ptr
(method_list_t *const) $8 = 0x0000000100008058
(lldb) p *$8
(method_list_t) $9 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $9.get(0).big()
// 我们定义的类方法
(method_t::big) $10 = {
name = "classFunc0"
types = 0x0000000100003e73 "@16@0:8"
imp = 0x0000000100003b00 (SXObjcDebug`+[TestClass classFunc0])
}
为什么类方法存储在元类中?或者苹果为什么要设计元类?
我们通过上面的lldb调试,可以得出结论,在类的class_rw_t中的methods里面只存在实例方法,元类的class_rw_t中的methods存放的才是类的类方法。为什么非要分开存放,而不是直接取消元类设计,把类方法也存在类中呢?
主要的目的是为了复用消息机制。在objc底层没有类方法和实例方法的区别。在OC中调用方法,其实是在给某个对象发送某条消息。消息的发送在编译的时候编译器就会把方法转换为objc_msgSend
这个函数。
id objc_msgSend(id self, SEL op, ...)
这个函数有俩个隐式的参数:消息的接收者,消息的方法名。通过这2个参数就能去找到对应方法的实现。 objc_msgSend
会通过第一个参数消息的接收者的isa指针,找到对应的类,如果我们是通过实例对象调用方法,那么这个isa指针就会找到实例对象的类对象,如果是类对象,就会找到类对象的元类对象,然后再通过SEL方法名找到对应的imp,然后就能找到方法对应的实现。
如果没有元类的话,类方法和实例方法放一起,那这个objc_msgSend方法还得多加俩个参数,一个参数用来判断这个方法到底是类方法还是实例方法。一个参数用来判断消息的接受者到底是类对象还是实例对象。消息的发送,越快越好。那如果没有元类,在objc_msgSend内部就会有有很多的判断,就会影响消息的发送效率。
所以元类的出现就解决了这个问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,符合设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。
所以这个元类的出现,最大的好处就是能够复用消息传递这套机制。不管是什么类型的方法,都是同一套流程。
class_rw_t 、class_rw_ext_t 、class_ro_t
通过源码我们可以看到虽然,方法、成员变量、协议等我们是通过 class_rw_t
获取的。但是却并不是直接存储在 class_rw_t
中的。而是从 class_rw_ext_t
或者 class_ro_t
中获取的。苹果为什么要如此设计呢?
根据 WWDC2020相关视频介绍 , 当 类在磁盘上 时,结构如下:
其中 class_ro_t
时 read only 类型的,所以是 Clean Menory
,它是不可更改的。MYClass
是 Dirty Memory
, 因为运行时会向它写入新的数据。
Clean Menory
: 指的是加载后不会发生更改的内存
Dirty Memory
: 指在进程运行时会发生更改的内存。
Dirty Memory
要比Clean Menory
昂贵,因为只要进程在运行,Dirty Memory
就必须一直存在;而Clean Menory
可以进行移除从而节省更多的内存空间,当需要时系统可以重新从磁盘中加载。
所以为了优化内存,Clean Menory
越多越好。这就是把不会被改变的数据,单独存放到class_ro_t
中去的原因。但是,我们运行时还需要更多的信息。这些信息,是会发生改变的。所以,当一个 类首次被使用 时,运行时会为类分配额外的内存,也就是 class_rw_t
部分。
在 class_rw_t
中,存放了只有运行时才会生成的新信息。而class_rw_t
也存在 Methods
、Properties
、Protocols
,这是因为这些在运行时是可以修改的。例如,当category被加载时,它就可以向类中添加新的方法,再例如我们可以使用runtime的api来动态的添加方法。但是原本用来存储这些信息的class_ro_t
是只读的,所以为了能够修改只能在 class_rw_t
重新存储一份。
根据苹果工程师的测试发现,大概只有10%左右的类会在运行时改变方法,还有类似的,Demangled Name
这个只有Swift中存在,而且只有访问Swift的OC名称时才会需要。这些类似的存储,仅仅存在少量的使用情况,如果直接存在于class_rw_t
中,无疑会浪费大量内存。所以苹果采用了 class_rw_ext_t
的设计。把这些不一定需要的信息,从 class_rw_t
存放到class_rw_ext_t
中。
当类不需要 class_rw_ext_t
中的信息时,class_rw_t
不会有 class_rw_ext_t
, 那么直接访问 class_ro_t
中的信息。90%的类都是不需要 class_rw_ext_t
的。
当类确实需要访问这些存储在class_rw_ext_t
中的信息时,采用把class_rw_ext_t
添加到class_rw_t
中去,然后把 class_ro_t
加入到 class_rw_ext_t
中去。
class_rw_ext_t
生成的条件:1.使用runtime的api进行过动态修改时
2.存在分类,且分类和本类都为非懒加载类的时候。即实现了
+load
方法。
相关源码
cache_t::insert
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
// Never cache before +initialize is done
if (slowpath(!cls()->isInitialized())) {
return;
}
if (isConstantOptimizedCache()) {
_objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
cls()->nameForLogging());
}
#if DEBUG_TASK_THREADS
return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
ASSERT(sel != 0 && cls()->isInitialized());
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
cache_t
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
// _bucketsAndMaybeMask is a buckets_t pointer
// _maybeMask is the buckets mask
static constexpr uintptr_t bucketsMask = ~0ul;
static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
static constexpr uintptr_t maskShift = 48;
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
static constexpr uintptr_t preoptBucketsMarker = 1ul;
static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
// _maybeMask is unused, the mask is stored in the top 16 bits.
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
// Ensure we have enough bits for the buckets pointer.
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
"Bucket field doesn't have enough bits for arbitrary pointers.");
#if CONFIG_USE_PREOPT_CACHES
static constexpr uintptr_t preoptBucketsMarker = 1ul;
#if __has_feature(ptrauth_calls)
// 63..60: hash_mask_shift
// 59..55: hash_shift
// 54.. 1: buckets ptr + auth
// 0: always 1
static constexpr uintptr_t preoptBucketsMask = 0x007ffffffffffffe;
static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) {
uintptr_t value = (uintptr_t)cache->shift << 55;
// masks have 11 bits but can be 0, so we compute
// the right shift for 0x7fff rather than 0xffff
return value | ((objc::mask16ShiftBits(cache->mask) - 1) << 60);
}
#else
// 63..53: hash_mask
// 52..48: hash_shift
// 47.. 1: buckets ptr
// 0: always 1
static constexpr uintptr_t preoptBucketsMask = 0x0000fffffffffffe;
static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) {
return (uintptr_t)cache->hash_params << 48;
}
#endif
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
// _bucketsAndMaybeMask is a buckets_t pointer in the top 28 bits
// _maybeMask is unused, the mask length is stored in the low 4 bits
static constexpr uintptr_t maskBits = 4;
static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
static constexpr uintptr_t bucketsMask = ~maskMask;
static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
#else
#error Unknown cache mask storage type.
#endif
bool isConstantEmptyCache() const;
bool canBeFreed() const;
mask_t mask() const;
#if CONFIG_USE_PREOPT_CACHES
void initializeToPreoptCacheInDisguise(const preopt_cache_t *cache);
const preopt_cache_t *disguised_preopt_cache() const;
#endif
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
static bucket_t *emptyBuckets();
static bucket_t *allocateBuckets(mask_t newCapacity);
static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));
public:
// The following four fields are public for objcdt's use only.
// objcdt reaches into fields while the process is suspended
// hence doesn't care for locks and pesky little details like this
// and can safely use these.
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
#if CONFIG_USE_PREOPT_CACHES
const preopt_cache_t *preopt_cache(bool authenticated = true) const;
#endif
mask_t occupied() const;
void initializeToEmpty();
#if CONFIG_USE_PREOPT_CACHES
bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = (uintptr_t)&_objc_empty_cache) const;
bool shouldFlush(SEL sel, IMP imp) const;
bool isConstantOptimizedCacheWithInlinedSels() const;
Class preoptFallbackClass() const;
void maybeConvertToPreoptimized();
void initializeToEmptyOrPreoptimizedInDisguise();
#else
inline bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = 0) const { return false; }
inline bool shouldFlush(SEL sel, IMP imp) const {
return cache_getImp(cls(), sel) == imp;
}
inline bool isConstantOptimizedCacheWithInlinedSels() const { return false; }
inline void initializeToEmptyOrPreoptimizedInDisguise() { initializeToEmpty(); }
#endif
void insert(SEL sel, IMP imp, id receiver);
void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
void destroy();
void eraseNolock(const char *func);
static void init();
static void collectNolock(bool collectALot);
static size_t bytesForCapacity(uint32_t cap);
#if __LP64__
bool getBit(uint16_t flags) const {
return _flags & flags;
}
void setBit(uint16_t set) {
__c11_atomic_fetch_or((_Atomic(uint16_t) *)&_flags, set, __ATOMIC_RELAXED);
}
void clearBit(uint16_t clear) {
__c11_atomic_fetch_and((_Atomic(uint16_t) *)&_flags, ~clear, __ATOMIC_RELAXED);
}
#endif
#if FAST_CACHE_ALLOC_MASK
bool hasFastInstanceSize(size_t extra) const
{
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
}
return _flags & FAST_CACHE_ALLOC_MASK;
}
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
uint16_t sizeBits;
// Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
// to yield the proper 16byte aligned allocation size with a single mask
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
sizeBits &= FAST_CACHE_ALLOC_MASK;
if (newSize <= sizeBits) {
newBits |= sizeBits;
}
_flags = newBits;
}
#else
bool hasFastInstanceSize(size_t extra) const {
return false;
}
size_t fastInstanceSize(size_t extra) const {
abort();
}
void setFastInstanceSize(size_t extra) {
// nothing
}
#endif
};
class_data_bits_t
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
class_rw_t
struct class_rw_t {
// ro
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
// 方法
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
}
}
// 属性
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
// 协议
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
}
entsize_list_tt
// Element:表示元素类型 List:表示容器类型 FlagMask:标记位
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
}
method_t
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
// 大端模式使用
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
private:
bool isSmall() const {
return ((uintptr_t)this & 1) == 1;
}
// The representation of a "small" method. This stores three
// relative offsets to the name, types, and implementation.
// 小端模式使用
struct small {
// The name field either refers to a selector (in the shared
// cache) or a selref (everywhere else).
RelativePointer<const void *> name;
RelativePointer<const char *> types;
RelativePointer<IMP, /*isNullable*/false> imp;
bool inSharedCache() const {
return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS &&
objc::inSharedCache((uintptr_t)this));
}
};
small &small() const {
ASSERT(isSmall());
return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);
}
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
objc_method_description *getDescription() const {
return isSmall() ? getSmallDescription() : (struct objc_method_description *)this;
}
}
property_t
struct property_t {
const char *name;
const char *attributes;
};
protocol_list_t
struct protocol_list_t {
// count is pointer-sized by accident.
uintptr_t count;
protocol_ref_t list[0]; // variable-size
}
protocol_ref_t & protocol_t
typedef uintptr_t protocol_ref_t; // protocol_t *, but unremapped
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;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
}
class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
protocol_list_t * baseProtocols;
// 成员变量存放的容器
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
class_rw_ext_t
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
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;
}
};