iOS OC底层探索
iOS Objective -C alloc 调用流程
iOS Object-C init&new
iOS OC 对象的内存对齐原则
iOS Objective-C isa
iOS Objective-C isa 走位分析
iOS OC 类原理
iOS OC 方法的本质
iOS Objective-C 消息的查找
iOS Objective-C 消息的转发
iOS 应用加载
dyld
篇Mach-O探索
iOS开发中『库』的区别与应用
iOS 应用的加载
objc
篇iOS Objective-C 分类的加载
iOS Objective-C 类扩展
iOS Objective-C 关联对象
iOS Objective-C KVC 的常见用法
iOS Objective-C KVC 详解
iOS Objective-C KVO 常见用法
iOS Objective-C KVO 详解
iOS多线程 基础
注: 本文使用的环境是objc4-779.1 Xcode 11.5 (11E608c)
1. 类
根据前面几篇文章的分析,我们知道Objective -C的对象通过isa与类关联起来,那么到底什么是类呢?下面我们来探索一下。
我们知道Objective-C的基类是NSObject,日常开发中我们我们使用到的类基本都是用NSObject派生来的,那么在编译后,他到底是什么样子呢?
- 我的另一篇文章:通过Clang 看OC对象的本质
在这篇文章中我们说道Class在底层是一个objc_class
那么它到底是如何实现的呢?我们来到objc源码中一探究竟。我们知道objc_class
是一个结构体我们搜索struct objc_class
,我们发现会有很多结果,那么我们到底去分析那个版本呢,我们应该知道runtime有old和new两个版本,那么新版本当然作为我们的首选,所有我们打开objc-runtime-new.h进行一探究竟。
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
class_rw_t *data() const {
return bits.data();
}
}
在这里我们再次看到了objc_object
所以说在面向对象里面真的是万物皆对象。OC中的NSObject就是对底层objc_object的封装。
C | Objective-C |
---|---|
objc_object | NSObject |
objc_class | Nsobjcet(Class) |
2. 类中包含的内容
通过objc_class
的源码我们知道类中包含:
-
ISA
//isa指针 ,继承自objc_object -
superclass
// 父类指针 -
cache
// cache_t类型的结构体 -
bits
// class_data_bits_t结构体
特别提醒
isa 在源码中是以注释的形式体现出来的,并不是没有写,而是继承自objc_object
struct objc_object {
private:
isa_t isa;
}
2.1 ISA指针
在以前的文章中我们已经详细的介绍了isa
,在对象初始化的时候通过isa
使对象和类关联起来,那么类里面为什么还会有isa
呢,通过我们的isa走位分析那篇文章我就可以知道,类里面的isa
指向元类。元类与类同样通过isa
进行了关联。
2.2 superclass
顾名思义,superclass就是指向父类,继承自哪个父类,一般来说根父类基本都是NSObject,根元类的父类也是NSObject。
2.3 cache
顾明思议,cach是缓存的意思,肯定存储的是类中的一些缓存。cache是一个cache_t类型的结构体。在objc-runtime-new.h中查看cache_t源码如下:
主要有bucket_t
的结构体指针,mask_t
的mask
,uint16_t
的_flags
和 _occupied
类型 | 占用空间 |
---|---|
bucket_t* | 8字节 |
mask_t(uint32_t) | 4字节 |
uint16_t | 2 字节 |
总计是8+4+2+2=16字节
cache_t 源码实现:
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
// 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.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
// _maskAndBuckets stores the mask shift in the low 4 bits, and
// the buckets pointer in the remainder of the value. The mask
// shift is the value where (0xffff >> shift) produces the correct
// mask. This is equal to 16 - log2(cache_size).
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
static constexpr uintptr_t maskBits = 4;
static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
、
、
、
、省略代码
}
通过objc源码查看 cache_t 的实现,我们发现主要有
- _buckets bucket_t类型的结构体指针
- _mask mask_t类型的结构体
- _flags
- _occupied
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 _imp;
explicit_atomic _sel;
#else
explicit_atomic _sel;
explicit_atomic _imp;
#endif
}
mask_t 源码实现:
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
method_t 源码实现:
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
struct SortBySELAddress :
public std::binary_function
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
由bucket_t
源码我们大概就能够知道它是存储方法的,因为方法的本质就是SEL和IMP,这个有method_t源码也可以证实,所以cache的主要作用就是存储我们的方法的,下面我们通过lldb
来进行验证一下:
- bucket_t分析:
首先我们在objc源码中实现一个LGPerson类,代码如下:
@interface LGPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickname;
- (void)sayHello;
- (void)sayCode;
- (void)sayMaster;
- (void)sayNB;
+ (void)sayHappy;
@end
main代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [[LGPerson alloc] init];
Class pClass = [LGPerson class];
[person sayHello];
[person sayCode];
[person sayNB];
}
return 0;
}
分别在sayHello,sayCode, sayNB处断点进行lldb查看,结果如下:
特别提醒!
类的isa占用8字节,superclass占用8字节,所以我们对cache的分析由首地址加16进行分析,16字节在16进制中就是加10。
根据图一我们可以看到在没有执行方法前我们的cache_t
的buckets()
里面是取不出数据的直接是个null,当我们执行完sayHello
方法后再buckets里面取出的数据,并通过打印sel获取到了一个叫sayHello
的SEL,在后面的两个图里面我们分别执行了sayCode
和sayNB
方法后也分别获取到了sayCode
和sayNB
的SEL,当我们越界获取的时候就是空了,所以我们通过lldb
分析可知,cahce主要是缓存方法的。那么为什么没有找到alloc
和class
方法呢?因为它们是类方法,会存储在元类里面,在本文的后续过程中我们会进一步分析。
- 补充init
在我们没有执行任何自定义方法的时候,我们会发现cache里面有了一个数据,通过lldb打印我们看到其实是init,因为init是个实例方法,所以当我们调用了init后也可以在cache里面找到init方法。
- _ocuupied
lldb分析:
当我们没有执行任何方法的时候,我们通过lldb打印cache,我们发现_ocuupied的值为0,_mask的值为0。
当我们执行了一个方法后再次通过lldb打印cache,我们发现_ocuupied的值为1_msak的值为3,那么_ocuupied是不是记录了我们缓存方法的个数呢?
当我们执行了两个方法后再次通过lldb打印cache,我们发现_ocuupied的值为2_mask
的值为3,这个时候我们肯定会觉得_ocuupied大概率是记录了我们缓存方法的个数,下面我们继续进行探索。
当我们执行了三个方法后再次通过lldb打印cache,我们发现_ocuupied的值为3_mask的值为3,这个时候我们基本确定_ocuupied记录了我们缓存方法的个数,下面我们继续进行探索。
当我们执行了四个方法后再次通过lldb打印cache,我们发现_ocuupied
的值为1_mask
的值为7,这个时候按照我们的猜想_ocuupied
的值应该为4,但是他却成了1,那么到底是什么原因导致了这个情况呢?虽然_ocuupied
的值变成了1但是_mask
的值也变了,并且为7,刚刚一直是3的mask现在变成可7,而我们的ocuupied
刚刚也是三,好像这个3记录在了mask
里面,ocuupied
重新开始计数一样,mask开始为3,当ocuupied
为3后mask
就满了,将mas
k进行扩容后,继续重新对ocuupied
进行计数。这个很像哈希表这种数据结构,并且为了解决哈希冲突,使用的是开放寻址法,而开放寻址法必然要在合适的时机进行扩容,这个时机应该是表快满的时候。为了验证我们的猜想,还是查看cache_t
的源码进行分析吧。
在源码中我们发现了mask
和occupied
两个函数。
mask_t mask();
mask_t occupied();
跳转进occupied
函数后,源码如下,紧随其后的还有incrementOccupied()
函数。
mask_t cache_t::occupied()
{
return _occupied;
}
void cache_t::incrementOccupied()
{
_occupied++;
}
根据上面的源码,我们进行全局搜索,查找调用occupied
和 incrementOccupied()
地方,发现occupied
的调用有三处incrementOccupied()
的调用有一处,但他们两个都同事出现在了一个insert
函数中,看到这个函数后我们的第一想法就是,这个函数是cache缓存的核心函数,下面我们做进一步的验证,再分析一下mask
函数。mask()
函数的实现如下:主要就是返回_mask
的值。
mask_t cache_t::mask()
{
return _mask.load(memory_order::memory_order_relaxed);
}
下面我们就搜索一下mask()
,发现共有三处调用,有两处在同一函数内,有一处是返回值,所以我们重点分析两处在一起的那个函数,函数实现如下:
unsigned cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
既然没找到mask
直接在cahce
中的调用与影响,那么我们可以继续搜索一下capacity()
函数,这里的mask()
被间接调用的可能性很大,通过搜索capacity()
函数后发现共有四处调用,其中一处就在insert
函数内,这时我们上面的猜想又得到了一些可能性。下面我们直接上insert
函数的源码作进一步的分析吧。
insert函数源码:
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
ASSERT(sel != 0 && cls->isInitialized());
// Use the cache as-is if it is less than 3/4 full
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 <= capacity / 4 * 3)) {
// Cache is less than 3/4 full. Use it as-is.
}
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 because the
// minimum size is 4 and we resized at 3/4 full.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set(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));
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
-
- 排除前面的加锁和断言
-
- 首先获取了
occupied
的值在加1,在获取mask
的值,放在变量capacity内
- 首先获取了
-
- 先判断cache是否为空,如果为空则初始化一个值
INIT_CACHE_SIZE
,这里的初始化值为4,源码放在后面,就是1左移2位,二进制为 100, 10进制为4,然后调用reallocate
函数开辟空间。
- 先判断cache是否为空,如果为空则初始化一个值
-
- 如果不大于四分之三则不作处理,(这里应该是个扩容算法,后面则进一步验证了)
-
- 其他情况,也就是大于四分之三后,则对
capacity
进行扩容,扩容为当前值的两倍,并且如果扩容后的值大于最大值MAX_CACHE_SIZE
,也就是1左移16位,1 0000 0000 0000 0000, 对应的10进制的值是65536。则不再进行扩容。扩容完毕后调用reallocate
函数开辟空间。
- 其他情况,也就是大于四分之三后,则对
-
- 执行完上述操作后,获取
bucket_t
和mask
,并通过cache_hash
函数计算出一个begin
(应该是缓存新调用方法的位置下标),把begin
的值赋值给变量i
- 执行完上述操作后,获取
-
- 通过一个
do while
循环,判断计算出的位置是否为空,不为空则occupied
自增,通过set
方法将改类的方法进行缓存到上面初始化的bucket
里面,如果不为空则判断bucket
内的sel
是否等于要缓存的sel,直到通过cache_next
计算出下一个位置不等于begin
。
- 通过一个
上面提到的源码
INIT_CACHE_SIZE
和 MAX_CACHE_SIZE
/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
MAX_CACHE_SIZE_LOG2 = 16,
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2),
};
isConstantEmptyCache
bool cache_t::isConstantEmptyCache()
{
return
occupied() == 0 &&
buckets() == emptyBucketsForCapacity(capacity(), false);
}
reallocate
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
cache_hash
和 cache_next
// Class points to cache. SEL is key. Cache buckets store SEL+IMP.
// Caches are never built in the dyld shared cache.
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unknown architecture
#endif
buckets
struct bucket_t *cache_t::buckets()
{
return _buckets.load(memory_order::memory_order_relaxed);
}
通过对核心方法insert
的分析我们大概知道了cache
的基本原理与实现,下面我们总结一下:
1.当方法调用的时候会进行缓存
2.缓存时需要判断缓存是否为空,空则初始化空间,不空则判断是否到达扩容临界点,到了则扩容,不到则直接缓存
3.缓存时则通过哈希计算缓存位置进行存储
我们知道在调用方法的时候会触发方法的缓存,那么这倒地是怎样一个调用堆栈呢,我么通过搜索insert(
进行查看,我们发现在cache_fill
函数内调用了insert
cache_fill
源码
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
runtimeLock.assertLocked();
#if !DEBUG_TASK_THREADS
// Never cache before +initialize is done
if (cls->isInitialized()) {
cache_t *cache = getCache(cls);
#if CONFIG_USE_CACHE_LOCK
mutex_locker_t lock(cacheUpdateLock);
#endif
cache->insert(cls, sel, imp, receiver);
}
#else
_collecting_in_critical();
#endif
}
通过cache_fill
源码我们发现主要就是获取cache
然后调用insert
函数缓存方法,当我们想进一步通过搜索cache_fill
进行查找调用关系时,却发现并没有相应的源码了,但是在``文件的注释中发下了一些东西:其中cache_fill
、cache_expand
、cache_create
、bcopy
、flush_caches
、cache_flush
、cache_collect_free
、等,这些在新的objc-cache.mm
中并没有什么踪迹,但是在objc-cache-old.mm
中却能看见其踪影,其实在 objc4-756
中这些实现还是在的,应该是苹果通过一些优化或者代码的整合成了现在的样子,虽然代码在修改,但是原理基本是不变的,都是为了缓存方法。也都是通过扩容和哈希去实现的。
/***********************************************************************
* objc-cache.m
* Method cache management
* Cache flushing
* Cache garbage collection
* Cache instrumentation
* Dedicated allocator for large caches
**********************************************************************/
/***********************************************************************
* Method cache locking (GrP 2001-1-14)
*
* For speed, objc_msgSend does not acquire any locks when it reads
* method caches. Instead, all cache changes are performed so that any
* objc_msgSend running concurrently with the cache mutator will not
* crash or hang or get an incorrect result from the cache.
*
* When cache memory becomes unused (e.g. the old cache after cache
* expansion), it is not immediately freed, because a concurrent
* objc_msgSend could still be using it. Instead, the memory is
* disconnected from the data structures and placed on a garbage list.
* The memory is now only accessible to instances of objc_msgSend that
* were running when the memory was disconnected; any further calls to
* objc_msgSend will not see the garbage memory because the other data
* structures don't point to it anymore. The collecting_in_critical
* function checks the PC of all threads and returns FALSE when all threads
* are found to be outside objc_msgSend. This means any call to objc_msgSend
* that could have had access to the garbage has finished or moved past the
* cache lookup stage, so it is safe to free the memory.
*
* All functions that modify cache data or structures must acquire the
* cacheUpdateLock to prevent interference from concurrent modifications.
* The function that frees cache garbage must acquire the cacheUpdateLock
* and use collecting_in_critical() to flush out cache readers.
* The cacheUpdateLock is also used to protect the custom allocator used
* for large method cache blocks.
*
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
*
* Cache writers (hold cacheUpdateLock while reading or writing; not PC-checked)
* cache_fill (acquires lock)
* cache_expand (only called from cache_fill)
* cache_create (only called from cache_expand)
* bcopy (only called from instrumented cache_expand)
* flush_caches (acquires lock)
* cache_flush (only called from cache_fill and flush_caches)
* cache_collect_free (only called from cache_expand and cache_flush)
*
* UNPROTECTED cache readers (NOT thread-safe; used for debug info only)
* cache_print
* _class_printMethodCaches
* _class_printDuplicateCacheEntries
* _class_printMethodCacheStatistics
*
***********************************************************************/
留下一些问题?
为什么要在3/4处扩容?
因为此处缓存使用的是哈希这种数据结构,哈希中有一个叫做装载因子的概念,表示空位的大小,在3/4处扩容则说明装载因子是1/4,装载因子越大说明可能产生的冲突越多,这里取1/4应该是苹果评估的一个合理的数值。
方法缓存是有序的吗?
因为用了哈希,所以肯定无序,这里也稍微做了一些验证,简单说说吧,就不上图已进行说了,验证的对错也不太敢保证,只是自己的一些想法。其实在扩容的时候原来缓存是清除了的,开辟了新的缓存来保存,在objc4-756
中我记得是拷贝到新的缓存里,但是在objc4-779.1
中我发现原来调用的方法已经不再缓存内了,就是通过上面的lldb验证的,而且第一次扩容后的第一方法会存储在最后一个位置,而不是扩容后的第一个位置,验证了几次都是这样,然后也没仔细探究了。我的想法是:
- 扩容说明方法够多,如果都调用则需要这么多空间进行缓存
- 但是既然用到了扩容则说明有些方法没有频繁调用,则触发的缓存
- 扩容前缓存的方法再次被调用的概率不高了,所以就没有拷贝到新的缓存内,如果再次调用应该会存储到缓存内
总结
- cache_t就是缓存我们OC方法的,每调用一个OC方法他就会将该方法缓存;
- 缓存的开辟从4个开始,到了3/4就开始扩容2倍,直到65536
- 缓存内主要存储sel和imp
2.4 bits
bits
是一个class_data_bits_t
的结构体,在objc_class
源码中很多方法的返回值都是bits
中的例如:
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
#if FAST_HAS_DEFAULT_RR
bool hasCustomRR() const {
return !bits.getBit(FAST_HAS_DEFAULT_RR);
}
void setHasDefaultRR() {
bits.setBits(FAST_HAS_DEFAULT_RR);
}
void setHasCustomRR() {
bits.clearBits(FAST_HAS_DEFAULT_RR);
}
#else
bool hasCustomRR() const {
return !(bits.data()->flags & RW_HAS_DEFAULT_RR);
}
void setHasDefaultRR() {
bits.data()->setFlags(RW_HAS_DEFAULT_RR);
}
void setHasCustomRR() {
bits.data()->clearFlags(RW_HAS_DEFAULT_RR);
}
#endif
放眼望去,还是那个data()
最显眼,下面我们就来研究一下它。首先让我们来看看class_rw_t
的源码:里面除了flags
、version
、witness
这些,主要还有个ro
以及methods
、properties
、protocols
,这个ro
是一个class_ro_t
类型的结构体指针,其他看样子是个数组,methods
应该是存储方法的,properties
应该是存储属性的,protocols
应该是存储协议的。下面我们来进行验证。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t version;
uint16_t witness;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
void setFlags(uint32_t set)
{
__c11_atomic_fetch_or((_Atomic(uint32_t) *)&flags, set, __ATOMIC_RELAXED);
}
void clearFlags(uint32_t clear)
{
__c11_atomic_fetch_and((_Atomic(uint32_t) *)&flags, ~clear, __ATOMIC_RELAXED);
}
// set and clear must not overlap
void changeFlags(uint32_t set, uint32_t clear)
{
ASSERT((set & clear) == 0);
uint32_t oldf, newf;
do {
oldf = flags;
newf = (oldf | set) & ~clear;
} while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
}
};
工程还是当前的工程,分析方法依旧使用lldb
,断点打在获取pClass之后。首先说明一下,要想获取到class_data_bits_t
的首地址,就要先获取到类的首地址,然后向下偏移32个字节,为什么呢?因为类的 isa
指针占用8字节,superClass
占用8字节,cache
通过我们的分析占用16字节所以拿到首地址后加32就是我们的bits
的首地址。首先我们先打印一下data()
的内容
打印完我们看到了源码中的很多东西都打印出来了,迫不及待的我们赶紧打印一下methods
看看,看到打印出来的是个method_array_t
的类型,里面还有个list
我们不妨看看这个list
里面是否存储的就是我们的方法,打印list
后得到的是一个method_list_t
类型的指针,既然是list
指针,那么首地址应该是第一元素吧,我们通过p *
去打印,发现确实打印出了我们的sayHello
方法,我么继续打印,分别打印出了sayCode
、sayNB
、sayMaster
以及一些C++的方法,还有属性的setter和get方法,那么我们的方法原理是什么呢,为什么要把方法存储在这里呢,我们还不知道,在后面的探索中我们会继续研究这些。
下面我们看看属性 properties
,按照上述步骤打印,第一个就是我们的nickname
,后面就没有了,那么我们测成员变量hobby
去哪了呢?
暂时不探索protocols
通过上面的探索,感觉确实是这样存储的,但是没有找到类方法sayHappy
,我们的成员变量hobby
也没有出现在其中。这个时候突然想起,刚才我们在class_rw_t
中还发现了一个class_ro_t
类型的ro
,那么我们在探索一下这个ro
吧,首先看看class_ro_t
的源码吧:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
_objc_swiftMetadataInitializer swiftMetadataInitializer() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
return _swiftMetadataInitializer_NEVER_USE[0];
} else {
return nil;
}
}
method_list_t *baseMethods() const {
return baseMethodList;
}
class_ro_t *duplicate() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
size_t size = sizeof(*this) + sizeof(_swiftMetadataInitializer_NEVER_USE[0]);
class_ro_t *ro = (class_ro_t *)memdup(this, size);
ro->_swiftMetadataInitializer_NEVER_USE[0] = this->_swiftMetadataInitializer_NEVER_USE[0];
return ro;
} else {
size_t size = sizeof(*this);
class_ro_t *ro = (class_ro_t *)memdup(this, size);
return ro;
}
}
};
这个源码跟刚才的class_rw_t
有很多类似的地方,也有方法、属性和协议相关的东西,那么是不是这个ro
也存储了一些方法和属性相关的东西呢?下面我们继续通过lldb
去查看。
通过lldb
打印我们看见与其源码内的内容一样,下面我们来探索一下方法baseMethodList
通过上面的图片我们看到ro
内部存储的方法月rw
一样,也没有类方法,那么他为什么要存储两份呢?这个只能通过我们后续的探索进行考证了。那么类方法倒地存储在了哪里呢?其实山重水复疑无路,柳暗花明又一村啊,我们还有元类没探索呢,实例方法存储在类中,类方法是不是存储在元类中呢?
我们通过上面的方法lldb
去元类里面看看
1.首先打印类的地址
2.然后取出类的isa
,类的isa
指向元类
3.&上isa_mask
,就是元类
4.查看元类的bits
中的rw
和ro
果然我们在元类中找到了我们的sayHappy
方法,这回就可以尽情happy了。其实实例方法都是由对象调用的,类方法由类调用,实例方法存储在类中,类方法存储在元类中也就不难理解了。
这回我们就找到了实例方法和类方法的存储位置,下面继续在ro
里面探索一下属性,以及我们还未找到的成员变量hobby
。
通过查看存储在ro
里面的baseProperties
其内容跟rw
也是一样的,依旧没有我们的hobby
,这个和类方法的思路不太一样,成员变量也是类里面的,我们暂不考虑去元类里面找成员变量。这时候我们发现还有个ivars
可能存在我们想要的东西,下面我们查看一下ivars
:
结论:
果不其然,我们的成员变量hobby
就存在于这里,并且我们还发现了_nickname
,所以成员变量和属性自动生成的带下划线的成员变量都存储在ivars
里面。
至此我们的探索已经差不多了,下面我们通过代码来验证一下我们的上面的探索:
首先我们打印一下ivars
和properties
实现代码:
void testObjc_copyIvar_copyProperies(Class pClass){
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
//获取实例变量名
const char*cName = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:cName];
NSLog(@"class_copyIvarList:%@",ivarName);
}
free(ivars);
unsigned int pCount = 0;
objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
for (unsigned int i=0; i < pCount; i++) {
objc_property_t const property = properties[i];
//获取属性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
//获取属性值
NSLog(@"class_copyProperiesList:%@",propertyName);
}
free(properties);
}
方法调用:
Class pClass = [LGPerson class];
testObjc_copyIvar_copyProperies(pClass);
打印结果:
实例方法打印:
实现代码:
void testObjc_copyMethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
NSLog(@"Method, name: %@", key);
}
free(methods);
}
打印结果:
判断该类是否包含该实例方法:
实现代码:
void testInstanceMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
NSLog(@"%s",__func__);
}
打印结果:
这里我们用到了
LGPerson
的实例方法sayHello
和其类方法sayHappy
,在获取实例方法的时候,在类中获取到了实例方法sayHello
,在元类在获取到了sayHappy
,说明类方法也是以实例方法的形式存储在元类中。
下面我们在看看类中是否包含类方法:
实现代码:
void testClassMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
// 类方法形式
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
NSLog(@"%s",__func__);
}
打印结果:
由于sayHello
不是类方法,所前两个打印是0x0,但是后面的就有些出乎我们的意料了,在上面我们通过lldb
查看时,并没有在类中找到类方法,下载打印是居然是有的,那么这到底是为什么呢?我们查看了class_getClassMethod
的源码
源码:
/***********************************************************************
* class_getClassMethod. Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}
源码一看,一目了然,获取类方法的实质就是去元类里面查找元类的实例方法,上面我们也提到了,类方法本来就是已实例方法的形式存储在元类中,并且在获取元类的时候做了判断,如果是元类就直接返回Class
,如果不是就返回类的isa
,其实类的isa
就是元类。所以为什么打印结果是刚才的样子也就清楚了。
全篇总结
Objective-C
类有四个属性
-
isa
指向元类; -
superClass
指向父类; -
cache
缓存调用过的方法,并通过哈希这种数据结构进行扩容,从4到65536,其中的mask
作为一个掩码,用作哈希计算时的盐,避免哈希冲突,一直是减一的状态,所以一直不会满,保证哈希安全,也用作记录缓存大小,mask一直是缓存大小减1,所以获取到mask
加上1就是缓存的大小。occupied
作为开辟新空间(新缓存方法)的计数,以及判断是否到了临界点3/4处需要扩容的重要条件; -
bits
其中有rw
存储了类的实例方法(methods)和属性(properties),rw
中有个ro
存储了类的实例方法(baseMethodList)、属性(baseProperties)和成员变量(ivars); - 类的类方法以实例方法的形式存储在元类中。