Welcome to the iOS Basics series (suggested in order)
IOS low-level – Alloc and init explore
IOS Bottom – Isa for everything
IOS low-level – Analysis of the nature of classes
IOS Underlying – cache_t Process analysis
IOS Low-level – Method lookup process analysis
IOS bottom layer – Analysis of message forwarding process
IOS Low-level – How does Dyld load app
IOS low-level – class load analysis
IOS low-level – Load analysis of categories
In this paper,
This article aims to understand the process leading up to method lookups through source code analysis of cache_T related issues, such as caching policies, dynamic scaling, and so on. Because of its close association with the objc_msgSend process, and because sending messages is the essence of the iOS method, it’s important to understand Cache_t.
Cache_t que
1. Cache_t structure
We took a look at the cache_t structure in the last iOS Underlying class Nature analysis. The underlying structure is as follows:
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; class_data_bits_t bits; . }Copy the code
Cache_t is in objc_class
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}
Copy the code
Its own structure consists of three properties:
buckets
It’s a method bucket that holds the method implementationimp
, numbered according to the methodsel
The generatedkey
mask
Is the expansion factor, and at the same timekey
Calculate together to findbucket_t
Hash index ofoccupied
Is the capacity currently occupied
2. LLDB debugging
Create a new CJPerson class in the objC source code, customize four methods, and call them externally
CJPerson * person = [[CJPerson alloc]init];
Class cls1 = object_getClass(person);
[person firstAction];
Class cls2 = object_getClass(person);
[person secondAction];
Class cls3 = object_getClass(person);
[person thirdAction];
Class cls4 = object_getClass(person);
[person fourthAction];
Class cls5 = object_getClass(person);
Copy the code
Output the following CLS before each method call, in cls1’s case, at the break point at [Person firstAction] x/4gx
printcls1
The first four bits of the memory address becausecache_t
The first two propertiesisa
andsuperclass
A total of 16 bytes, socache_t
The address ofisa
Is offset 16 bytes, that is, the address marked in the figure, strong output this addresscls1
thecache_t
And so on, output the restcls
Class object | _mask | _occupied |
---|---|---|
cls1 | 3 | 1 |
cls2 | 3 | 2 |
cls3 | 3 | 3 |
cls4 | 7 | 1 |
cls5 | 7 | 2 |
The final results of CLS are as follows: Occupied +1 each time; however, the final results of CLS4 are not consistent with this rule, and the mask value has also changed. It seems that only look at the print result is no way to see who is at fault, or to take a look at the source code.
Cache_t source
1. Find an entry point
Although starting with the source code is a good idea, but so much source code, where to look from. Think about it, but let’s do some researchcache_t
.There are 28 related results, and one by one that’s a lot of searching. Think about it, sincecache_t
Is a structure, then the system should initialize it, then operate on it, then try to searchcache_t *
.With only four relevant results, the pressure was instantly reduced, and there was no problem looking at each one.
Article 1. The first
cache_t *getCache(Class cls)
{
assert(cls);
return &cls->cache;
}
Copy the code
This is just a get method. Exclude
Article 2. The second
void cache_t::bad_cache(id receiver, SEL sel, Class isa) { // Log in separate steps in case the logging itself causes a crash. _objc_inform_now_and_on_crash ("Method cache corrupted. This may be a message to an " "invalid object, or a memory error somewhere else."); cache_t *cache = &isa->cache; _objc_inform_now_and_on_crash ("%s %p, SEL %p, isa %p, cache %p, buckets %p, " "mask 0x%x, occupied 0x%x", receiver ? "receiver" : "unused", receiver, sel, isa, cache, cache->_buckets, cache->_mask, cache->_occupied); . }Copy the code
This is in the bad_cache method, and judging from the name and implementation, it is not hard to tell that this is a warning and crash message when the cache is bad
Article 3. The third
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { cacheUpdateLock.assertLocked(); if (! cls->isInitialized()) return; if (cache_getImp(cls, sel)) return; cache_t *cache = getCache(cls); . }Copy the code
This is in the cache_fill_NOLock method, the name of which stands for cache fill, which feels like it, but is not certain
Article 4. The fourth
void cache_erase_nolock(Class cls) { cacheUpdateLock.assertLocked(); cache_t *cache = getCache(cls); . }Copy the code
This is in the cache_erase_nolock method, whose name stands for cache erase
In summary, the third option is the most likely, but you need to verify that the result is executed after the [person firstAction] call is broken at the point within each method
The breakpoint tocache_fill_nolock
In, you can verify that the method is executed for caching when the method is called.
2. Cache_fill_nolock parsing
Having confirmed that this is the way to perform caching on method invocation, it is necessary to take a closer look at its implementation.
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { cacheUpdateLock.assertLocked(); 1) if (! cls->isInitialized()) return; ② if (cache_getImp(CLS, sel)) return; ③ cache_t *cache = getCache(CLS); ④ cache_key_t key = getKey(sel); ⑤ mask_t new_occupied = cache->occupied(); ⑥ mask_t capacity = cache->capacity(); ⑦ If (cache->isConstantEmptyCache()) {⑧ Cache ->reallocate(capacity, capacity? : INIT_CACHE_SIZE); ⑨} else if (newOccupied <= capacity / 4 * 3) {⑩} else {cache->expand(); Bucket = cache->find(key, receiver); Infinitely (bucket->key() == 0) incrementOccupied(); ⑬ bucket - > set (key, imp); ⑭}Copy the code
(1) Cache lock (2) Determine whether the class is initialized, if not, directly return (3) determine whether SEL has been cached by CLS. If it has been cached, there is no need to go through the following cache process. Direct return (4) through the CLS for cache_t memory address (5) by strong sel metempsychosis into key 6 to obtain the value of the occupancy of the current occupied + 1 in the cache, ⑨ If the bucket capacity is 0 and the bucket capacity is empty, the bucket capacity is 0 and the bucket capacity is empty. However, if the new capacity of Buckets and masks is less than or equal to three quarters of the total capacity, or if the new capacity is more than three quarters of the total capacity, the next step is to expand the capacity by using key as a hash subscript to find the corresponding bucket passing by. What do you want to do with a OCCUPIED cache? If we do not find a occupied cache, we need to cache the occupied keys and IMPsCopy the code
Conclusion:
Cache_fill_nolock Obtains the cache of a class. If the cache is empty, a cache is created. If the cache already exists, the capacity is expanded if the capacity is greater than three quarters of its capacity. Once this is done, the key and IMP are placed in the bucket, and the bucket is placed in the cache.
Understand the main process of cache, in the inside of the more important methods, such as reallocate and expand to do further analysis
a.reallocate(mask_t oldCapacity, mask_t newCapacity)
INIT_CACHE_SIZE definition
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
Copy the code
implementation
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { ... setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); }}Copy the code
As you can see, when capacity is empty on the first call, 4 is passed, setBucketsAndMask is added, _BUCKETS is regenerated, and the old bucket oldBuckets is discarded to empty the old cache
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
mega_barrier();
_buckets = newBuckets;
mega_barrier();
_mask = newMask;
_occupied = 0;
}
Copy the code
After processing, _mask = 4-1 = 3, _occupied = 0. Mask = newCapacity -1, newCapacity = 2^n(minimum initialization is 4, n is capacity expansion times +2), so mask = 2^ n-1, LLDB debugging is worth to verify
b.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) { newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); }Copy the code
Obtain oldCapacity.
mask_t cache_t::capacity()
{
return mask() ? mask()+1 : 0;
}
Copy the code
OldCapacity = 4, newCapacity = oldCapacity * 2 = 8, and realLocate is added to realLocate to allocate buckets and masks
c.find(cache_key_t k, id receiver)
bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k ! = 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t i = begin; do { if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) ! = begin); Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); }Copy the code
Use the cache_hash function [begin = k & m] to calculate the index value begin corresponding to the key value K, which is used to record the start index. The value of BEGIN is assigned to I, which is used to switch the index. If the key of bucket_t is k, the query succeeds and the bucket_t is returned. If key = 0, no method has been cached at index I, and bucket_t is returned to abort the cached query. If bucket_t is not found, or bucket_t is empty, then the loop ends, indicating that the search failed. Call the bad_cache method to throw an exception.
Cache_t partial problem summary
1. Does class cache_t cache any methods?
Of the classcache_t
Only object methods are cached; class methods are cached in metaclasses
2. Why does method number SEL need to be strongly converted to key?
The method number SEL is a string and is not passed as efficiently underneath as a key of type long
3. What does mask do?
mask
As one of the parameters to judge expansion, it can be called expansion factor. At the same timemask
andkey
Co-generated hash table used for lookupbucket_t
. Here,mask
andisa_mask
The functions of masks are different and need to be distinguished.
4. Is method cache in order?
Method cachedbucket_t
It’s based on the hash table, where you find an empty seat and you sit down and occupy it, there’s no order
5. Why is the expansion 3/4?
HashMap
When the load factor of is equal to 0.75, the space utilization rate is relatively high, and considerable Hash conflicts are avoided, which makes the height of the underlying linked list or red-black tree relatively low, and improves the space efficiency.
6. The relationship between bucket and mask, capacity, SEL and IMP
bucket
Within theimp
andsel
The generatedkey
bucket
Is through thekey
andmask
In search ofbucket
The amount depends oncapacity
7. Why do I need to discard all caches after capacity expansion?
In a sense, the cache is discarded after expansion, and the next call to recache makes the cache meaningless. In fact, there are many methods in the general class, if the expansion, but also need to save the original method, newbucket_t
Need to get from the oldbucket_t
To get all the methods and put them in one by one, which is itself a time-consuming activity. The purpose of method caching is to make the message sending process faster, and Apple also makes method lookup go through the assembly process for faster, all for the sake of one word — fast. Discarding the original cache after capacity expansion was designed for this purpose, and this design, in a sense, fitsLRU
Caching policies, recently cached methods also have higher utilization, and older cached methods have lower utilization.
Write in the last
That’s part of my understanding of cache_t, which is an entry point for sending messages. The next chapter, objc_msgSend, gives you a broader understanding of Cache_t. Stay tuned.