支那
支那
Basic use of a Category
** The underlying structure of the two categories **** the loading process of the three categories **** the load method of the four categories ** the initialize method of the five categories
Also called categories or categories, a Category is a way to extend classes provided by OC. Whether it is a custom class or a system class, we can extend methods (instance methods and class methods) by Category, and the extended methods are called exactly the same way as the original methods. For example, in my project, we often need to count the number of letters in a string, but the system does not provide this method. Then we can extend a method to the NSString class by Category, and then we can call the extended method just like calling the system method by introducing the header file of the Category
Basic use of a Category
Let’s take a look at a basic Category usage example:
#import <Foundation/Foundation.h> @interface People : NSObject - (void)talk; @end #import "People.h" @implementation People - (void)talk{ NSLog(@"%s:can I speak?" ,__func__); } @end #import "People.h" @interface People (Speak) -(void)speak; @end #import "People+Speak.h" @implementation People (Speak) -(void)speak{ NSLog(@"%s: I can speak",__func__); } @end #import "People.h" @interface People (Eat) -(void)eat; @end #import "People+Eat.h" @implementation People (Eat) -(void)eat{ NSLog(@"%s: I can eat food",__func__); } @end #import <Foundation/Foundation.h> #import "People.h" #import "People+Speak.h" #import "People+Eat.h" extern void _objc_autoreleasePoolPrint(void); int main(int argc, const char * argv[]) { @autoreleasepool { People *people = [[People alloc] init]; [people talk]; [people speak]; [people eat]; } return 0; }Copy the code
The use of classification is very simple, why to add a classification to a class and the method of classification and the original method is called in the same way can be called through the object of this class, let’s explore.
The underlying structure of two categories
Let’s convert People+ speak.m to C++ and look at xcrun-sdk iphoneos clang-arch arm64-rewrite-objc People+ speak.m
#ifndef _REWRITER_typedef_People #define _REWRITER_typedef_People typedef struct objc_object People; typedef struct {} _objc_exc_People; #endif struct People_IMPL { struct NSObject_IMPL NSObject_IVARS; }; static void _I_People_Speak_speak(People * self, SEL _cmd) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_rn_r6_l2xln77j0bv69j2c_5rg00000gp_T_People_Speak_8c87eb_mi_0,__func__); } // @end struct _prop_t { const char *name; const char *attributes; }; struct _protocol_t; struct _objc_method { struct objc_selector * _cmd; const char *method_type; void *_imp; }; struct _protocol_t { void * isa; // NULL const char *protocol_name; const struct _protocol_list_t * protocol_list; // super protocols const struct method_list_t *instance_methods; const struct method_list_t *class_methods; const struct method_list_t *optionalInstanceMethods; const struct method_list_t *optionalClassMethods; const struct _prop_list_t * properties; const unsigned int size; // sizeof(struct _protocol_t) const unsigned int flags; // = 0 const char ** extendedMethodTypes; }; struct _ivar_t { unsigned long int *offset; // pointer to ivar offset location const char *name; const char *type; unsigned int alignment; unsigned int size; }; struct _class_ro_t { unsigned int flags; unsigned int instanceStart; unsigned int instanceSize; const unsigned char *ivarLayout; const char *name; const struct _method_list_t *baseMethods; const struct _objc_protocol_list *baseProtocols; const struct _ivar_list_t *ivars; const unsigned char *weakIvarLayout; const struct _prop_list_t *properties; }; struct _class_t { struct _class_t *isa; struct _class_t *superclass; void *cache; void *vtable; struct _class_ro_t *ro; }; struct _category_t { const char *name; struct _class_t *cls; const struct _method_list_t *instance_methods; const struct _method_list_t *class_methods; const struct _protocol_list_t *protocols; const struct _prop_list_t *properties; }; extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache; #pragma warning(disable:4273) static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_INSTANCE_METHODS_People_$_Speak __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"speak", "v16@0:8", (void *)_I_People_Speak_speak}} }; extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_People; // According to the definition of classification, Static struct _category_t _OBJC_$_CATEGORY_People_$_Speak __attribute__ ((used, section ("__DATA,__objc_const"))) = { "People", 0, // &OBJC_CLASS_$_People, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_People_$_Speak, 0, 0, 0, }; static void OBJC_CATEGORY_SETUP_$_People_$_Speak(void ) { _OBJC_$_CATEGORY_People_$_Speak.cls = &OBJC_CLASS_$_People; } #pragma section(".objc_inithooks$B", long, read, write) __declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = { (void *)&OBJC_CATEGORY_SETUP_$_People_$_Speak, }; static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= { &_OBJC_$_CATEGORY_People_$_Speak, }; static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };Copy the code
We’ll see that we end up with _category_T as a structure
2.1 Classification structure
struct category_t { const char *name; Struct _class_t * CLS; const struct _method_list_t *instance_methods; // list of object methods const struct _method_list_t *class_methods; Const struct _protocol_list_t *protocols; // const struct _prop_list_t *properties; // Attribute list};Copy the code
As you can see from the classification structure, you can add instance methods, class methods, follow protocols, define attributes. We see that when we compile a classification, all of its information is put into the structure below
static struct _category_t _OBJC_$_CATEGORY_People_$_Speak __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"People",
0, // &OBJC_CLASS_$_People,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_People_$_Speak,
0,
0,
0,
};
Copy the code
The classification hierarchy is a structure, but how to make an object of a class call its methods, let’s see.
As a developer, it is particularly important to have a learning atmosphere and a communication circle. Here is an iOS communication group: 642363427. Welcome to join us, no matter you are small white or big bull, share BAT, Ali interview questions, interview experience, discuss technology, iOS developers exchange learning and growth together!
The loading process of the three categories
The classification loading process mainly includes the following three steps:
**1. Load all Category data of a class via Runtime ****2. You merge all of the Category methods, properties, protocol data into one big array and the Category data that you compile later will be at the front of the array **3. Inserts the merged classification data (methods, attributes, protocols) in front of the original data of the class
Let’s look at how each step is implemented through the source code. Let’s start with the Runtime initialization function
Step 1
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
Copy the code
Step 2
Then we go to the &map_images reading module (images stands for module), go to the map_images_NOLock function to find the _read_images function, and in the _read_images function we find the classification code
void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) { rwlock_writer_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs); } void map_images_nolock(unsigned mhCount, const char * const mhPaths[], Struct mach_header * const MHDRS []) {****** if (hCount > 0) {_images(hList, hCount, totalClasses, unoptimizedTotalClasses); } firstTime = NO; } void _read_images(header_info **hList, uint32_t hCount, int totalClasses, Int unoptimizedTotalClasses) {***** omit ****** // Discover categories. For (EACH_HEADER) {category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties(); for (i = 0; i < count; i++) { category_t *cat = catlist[i]; Class cls = remapClass(cat->cls); if (! cls) { // Category's target class is missing (probably weak-linked). // Disavow any knowledge of this category. catlist[i] = nil; if (PrintConnecting) { _objc_inform("CLASS: IGNORING category \? \? \? (%s) %p with " "missing weak-linked target class", cat->name, cat); } continue; } // Process this category. // First, register the category with its target class. // Then, rebuild the class's method lists (etc) if // the class is realized. bool classExists = NO; if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized()) { remethodizeClass(cls); classExists = YES; } if (PrintConnecting) { _objc_inform("CLASS: found category -%s(%s) %s", cls->nameForLogging(), cat->name, classExists ? "on existing class" : ""); } } if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { addUnattachedCategoryForClass(cat, cls->ISA(), hi); if (cls->ISA()->isRealized()) { remethodizeClass(cls->ISA()); } if (PrintConnecting) { _objc_inform("CLASS: found category +%s(%s)", cls->nameForLogging(), cat->name); } } } } }Copy the code
After the _getObjc2CategoryList function is used to obtain the classification list, the method, protocol, attribute, etc. You can see that remethodizeClass(CLS) is eventually called; Function. We come to remethodizeClass(CLS)
Step 3
The attachCategories function takes the class object CLS and the class array CATS. As the code we started with shows, a class can have multiple classes. Earlier we said that category information is stored in the CATEGORY_T structure, so multiple categories are stored in category_list
static void remethodizeClass(Class cls) { category_list *cats; bool isMeta; runtimeLock.assertWriting(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "(meta)" : ""); } attachCategories(cls, cats, true /*flush caches*/); free(cats); }}Copy the code
Step 4
1. First, allocate memory according to method list, attribute list, protocol list and MALloc. Allocate memory address according to how many categories and how much memory each piece of method needs.
2 Then store the classification methods, attributes and protocols in the three arrays from the classification array into the corresponding mList, Proplists and Protolosts arrays, which contain all the classification methods, attributes and protocols.
Class_rw_t contains the methods, attributes, and protocols of the class_rw_t. The rW is obtained by the data() method of the class_rw_T. So the RW holds the data in this type of object.
4. Finally, the attachList function of method list, attribute list and protocol list was called by RW respectively, and all the classified methods, attributes and protocol list arrays were uploaded. We can guess that the classification and corresponding object methods, attributes and protocols were combined within the attachList method
static void attachCategories(Class cls, category_list *cats, bool flush_caches) { if (! cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); bool isMeta = cls->isMetaClass(); // According to the method list, attribute list in each category, // fixme rearrange to remove these intermediate allocations method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists)); property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists)); protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists)); // Count backwards through cats to get newest categories first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) { auto& entry = cats->list[i]; Method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; // Store all methods in all categories into mlists [[method_t,method_t] [method_t,method_t]......] fromBundle |= entry.hi->isBundle(); } // All properties property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { proplists[propcount++] = proplist; } protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; Rw = CLS ->data(); prepareMethodLists(cls, mlists, mcount, NO, fromBundle); Rw ->methods.attachLists(mlists, McOunt); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls); rw->properties.attachLists(proplists, propcount); free(proplists); rw->protocols.attachLists(protolists, protocount); free(protolists); }Copy the code
Step 5
Methods combined
void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount; setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount; memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); Memcpy (array()-> Lists, addedLists, addedCount * sizeof(array()->lists[0])); // Copy new data to free memory} else if (! list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1:0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); }}Copy the code
We can see that the method attribute protocol for a class is appended to the method attribute protocol list of the original class. This means that if a class has the same method as its class, its class’s method will be called first.
The above source with read, the main source process is as follows: Objc-os. mm _objc_init map_images map_images_nolock objC-Runtime-new. mm _read_images remethodizeClass AttachCategories attachLists RealLOc, MEMmove, memCPY Here we summarize the whole process of category: Each time we create a category, a category_T structure is generated at compile time and the category_T structure is stored with the list of methods for the category. The information about the classification at compile time is separate from the information about the class. At the runtime stage, the runtime will load all the Category data of a class, and merge the methods, attributes, and protocol data of all categories into an array respectively. And then we’re going to insert the merged data in front of the data in the category and that’s it, and then we’re going to talk about the two methods in the category.
Load method of four categories
4.1 Load Basic Usage
Let’s look at a basic use example of Load
#import <Foundation/Foundation.h> @interface People : NSObject @end #import "People.h" @implementation People + (void)load { NSLog(@"%s",__func__); } @end #import "People.h" @interface People (Speak) @end #import "People+Speak.h" @implementation People (Speak) + (void)load { NSLog(@"%s",__func__); } @end #import "People.h" @interface People (Eat) @end #import "People+Eat.h" @implementation People (Eat) + (void)load { NSLog(@"%s",__func__); } @end #import "People.h" @interface SubPeople : People @end #import "SubPeople.h" @implementation SubPeople + (void)load { NSLog(@"%s",__func__); } @end #import <Foundation/Foundation.h> //#import "People.h" //#import "People+Speak.h" //#import "People+Eat.h" extern void _objc_autoreleasePoolPrint(void); int main(int argc, const char * argv[]) { @autoreleasepool { // People *people = [[People alloc] init]; } return 0; }Copy the code
You can see that I’m just overwriting the load method, and as soon as it’s compiled and run, it’s going to call the load method. Even though I don’t have any quotes to use. Why? Let’s see. Right
4.2 Load Invocation Principle
The +load method is called when the Runtime loads a class or class, and only once during the program’s run
void _objc_init(void) { static bool initialized = false; if (initialized) return; initialized = true; // fixme defer initialization until an objc-using image is found? environ_init(); tls_init(); static_init(); lock_init(); exception_init(); _dyld_objc_notify_register(&map_images, load_images, unmap_image); } // load_images void load_images(const char *path __unused, const struct mach_header *mh) { // Return without taking locks if there are no +load methods here. if (! hasLoadMethods((const headerType *)mh)) return; recursive_mutex_locker_t lock(loadMethodLock); // Discover load methods { rwlock_writer_t lock2(runtimeLock); prepare_load_methods((const headerType *)mh); } // Call +load methods (without runtimeLock - re-entrant) call_load_methods(); } // follow up call_load_methods(); void call_load_methods(void) { static bool loading = NO; bool more_categories; loadMethodLock.assertLocked(); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading = YES; void *pool = objc_autoreleasePoolPush(); do { // 1. Repeatedly call class +loads until there aren't any more while (loadable_classes_used > 0) { call_class_loads(); } // 2. Call category +loads ONCE more_categories = call_category_loads(); // 3. Run more +loads if there are classes OR more untried categories } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading = NO; }Copy the code
Since it is called while the program is running, we also start with the Runtime initialization methods. Load is called in the following order
4.3 Call Sequence
1. Call the +load class first
Call in compile order (compile first, call first) the child’s +load is called before the parent’s +load
2. Invoke + Load of the category
Call in compile order (compile first, call first)
void call_load_methods(void) { static bool loading = NO; bool more_categories; loadMethodLock.assertLocked(); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading = YES; void *pool = objc_autoreleasePoolPush(); Do {// 1. Repeatedly call class +loads until there aren't any more while (loadable_classes_used > 0) call_class_loads(); } // 2. Call category +loads ONCE more_categories = call_category_loads(); // 3. Run more +load if there are classes OR more untried categories} while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading = NO; } /*********************************************************************** * call_class_loads * Call all pending class +load methods. * If new classes become loadable, +load is NOT called for them. * * Called only by call_load_methods(). **********************************************************************/ static void call_class_loads(void) { int i; // Detach current loadable list. struct loadable_class *classes = loadable_classes; int used = loadable_classes_used; loadable_classes = nil; loadable_classes_allocated = 0; loadable_classes_used = 0; // Call all +loads for the detached list. for (i = 0; i < used; i++) { Class cls = classes[i].cls; load_method_t load_method = (load_method_t)classes[i].method; // retrieve classes method if (! cls) continue; if (PrintLoading) { _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging()); } (*load_method)(cls, SEL_load); // Destroy the detached list. If (classes) free(classes); }Copy the code
The +load method is called directly from the method address, not through the objc_msgSend function. Let’s look at the Initialize method again. If you manually call load,[people Load], that will override the message sending mechanism.
The initialize method of five categories is explained
5.1 Initialize Basic use
Load = initialize; load = initialize;
#import <Foundation/Foundation.h>
@interface People : NSObject
@end
#import "People.h"
@implementation People
+ (void) initialize {
NSLog(@"%s",__func__);
}
@end
#import "People.h"
@interface People (Speak)
@end
#import "People+Speak.h"
@implementation People (Speak)
+ (void) initialize {
NSLog(@"%s",__func__);
}
@end
#import "People.h"
@interface People (Eat)
@end
#import "People+Eat.h"
@implementation People (Eat)
+ (void) initialize {
NSLog(@"%s",__func__);
}
@end
#import "People.h"
@interface SubPeople : People
@end
#import "SubPeople.h"
@implementation SubPeople
+ (void)initialize
{
NSLog(@"%s",__func__);
}
@end
#import <Foundation/Foundation.h>
#import "People.h"
#import "People+Speak.h"
#import "People+Eat.h"
#import "SubPeople.h"
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
SubPeople *people = [[SubPeople alloc] init];
SubPeople *people2 = [[SubPeople alloc] init];
}
return 0;
}
Copy the code
The final output is the initialize method of the People Eat class. Then we call the initialize method of SubPeople. We generate two objects and output the initialize method only once.
5.2 Initialize Call Principle
The + Initialize method is called the first time the class receives a message
The source code call process is as follows: Objc_msgSend is assembly code. Its implementation is in objC-MSG-arm64.s. The deeper implementation is invisible, but since it is through the message mechanism, If initialize is called when a method is found or called, the method must be found before it can be called
Method class_getInstanceMethod(Class cls, SEL sel) { if (! cls || ! sel) return nil; // This deliberately avoids +initialize because it historically did so. // This implementation is a bit weird because it's the only place that // wants a Method instead of an IMP. #warning fixme build and search caches // Search method lists, try method resolver, etc. lookUpImpOrNil(cls, sel, nil, NO/*initialize*/, NO/*cache*/, YES/*resolver*/); #warning fixme build and search caches return _class_getMethod(cls, sel); } IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver); if (imp == _objc_msgForward_impcache) return nil; else return imp; } IMP lookUpImpOrForward(Class CLS, SEL SEL, ID INst, bool initialize, bool cache, bool resolver) {----- omit ------ if (! cls->isRealized()) { // Drop the read-lock and acquire the write-lock. // realizeClass() checks isRealized() again to prevent // a race while the lock is down. runtimeLock.unlockRead(); runtimeLock.write(); realizeClass(cls); runtimeLock.unlockWrite(); runtimeLock.read(); } if (initialize && ! cls->isInitialized()) { runtimeLock.unlockRead(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.read(); // If sel == initialize, _class_initialize will send +initialize and // then the messenger will send +initialize again after this // procedure finishes. Of course, If this is not being called // from the messenger then it won't happen. 2778172} ----- omit ------ void _class_initialize(Class cls) { assert(! cls->isMetaClass()); Class supercls; bool reallyInitialize = NO; // Make sure super is done initializing BEFORE beginning to initialize CLS. // See note about deadlock. supercls = cls->superclass; if (supercls && ! Supercls ->isInitialized()) {// If the parent has not initialized _class_initialize(supercls); // Try atomically set CLS_INITIALIZING. {monitor_locker_t lock(classInitLock); if (! cls->isInitialized() && ! CLS ->isInitializing()) {// If you have not initialized CLS ->setInitializing(); ReallyInitialize = YES; } } if (reallyInitialize) { // We successfully set the CLS_INITIALIZING bit. Initialize the class. // Record that we're initializing this class so we can message it. _setThisThreadIsInitializingClass(cls); if (MultithreadedForkChild) { // LOL JK we don't really call +initialize methods after fork(). performForkChildInitialize(cls, supercls); return; } // Send the +initialize message. // Note that +initialize is sent to the superclass (again) if // this class doesn't implement +initialize. 2157218 if (PrintInitializing) { _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]", pthread_self(), cls->nameForLogging()); } // Exceptions: A +initialize call that throws an exception // is deemed to be a complete and successful +initialize. // // Only __OBJC2__ adds these handlers. ! __OBJC2__ has a // bootstrapping problem of this versus CF's call to // objc_exception_set_functions(). #if __OBJC2__ @try #endif { callInitialize(cls); if (PrintInitializing) { _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]", pthread_self(), cls->nameForLogging()); }}}} // Send Initialize message void callInitialize(Class CLS) {((void(*)(Class, SEL))objc_msgSend)(CLS, SEL_initialize); asm(""); }Copy the code
We traced the source code and found that the Initialize method was already handled during the method search. From the final source we can see that the initialize call order is as follows:
Call +initialize from parent class and +initialize from subclass
2 Initialize the parent class first and then the child class. Each class is initialized only once
Initialize will only be called once, so it will not be called again after the second alloc message is sent. The initialize print method of the parent class Eat will be printed first. Then output your own initialize method. OK, initialize is finished here. Finally, let’s compare these two methods
Load, initialize
Load is called from the address of the function, initialize is called from objc_msgSend. Load is called when the Runtime loads a class or class (only once). Initialize is called when the class first receives a message. Each class is initialized only once (the parent class’s initialize method may be called multiple times).
Call order: the load method of the class is called first, and the load method is called first when the class is compiled. The load method of the parent class is called before calling load. The load method in a class does not override the load method of the class. The first compiled class calls the load method first. Initialize first initializes the parent class and then the child class. If the subclass does not implement +initialize, the parent class’s +initialize is called (so the parent class’s +initialize may be called multiple times), and if the class implements +initialize, the class’s +initialize call is overridden.
Recommend a 👇 :
If you want to advance together, know the latest interview market might as well add the exchange group 642363427
OK, so I’m done with this category.