This article addresses the problem

  1. How Category works
  2. What is the difference between Category and Extension?
  3. Is there a load method in Category? When is the load method called? Can the load method inherit?
  4. What is the difference between the load and initialize methods? In what order are they called in categories? And how they call each other when inheritance occurs?
  5. Why can you only add methods to a Category and not properties?
  6. Can you add member variables to a Category? If so, how do YOU add member variables to a Category?

1. Realization principle of Category

The answer:

The compiled structure of a classification is a _category_T structure, which stores information about the class name, object methods, class methods, attributes, and protocols of the classification. At runtime, the Runtime merges this information into class and metaclass objects. After merging, the classified information is inserted in front of the original information.

Analysis:

Suppose we now have the following class and classification

@interface Person

@end

@implmentation

- (void)run;

@end

**************************************************

@interface Person (test)

@end

@implementation Person (test)

- (void)testInstanceMethod;
+ (void)testClassMethod;

@end
Copy the code

Let’s recall that when one of our Person objects calls the run method:

    Person *person = [[Person alloc] init];
    [person run];
Copy the code

What happens is that the compiler converts the code [person run] to objc_msgSend(person, @selector(run)), and the system sends a message to the person, finds the Person’s ISA, Find the Person Class object through ISA, and find the implementation of Run in the list of the Person Class object’s methods and call it.

Now use person to call the classified method:

    [person test];
Copy the code

In fact, the process is still the same as above. The system will first find the Isa of Person, find the Person class object through ISA, find the implementation of Test in the list of Person class methods and call it. As the program runs, the runtime dynamically adds the object methods in the class to the list of methods in the Person Class object.

The situation is similar if you use Person to call a class method of a classification:

    [Person test1];
Copy the code

As the program runs, the runtime dynamically adds classified class methods to the list of methods on the Meta-class object of Person. To call a method, the system finds the Person class’s ISA, then the Person Meta-class object, finds the implementation of Test1 in its list of methods, and calls it.

Let’s look at the underlying structure of categorization.

Create a project, create a Person class, create a Person+ Test category, and add the methods we talked about above.

Go to the directory where Person+test.m resides and run the following command: $xcrun – SDK iphoneOS clang -arch i386-rewrite-objc Person+test.m Convert the Person+test.m code into C/C ++ code and open it.

Compilation phase

We can also find a piece of code like this

Person
&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test



&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test

- (void)test;
&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test

In summary, we can tell that the classification does compile to type _category_t after compilation.

We have now concluded that after adding the classification: at compile time, the compiler compiles the classification into a structure of type _category_t, which stores the class name, object methods, class methods, attribute information, and protocol information of the classification. At Runtime, the Runtime merges the list of object methods in the corresponding structure of all classes into the list of methods of the class object and the class methods into the list of methods of the metaclass object.

If you read the source code, you can also see that after merging, the Category data is inserted in front of the original data. Thinking about how objects call methods, we can now explain why methods in a class and a class are always called first. If more than one class has this method, which class is called depends on the order in which it is compiled.

2. What is the difference between Category and Extension?

The answer:

The data in Extension is incorporated into the class at compile time. A Category is compiled into a structure at compile time and incorporated into the class at run time. (Extension can be used to write attributes and methods that need to be privatized in a.m file.)

3. Is there a load method in Category? When is the load method called? Can the load method inherit?

The answer:

  • CategoryThere areloadMethods.
  • inruntimeCalled when loading classes, classes, each class, classloadMethod that is called only once during program execution.
  • loadMethods can be inherited. But it’s usually not called on its own initiativeloadMethods are called automatically by the system.

Analysis:

Create a new project. Create a class Person and create its class Person+test. Add the following code to both the Person and Person+test implementations:

+ (void)load
{
    NSLog(@"Person +load"); // NSLog(@"Person (test) +load");
}
Copy the code

Then click Run.

Here’s what we found:

  • We haven’t called it anywhere yet+ (void)load;Method, without even importing a class or classification header file anywhere.
  • + (void)load;The main functionBefore executing becauseThe main functionThe inside of thehello worldTo print first.
  • What we learned earlier is that if there is a method in both the class and the class, then only the methods in the class will be executed, which we did here+ (void)load;Methods.

