Welcome to the iOS Exploration series.
- IOS explores the alloc process
- IOS explores memory alignment &malloc source code
- IOS explores ISA initialization & pointing analysis
- IOS exploration class structure analysis
- IOS explores cache_T analysis
- IOS explores the nature of methods and the method finding process
- IOS explores dynamic method resolution and message forwarding mechanisms
- IOS explores the dyLD loading process briefly
- The loading process of iOS explore classes
- IOS explores the loading process of classification and class extension
- IOS explores isa interview question analysis
- IOS Explore Runtime interview question analysis
- IOS explores KVC principles and customization
- IOS explores KVO principles and customizations
- IOS explores the principle of multithreading
- IOS explores multi-threaded GCD applications
- IOS explores multithreaded GCD underlying analysis
- IOS explores NSOperation for multithreading
- IOS explores multi-threaded interview question analysis
- IOS Explore the locks in iOS
- IOS explores the full range of blocks to read
Writing in the front
The class structure was covered in detail in the last article, but cache_t cache remains uncovered. This article will examine Cache_t from the source level
Cache_t
1. Cache_t structure
Here is the underlying structure of the class
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() {
returnbits.data(); }... }Copy the code
Cache_t has the following structure
struct cache_t {
struct bucket_t* _buckets;
mask_t _mask;
mask_t_occupied; . };Copy the code
As mentioned in previous articles, we can see from the structure of cache_t that it consists of two uint32_t struct Pointers of _mask and _occupied and 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__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
public:
inline cache_key_t key(a) const { return _key; }
inline IMP imp(a) const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
Copy the code
From bucket_t’s properties and methods, you can see that it is associated with IMP — bucket_t is actually a bucket that holds the IMP method implementation and its key
In cache_T, _buckets, _mask, and _occupied are represented by their names: “occupied”; “occupied”; “occupied”; “occupied”
2. LLDB debugging
Prepare the code in objC source
#import <objc/runtime.h>
@interface FXPerson : NSObject
- (void)doFirst;
- (void)doSecond;
- (void)doThird;
@end
@implementation FXPerson
- (void)doFirst {}
- (void)doSecond {}
- (void)doThird {}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *p = [[FXPerson alloc] init];
Class cls = object_getClass(p);
[p doFirst];
[p doSecond];
[p doThird];
}
return 0;
}
Copy the code
_buckets is an imp method barrels, that we are in a method call a breakpoint (article said that class isa pointer of 8 bytes, superclass pointer of 8 bytes, as long as to get the class first address + 16 bytes can get cache_t address)
_mask is 3, _occupied is 1, and we continue to print _buckets
Print multiple $3’s and only find one [NSObject init] cached
[p doSecond]; One line (I re run the project here)
[p doThird]; Line, get the following data:
breakpoint | _occupied | The _buckets contains methods |
---|---|---|
[p doFirst] | 1 | -[NSObject init] |
[p doSecond] | 2 | -[NSObject init], -[FXPerson doFirst] |
[p doThird] | 3 | -[FXPerson doFirst], -[FXPerson doSecond] |
According to the above data, _buckets is the bucket with methods; _occupied is the number of methods in the bucket
Wait, someone here must be wondering, how come FXPerson is calling the alloc method and there is no cache – as mentioned in the last article, the alloc method is a class method and exists in the FXPerson metaclass
Just when you think everything is going well, something happens — break point goes to the next line
_mask and _occupied have both changed dramatically, so what’s going on at the bottom? Why was bucket[0] empty when it was printed earlier?
Dive into cache_t
Find an entry point
Given that the value of _mask is increased, we find mask_t mask() in cache_t, which returns only _mask itself
mask_t cache_t::mask()
{
return _mask;
}
Copy the code
We continue to search for the mask() method, and find that there is a corresponding operation of mask in the Capacity method, but the purpose of operation is not very clear
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
Copy the code
Continuing the search for the Capacity () method, you see a meaningful call to the Capacity method in the expand method
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t) (mask_t)newCapacity ! = newCapacity) {// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
Copy the code
Expand method should be a capacity expansion method, moving up to cache_FILL_NOLock
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if(! cls->isInitialized())return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.cache->reallocate(capacity, capacity ? : INIT_CACHE_SIZE); }else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
// 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.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
Copy the code
Adding a breakpoint in the function call stack verifies that we are looking in the right direction
1.cache_fill_nolock
The cache_FILL_NOLock method is complex, and I’ll take a step-by-step look at it here
1) if (! cls->isInitialized()) return;
Class whether to initialize the object, no return
(2) if (cache_getImp (CLS, sel)) return;
CLS and SEL are passed in, and imp is returned if found in the cache, not the next step
(3) cache_t * cache = getCache (CLS);
Call getCache to get the CLS cache object
(4) cache_key_t key = getKey (sel);
Get the cached key with getKey — forcing SEL to be cache_KEY_t
⑤mask_t new_occupied = cache->occupied();
NewOccupied; newOccupied; occupied; newOccupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied; occupied
6 mask_t capacity = cache – > capacity ();
Read the current cache capacity
⑥ Check cache usage
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.cache->reallocate(capacity, capacity ? : INIT_CACHE_SIZE); }else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
}
else {
// Cache is too full. Expand it.
cache->expand();
}
Copy the code
- If the cache is empty, reapply memory and overwrite the previous cache
- If the new cache takes up size
< =
Three quarters of the cache capacity, the cache process can be carried out - If the cache is not empty and the cache usage exceeds three-fourths of its capacity, expand the cache capacity
⑦bucket_t *bucket = cache->find(key, receiver);
Bucket_t is found in the cache using the key
⑧if (bucket->key() == 0) incrementOccupied();
If the key of the bucket is 0, it is _occupied++
Pet-name ruby bucket – > set (key, imp);
Put keys and IMPs into buckets in pairs
Conclusion:
Cache_fill_nolock finds the class’s cache cache first and overwrites it if it is empty. If the occupied part is larger than three quarters of the cache capacity, expand the occupied part and then load the occupied part into the bucket with the corresponding key value. Otherwise, the bucket is directly loaded into the bucket with the corresponding key value
After analyzing the cache_FILL_NOLock main flow, you can extend it in a few ways
2.cache_t::reallocate
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
bool freeOld = canBeFreed();
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_collect(false); }}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.
// fixme instead put the end mark inline when +1 is malloc-inefficient
bucket_t *newBuckets = (bucket_t *)
calloc(cache_t::bytesForCapacity(newCapacity), 1);
bucket_t *end = cache_t::endMarker(newBuckets, newCapacity);
#if __arm__
// End marker's key is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->setKey((cache_key_t) (uintptr_t)1);
end->setImp((IMP)(newBuckets - 1));
#else
// End marker's key is 1 and imp points to the first bucket.
end->setKey((cache_key_t) (uintptr_t)1);
end->setImp((IMP)newBuckets);
#endif
if (PrintCaches) recordNewCache(newCapacity);
return newBuckets;
}
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
// objc_msgSend uses mask and buckets with no locks.
// It is safe for objc_msgSend to see new buckets but old mask.
// (It will get a cache miss but not overrun the buckets' bounds).
// It is unsafe for objc_msgSend to see old buckets and new mask.
// Therefore we write new buckets, wait a lot, then write new mask.
// objc_msgSend reads mask first, then buckets.
// ensure other threads see buckets contents before buckets pointer
mega_barrier();
_buckets = newBuckets;
// ensure other threads see new buckets before new mask
mega_barrier();
_mask = newMask;
_occupied = 0;
}
Copy the code
- First determine whether it can be released (cache is empty take negative value) and save
oldBuckets
Get the currentbucket
- Pass in a new cache capacity
allocateBuckets
Initialize thebucket_t
And stored in thenewBuckets
setBucketsAndMask
Actions done: With the newly createdbucket
Save,mask=newcapcity-1
.occupied
Zero (because there is no method cache yet)- If the cache is not empty (needs to be freed), the original is freed
bucket
,capacity
Why use cache_Collect_free to erase memories rather than read/write, memory copy? First, it is not safe to read and write again. Second, erasure speed is fast
3.cache_t::expand
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t) (mask_t)newCapacity ! = newCapacity) {// mask overflow - can't grow further
// fixme this wastes one bit of mask
newCapacity = oldCapacity;
}
reallocate(oldCapacity, newCapacity);
}
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
Copy the code
oldCapacity
The value ofmask+1
- in
oldCapacity
In the presence of,newCapacity
takeoldCapacity
Twice; Otherwise takeINIT_CACHE_SIZE
- Here,
INIT_CACHE_SIZE
forBinary 100
= >Decimal four
- Create and overwrite the original cache
reallocate
4.cache_t::find
Cache_t ::find: Searches for the corresponding bucket
bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k ! = 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t begin = cache_hash(k, m); mask_t i = begin;do {
if (b[i].key() == 0 || b[i].key() == k) {
return&b[i]; }}while((i = cache_next(i, m)) ! = begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); }Copy the code
- through
buckets()
Method to get the currentcache_t
Run all cache bucketsbucket
- through
mask()
Method to get the currentcache_t
The value of the cache capacity minus onemask_t
key & mask
Calculates the starting indexbegin
Assigned toi
, used to switch indexes- in
do-while
I’m going to loop through the whole thingbucket_t
If thekey = 0
, indicating that no method has been cached at index Ibucket_t
, used to abort cached queries; If I take it outbucket_t
thekey = k
“Is displayed, the query is successfulbucket_t
- through
cache_next
returni-1
Update the index to query each element in the hash table (equivalent to going in a loop) - If it can’t find a proof of a cache problem, return
bad_cache
5. The LRU algorithm
LRU algorithm is the Least Recently Used strategy — the core idea of this strategy is to first eliminate the Least Recently Used content, and this algorithm is also Used in the method cache
- Before capacity expansion, the instance method chooses a seat
- After expansion, the new instance method finds the least recently used location and sits down and clears the previous bucket
Cache_t query point
1. The role of the mask
mask
As acache_t
Property exists, which represents the value of the cache capacity minus onemask
forbucket
Is mainly used in the cache lookup of the hash algorithm
2. The change of capacity
Capacity changes mainly when the cache->expand() is expanded. When the cache is three quarters full, the capacity is doubled to avoid hash conflicts
3. Why is the expansion done at 3/4
In data structures such as hashes, there is a concept used to indicate how many empty Spaces there are called the load factor – the larger the load factor, the fewer free Spaces there are, the more conflicts there are, and the performance of the hash table deteriorates
When the load factor is 3/4, the space utilization is relatively high, and considerable Hash conflicts are avoided, which improves the space efficiency
Why is the default load factor for reading HashMap 0.75?
4. Whether the method cache is in order
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
Copy the code
The method cache is unordered because the cache subscript is computed using a hash algorithm — the subscript value depends on the key and mask values
5. Relationship between bucket and Mask, capacity, SEL, and IMP
- class
cls
Have attributescache_t
.cache_t
In thebuckets
There are multiplebucket
— Stores method implementationsimp
And method numbersel
The key value of the strong conversioncache_key_t
mask
forbucket
Is mainly used in the cache lookup of the hash algorithmcapacity
You can getcache_t
In thebucket
The number of
The main purpose of caching is to allow the compiler to execute the logic of sending messages faster through a series of policies
Write in the back
There’s not much to read about cache_t, but it’s pretty convoluted. Read the source code for a better understanding. The next article will look at objc_msgsend, which, as a cache_fill_NOLock predisposition method, will be programmatically helpful in understanding cache_t