With an understanding of the basic structure of classes, this article begins to explore message sending, or message invocation, in iOS. The first thing to discuss is that before the actual message is called, we look for the actual function address in the method cache. IOS provides caching mechanisms to improve efficiency.

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.

First, the relevant structure of the method

1.1 Review the Class structure

No matter what we’re talking about, we’re going to go back to the basic Class structure, but this time we’re going to focus on methods.

1.1.1 the Class structure

This is the end result of using a class at runtime, but at compile time, as I mentioned in a previous article, the class structure is slightly different, mainly because bits refer to class_ro_t instead of class_rw_t.

1.1.2 class_ro_tstructure

The result above represents the methods defined in the class, which we hardcoded in code rather than adding by:

  • Classification method;
  • Methods added by run-time;

At runtime, the program reorganizes the bits to get bits.data(), the class_rw_t structure, which is the run-time structure.

1.1.3 class_rw_t

class_rw_tAfter the program starts running, the classification methods are loaded and reorganized into the following structure:2 d array.

1.2 method_tMethods the structure

Based on the class_rw_t structure above, we can clearly observe that the underlying method structure is method_t, i.e., one method corresponds to one method_t. Here is the composition of the method_t structure.

The member variables for method_t are stated clearly above.

  • This structure contains Pointers to functions that point to concrete implementations.
  • throughtypesTo declare the return value and parameters of the method for validation of the underlying invocation implementation.
  • SELIs the method name.

So we want to call a function, also need to solve two problems: the first problem: how to find the function implementation address IMP according to SEL. Second problem: method declaration validation.

1.2.1 Type Encoding

Let’s first discuss the second problem, method declaration verification, which is implemented by assigning a code to the method. The corresponding code is as follows:

Method caching

We continue our discussion of the first problem above – how to find the function implementation address IMP according to SEL. In the absence of caching, find the class structure that ISA points to and iterate over the list of methods in class_rw_t method_array_t. What about when you have a cache?

2.1 Snooping method cache

We’re going to peek into the cache structure, starting with the source code. Here is the sequence diagram of the source code:

After analyzing the source code, we have the following results:

2.1.1 Method cache structure

Among thembucket_t *_bucketsThis is a structure that holds a cache list, which is essentially a hash table. And what’s in the hash table isbucket_tThe structure of.

2.1.2 validation

The verification code is in01 Method cache exploration.

2.2 a hash table

Through code exploration, we have sorted out the most important node processing in the hash table below.

2.2.1 Hash table processing

Here are a few things to note:

(1) Hash function

Mask is -1, so the hash space must not exceed the size of the cache space.

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
Copy the code

#### (2) Collision handling

Collision handling is platform-specific. In iOS, the following processing is done, which is simply open addressing for collision handling:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i- 1 : mask;
}
Copy the code

(3) Cache expansion

Cache space changes dynamically, as follows:

void cache_t::expand(a)
{
    uint32_t oldCapacity = capacity();
    // If the old capacity is 0, allocate 4 (INIT_CACHE_SIZE)
    // The old capacity is not 0. Allocate twice the current 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

(4) Pay attention

  • maskIs the cache space size -1, so the hash must not exceed the cache space size.

2.2.2 Read/write Cache

(1) Cache method

Let’s look at how the method cache hash table holds the method.

  1. Pass key (**@selector(method) **) via hash —cache_hashTo obtain the indexindex;
  2. Check whether the current INDEX is occupied
    • If it is occupied, i.e., this time in a hash conflict, redo the addressing — cache_next, evaluate the index, and return to 2.
  3. If not, store it inindexPlace.

(2) Lookup cache

So how do you find the cache?

  1. Pass key (**@selector(method) **) via hash —cache_hashTo obtain the indexindex;
  2. According to theindexTo obtainbucket_t, check thebucket._keyWhether it is the same as the key passed in.
    • If they are inconsistent, that is, there is a hash conflict when storing methods, hash again —cache_next, calculate index, go back to 2.
  3. If the key consistently passes, the key is obtainedbucket._impReturn;
  4. callbucket._impPhi is a function of phi.

Third, summary

3.1 Method Caching

  1. Each class object holds a cache — a list of method caches;
  2. A cache is essentially a hash table whose hash function is f(@selector()) = @selector() &_mask;
  3. The subclass does not implement methods that call the parent’s methods and add the parent’s methods to the subclass’s own cache.

3.2 Method Invocation

After the above discussion, we have a rough idea of how to call a method.

Of course, this is not the complete process of sending a message, and the next article will start exploring the complete process of calling a method.

reference

link

  1. Type Encodings
  2. Apple souce objc4

The sample code

  1. 01 Method cache exploration