To explain this phenomenon, take a look at the source code:

objc-os.mm
_objc_init



load_images

load
call_load_methods()
load
load

Moving on to what call_load_methods does:

load
load
call_class_loads();
load
call_categry_loads();
objc_msgSend()
load

If you look at the source code further, you can find that if there are multiple classes and categories, the call order is like this: first call all the load methods in compile order, then call all the load methods in subclasses in compile order, and finally call all the load methods in compile order.

4. What is the difference between load and initialize methods? In what order are they called in categories? And how they call each other when inheritance occurs?

A:

  • loadMethod is inruntimeWhen loading classes and categories, use the function pointer to find each class and categoryloadMethod, directly called;initializeThe class uses the message mechanism the first time it receives a messageObjc_msgSend (class, @ the selector (the initialize))To send a message to the class to make the call.
  • When there are categories: class and classifiedloadMethod will be called becauseloadThe principle is to find the function pointer for each class and class and call it directly, calling the parent class firstloadMethod, and then call all classified classes in compile orderloadMethods; whileinitializeIt works through the messaging mechanismobjc_msgSend()Call, so it will pass through the class objectisaFind the list of methods on the metaclass objectinitializeImplement and call, if there is one in the classinitializeMethod, so the first thing you find is classifiedinitializeMethod is called first.
  • When inheritance occurs: the system calls all classes firstloadMethod, and then call all of the subclassesloadMethod, and finally call all classifiedloadMethods. When a message is first sent to a subclass object, the message is first sent to the parent classObjc_msgSend (parent, @selector(initialize)), let the parent class call itinitializeAnd then sends a message to the subclassObjc_msgSend (subclass, @selector(initialize)), and let subclasses call itinitialize;

Analysis:

Initialize is called the first time a class receives a message. For example, both [NSObject alloc] and [Class alloc] are called the first time a message is sent to the Class. That is, if we use the class, initialize will be called, and if we never use the class, Initialize will never be called. To verify this, create a class and implement + (void) Initialize in the.m file of the class.

When we implement + (void)initialize on a class, its class, or multiple classes, we find that the first time a class sends a message, only one initialize method is called, and the load method is called: All load methods are called when the Runtime loads the class. The initialize method is called using objc_msgSend(), a message mechanism. Use Class isa to find the metaclass object and find the initialize method in the list of metaclass methods. The initialize method of the class is called because the initialize method of the class has been merged to the front of the list of metaclass object methods.

Initialize of the parent class is called before initialize of the subclass is called (if the parent class has already been called, it is not called). We can guess that something like this is called after compilation:

Objc_msgSend (parent, @selector(initialize)); Objc_msgSend (subclass, @selector(initialize));Copy the code

We already know that when the class receives this message, it finds the metaclass object through the isa of the class object, finds the list of methods on the metaclass object, and looks for the methods in it.

We can find this passage in the source code:

lookUpImpOrNil
lookUpImpOrForward
_class_initialize

CallInitialize (); callInitialize();

callInitialize(cls)
objc_msgSend(cls, @selector(initialize))

We then combine this with the big imporforward from the previous lookUpImpOrForward and simplify it with pseudocode, leaving only the parts we care about and getting something like this:

if(CLS not initialized) {if{objc_msgSend(parent, @selector(initialize)); Initialize the parent class; } objc_msgSend(cls, @selector(initialize)); Initialize the CLS; }Copy the code

This pseudocode makes all the sense+ (void)initializeThe root implementation of the method.

So the phenomenon of testing subclasses above is understandable.

The initialize method of the parent class may be called many times, so let’s look at an example.

@interface Person : NSObject
@end

@implementation Person

+ (void)initialize
{
    NSLog(@"Person - initialize");
}

@end

**************************************************

@interface Person (test)
@end

@implementation Person (test)

+ (void)initialize
{
    NSLog(@"Person test - initialize");
}

@end

**************************************************

