Objective-C Runtime — Methods and Messages
Basic Data Types SEL , also called a selector, is a pointer representing a method's selector, defined as: The detailed definition of the struct is not found in the header. A method's selector is used to represent the name of a method at runtime. At compile time, Objective-C generates a unique integer identifier (an -typed address) based on each method's name and parameter sequence — that identifier is the . For example: Output: Regardless of whether two classes have a parent-child relationship or no relationship at all, if their methods share the same name, the is identical. Every method corresponds to exactly one . Therefore, within the same Objective-C class hierarchy, you cannot have two methods with the same name — even if their parameter types differ. The same method name can only map to one . This makes Objective-C particularly weak at handling methods with the same name, same parameter count, but different parameter types. For example, defining the following two methods in a class: This is considered a compile error, so we cannot do what C++ or Callow. Instead, we must declare them like: Of course, different classes can share the same — that's fine. When instances of different classes execute the same , each class searches its own method list for the corresponding . All s in a project form a (a collection with unique elements), so each is unique. Therefore, to find a method in this set, we simply find the corresponding to that method. A is essentially a hashed string based on the method name, and comparing strings only requires comparing their addresses — incredibly fast. However, as the number grows, hash collisions may degrade performance (or may not, if a perfect hash is used). Regardless of the speed optimization method, reducing the total count (multiple methods mapping to the same ) is the most effective approach — which is exactly why a is just the function name. In essence, a is just a pointer to a method (more precisely, a KEY value hashed from the method name that uniquely identifies a method), and its sole purpose is to speed up method lookup. We'll discuss this lookup process below. We can add new s at runtime and retrieve existing ones. There are three ways to obtain a : 1. The function 2. The compiler directive provided by Objective-C 3. The method IMP is actually a function pointer pointing to the starting address of a method's implementation: This function uses the standard C calling convention for the current CPU architecture. The first parameter is a pointer to (for instance methods, the memory address of the class instance; for class methods, a pointer to the metaclass). The second parameter is the method selector. What follows are the method's actual arguments. The introduced earlier exists precisely to find the method's final implementation . Since each method has a unique , we can use to conveniently, quickly, and accurately retrieve the corresponding . We'll discuss the lookup process below. Once we have the , we have the entry point to execute the method's code — at which point we can use this function pointer just like an ordinary C function. By obtaining the directly, we can bypass the Runtime's message-passing mechanism and execute the implementation directly. This skips the series of lookup operations performed during the runtime's message-passing process, making it somewhat more efficient than sending a message to the object directly. Method Having introduced both and , we can now discuss . is used to represent a method in a class definition: We can see that this struct contains both a and an — essentially a mapping between them. With a , we can find the corresponding and call the method's implementation. We'll discuss the specific flow below. objcmethoddescription defines an Objective-C method: Method-Related Operation Functions The Runtime provides a set of methods for handling method-related operations, including methods themselves and s. This section introduces these functions. Methods Method operation functions include: : returns the actual implementation's return value. The parameter cannot be nil. This function is faster than using and separately. : returns a . To get the method name as a C string, use . : the type string is copied into . : note that this function returns the method's previous implementation. Method Selectors Selector-related operation functions include: : when adding a method to a class definition, we must register a method name in the Objective-C Runtime system to obtain the method's selector. Method Dispatch Flow In Objective-C, messages are not bound to method implementations until runtime. The compiler transforms the message expression into a call to the message function , which takes the message receiver and method name as its base parameters: This function handles everything required for dynamic binding: 1. First, it finds the method implementation corresponding to the selector. Because the same method can have different implementations in different classes, we need to rely on the receiver's class to find the exact implementation. 2. It calls the method implementation, passing in the receiver object and all method arguments. 3. Finally, it returns the value returned by the implementation as its own return value. The key to message dispatch is the struct discussed in earlier sections. Two fields of this struct are important for message dispatch: 1. A pointer to the superclass 2. The class's method dispatch table, . When we create a new object, memory is allocated and its member variables are initialized. The pointer is also initialized, allowing the object to access its class and the class hierarchy. The following diagram illustrates the basic framework of message dispatch: When a message is sent to an object, uses the object's pointer to get the class struct, then searches the method dispatch table for the . If not found, it follows the pointer in the struct to the superclass and searches its dispatch table. This continues up the class hierarchy to . Once the is located, the function obtains the implementation's entry point and executes the method with the appropriate arguments. If the is never found, the message-forwarding process kicks in — we'll discuss that later. To speed up message dispatch, the runtime caches used s and their corresponding method addresses. This was discussed earlier and won't be repeated. Hidden Parameters has two hidden parameters: 1. The message receiver object 2. The method's selector These two parameters provide the implementation with information about the caller. They are called "hidden" because they are not declared in the source code defining the method — they are inserted into the implementation code at compile time. Although these parameters are not explicitly declared, they can still be referenced in code. We can use to reference the receiver object and to reference the selector: Of these two parameters, is used far more commonly; is rarely used in practice. Obtaining the Method Address Dynamic binding in the Runtime gives us great flexibility when writing code — we can forward messages to the objects we want, or swap a method's implementation on the fly. However, this flexibility comes with some performance cost, since we need to look up the method's implementation rather than calling a function directly. Method caching addresses this to some extent. As mentioned above, if we want to bypass dynamic binding, we can obtain the address of a method's implementation and call it directly like a function. This is especially useful when a particular method needs to be called frequently inside a loop, as it can significantly improve performance. provides the method, which lets us get a pointer to the method, which we can then use to call the implementation. We need to cast the pointer returned by to the appropriate function type — both the function parameters and return value must match. The following code shows how to use : Note that the first two parameters of the function pointer must be and . Of course, this technique is only appropriate for situations like loops where the same method is called frequently to boost performance. Also, is provided by the Cocoa runtime; it is not a feature of the Objective-C language itself. Message Forwarding When an object can receive a message, it follows the normal method dispatch flow. But what happens when an object cannot receive a specific message? By default, if you call a method using and cannot respond to , the compiler reports an error. However, if you use the form, the check is deferred to runtime. If the object cannot respond, the program crashes. When we're not sure whether an object can receive a particular message, we typically call first: However, we want to discuss the case where is not used — that's the focus of this section. When an object cannot receive a message, the "message forwarding" () mechanism kicks in. Through this mechanism, we can tell the object how to handle unknown messages. By default, an object that receives an unknown message causes the program to crash, and the console shows an exception like: This exception is actually thrown by NSObject's method. However, we can take steps to execute specific logic and prevent the crash. The message-forwarding mechanism has three basic steps: 1. Dynamic method resolution 2. Fallback receiver 3. Full forwarding Let's discuss each step in detail. Dynamic Method Resolution When an object receives an unknown message, it first calls the class method (for instance methods) or (for class methods) of its class. In this method, we have the opportunity to add a new "handler method" for the unknown message. The prerequisite is that we have already implemented that handler method — we just need to dynamically add it to the class at runtime using . For example: This approach is more commonly used for implementing properties. Fallback Receiver If the message cannot be handled in the previous step, the Runtime continues by calling: If an object implements this method and returns a non-nil result, that object becomes the new receiver of the message and the message is dispatched to it. Of course, that object cannot be , which would create an infinite loop. If no suitable object is specified to handle , the parent class's implementation should be called to return a result. This method is typically used when the object internally has a series of other objects that can handle the message. We can use those objects to handle and return the message, so from the outside it appears the original object handled it itself: This step is appropriate when we only want to forward the message to another object capable of handling it. However, at this step we cannot manipulate the message, such as modifying its arguments or return value. Full Message Forwarding If the message still cannot be handled after the previous step, the only option is to engage the full message-forwarding mechanism. This calls: The runtime gives the message receiver one final chance to forward the message to another object. The system creates an object representing the message, with all details of the unhandled message encapsulated in — including the , target, and arguments. We can choose to forward the message to another object inside . The method has two tasks: 1. Locate an object that can respond to the message encapsulated in . That object does not need to be able to handle all unknown messages. 2. Send the message to the selected object using as the parameter. will retain the call result, and the runtime will extract and send it back to the original sender. In this method, we can also implement more complex logic — for example, modifying the message content (such as appending an argument) before dispatching it. Also, if a message should not be handled by the current class, the parent class's method of the same name should be called so that every class in the hierarchy has a chance to handle the request. There is one more important requirement — we must override: The message-forwarding mechanism uses the information from this method to create the object. Therefore, we must override it to provide a suitable method signature for the given . A complete example: NSObject's implementation simply calls and does not forward any message. So if the unknown message is not handled in any of the three steps above, an exception is raised. In a sense, acts as a distribution center for unknown messages, forwarding them to other objects. Or it can act like a relay station that sends all unknown messages to the same receiver. It all depends on the specific implementation. Message Forwarding and Multiple Inheritance Looking back at steps 2 and 3: through these two methods, we can allow an object to build relationships with other objects to handle certain unknown messages, while to the outside world it still appears that the original object is handling the messages. Through this relationship, we can simulate some characteristics of "multiple inheritance," letting an object "inherit" features from other objects. However, there is an important distinction: multiple inheritance integrates different functionalities into a single object — making it bulkier and more complex — while message forwarding decomposes functionality into independent small objects, connects them in some way, and performs appropriate message forwarding. Although message forwarding resembles inheritance, some NSObject methods can still distinguish between the two. Methods like and only work on the inheritance hierarchy, not on the forwarding chain. If we want this forwarding to look like inheritance, we can override these methods: Code: All code from this post can be found on my GitHub .