preface
This article will examine cache_t in a class structure.
The structure of cache_t
Cache_t source code.
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED//macOS, emulator
// explicit_atomic displays atomicity. The purpose is to ensure the safety of threads when adding, deleting, or checking
Struct bucket_t * _buckets;
// buckets put sel IMP
// buckets()
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 / / 64 - bit machine
explicit_atomic<uintptr_t> _maskAndBuckets;// Write together for optimization purposes
mask_t _mask_unused;
// The following are masks, i.e., masks -- similar to isa masks, i.e., bitfields
//.... is omitted
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 // Non-64-bit true
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
// The following are masks, i.e., masks -- similar to isa masks, i.e., bitfields
//.... is omitted
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
// the method is omitted.....
}
Copy the code
- As can be seen from the source code, there are different processing in macOS, 64-bit true and non-64-bit true environments.
CACHE_MASK_STORAGE_OUTLINED
MacOS, emulatorsCACHE_MASK_STORAGE_HIGH_16
A 64 – bit machineCACHE_MASK_STORAGE_LOW_4
Non – 64-bit true machineexplicit_atomic
Display atomicity for thread safety.- In the real environment, mask and bucket are written together for optimization purposes.
Bucket_t source
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
// Compute the ptrauth signing modifier from &_imp, newSel, and cls.
uintptr_t modifierForSEL(SEL newSel, Class cls) const {
return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls;
}
Copy the code
bucket_t
Divided into two versions,A:
和The real machine
The difference is thatsel
和imp
In different order.- From the above source code, it is also clear that
cache_t
The cacheimp
andsel
, stored inbucket_t
In the.
Get _buckets
- Now that we know
imp
andsel
Stored in the_buckets
, so how to obtain? - Define a Person class that inherits NSobject.
@interface Person : NSObject{
NSString *hobby;
}
@property (nonatomic.copy) NSString *name;
- (void)eat;
- (void)say;
+ (void)run;
@end
@implementation Person
- (void)eat{
NSLog(@"eat something"); } - (void)say{
NSLog(@"say hi");
}
+ (void)run{
NSLog(@"run run");
}
@end
Copy the code
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
Class pClass = object_getClass(person);
[person eat];
[person say];
[person say];
}
return 0;
}
Copy the code
- in
[person eat]
Before the interruption point, through the LLDB debugging tool, view the memory structure of the person class, LLDB executionp/x pClass
Gets the first address.
(lldb) p/x pClass
(Class) $0 = 0x00000001000033a8 Person
Copy the code
- In the previous article, we learned about the memory layout of the class
isa
,superclass
,cache_t
,class_data_bits_t
Arranged sequentially, whereisa
与superclass
Respectively take up8
Two bytes, socache_t
The position of should be offset from the first address of the class16 bytes
So let’s take0x00000001000033a8
The offset16
Byte, i.e.,0x00000001000033b8
The address. - perform
p (cache_t *)0x00000001000033b8
To obtaincache_t
The first address of.
(lldb) p (cache_t *)0x00000001000033b8
(cache_t *) $1 = 0x00000001000033b8
Copy the code
- perform
p *$1
, print the cache
(lldb) p *$1
(cache_t) $2 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x000000010032f410 {
_sel = {
std::__1::atomic<objc_selector *> = (null)
}
_imp = {
std::__1::atomic<unsigned long> = 0
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 0
}
_flags = 32804
_occupied = 0
}
(lldb)
Copy the code
- No method is called at this point,
_sel = null
._imp = 0
._occupied = 0
. - Let’s make a break point at
[person say]
In the above[person eat]
To perform the next stepstep over
. - Continue to perform
p *$1
.
(lldb) p *$1
(cache_t) $3 = {
_buckets = {
std::__1::atomic<bucket_t *> = 0x0000000100694ab0 {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 10424
}
}
}
_mask = {
std::__1::atomic<unsigned int> = 3
}
_flags = 32804
_occupied = 1
}
Copy the code
- At this time,
_sel
and_imp
Has the value,_occupied = 1
. But how_buckets
“And lookedcache_t
Source code, finally have a new discovery.
- So it was very pleasant to execute
buckets()
To obtain,p $3.buckets()
.
(lldb) p $3.buckets()
(bucket_t *) $4 = 0x0000000100694ab0
Copy the code
- perform
p *$4
print_buckets
Information.
(lldb) p *$4
(bucket_t) $5 = {
_sel = {
std::__1::atomic<objc_selector *> = ""
}
_imp = {
std::__1::atomic<unsigned long> = 10424}}Copy the code
- So how do you get it
_sel
and_imp
Yeah, that’s why I read it_buckets
Source code, seen in the source code, get_sel
and_imp
Methods.
- perform
p $4.sel()
To obtainsel
(lldb) p $5.sel()
(SEL) $6 = "eat"
Copy the code
- perform
p $5.imp(pClass)
To obtainimp
(lldb) p $5.imp(pClass)
(IMP) $7 = 0x0000000100001b10 (KCObjc`-[Person eat])
Copy the code
Get the _BUCKETS summary
- 1.
cache
To obtain, need to passpClass
The first address of the translation16
Byte, i.e.,The first address + 0 x10
To obtainThe address of the cache
. - 2,
cache_t
Provided in the structurebuckets()
To obtain_buckets
. - 3,
_buckets
In the structure, throughsel()
andimp(pClass)
Respectively to obtainsel
andimp
. - 4. There is no cache when the method is not called. After the method is called, there is a cache in the cache, which is cached after the method is called
cache
In the.
The structure diagram of cache_t
How to store cache?
- in
cache_t
The incrementOccupied() function that causes changes is found in the source code.
void cache_t::incrementOccupied()
{
_occupied++; / / occupied since
}
Copy the code
- Global search
incrementOccupied()
Function found to be called only in the INSERT method of cache_t
- Global search
insert(
Function, foundcache_fill
To fit the call
- Global search
cache_fill
After the message is sent, the IMP is retrieved and then calledcache_fill
Method writes to the cache, i.eobjc_msgSend->cache_getImp->cache_fill
Insert source code analysis
- Insert (), whose source code is implemented as follows
cache->insert
I’ve done the function roughly3
thing
1. Initialize the cache space
2. Determine whether the capacity needs to be expanded. If yes, expand the capacity by twice the original capacity, reallocate the space, and release the existing cache information
3. Insert the cache according to whether there is already a cache for this method in the hash table
1. Initialize the cache space
- if
occupied()
To 0, andbuckets
If no cached content is displayed in4
The storage space size isDefault initial value
. 4if (! capacity) capacity = INIT_CACHE_SIZE
)
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
// 1 << 2 = 4
Copy the code
- call
reallocate(oldCapacity, capacity, /* freeOld */false);
Allocate space.
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
// Create space
bucket_t *newBuckets = allocateBuckets(newCapacity);
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity- 1) == newCapacity- 1);
setBucketsAndMask(newBuckets, newCapacity - 1);
// Release old cached information
if(freeOld) { cache_collect_free(oldBuckets, oldCapacity); }}Copy the code
- call
allocateBuckets
bucket_t *allocateBuckets(mask_t newCapacity)
{
// Allocate one extra bucket to mark the end of the list.
// This can't overflow mask_t because newCapacity is a power of 2.
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);
bucket_t *end = cache_t::endMarker(newBuckets, newCapacity);
#if __arm__
// End marker's sel is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->set<NotAtomic, Raw>((SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
// End marker's sel is 1 and imp points to the first bucket.
end->set<NotAtomic, Raw>((SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
Copy the code
In the reallocate function, calloc of the allocateBuckets function applies for space of newCapacity to the system. SetBucketsAndMask is used to setBucketsAndMask, where mask is updated to the total capacity of the newly applied space -1 (capacity-1).
2. Determine whether there is enough space
- If the space is insufficient, expand the space to twice the original size and reallocate the space
Release the stored cache and insert a new cache.
// Under ARM64, if newOccupied <= 3/4 of the capacity, the storage space is sufficient and no additional processing is required
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
// Cache is less than 3/4 full. Use it as-is.
}
// If more than 3/4
else {
// Double the size of the space
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE; // The maximum value cannot exceed 1<< 16
}
reallocate(oldCapacity, capacity, true); // Reallocate space to store new data, erase existing cache
}
Copy the code
3. Insert cache
/** 3; dip; dip; dip; dip; dip; dip; If a hash conflict is encountered, cache_t looks for the next one until it returns to BEGIN and looks for all */
// Get the hash table
bucket_t *b = buckets();
// Get the hash table size -1
mask_t m = capacity - 1;
// Use the cache_hash function [begin = sel & m] to calculate the index corresponding to the key value k
// begin records the query start index
mask_t begin = cache_hash(sel, m);
// begin assigns the value to I to switch indexes
mask_t i = begin;
do {
if (fastpath(b[i].sel() == 0)) { // If no way to cache is found
incrementOccupied(); // _occupied ++;
b[i].set<Atomic, Encoded>(sel, imp, cls); // Cache instance methods
return;
}
if (b[i].sel() == sel) { // If you find a method that needs caching, do nothing and exit the loop
return; }}while(fastpath((i = cache_next(i, m)) ! = begin));// When a hash collision occurs, cache_t looks for the next one until it returns to begin
cache_t::bad_cache(receiver, (SEL)sel, cls);
Copy the code
Begin, as the initial query subscript of the hash table, is calculated from SEL & Mask
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
Copy the code
do … The condition for the while loop is (I = cache_next(I, m)! = begin, which is not equal to the initial subscript value. Begin is used to iterate over all data in the hash table, while cache_next() is used for a second hash to resolve hash conflicts.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
Copy the code