@interface Student : Person
@end

@implementation Student
@end

**************************************************

@interface Teacher : Student
@end

@implementation Teacher
@end

Copy the code

Then call the following methods:

 [Student alloc];
 [Teacher alloc];
Copy the code

You can try to guess what the printed statement will be.

5. Why only methods can be added to categories but not attributes?

A:

Classes can add methods because the compiled structure _category_t of a class has member variables that can store a list of methods.

Categories can also add attributes, because _category_t also has member variables that can store attributes.

Categories cannot add member variables directly because _CATEGORY_t cannot store member variables.

Analysis:

Let’s look at the compiled structure of the classification:

instance_methods
class_methods

Categories can actually add attributes because _category_t has properties, which are used to store attributes

Person (test)
@property (nonatomic, copy) NSString *name;

Normally when we add @property to a class, the system automatically generates member variables, adds setter and getter declarations to @interface, and implements setter and getter in @implementation.

If you add @property to a class, the system just adds the setter and getter method declarations to the @interface.

6. Can I add member variables to a Category? If so, how do YOU add member variables to a Category?

A:

Analysis:

You can’t add member variables directly to a Category

Associated objects provide the following apis [6. Which methods have you used in Runtime?] :

  • Adding an Associated Object
/* The object parameter indicates the object to be associated with. The second argument, key, is of type void *, pointer, which is an address; The third parameter, value, indicates which value to associate. The fourth argument, policy, is associated policy. Setting it is similar to setting the modifier after @property. It has the following options:  OBJC_ASSOCIATION_ASSIGN = 0, OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, OBJC_ASSOCIATION_COPY_NONATOMIC = 3, OBJC_ASSOCIATION_RETAIN = 01401, OBJC_ASSOCIATION_COPY = 01403 */ void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)Copy the code
  • Get the associated object
Value */ id objc_getAssociatedObject(id object, const void * key)Copy the code
  • Remove all associated objects

    id objc_removeAssociatedObjects(id object)

There are three easy ways to set the key parameter:

The first way

Person+test.h

@interface Person (test)

@property (nonatomic, assign) int weight;

@end


Person+test.m

const int WeightKey;
@implementation Person (test) 

- (void)setWeight:(int)weight
{
    objc_setAssociatedObject(self, &WeightKey, @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);   
    //因为 value 是 id 类型,所以把它转成 NSNumber
}

- (int)weight
{
    return [objc_getAssociatedObject(self, &WeightKey) intValue];
}

@end
Copy the code

In this way, the desired effect can be achieved. But there are two problems: a. The global variables we define can be accessed by external files. You can limit its scope by prefixing it static.

static const int WeightKey;
Copy the code

B. It is only used as a key. If it is an int, it takes up 4 bytes. I can change it to a char, so it only takes 1 byte.

static const char WeightKey;
Copy the code

The second way is to set key in.m like this:

- (void)setWeight:(int)weight
{
    objc_setAssociatedObject(self, @"weight", @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)weight
{
    return objc_getAssociatedObject(self, @"weight");
}
Copy the code

Key is of type pointer, and passing in at sign weight is actually passing in the address of this string.

So, there’s a question, there’s two @”weight” here, are they the same address?

It’s the same. The way we write strings directly in iOS, strings are stored in constants. Data stored in the constant area cannot be modified once initialized and is released by the system at the end of the program. So no matter how many times we write at sign weight, it’s just an at sign weight stored in the constant area.

This extends to another question: why use isEqualToString instead of == to determine if two string literals are the same? We’ll talk about that in future questions.

And then a good way to do that is to use @selector() :

- (void)setWeight:(int)weight
{
    objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)weight
{
    return [objc_getAssociatedObject(self, @selectory(weight)) intValue];   
}
Copy the code

If we test it, we can see that the address for @selector(weight) is the same:

NSLog(@"%p %p %p", @selector(weight), @selector(weight), @selector(weight));Copy the code

There are three common ways that you can indirectly implement the effect of adding member variables and parameter keys in a classification by associating them with objects.

For more information about AssociatedObjects, see this and this article.