series

  • Two months into Effective Objective-C 2.0
  • Effective Objective-C 2.0
  • Effective Objective-C 2.0: Effective Objective-C 2.0

Chapter 1: Getting familiar with Objective-C

General theory of the core concepts of the language.

1. Understand the origins of objective-C

The main points of

  • Objective-c adds object-oriented features to C, and is its superset. Objective-c uses a dynamically bound message structure, which means that the object type is checked at run time. It is up to the runtime environment, not the compiler, to decide what code to execute after receiving a message.
  • Understanding the core concepts of C helps you write objective-C programs. In particular, master the memory management model and Pointers.

The author

  • Objective-c is, as the name suggests, the object-oriented C language, and it’s a superset of C. OC evolved from Smalltalk, the ancestor of message-based languages. OC uses “message structures” rather than “function calls” and is a dynamic runtime language.
  • The difference between dynamic runtime and compile-time languages:
    • Languages that use message structures (dynamic runtime languages), whose runtime code is determined by the runtime environment; The type of the object is checked at run time, and the method to execute is looked for at run time. 11 】 【 🚩
    • The language in which the function is called (compile-time language) is used, and the code executed at runtime is determined by the compiler.

      To understand the dynamic runtime language, look at the 🌰 example:

      For NSString *string = [[NSMutableArray Alloc]init];

      • Compile time: The compiler performs type checking when given aNSStringType is assigned to aNSMutableArrayObject, so the compiler warns of type mismatches. But the compiler willstringAs aNSStringInstance of, sostringThe objectNSStringMethods that compile without any problems while callingNSMutableArrayThe compiler will directly report an error.
      • Runtime: Due tostringIt actually points to oneNSMutableArrayObject,NSMutableArrayObjects have nostringByAppendingString:Method, so Crash:unrecognized selector send to instance.
      NSString *string = [[NSMutableArray alloc]init];  //⚠️ Incompatible pointer types initializing 'NSString *' with an expression of >type 'NSMutableArray *'
      [string stringByAppendingString:@"abc"];
      [string addObject:@"abc"];  //❌ No visible @interface For 'NSString' declares the selector 'addObject:'
      Copy the code
  • OC’s dynamics are all implemented by the “Runtime components”, the Runtime library (ObjC4), where all the data structures and functions needed to use OC’s object-oriented features are located. Run-time components are essentially dynamic libraries linked to code written by developers that glue together all programs written by developers, so you can improve application performance by updating run-time components.
  • Understanding the core concepts of C helps you write OC programs, especially memory models and Pointers.
  • forNSString *string = @"the string", the NSString instance is allocated on the heap, and the string pointer variable is allocated on the stack and points to that instance.

    Memory allocated to the heap must be managed directly, while memory allocated to hold variables on the stack is automatically cleared when its stack frame pops.
  • The OC runtime environment abstracts the work of “using malloc and free to allocate or free the memory occupied by objects” into a set of memory management architecture called “reference counting”, but it is important to note that the actual allocation and free operations are malloc and free. Not alloc and dealloc. IOS – Commonplace Memory Management (4) : Source Code Analysis of Memory Management Methods
  • There are additional costs associated with creating objects, such as allocating and freeing memory, compared to creating structures.

2. Introduce as few other header files as possible into the class header files

The main points of

  • Do not introduce header files unless it is absolutely necessary. In general, you should use forward declarations in the header file of one class to refer to other classes, and introduce those classes’ headers in the implementation file. Doing so minimizes coupling between classes.
  • Sometimes forward declarations cannot be used, such as when a class to be declared follows a protocol. In such cases, try to move the “class follows a protocol” declaration into the class-continuation class. If not, put the protocol in a separate header file and introduce it.

The author

  • Introduce as few other header files as possible into the class header file, and postpone the introduction of header files until they are really needed. This can be done:
    1. Reduce compilation time and reduce the number of header files that users of the class need to introduce

      Let’s say we’re in A.h#importB.h, so if class C#importH will import B, and C will not use B at all, so this will only increase compilation time.
    2. Avoid problems where two classes reference each other in header files and one of them fails to compile

      If two classes are in each other’s.h file#importThe other side, though it doesn’t look like#includeCauses an infinite loop, but prevents one of the classes from compilingCannot find interface declaration for 'Class'.
    3. Reduce dependencies, reduce coupling between classes, and avoid problems where dependencies are too complex to maintain
  • use@classDeclare a class forward

    Delay the introduction of header files and use them in.h files when not necessary@classTo declare a class forward and then use it in.m where you need to know the details of the class#import.
  • use@protocolForwards a protocol

    with@classIf you don’t need to know all the details of the protocol in the header file, such as declared attributes or method parameters, return values, and so on, that conform to a protocol@protocolForward declaration of the agreement. However, if the class complies with a protocol, it must#importThe file where the protocol resides.
  • Sometimes it is necessary to introduce additional files into.h
    1. Inheritance, must be in the.h of a subclass#importThe parent class
    2. To comply with a protocol, use@protocolYou can only tell the compiler that there is a protocol, and the compiler needs to know the methods and properties of that protocol.
      • This is inevitable, but it is better to keep the protocol in a separate header file. If the protocol is defined in A header file of some larger class A, then if we want to introduce the protocol, we have to introduce the entire contents of A, which increases compilation time and may even cause cross-reference problems.
      • But you don’t need to write a separate header file for delegate protocol, because delegate protocol only makes sense when it’s defined together with delegate class. In this case, the proxy class should transfer the “this class complies with a protocol” declaration to the class extension, so that the header containing the delegate protocol is simply included in.m, rather than in.h. 【 🚩 27 】
    3. classification
    4. Enumeration, which uses enumerations defined in other classes
  • Every time in the header file#importBefore any other header file, think about whether it is necessary.

3. Use more literal syntax and fewer equivalents

The main points of

  • You should use literal syntax to create strings, numbers, arrays, and dictionaries. This is much more concise than the usual way to create such objects.
  • The element corresponding to an array index or dictionary key should be accessed by subscripting.
  • When you create an array or dictionary using literal syntax, you throw an exception if there is nil in the value. So make sure you don’t have nil in your value.

The author

  • Using literal syntax (syntax sugar) to create strings, numbers, arrays, and dictionaries can reduce code length, make it cleaner, and improve readability.
    NSString *someString = @ "Effective Objective - C 2.0";
    	
    NSNumber *someNumber = @1; // Contains only numeric values and no extra grammatical elements
    	
    NSArray *animals = @[@"cat".@"dog".@"mouse".@"badger"];
    NSString *dog = animals[1]; // The "subscript" operation is more concise and easier to understand, similar to the syntax of other languages
    	
    NSDictionary *personData = @{@"firstName" : @"Matt".@"lastName" : @"Galloway".@"age" : @28};   // 
            
             :
             
              , which is easier to read than the method created with 
              
               , 
               
    NSString *lastName = personData[@"lastName"];
    	
    NSMutableArray *mutableArray = animals.mutableCopy;
    NSMutableDictionary *mutableDictionary = personData.mutableCopy;
    mutableArray[1] = @"dog"; // For mutable arrays or dictionaries, you can change the values of the elements by subscripting
    mutableDictionary[@"lastName"] = @"Galloway";
    Copy the code
  • For arrays and dictionaries, it is safer to use literal syntax. If an object with a nil value exists in the array, an exception will be thrown. And if you useinitWithObjects:If you do, the number of elements in the array will decrease without your knowing it, and the error will be difficult to troubleshoot.
    id obj1 = [[NSObject alloc]init];
    id obj2 = nil;
    id obj3 = [[NSObject alloc]init];
    NSArray *array2 = [[NSArray alloc]initWithObjects:obj1, obj2, obj3, nil];
    // array2 = @[obj1], not @[obj1, obj3]
    // initWithObjects: the method processes each argument in turn until nil is found, which ends early because obj2 is nil
    NSArray *array1 = @[obj1, obj2, obj3];
    // This literal creates an array, which is equivalent to creating an array and then adding all the objects inside square brackets to the array. An exception is thrown if there is nil in the numeric element object.
    // Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]'
    // Similarly, if you add nil to an array using addObject, it will throw an exception
    NSMutableArray *mArr = [NSMutableArray arrayWithObjects:obj1, obj2, obj3, nil];
    [mArr addObject:nil]; // The compiler has given a warning ⚠️ Null passed to a callee that requires a non-null argument
    // Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
    Copy the code
  • Limitations If you are a subclass of a custom NSNumber, NSArray, or NSDictionary class, you cannot create instances using literal syntax. However, since NSNumber, NSArray, and NSDictionary are all established “subfamilies” [🚩 9], few people will define subclasses from them. Custom NSString subclasses support literal syntax, but only if you modify the compiler options.
  • All objects created using literal syntax are immutable objects. If you want mutable objects, you need to call themmutableCopyMethods. Doing so calls one more method and creates one more object (or a mutable object if you use methods), but this disadvantage is outweighed by the benefits of using literal syntax.
    NSMutableArray *mutableArray = [@[@1The @2The @3] mutableCopy];
    Copy the code

4. Use more type constants and less #define preprocessor instructions

The main points of

  • Do not define constants with preprocessor instructions. The constants thus defined have no type information, and the compiler simply finds and replaces them before compiling. Even if someone redefines a constant value, the compiler does not generate a warning, which can lead to inconsistent constant values in the application.
  • Used in implementation filesstatic constTo define a translation-unit-specific constant visible only within the compilation unit. Since such constants are not in the global symbol table, there is no need to prefix their names.
  • Used in header filesexternTo declare a global constant and define its value in the relevant implementation file. This constant is to appear in the global symbol table, so its name should be distinguished, usually prefixed by the class name associated with it.

The author

  • Type constants are used to define constants. Preprocessor directives are not recommended.
  • The difference between a preprocessor instruction and a type constant:
    Preprocessing instruction Type constants
    Simple text substitution
    Type information is not included Includes type information (helps with development documentation by clearly describing the meaning of constants)
    Can be modified at will Cannot be modified
    You can set its use range
    Compile time precompiled compile
    Compile the inspection There is no There are
  • Preprocessing instruction
    #define ANIMATION_DURATION 0.3
    Copy the code
    1. The preprocessor directive replaces all ANIMATION_DURATION in the source code with 0.3.
    2. This constant has no type information. It should be of type NSTimeInterval.
    3. Assuming this directive is declared in a header file, all code that introduces this header file will have its ANIMATION_DURATION replaced by 0.3 at compile time.
  • Type constants
    static const NSTimeInterval kAnimationDuration = 0.3;
    Copy the code
    1. The constant kAnimationDuration contains type information, which is type NSTimeInterval (including type information).
    2. The static modifier means that the constant is only visible in the.m where it is defined (setting its scope).
    3. The const modifier means that the constant is unmodifiable (unmodifiable).
  • Type constant nomenclature
    1. If the constant is confined to a “compilation unit” (i.e..m), the name is preceded by a letterk, such askAnimationDuration.
    2. If a constant is defined as a global constant that is visible outside the class, it is usually defined asThe name of the classAs a prefix, for exampleEOCViewClassAnimationDuration.
  • Local type constant
    static const NSTimeInterval kAnimationDuration = 0.3;
    Copy the code
    1. Always use both static and const declarations. Instead of creating symbols, the compiler performs value substitution like a preprocessor instruction.
    2. If you try to modify a variable that is modified by const, the compiler reports an error.
    3. If static is not used, then the compiler creates a “external symbol”. If a variable with the same name is declared in another compilation unit, the compiler throws a “double-defining symbol” error:
    duplicate symbol _kAnimationDuration in:
        EOCAnimatedView.o
        EOCOtherView.o
    Copy the code
    1. Local type constants are not placed in the global symbol table, so they do not need to be prefixed with the class name.
  • Global type constants
    // In the header file
    extern NSString *const EOCStringConstant;
    	
    // In the implementation file
    NSString *const EOCStringConstant = @"VALUE";
    Copy the code
    1. Such constants are placed in the “global symbol table” so that they can be used outside the compilation unit in which they are defined.
    2. We define a pointer constant, EOCStringConstant, that points to NSString. In other words, EOCStringConstant is not going to point to another NSString.
    3. Extern tells the compiler that there will be a symbol called EOCStringConstant in the “global symbol table.” The compiler then allows code to use the constant. Because it knows that when it links into a binary, it will find this constant.
    4. Must be defined, and only once, usually in the.m corresponding to the.h in which the constant is declared.
    5. When the implementation file generates an object file (the compiler outputs an object file every time it receives a “compile unit”.m), the compiler allocates storage space for strings in the “data segment.” The linker links this object file with other object files to produce the final binary. Wherever the global symbol EOCStringConstant is used, the linker can parse it.
    6. Because symbols are placed in the global symbol list, constants must be named with care. To avoid name conflicts, they are usually prefixed with the class name.

5. Use enumerations to represent states, options, and status codes

The main points of

  • Enumerations should be used to represent the state of the state machine, the options passed to the method, and the status code equivalents, giving these values understandable names.
  • If the options passed to a method are represented as enumerated types and more than one option can be used at the same time, the option values are defined as powers of 2 so that they can be combined by bits or operations.
  • withNS_ENUM 与 NS_OPTIONSMacro to define an enumeration type and specify its underlying data type. This ensures that the enumeration is implemented using the underlying data type chosen by the developer, rather than the type chosen by the compiler.
  • Do not implement the default branch in switch statements that deal with enumerated types. In this case, when a new enumeration is added, the compiler will prompt the developer that the switch statement does not handle all enumerations.

The author

  • States, options, and status code equivalents should be represented by enumerations, and these values should be given easy-to-understand names.
  • The original writing
    enum EOCConnectionState {
        EOCConnectionStateDisconnected,
        EOCConnectionStateConnecting,
        EOCConnectionStateConnected,
    };
    // Declare variables
    enum EOCConnectionState state = EOCConnectionStateDisconnected;
    Copy the code
  • Defining type aliases
    // Typepedef defines a type alias
    typedef enum EOCConnectionState EOCConnectionState;
    // Declare variables
    EOCConnectionState state = EOCConnectionStateDisconnected;
    Copy the code
  • Instead of using the compiler’s ordinal number, you can specify it manually, and subsequent enumerations will increment by 1 from the value you specify
    enum EOCConnectionState  {
        EOCConnectionStateDisconnected = 1,
        EOCConnectionStateConnecting,
        EOCConnectionStateConnected,
    };
    Copy the code
  • Specifies the underlying data type of the enumeration
    • Previously, the data type used to implement the enumeration was up to the compiler, but the number of bits had to fully represent the enumeration number. For example, the above enumeration has a maximum number of 3, so use a char type of 1 byte. Disadvantages: You can’t declare an enumeration variable forward, because the compiler doesn’t know the size of the underlying data type, so when you use the enumeration type, you don’t know how much space to allocate to the variable.
    • C++11 standard revision to specify what “underlying data types” are used to hold variables of enumerated types. In this way, we can declare the enumeration variable forward, which is written as:
      enumEOCConnectionState:NSInteger {
          EOCConnectionStateDisconnected,
          EOCConnectionStateConnecting,
          EOCConnectionStateConnected,
      };
      // Forward declaration
      enumEOCConnectionState:NSInteger;
      Copy the code
  • Define options with an enumeration

    The options can be combined by a bitwise or operation
    enum UIViewAutoresizing  {
        UIViewAutoresizingNone                  = 0.UIViewAutoresizingFlexibleLeftMargin,   = 1 << 0.UIViewAutoresizingFlexibleWidth,        = 1 << 1.UIViewAutoresizingFlexibleRightMargin,  = 1 << 2};typedef enum UIViewAutoresizing UIViewAutoresizing;
    
    UIViewAutoresizing resizing = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleWidth;
    if (resizing & UIViewAutoresizingFlexibleLeftMargin) {... }Copy the code
  • Helper macros for enumerations are defined in Foundation
    1. You can use these macros to specify the underlying data type used to hold the enumeration values.
    2. These macros are also backward-compatible — new syntax if the target platform’s compiler supports the new standard, old style if not.
    3. These macros are defined with the #define precompile directive, one for the state enumeration type and one for the Options enumeration type.
    typedef NS_ENUM(NSUInteger, EOCConnectionState) {
        EOCConnectionStateDisconnected,
        EOCConnectionStateConnecting,
      	EOCConnectionStateConnected,
    };
    	
    typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection) {
        EOCPermittedDirectionUp    = 1 << 0,
        EOCPermittedDirectionDown  = 1 << 1,
        EOCPermittedDirectionLeft  = 1 << 2,
        EOCPermittedDirectionRight = 1 << 3};Copy the code
  • NS_ENUM, NS_OPTIONS macro definitions (I won’t post them here)
  • NS_OPTIONS should be used to define the Options type enumeration
    1. Improve the generality, avoid type strong operation

      NS_OPTIONS differs in C++ compilation mode from non-c ++ compilation mode

      • In non-C ++ mode, it is the same as NS_ENUM
      • In C++ mode, if NS_ENUM is pressed, there will be a compilation error. The reason is that, for example, C++ thinks that the data type of the bitwise or operation result should be the underlying data type of the enumeration, NSUInteger, and C++ does not allow this underlying type to be “implicitly converted” to the enumeration type itself, EOCPermittedDirection. So the following code will compile error in C++ mode or objective-c ++ mode. Cannot initialize a variable of type ‘EOCPermittedDirection’ with an rvalue of type ‘int’)
      EOCPermittedDirection permittedDirections = EOCPermittedDirectionLeft | EOCPermittedDirectionRight;Copy the code

      So in C++ mode, NS_OPTIONS should be used to define the “options” type enumeration because it is # defined differently in C++ and non-c ++ mode.

    2. Improved readability
  • Do not implement the default branch in switch statements that deal with enumerated types. This way, if we add a new enumeration type, the compiler will warn us that the new enumeration type is not handled in the switch. If the default branch is added, no warning is given. This does not ensure that the switch statement handles all enumerated types correctly.
  • withNS_ENUM ε’Œ NS_OPTIONSTo define an enumeration type and specify its underlying data type. This ensures that the enumeration is implemented using the underlying data type chosen by the developer and not the type chosen by the compiler.

Chapter 2: Objects, Messages, runtimes

The ability of objects to relate and interact with each other is an important feature of object-oriented languages. This chapter covers these characteristics and delves into the behavior of the code at runtime.

6. Understand the concept of “properties.

The main points of

  • You can use the @ Property syntax to define the data encapsulated in an object.
  • Attributes are used to specify the correct semantics for storing data.
  • When setting the instance variable to which a property corresponds, be sure to follow the declared semantics of the property.
  • Nonatomic properties should be used when developing iOS applications, because atomic properties can seriously affect performance.

The author

  • Properties are used to encapsulate data in an object. You can use@propertySyntax to define attributes.
  • @synthesize ε’Œ @dynamic
    1. Can be achieved by@synthesizeTo specify the instance variable name, if you don’t like the default instance variable name beginning with an underscore. But it’s best to use the default, otherwise people may not understand.

      If you don’t want the compiler to synthesize access methods, you can do it yourself. If you implement only one of the accessor setters or getters, the other one will still be synthesized by the compiler. Note, however, that if you implement all the methods required for the property (the setter and getter for readWrite, and the getter for readOnly), then the compiler will not do it automatically@synthesizeIn this case, the instance variable of this property will not be generated, and you need to manually according to the actual situation@synthesizeOnce in a while.
      @synthesize name = _myName;
      Copy the code
    2. @dynamicIt tells the compiler not to automatically create instance variables used to implement properties and not to create access methods for them, telling the compiler to do this yourself. When using the@dynamicEven if you do not implement an access method for it, the compiler will not report an error because you have told it to do it yourself.
  • The types of attributes that specify the correct semantic attribute keywords for storing data through attributes are atomicity, read and write permissions, memory management semantics, method names, and nullability introduced by Xcode 6.3. Link: OC – Attribute Keywords and Ownership Modifiers
  • When setting the instance variable to which a property corresponds, be sure to follow the declared semantics of the property

    Note: Try not to call access methods in initialization methods and dealloc methods. 【 7 】 🚩

    @property (nonatomic.copy) NSString *name;
    
    β€” (instancetype) initWithName:(NSString *)name {
        if(self = [super init]) {
            _name = [name copy];
        }
       	return self;
    }
    Copy the code

    If you implement the access method yourself, you should also ensure that it has the properties declared by the related properties.

  • When developing iOS applications, should usenonatomicRather thanatomicBecause theatomicPerformance can be severely affected.

7. Try to access instance variables directly from within the object

The main points of

  • When reading data inside an object, it should be read directly through instance variables, and when writing data, it should be written through properties.
  • In initialization methods and dealloc methods, you should always read and write data directly through instance variables.
  • Sometimes a piece of data is configured using lazy initialization techniques, in which case the data needs to be read through properties.

The author

  • The difference between direct access to instance variables and access through properties:
    1. Direct access to instance variables is faster, and code generated by the compiler accesses the memory that holds the object instance variables directly, rather than calling an access method to access them.
    2. Instance variables are accessed directly without calling setter methods, bypassing the “memory management semantics” defined for the related properties. For example, if you have a property that’s declared as copy, and you access that property for assignment, it will copy the new object and release the old object. Direct access to instance variables is to retain the new object and release the old one.
    3. Direct access to instance variables does not trigger KVO, because KVO is notified to all observers by generating derived classes and overriding setter methods at run time. Whether this is a problem depends on the specific object behavior.
    4. Using property methods helps you troubleshoot errors associated with them, because you can set breakpoints in setter and getter methods to debug.
  • Access methods are not recommended in initialization methods and dealloc methods, and instance variables should be accessed directly.
    • Why are access methods not recommended for initialization methods?

      Because subclasses might override setter methods and that might cause some problems.

      Suppose the EOCPerson class has a subclass called EOCSmithPerson that specifically represents people whose last name is “Smith.” This subclass may override the lastName property’s setting method:

      - (void)setLastName:(NSString *)lastName {
          if(! [lastName isEqualToString:@"Smith"{[])NSException raise:NSInvalidArgumentException format:@"Last name must be Smith"];
          }
          self.lastName = lastName;
      }
      Copy the code

      In the default initialization method of the parent class EOCPerson, you might set the lastName to the empty string self.lastname = @””. If you do this by a setter method, then the subclass’s setter method will be called, causing an exception to be thrown. Why is it calling a setter method on a subclass? Because calling [super init] in a subclass initializes something in the superclass first, super’s principle is to start from the superclass to find the implementation of the method, and the recipient is still the subclass. That is, calling [self setLastName] in the init method of the parent class where self is a subclass object, and calling the subclass because the subclass overrides the method. Link: Runtime 4: The Nature of Super

    • However, there are cases where you must call an access method in the initialization method:
      1. The instance variable to be initialized is declared in the parent class, so we can’t access it directly in the subclass, so we can only call the setter method.

        Since subclasses cannot directly access instance variables declared in the parent class, how can we avoid this problem? The parent class initializer accesses the instance variable directly, and the child class initializer accesses the instance variable directly through the setter method.

      2. If the property is lazily loaded, it must be accessed through the getter method or the instance variable will never be initialized.
  • The two access schemes have their advantages and disadvantages, and the compromise scheme is as follows:

    When reading data inside an object, it should be read directly through instance variables, and when writing data, it should be written through properties. This can improve the speed of the read operation and control the write operation to the property. This is because writing data using instance variables bypassed the “memory management semantics” defined for the related properties. If we write data directly by accessing an instance variable, it should be set according to its memory management semantics. For example, the name attribute declared as copy:_name = [name copy].

8. Understand the concept of “object equality”

The main points of

  • To detect the equivalence of objects, please provide”isEqual:“And”hash“Approach.
  • The same object must have the same hash code, but two objects with the same hash code are not necessarily the same.
  • Do not blindly check each attribute, but should be based on the specific requirements of the test plan.
  • When writing a hash method, you should use an algorithm that computations quickly and has a low probability of hash code collisions.

The author

  • = =The operator compares the two Pointers themselves, not the objects to which they point. So sometimes that’s not what we want.

    It should be declared in the NSObject protocolisEqual:Method to determine the equality of two objects. Some objects provide special “equality determination methods,” such as NSString’sisEqualToString:, NSArrayisEqualToArray:And NSDictionaryisEqualToDictionary:.
    • isEqualToString:The argument must be of type NSString, otherwise the result is undefined, but no exception is thrown.
    • isEqualToArray: ε’Œ isEqualToDictionary:The arguments to must also be of type NSArray and NSDictionary, otherwise an exception will be thrownreason: '*** -[NSArray isEqualToArray:]: array argument is not an NSArray'.

    We can also write our own “equality determination method” according to the needs of our class.

  • Let’s look at some code
    NSString *foo = @"Badger 123";
    NSString *bar = [NSString stringWithFormat:@"Badger %i".123];
    BOOL equalA = (foo == bar);               // equalA = NO
    BOOL equalB = [foo isEqual:bar];          // equalB = YES
    BOOL equalC = [foo isEqualToString:bar];  // equalC = YES
    Copy the code

    = =The operator determines that foo and bar are unequal strings because it is comparing Pointers;

    callisEqualToString:Method ratio callisEqual:Method has to be fast because the latter does an extra step and doesn’t know the type of the object being tested (compared). Whereas isEqualToString: knows that the message receiver and the parameter are compared as NSString.

  • The NSObject protocol defines two methods for determining equality.
    - (BOOL)isEqual:(id)object; - (NSUInteger)hash;
    Copy the code

    The default implementation of these two methods is that the two objects are equal if and only if their pointer values are exactly equal. That is, the default implementation is equivalent to= =The operator

  • If you want to properly override these two methods to implement the “equality determination” of a custom object, you must follow these rules:
    1. ifisEqual:Method to determine that two objects are equal, thenhashMethod must return the same value.
    2. However, if two objectshashMethod returns the same valueisEqual:The method doesn’t necessarily think that they’re equal.

    Here are some examples:

    @interface Person : NSObject
    @property (nonatomic.copy) NSString *firstName;
    @property (nonatomic.copy) NSString *lastName;
    @property (nonatomic.assign) NSInteger age;
    @end
    
    @implementation Person
    
    - (BOOL)isEqual:(id)other {
    
        if (other == self) return YES;
        if ([self class] != [other class]) return NO; // Sometimes we might think that an instance of Person can be equal to an instance of its subclass, so consider this case
    
        Person *otherPerson = (Person *)other;
        if(! [_firstName isEqualToString:otherPerson.firstName])return NO;
        if(! [_lastName isEqualToString:otherPerson.lastName])return NO;
        if(_age ! = otherPerson.age)return NO;
    
        return YES;
    }
    
    - (NSUInteger)hash {
    
        NSUInteger firstNameHash = [_firstName hash];
        NSUInteger lastNameHash  = [_lastName hash];
        NSUInteger ageHash = _age;
        return firstNameHash ^ lastNameHash ^ ageHash;
    }
    
    @end
    Copy the code
  • The efficiency of the overridden hash method

    According to the equality convention, if two objects are equal, their hashes are equal, but objects with equal hashes are not necessarily equal. So it makes sense to rewrite the hash as follows:
    - (NSUInteger)hash {
        return 1337;
    }
    Copy the code

    Writing this way, however, using such objects in a collection (Array, Dictionary, Set) would cause performance problems because the collection would use the hash of the object as its index when retrieving the hash table. If a collection is implemented using a set, the set might divide the objects into different “bin arrays” based on the hash. When you add a new object to a set, you find the array associated with it based on its hash and check each file in turn to see if the existing object in the array is equal to the object to be added. If it is, the object to be added is already in the set. Therefore, if every object returns the same hash, then if you want to add objects to a set that already has 1000000 objects, you need to scan all the 1000000 objects, because all the objects with the same hash are in an array.



    The following works, but there is the overhead of creating a string, so it is slower than returning a single value. Adding such objects to the collection can also have performance problems, since they must be hashed before they can be added.

    - (NSUInteger)hash {
        NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName, lastName, _age];
        return [stringToHash hash];
    }
    Copy the code

    Best practices are as follows. This approach maintains high efficiency while keeping the generated hash at least within a certain range and not repeating too often. Of course, the hash generated by this algorithm will still collide, but at least you can guarantee that there are many possible values for the hash. When writing a hash method, you should experiment with the current object to choose between reducing the frequency of collisions and reducing the complexity of the operation.

    - (NSUInteger)hash {
        NSUInteger firstNameHash = [_firstName hash];
        NSUInteger lastNameHash  = [_lastName hash];
        NSUInteger ageHash = _age;
        return firstNameHash ^ lastNameHash ^ ageHash;
    }
    Copy the code
  • We can also write our own “method of determining equality”.
    1. Benefits:
      1. No need to detect parameter types, can greatly improve the detection speed;
      2. Self-defined method names can be more aesthetically pleasing and easier to read, like NSString’s isEqualToString: and so on.
    2. When writing your own “method for determining equality,” you should also rewrite the isEqual: method. This method is usually implemented as follows:
      - (BOOL)isEqualToPerson:(Person *)otherPerson {
          if (self == object) return YES;
      
          if(! [_firstName isEqualToString:otherPerson.firstName])return NO;
          if(! [_lastName isEqualToString:otherPerson.lastName])return NO;
          if(_age ! = otherPerson.age)return NO;
      
          return YES;
      }
      
      - (BOOL)isEqual:(id)other {
          if ([self class] == [other class]) {
              return [self isEqualToPerson:(Person *)other]
          } else {
              return [superisEqual:other]; }}Copy the code
    3. When you create an equality determination method, you need to decide whether to determine equality based on the entire object or just a few of its fields — the depth of the execution of the equality determination.
  • NSArray isEqualToArray:
    1. Let’s see if the two arrays have the same number of objects.
    2. If they are, the isEqual: method is called on both objects at each corresponding location.
    3. If all the objects in the corresponding position are equal, then the two arrays are equal. This is called the “depth equality determination.”
  • Sometimes we may only judge the equality according to the identifier or some fields, so whether we need to detect all fields in the equality determination method depends on the tested object. We can write an equality determination based on the circumstances under which two object instances should be judged equal.
  • When an object is placed in the container, the hash value should not be changed, otherwise it will cause problems. Because a collection divides objects into different “bin arrays” based on their hashes, if an object’s hash changes after it is placed in a bin, the bin it is in is “wrong” for it. So, make sure that the hash of the object added to the container is not calculated against the “mutable part” of the object, or that the contents of the object are not changed later. Therefore, it is best not to add mutable objects such as NSMutableArray to the container, as shown in the example in the book.

9. Hide implementation details in “family pattern”

The main points of

  • The class family pattern can hide implementation details behind a simple set of public interfaces.
  • Class families are often used in system frameworks.
  • Take care to subclass from the public abstract base class of a class family, and read the development documentation first, if it exists.

The author

  • Class family is a design pattern that defines an abstract base class, uses “class family” to place concrete behavior in its subclasses, and hides their implementation details behind the abstract base class to keep the interface concise.
  • The significance of the class family pattern is that the user does not have to worry about the specific type of the instance being created, nor does he have to worry about the implementation details, but simply calls the base class method to create it. You can read a book for specific examples.
  • UIButton uses a class family, buttonWithType: The type of button returned by the method depends on the type passed in. A “factory pattern” like this is one way to create a class family. In addition, most collection classes such as NSArray and NSMutableArray use the class family pattern.
  • If the class of an object is in a class family, you should be careful when querying its type. You should useisKindOfClass:Rather thanisMemberOfClass:or[A class] == [B class].
  • Since OC has no way to specify that a base class is “abstract,” you should specify the use of the class in the documentation to tell the user that this is an abstract base class of the class family. Instead of using methods such as init to instantiate an abstract base class, you should use the specified methods to create an instance. We can also throw an exception in a base method to remind the user that this is an abstract base class of the family, but this is an extreme approach and is rarely used.
  • Be careful when adding entity subclasses to a class family. If there is a development document, read it first to understand the rules.

    For example, to add a subclass of the NSArray family, follow the following rules:
    1. A subclass should inherit from an abstract base class in the class family;
    2. Subclasses should define their own way of storing data;
    3. Subclasses should override the methods specified in the superclass documentation to overridecount 及 objectAtIndex:.

10. Use associated objects in existing classes to store custom data

The main points of

  • Two objects can be linked through the “associated objects” mechanism.
  • When defining associated objects, you can specify memory management semantics that mimic the “owning” and “non-owning” relationships used to define attributes.
  • Use associated objects only when nothing else is possible, because this often introduces bugs that are hard to find.

The author

  • When you need to store relevant information in an object, and it is not possible to store values in the instance of a subclass by inheritance, you can create a category of the class to which the object belongs and store values by associating objects in the category.
  • Associated object related API:
    1. Set the associated object value for an object with the given key and policy:
    void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    Copy the code
    1. Gets the corresponding associated object value from an object based on the given key:
    id objc_getAssociatedObject(id object, void *key)
    Copy the code
    1. Removes all associated objects of a specified object
    void objc_removeAssociatedObjects(id object)
    Copy the code
    1. The association policy is defined by the enumeration named objc_AssociationPolicy, which corresponds to the attribute keyword as follows:
    Association types The equivalent @property property
    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
    OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
    OBJC_ASSOCIATION_RETAIN retain
    OBJC_ASSOCIATION_COPY copy
  • Since I have written a detailed article on “associated objects” before, I won’t make a specific summary here. For those interested, see Link: OC Low-level Exploration – Association Associated Objects.

11. Understand the role of objc_msgSend

The main points of

  • A message consists of a receiver, selector, and parameters. To “send a message” to an object is to “call a method” on that object.
  • All messages sent to an object are handled by the Dynamic messaging dispatch system, which finds the corresponding method and executes its code.

The author

  • Static and dynamic binding:
    • The C language uses “static binding” for function calls. At compile time you can decide which functions to call at run time. If “inlining” is not taken into account, the compiler already knows which functions are in the program when it compiles the code, so it generates instructions to call those functions, and the addresses of the functions are actually hard-coded in the instructions.
    • In OC, if a message is passed to an object, a “dynamic binding” mechanism is used to determine which method to call, which is determined at run time.
  • In OC, the syntax for sending a message to an object is:
    id returnValue = [someObject messageName:parameter];
    Copy the code

    Here, someObject is called”The receiver“, the messageName is called”Select the son, the selector and the parameter together are calledThe message“. When the compiler sees this message, it converts it into a standard C function call that is the core function of the message mechanismobjc_msgSend:

    void objc_msgSend(id self, SEL _cmd, ...)
    Copy the code

    This function takes a variable number of arguments and can take two or more arguments. The first two arguments “self message receiver” and “_cmd selector” are the two implicit arguments to the OC method, and the subsequent arguments are those in the message (that is, the explicit method arguments).

    Method calls in OC are converted to this function call after compilation. For example, after the above method call is converted to:

    id returnValue = objc_msgSend(someObject, @selector(message:), parameter);
    Copy the code
  • Understanding the execution process of the objc_msgSend function Link
  • Called methods are cached, which makes method lookups faster.
  • Other “boundary case (special case)” messages are handled by other functions, as explained briefly below:
    • Objc_msgSend_stret: Messages to be sent return structures
    • Objc_msgSend_fpret: Messages to be sent return floating point numbers
    • Objc_msgSendSuper: Sends a message to the super class, [super messege:parameter]

    Link: Runtime 4: The Nature of Super

  • Understand the “tail call optimization” technique.

12. Understand the message forwarding mechanism

The main points of

  • Object cannot respond to a selector, and enters the message forwarding process.
  • With dynamic method parsing at runtime, we can add this method to the class when it is needed.
  • An object can transfer some of its selectors that it cannot read to other objects for processing.
  • After these two steps, if the selector still cannot be processed, the full message mechanism is turned on.

The author

  • This section describes in detail the “message forwarding” phase of the OC message mechanism. When an object fails to process a message during the “message send” phase (no method implementation can be found), it enters the “message forward” phase, where the developer can process unknown messages.
  • The message mechanism can be divided into three stages: message sending, dynamic method parsing, and message forwarding. The authors of the book have merged “dynamic method parsing” into the “message forwarding” phase.
  • Message forwarding is divided into two stages:
    • Dynamic method parsing: Processes unknown messages by dynamically adding method implementations.
    • The real message forwarding stage is divided into two stages: Fast and Normal:
      • Fast: Find an alternate receiver and try to forward the unknown message to the alternate receiver for processing.
      • Normal: Starts full message forwarding, encapsulating all the details about the message into the NSInvocation object, and giving the receiver one last chance to process the unknown message.
  • Dynamic method analysis
    + (BOOL)resolveInstanceMethod:(SEL)selector;
    + (BOOL)resolveClassMethod:(SEL)selector
    Copy the code

    We can enable “dynamic method resolution” by implementing the above method in the message receiver class, depending on whether it is an instance method or a class method, and process unknown messages by dynamically adding method implementations. The method parameters are selectors for the unknown message, and the return value indicates whether the class has been dynamically added to the method implementation for processing (in fact, the return value is only used to determine whether the information is printed, which doesn’t matter much, but it should be written properly anyway).

    “Dynamic method parsing” is often implemented@dynamicProperty, dynamically adding an implementation of the setter and getter methods for the property at run time. The book has a complete example of how to implement the @dynamic property with dynamic parsing, which is for those interested to check out.

  • forward
    • Fast – Find an alternate receiver:
    (+ / -id)forwordingTargetForSelector:(SEL)selector;
    Copy the code

    The method parameter is the selector for the unknown message, and the return value is the alternate receiver.

    Through this scheme, we can use “composition” to simulate some features of “multiple inheritance”. Inside an object, there may be a series of other objects that can be returned by this method with related internal objects capable of handling a selector, so that it appears to the outside world as if the object itself handled the messages.

    Note that the content of the unknown message cannot be modified at this stage, if necessary, it should be processed in the Normal stage.

    • Normal – Complete message forwarding:
    (+ / -NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    Copy the code

    By implementing the above method to return a method signature suitable for the unknown message, Runtime will create a method that encapsulates the entire contents of the unknown message (target, selector, argument) based on the method signatureNSInvocationObject. Then call the following method and pass in the NSInvocation object as a parameter.

    (+ / -void)forwardInvocation:(NSInvocation *)invocation;
    Copy the code

    You only need to change the target in this method, but you don’t normally do that because it’s not as good as doing it in the Fast phase. A more useful implementation is to change the message content, such as changing the selector, append a parameter, and then forward it to another object for processing.

  • When implementing the above method, the unknown message should not be handled by this class, but the implementation of the parent class should be called, so that every class in the inheritance system has a chance to process the unknown message, all the way down to NSObject.
  • The default implementation of NSObject is the final calldoseNotRecognizeSelectorMethod and throws a well-known exceptionunrecognized selector send to instance/class, indicating that the unknown message was ultimately not processed.
  • Each of these phases has the opportunity to process the message, but the earlier the message is processed, the better the performance.
    • It is better to do this during the “dynamic method resolution” phase so that Runtime can cache the method so that the message forwarding process does not have to start when the instance receives the same message again later.
    • If you simply want to forward the message to the alternate recipient in the “message forward” phase, it is best to do so in the Fast phase. Otherwise you have to create and process the NSInvocation object.
  • For more information on the process of method caching and messaging mechanisms, read the author’s blog: Runtime 2: Data Structures

13. Debug the Black Box Method using Method Deployment Techniques

The main points of

  • At run time, you can add or replace the method implementation of the selector to the class.
  • Replacing an original method implementation with another implementation is a process called “method blending,” and developers often use this technique to add new functionality to an original implementation.
  • In general, you only need to modify the method implementation at run time to debug the program, and this practice should not be abused.

The author

  • This article is a novel title, in fact, using the Runtime dynamic exchange method (method swizzling).
  • throughmethod swizzlingWe can change the functionality of the class itself without modifying the source code or overwriting the methods by subclass. You can add logging to black box methods that are “completely opaque” and can be very useful for debugging. But it should be used properly, not just because it is a feature of OC, because overusing it can make the code difficult to read and maintain.
  • And the way to do that isSEL 到 IMPThe mapping. When we call the method, we are actually looking for the IMP based on the method SEL. The IMP is a function pointer to the method implementation.
  • Do not forget to #import <objc/runtime.h>.
    Method originalMethod = class_getInstanceMethod([NSString class].@selector(lowercaseString));
    Method swappedMethod  = class_getInstanceMethod([NSString class].@selector(uppercaseString));
    method_exchangeImplementations(originalMethod, swappedMethod);
    Copy the code

    In general, such as the above direct exchange of two methods of implementation, meaning is not. We can go throughmethod swizzlingTo add new functionality to existing method implementations. Let’s say we call lowercaseString to record something like this:

    Method originalMethod = class_getInstanceMethod([NSString class].@selector(lowercaseString));
    Method swappedMethod  = class_getInstanceMethod([NSString class].@selector(eoc_myLowercaseString));
    method_exchangeImplementations(originalMethod, swappedMethod);
    
    - (NSString *)eoc_myLowercaseString {
        NSString *lowercase = [self eoc_myLowercaseString];
        NSLog(@ % @ = > % @ "".self, lowercase);
        return lowercase;
    }
    Copy the code

    Rest assured, the above code does not recursively call the loop, because after the method exchange is implemented, the SEL of eoc_myLowercaseString corresponds to the IMP of the lowercaseString method.

14. Understand the meaning of “class objects.

The main points of

  • Each instance has a pointer to a Class object to indicate its type, and these Class objects form the Class inheritance system.
  • If the object type cannot be determined at compile time, then the type information query method should be used to find out.
  • Try to use the type information query method to determine object types rather than directly comparing class objects, as some may implement message forwarding.

The author

  • About ID types:
    • Id can refer to any OC object type. In general, you should specify the specific type of message receiver so that the compiler will warn you if you send an unreadable message to the object. An object of type ID does not, because the compiler assumes that it can respond to all messages.
    • Id is just a pointer to the objC_object structure.
      // A pointer to an instance of a class.
      typedef struct objc_object *id;
      Copy the code
  • The underlying structure of each OC object isobjc_objectStructure. The underlying structure of both classes and metaclass objects isobjc_classStructure, which is inherited fromobjc_objectBetween them by”is a“Pointer contact.

    For detailed explanation of the underlying data structure of OC objects, ISA Pointers, etc., please refer to:

    Link: Runtime 2: Data Structures
  • The super_class pointer establishes the inheritance relationship, while the ISA pointer describes the class to which the instance belongs.
  • The compiler cannot determine whether an object of a type can handle an unknown message because it can be added dynamically at runtime. But even then, the compiler feels that it should at least have a declaration of the method from which to understand the full Type Encoding and generate the correct code to send the message.
  • Type information query method – View object types at run time.
    - / + (BOOL)isMemberOfClass:(Class)cls; - / + (BOOL)isKindOfClass:(Class)cls;
    Copy the code

    isMemberOfClass:Method is used to determine the current instance/class objectisaRefers to a class/meta-class object type (that is, whether the current object is an instance of a class);

    isKindOfClass:Method is used to determine the current instance/class objectisaRefers to whether a class/meta-class object or its subclass type (that is, whether the current object is an instance of a class or its subclass).

    If you are interested in the following interview question, you will find out more about these two methods.

    Runtime (5) : Related Interview Questions

  • Because OC is a dynamic runtime language, the “type information query method” is very useful. When retrieving objects from a Collection (Array, Dictionary, Set), it is common to query for type information because they are usually fetched with id type rather than “strongly typed”. Query the type information to avoid accidentally calling a method that the object of the type does not respond to and causing Crash. For example:
    for (id object in array) {
        if ([object isKindOfClass:[NSString class]]) {
            NSString *string = (NSString *)object;
            NSString*uppercaseString = [string uppercaseString]; . }}Copy the code

    If we omit the “type information query” step and just call[object uppercaseString];If a non-NSString instance is accidentally stored in the array, then Crash will occur because the unknown message cannot be responded to (that is, the method implementation cannot be found).

    It can also be used directly by comparing class objects for equivalence= =You don’t have to use the operatorisEqual:Methods. The reason is that class objects are “singletons” and there are no classes with the same name within the scope of the application. Using isEqual: The method only incurs unnecessary performance overhead.

    if ([object class] = = [NSString class]) {... }Copy the code
  • In the program, try not to directly compare the class of the object, but call the “type information query method” to determine the type of the object. Because the latter can properly handle objects that use message forwarding.

    For example, an object might forward all the selectors it receives to another object. Such objects are called proxies, and they have NSProxy as their root class.

    If the class method is called on such a proxy object, it returns the class of the proxy object itself, not the class to which the object of the accepted proxy belongs. For example, HTProxy inherits from NSProxy and forwards messages to an instance of class called HTPerson. Now we have an instance of HTProxy, so:
    Class cls = [proxy class];
    Copy the code

    CLS is HTProxy, not HTPerson. If the type information query method is used instead, the proxy object forwards this message to the “object that received the proxy.”

    BOOL res = [proxy isKindOfClass:[HTPerson class]].Copy the code

    The value of res is YES.

    As a result, the two methods query for different types of objects.

Chapter 3: Interface and API design

There is very little code that can be written and then reused. Even if the code is not made public to more people, it is still possible to use it in more than one of your own projects. This chapter shows you how to write classes that work well with Objective-C.

15. Use prefixes to avoid namespace conflicts

The main points of

  • Choose a name associated with your company, your application, or both as a prefix for the class name, and use that prefix in all your code.
  • If your library uses a third party library, you should prefix the name of the library.

The author

  • Use prefixes to avoid namespace conflicts. OC does not have the built-in namespace mechanism of other languages, so we should be careful when naming names. If a naming conflict occurs, the linking process of the application will go awry due to the presence of duplicate symbols. For example, if a class named Person is implemented in two files in the project, the class symbol and metaclass symbol for Person are defined twice, resulting in a compilation error:
    duplicate symbol '_OBJC_CLASS_$_Person' in: /Users/... /x86_64/Person.o /Users/... /x86_64/Person duplicate symbol'_OBJC_METACLASS_$_Person' in:
        /Users/.../x86_64/Person.o
        /Users/.../x86_64/Personηš„ε‰―ζœ¬.o
    ld: 2 duplicate symbols for architecture x86_64
    Copy the code

    This is mostly because we have introduced two libraries with the same named classes into the current project.

    Worse than the failure of the compiler to link is loading the library with the same class at run time, causing the program to Crash.

  • How to name it? With what prefix?
    • If your company is called Effective Widgets, you can use EWS as a prefix in the part of your code that is used in all your applications.
    • If you have code that is only used in a Browser project called Effective Browser, prefix that part of the code with EWB.
    • Note that Apple says it reserves the right to use all “two-letter prefixes,” so we’d better choose a three-letter prefix.

    If you don’t follow this rule, for example, if you use the letter “TW” as a prefix, then you have a problem. The Twitter framework, which was released with the iOS 5.0 SDK, is prefixed with TW. This makes it very likely that there will be repeated sign errors.

  • All names in the application, not just class names, should be prefixed
    • Classification and methods of classification. [🚩 25] explains why.
    • Class implementation files used in pure C functions and global variables. These names are counted as “top-level symbols” in compiled object files.
  • If your library uses a third party library, you should prefix the name of the library.
    • This is especially true if you are using third-party libraries (managed by manual import rather than Cocoapods) and are ready to redistribute them as libraries for others to develop and use. Because your library contains a tripartite library that may also be introduced by someone else’s application itself, or by other tripartite libraries that it has introduced, it is easy to make duplicate notation errors.
    • For example, if you are releasing a library called EOCLibrary and introduce a third party library named XYZLibrary, then all names in XYZLibrary should be labeled EOC.

16. Provide “all-purpose initialization methods”

The main points of

  • Provide a universal initialization method in the class and specify it in the documentation. All other initialization methods should call this method.
  • If the universal initialization method is different from the superclass, the corresponding method in the superclass needs to be overridden.
  • If the initialization method of the superclass does not apply to the subclass, then the superclass method should be overridden and an exception should be thrown in it.

The author

  • There may be more than one method for initializing an instance of a class. We want to select one as a universal initializer and let all other initializers call this method. In this way, if the initialization operation changes, only the all-purpose initialization method needs to be changed, and no other initialization methods need to be changed.
  • Make some changes to initialization methods like init

    For example, we specify the initialization method of the rectangle class EOCRectangle to be:
    - (instancetype)initWithWidth:(float)width andHeight:(float)height {
        if (self = [superinit]) { _width = width; _height = height; }}Copy the code
    // 1. Use the macro NS_UNAVAILABLE to disable the initialization method
    + (instancetype)new NS_UNAVAILABLE;
    - (instancetype)init NS_UNAVAILABLE;
    // 2.Using default values
    - (instancetype)init {
        return [self initWithWidth:5.0 andHeight:10.0];
    }
    // 3.Throwing an exception
    - (instancetype)init {
        @throw [NSException exceptionWithName:NSInternalInconsistencyException 
                       reason:@"Must use initWithWidth:andHeight: instead." userInfo:nil];
    }
    Copy the code
  • The square class EOCSquare inherits from EOCRectangle

    Specify its initialization method as
    - (instancetype)initWithDimension:(float)dimension {
        return [super initWithWidth:dimension andHeight:dimension];
    }
    Copy the code

    The caller might then use the initWithWidth:andHeight: or init method to initialize the EOCSquare instance

    So when the class inherits, if the subclass’s omnipotent initializer has a different name than the parent’s method, then the superclass’s omnipotent initializer should always be overridden, as follows:

    - (instancetype)initWithWidth:(float)width andHeight:(float)height {
        float dimension = MAX(width, height);
        return [self initWithDimension:dimension];
    }
    Copy the code

    Note that you do not need to override the init method at this point, because the init method called in the parent EOCRectangle init method calls the initWithWidth:andHeight: method, and this method has been overridden by the subclass, so the subclass implementation is called.



    If the initialization method of the superclass does not apply to the subclass:

    Sometimes we don’t want to override a superclass’s omnipotent initializer because it doesn’t make sense to do so. For example, float Dimension = MAX(width, height); To initialize the EOCSquare object by calculating the length of the side, we believe that the method caller made a mistake. At this point we can override the parent class’s omnipotent initializer (such as initWithWidth:andHeight:) and throw an exception. At the same time, override the init method so that it calls its own initialization method, and if not, it will also throw an exception because it calls initWithWidth:andHeight:.

    However, in OC programs, exceptions are only supposed to be thrown [🚩 21] if there is a serious error, so the initialization method throws an exception as a last resort, indicating that the instance really cannot be initialized.

    - (instancetype)init {
        return [self initWithDimension:5.0f];
    }
    Copy the code
  • If an instance of an object can be created in two completely different ways and must be handled separately, you need to write multiple all-powerful initializers. Such as:
    - (instancetype)initWithCoder:(NSCoder *)coder;
    Copy the code

    This method decompresses the object data through a “decoder,” so its implementation does not call other all-purpose initialization methods. The implementation should look like this:

    // EOCRectangle
    - (instancetype)initWithCoder:(NSCoder *)coder {
        if (self = [super init]) {
            _width = [decoder decodeFloatForKey:@"width"];
            _height = [decoder decodeFloatForKey:@"height"]; }}// EOCSquare
    - (instancetype)initWithCoder:(NSCoder *)coder {
        if (self = [super initWithCoder:coder]) {
            // EOCSquare's specific initializer}}Copy the code

    The universal initializer method of each subclass should call the corresponding method of its parent class and work its way up, including when initWithCoder: is implemented. Otherwise, if EOCSquare’s method does not call the parent class’s method of the same name, but instead calls itself or one of the parent class’s other all-powerful initialiters, then the parent class’s initWithCoder: method has no chance of being implemented, and therefore cannot decode the _width and _height instance variables.

Implement the description method

The main points of

  • implementationdescriptionMethod returns a meaningful string describing the instance.
  • If you want to print out more detailed object description information during debugging, you should implement thisdebugDescriptionMethods.

The author

  • We print an object, we send a description message to the object, and this method returns a string, so we print an object with %@.
  • This method is defined in the NSObject protocol, not in the NSObject class, because NSObject is not the only root class, and NSProxy is the root class that obeys the NSObject protocol.
  • If we don’t override the description method, then we call the default implementation of the method in class NSObject to return the name of the class and the memory address of the object. As follows:
    id object = [NSObject new];
    NSLog(@"object = %@", object);
    // object = <NSObject: 0x600000724650>
    Copy the code
  • If we want to print the details of the object, we can override the description method and return the information we need. Such as:
    - (NSString *)description {
        return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">"[self class].self, _firstName, _lastName];
    }
    Copy the code

    The proposed overridden implementation of the description method also returns the name of the class and the memory address of the object.

  • Use the DESCRIPTION method of the NSDictionary class to better print object property information:
    - (NSString *)description {
        return [NSString stringWithFormat:@"<%@: %p, %@>"[self class].self, @ {@"title":_title, @"latitude":_latitude, @"longitude":_longitude}];
    }
    // location = 
            
             , {latitude = "51.506"; longitude = 0; title = London}>
            
    Copy the code

    This is easier to maintain than directly splicing and printing property information. If you want to add attributes later and print them in the description, you can simply modify the dictionary contents.

  • There is also a debugDescription method, which differs from description in that it is only called during debugging when the console command prints an object (breakpoint, then LLDB command)po(print – object)).
    • The default implementation of debugDescription in the NSObject class is to call description directly.
    • If you don’t want to print a detailed description of the object in your code, you should do this during debugging. For example, you should print the “name of the class and the memory address of the object” (this is what the NSArray class does). Then implement the debugDescription method, which returns more detailed information than description.

18. Use immutable objects whenever possible

The main points of

  • Try to create immutable objects.
  • If an attribute can be modified only within an object, it is extended from the readonly attribute to the ReadWrite attribute in the classcontinuation class (class extension).
  • Instead of exposing mutable collections as properties, provide methods that modify mutable collections in objects.

The author

  • This article mainly tells aboutreadonly ε’Œ readwriteThe two “read-write” attribute keywords and their usage:
    • Readwrite: Default, the property can be read and written
    • Readonly: The property is read-only
  • You should make advertised properties read-only whenever possible, and publish properties only when absolutely necessary.
    • For example, if a model object is created by an initialization method and its property values are not changed afterwards (such as a map model), the property should be set to read-only externally. In this way, if the user tries to change the value of the attribute, it will compile incorrectly, and there will be no inconsistency in the data structure of the object itself, such as the longitude and latitude of the map model.
    • As stated in [🚩 8], if you put mutable objects into a collection and then modify its contents, it is easy to break the internal data structure of the set and make it lose its inherent meaning. So keep the mutable content of the object to a minimum.
  • When you set the property to read only, you can default without specifying its memory management semantics because it has no setter method, such as:
    @property (nonatomic.readonly) NSString *title;
    Copy the code

    That said, we should at least document the memory management semantics used in the implementation so that it will be easier to make it readable and writable later.

  • Sometimes we want to modify properties inside an object, but we are read-only. You can then redeclare the property as readWrite in the class extension:
    // .h
    @property (nonatomic.copy.readonly) NSString *title;
    // .m
    @property (nonatomic.copy.readwrite) NSString *title;
    Copy the code
    • If the property is nonatomic, doing so may result in a “race condition.” An external observer may be reading a property while it is being written inside the object. If you want to avoid this problem, you can use GCD [🚩 41] and other means, if necessary, to set all data access operations (including within the object) as synchronous operations.
    • When the property is a Collection type, it is also recommended that the property be externally read-only and set to an immutable property (such as NSArray), and internally mutable property (such as NSMutableArray), and externally provide related methods (insert, delete, etc.) for the user to manipulate the mutable collection in the object. Why not just expose the variable collection directly instead of doing it multiple times? Because sometimes we might need to do something else in the insert or delete method, making the mutable Collection an external direct operation is not going to do that because it directly operates on the underlying data.
  • Even if the property is set to readonly, the user can use KVC to change the value, so that the user is responsible for the consequences. More unconventionally, the user may even manually set the value of the instance variable by directly using the type information query function to find out the offset of the instance variable corresponding to the attribute in the memory layout.
  • Do not query the type on the returned object, such as using the isKindOfClass: method, to determine whether it is mutable. The developer may not advise the user to modify the data in the object, but the developer may declare the mutable collection as immutable because the collection is large and takes time to copy. Let’s not assume it’s a mutable collection and then operate on it after determining it’s a mutable collection via the isKindOfClass: method.

19. Use clear and coordinated naming

The main points of

  • Follow standard Objective-C naming conventions to create interfaces that are easier for developers to understand.
  • The name of the method should be simple and scary. From left to right, it should read like an everyday sentence.
  • Do not use abbreviated type names in method names.
  • The first thing to do when naming a method is to make sure it matches the style of your own code or the framework you are integrating with.

The author

  • The method names in OC are long, but the meaning is clear.

    Like the way to replace stringsstringByReplaceOccurrencesOfString:withString:.

    In other languages the legal name is onlyreplace(@"A", @"B"), can not clearly express the two parameters A and B is which to replace which, etc.
  • Use “hump nomenclature” for method and variable names — start with a lowercase letter and capitalize the first letter of each word after that; Class names are also humped, but they are capitalized and usually preceded by two or three capitalized prefix letters. 15 】 【 🚩
  • Method named
    • For example, create a Rectangle instance with a specified width and height

      In C++ it might look like this. When you look back at the code, you won’t know that the two parameters are rectangle size, or which is width and which is height. You’ll need to look at the function definition to figure that out.
      Rectangle *rectangle = new Rectangle(5.0 f.10.0 f);
      Copy the code

      It is possible to define an initialization method in OC like this, which, while grammatically correct, has the same problems as C++ constructors — the name is not clear and the meaning of each variable is not clear when called.

      - (instancetype)initWithSize:(float)width :(float)height;
      Copy the code
      EOCRectangle *rectangle = [[EOCRectangle alloc] initWithSize:5.0f :10.0f];
      Copy the code

      The correct way to write it is as follows:

      - (instancetype)initWithWidth:(float)width height:(float)height;
      Copy the code
      EOCRectangle *rectangle = [[EOCRectangle alloc] initWithWidth:5.0f height:10.0f];
      Copy the code
    • The method name should be short and not too long. It should be clear, not verbose, and accurately express the task the method performs.
    • Naming rules
      • If the return value of a method is newly created, the first word of the method name should be the type of the return value, unless it is preceded by a modifier, such as localizedString. Property access methods do not follow this naming because they are not expected to create new objects, and even if they sometimes return a copy of the inner object, we consider that to be equivalent to the original object. These access methods should be named after their corresponding properties.
      • You should put the noun that indicates the type of the argument before the argument.
      • If the method is to perform an operation on the current object, it should include a verb; If you need arguments to perform an operation, you should add one or more nouns after the verb.
      • Don’t use a short name like STR, use a full name like string.
      • Boolean attributes should be prefixed with is. If a method returns a Boolean value that is not a property, it should be prefixed with has or is, depending on its functionality.
        @property (nonatomic.assign.getter = isEnabled) BOOL enabled;
        // NSString
        - (BOOL)isEqualToString:(NSString *)string;
        Copy the code
      • Leave the get prefix to methods that store the return value as an output parameter, such as those that populate a c-style array with the return value.
        - (void)getCharacters:(unichar *)buffer range:(NSRange)aRange;
        Copy the code
  • Naming of classes and protocols
    • Class and protocol placings should be prefixed to avoid namespace conflicts. 15 】 【 🚩
    • Naming is the same as naming the method, making it easier to read from left to right.
    • When you inherit, you have to follow the naming convention, for example, when you inherit from a subclass of UIView, you have to end with view.
    • A custom Delegate protocol whose name should include the name of the Delegate initiator followed by the word Delegate, refer to UITableViewDeleagte.

20. Prefix private method names

The main points of

  • Prefix the names of private methods so that they can be easily distinguished from public methods.
  • Do not prefix private methods with an underscore, as this is reserved for Apple.

The author

  • The reason for prefixing the name of a private method:
    1. Add a prefix to distinguish it from the public method and help with debugging.
    2. The method name or method signature can be changed easily. Think twice before changing the name or signature of a public method, because public apis are hard to change. If changed, all developers using this method must update their code. If you change a private method, you only need to change the relevant code inside the class at the same time, without affecting the public API. By prefixing private methods, it is easy to see which methods should be changed and which should not.
    3. OC is a dynamic runtime language, and unlike C++ and Java, it can’t really declare private methods, so we generally want to include “private method” semantics in the naming.
  • The prefix you use depends on your personal preference. For example, you can use “p_” as the prefix, and P means private.
  • After some revision, the compiler no longer requires that methods be declared before they are used, so private methods are usually declared only at implementation time.
  • Apple likes to prefix private methods with an underscore, so apple says in the documentation that developers should not prefix private methods with an underscore. If we do this, we may inadvertently override the parent (Apple class) private method of the same name in a subclass, causing problems by calling the overridden implementation instead of the one that should have called the parent. For example, UIViewController has a private method called _resetViewController. You might overwrite it by accident but you won’t even know it unless you go into UIViewController. The resulting problem is that your subclass’s method will be called frequently.
  • In addition, you may inherit classes from the tripartite framework, and you don’t know what names prefix their private methods unless the framework states it in the documentation or you read the source code. Similarly, someone else may subclass from the class you wrote. So to avoid the same name problem, you can use the same class name prefix you always use to prefix private methods.

21. Understand the Objective-C error model

The main points of

  • Exceptions should only be used if there is a serious error that can crash the entire application.
  • In less serious cases, you can either assign a “delegate method” to handle the error, or you can put the error message in an NSError object and return it to the caller as an “output parameter.”

The author

  • Exceptions should only be used if there is a serious error that can crash the entire application.

    For example, someone who uses an abstract base class directly throws an exception in a superclass method that a child class must override.
    @throw [NSException exceptionWithName:@"ExceptionName" reason:@"There was an error" userInfo:nil];
    Copy the code
  • In the case of less serious errors, the programming paradigm used by OC is to make the method return nil/0, or to use NSError to indicate that an error occurred. For example, if the initialization method cannot initialize the current instance based on the arguments passed in, you can make it return nil/0. The use of NSError is more flexible, because with this object, we can report the cause of the error to the caller.
  • The NSError object encapsulates three pieces of information:
    • Error domain: The scope in which an Error occurs and is the source of the Error. It is usually defined as a global constant of type NSString.

      It is a good idea to specify a dedicated “error range” string for your own library so that the user knows that the error occurred in your library.
      // EOCErrors.h
      extern NSString *const EOCErrorDomain;
      // EOCErrors.m
      NSString *const EOCErrorDomain = @"EOCErrorDomain"; 
      Copy the code
    • Error code: Used to indicate exactly what errors occurred within a certain range. Usually defined as enumerated types.
      typedef NS_ENUM(NSUIntegerEOCError) {EOCErrorUnknown = --1,
          EOCErrorInternalInconsistency = 100,
          EOCErrorGeneralFault = 105,
          EOCErrorBadInput = 500};Copy the code

      Enumerations not only explain the meaning of error codes, but also give them meaningful names. You can also specify each error type in the header file that defines these enumerations.

    • User Info: Additional information about this error.
  • To create an NSError object
    + (instancetype)errorWithDomain:(NSErrorDomain)domain code:(NSInteger)code userInfo:(nullable NSDictionary<NSErrorUserInfoKey.id> *)dict;
    Copy the code
  • NSError
    1. Errors are passed through the delegate protocol. When an error occurs, the delegate passes the error message to the delegate object via the protocol method. This is better than throwing an exception, because the caller can decide whether to implement the protocol method and whether to handle the error.
    2. Returned to the caller via the “output function” of the method. As follows. Again, if the caller doesn’t care about the error, it can pass nil to the error argument.
      NSError * __autoreleasing error = nil;
      BOOL ret = [object doSomething:&error];
      if (error) {
          // There was an error
      }
      Copy the code

22. Understand the NSCopying agreement

The main points of

  • If you want to copy the objects you write, you need to implement thisNSCopyingThe agreement.
  • If the custom object has mutable and immutable versions, implement both NSCopying andNSMutableCopyingThe agreement.
  • When you copy an object, you need to decide whether to use a shallow copy or a deep copy. Generally, a shallow copy is recommended.
  • If you are writing objects that require a deep copy, consider adding a method dedicated to deep copying.

The author

  • Purpose of copying:
    • Creates a replica object that does not affect the source object.
    • The source object is modified without affecting the replica object.
    • The replica object is modified without affecting the source object.
  • IOS provides two copying methods:
    • Copy: to produce an immutable copy;
    • MutableCopy: mutableCopy.
  • Deep and shallow copies:
    Copy type Copy the way The characteristics of
    Deep copy Memory copy with target object pointer and source object pointer pointing toTwo pieces ofMemory space with the same content. 1. The reference count of the copied object is not increased.

    2. A memory allocation is generated and two pieces of memory appear.
    Shallow copy Pointer copy: a copy of a memory address to which the target object pointer and the source object pointer pointWith a piece ofMemory space. 1. The reference count of the copied object is increased.

    2. No new memory is allocated.

    Note: If it is a small object such as an NSString, it may be stored via Tagged Pointer, with no reference count.

    In short: 1. Deep copy: a copy of the content, creating a new object, without increasing the object reference count 2. Shallow copy: a pointer copy that does not generate new objects and adds object reference counts. Whether reference counting is affected; 2. Check whether new memory space is created

  • Copy mutableCopy to mutable and immutable objects
    Source object type Copy the way Target object type Copy Type (Deep/shallow)
    Mutable object copy immutable Deep copy
    Mutable object mutableCopy variable Deep copy
    Immutable object copy immutable Shallow copy
    Immutable object mutableCopy variable Deep copy

    Note: Here we refer to the system classes NSArray, NSDictionary, NSSet, NSString, NSData and their mutable versions such as NSMutableArray.

  • The above shallow copy of the objects in the collection refers to the container object itself, and the default is shallow copy of the objects in the collection. That is, only the container object itself is copied, not the data in it. The main reason is that not every object in the container can be copied, and the caller may not want to copy every object in the container along with it.

  • If you want to implement a copy of a custom object, you need to followNSCopyingProtocol and implementationcopyWithZone:Methods.

    What is NSZone? For current run-time systems (where the compiler macro OBJC2 is set), nszones are simply ignored under both MRC and ARC.

    • If I want a shallow copy,copyWithZone:Method returns the same object: return self;
    • If I want a deep copy,copyWithZone:Method to create a new object and assign values to the properties you want to copy.
  • If the custom object supports mutable and immutable copying, then you need to complyNSMutableCopyingProtocol and implementationmutableCopyWithZone:Method that returns a mutable copy. whilecopyWithZone:Method returns an immutable copy. The user can make an immutable or mutableCopy by calling the copy or mutableCopy method on the object as needed.

Chapter four: Agreement and classification

Protocol and classification are two important language features to master. When used properly, the code is easy to read, easy to maintain, and error-free. This chapter will help the reader become proficient in both concepts.

23. Object to object communication through delegate to data source protocol

The main points of

  • The delegate pattern provides a set of interfaces for objects to communicate related events to other objects.
  • Define the interfaces that the delegate object should support as protocols, and in the protocols define the events that may need to be handled as methods.
  • The delegate pattern is used when one object needs to retrieve data from another object. In this case, the pattern is also called data Source Protoco.
  • If necessary, implement structures with bit segments to cache information about whether the delegate object can respond to the relevant protocol method.

The author

  • You can communicate between objects with the data source protocol using a delegate.
  • What can be defined in the agreement? Methods and properties.
  • It can be passed in the agreement@optional ε’Œ @requireKeyword to specify whether the protocol method is optional or mandatory, and the compiler will warn the user if the @require method is not implemented.
  • Proxy mode (also known as “agent mode”)
    • What is agency? A software design pattern; Among the iOS to@protocolForm embodiment; The delivery mode is one-to-one.
    • Purpose of the proxy pattern: To define a proxy agreement by which an object, if it wishes to accept a proxy from another object (the principal), must comply in order to become an “agent.” The principal can send some information back to the agent through the protocol method, and can also notify the agent when relevant events occur. In this way, the principal can delegate the responsibility for a certain act to the agent.
    • Agent workflow:
      • The “principal” requires the “agent” to implement the interface, all defined in the “principal agreement”;
      • The Agent complies with the Agreement and implements the Methodology of the Agreement;
      • The “protocol” method implemented by the “agent” may have a return value, which is returned to the “principal”;
      • The proxy invokes the Protocol method that the agent complies with.

    • The delegate property is typically defined as Weak to avoid circular references. The agent strongly references the principal and the principal weakly references the agent.
    • If you want to expose a class that adheres to a protocol, declare it in the interface. If the protocol is a delegate protocol, then declare it in the class extension, since the protocol is usually only used inside the class.
    • When calling @optional, we need to determine whether the agent can respond (that is, whether it implements the method). If so, we can send a protocol message to it. Otherwise, the agent may not implement the method.
      if ([_delegate respondsToSelector:@selector(protocolOptionalMethod)]) {
          [_delegate protocolOptionalMethod];
      }
      Copy the code

      It’s better to determine whether the delegate has a value and put that judgment in front of it to improve performance.

      if (_delegate && [_delegate respondsToSelector:@selector(protocolOptionalMethod)]) { [_delegate protocolOptionalMethod];  }Copy the code
  • Data source pattern
    • Another use of the delegate pattern is to provide data to a class, so it is also called the “data source pattern.” A data source pattern is a protocol that defines a set of interfaces through which a class obtains the data it needs.
  • The difference between the data source pattern and the regular delegate pattern is:
    • In the Data Source pattern, information flows from the Data Source to the class (the principal).
    • In the conventional entrustment model, information flows from the class (principal) to the entrusted (agent).

  • The Data Source and Delegate modes are well understood by UITableView:
    • Retrieve data to be displayed in a list through the UITableViewDataSource protocol;
    • User interaction with lists is handled through the UITableViewDelegate protocol.
      @protocol UITableViewDataSource<NSObject>
      @required
      - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
      @optional
      - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView; .@end
      
      @protocol UITableViewDelegate<NSObject.UIScrollViewDelegate>
      @optional- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath; .@end
      Copy the code
  • Performance optimization

    If the protocol method is optional when implementing the delegate and data source patterns, then the protocol method needs to be invoked to determine whether it is responsive.
    if (_delegate && [_delegate respondsToSelector:@selector(protocolOptionalMethod)]) {
        [_delegate protocolOptionalMethod];
    }
    Copy the code

    If we need to call the protocol method frequently, then we just need to check the response the first time. The above code performs a performance optimization by caching information about whether an agent can respond to a protocol method:

    1. A structure containing bitfields is embedded in the delegate as an example variable. Each bitfield in the structure indicates whether the delegate object implements the protocol’s methods. This structure is used to cache whether the proxy can respond to a particular protocol method.
      @interface EOCNetworkFetcher(a){
          struct {
              unsigned int didReceiveData      : 1;
              unsigned int didFailWithError    : 1;
              unsigned int didUpdateProgressTo : 1;
          } _delegateFlags;
      }
      Copy the code
    2. Rewrite the delegate property setter method, symbol of _delegateFlags structure in the assignment, implement caching function.
      - (void)setDelegate:(id<EOCNetworkFetcher>)delegate {
          _delegate = delegate;
          _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
          _delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
          _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
      }
      Copy the code
    3. Instead of using the respondsToSelector: method to check whether the delegate can respond to a particular protocol method each time it calls the delegate method, it queries the flag in the structure directly, improving execution speed.
      if (_delegateFlags.didReceiveData) {
          [_delegate networkFetcher:self didReceiveData:data];
      }
      Copy the code

24. Divide the class implementation code into manageable categories

The main points of

  • Use a classification mechanism to divide the class implementation code into manageable chunks.
  • Put methods that should be considered “Private” into a category called Private to hide implementation details.

The author

  • This article explains one use of classification: decomposing a large class file. It can break a class into modules by function for easy code management.
  • This also makes debugging easier: for all methods of a class, the class name appears in its symbol.

    For example, the “symbol name” of the “addFriend:” method:
    -[EOCPerson(Friendship) addFriend:]
    Copy the code

    This allows you to pinpoint the functional area (classification) to which the method belongs by the symbol name.

  • Create a class called Private and put all the Private methods in it so that the consumer knows that the methods in it should not be called directly.
  • For more information on the classification, see: Link: OC Low-level Exploration – Category and Extension.

25. Always prefix the category names of third-party classes

The main points of

  • When adding a category to a third-party class, always prefix its name with your own specific prefix.
  • When adding classes to third-party classes, always prefix the method names with your own specific prefix.

The author

  • Classification mechanisms are also commonly used to add functionality to existing classes that have no source code.
  • Classification is a run-time decision. What are runtime decisions? The underlying structure of a Category is struct category_t, which stores object methods, class methods, attributes, and protocol information for the Category. At this time, the data in the Category has not been merged into the class. Instead, all the classification data is merged into classes (class objects, metaclass objects) by Runtime mechanism when the program is running. This is the biggest feature of categorization and the biggest difference between categorization and an extension, which combines all data into the class at compile time.
  • Note that:
    1. Classification methods “overwrite” host class methods of the same name, which can cause problems if used improperly. Methods that are overwritten will execute differently than you expected, and this bug is hard to fix.
    2. Depending on the compilation order, the methods with the same name in the last compiled category will take effect.
    3. A class with the same name will cause a compilation error.
  • To avoid these problems, you can use a namespace to distinguish the names of each category from the methods defined in the category. The only way to implement namespace functionality in OC is to add a common prefix to all related names. In this way, there is much less chance that methods with the same name will be “overwritten” in the category and host class. This is especially important when adding categories to third-party classes.

26. Do not declare attributes in categories

The main points of

  • All properties used to encapsulate data are defined in the main interface.
  • In classes other than class-continuation classes (class extensions), you can define access methods, but try not to define attributes.

The author

  • You can add attributes to a classification, but you should try to avoid doing so. Class extensions are compile-time decisions, and all the data in the extension is merged into the class at compile time, so there is no problem with adding attributes to the extension. Classification is a run-time decision. The memory layout of the class is determined at compile time, so no instance variables can be added to the class. Nor do attributes added to the class automatically generate instance variables and the implementation of setter and getter methods (because attributes are the encapsulation of instance variables).
  • If you add attributes to a category, the compiler will warn you:
    Warning: Property 'friends' requires method' friends' to be defined - use@dynamic or provide a method implementation in thisCategory [-wobjc-property-implementation] Warning: Property 'friends' requires method' setFriends' to be defined - use@dynamic or provide a method implementation in this category [-Wobjc-property-implementation]
    Copy the code

    Warning: the setter and getter methods for the property are not implemented. Because a property in a class does not automatically generate instance variables and the implementation of setter and getter methods, external calls to access methods for that property will Crash. There are two solutions:

    1. Manually adding the implementation of setter and getter methods;
    2. Use @dynamic to tell the compiler that you will provide implementations of these methods at run time to eliminate warnings. You can add method implementations to these methods dynamically using dynamic method parsing. But if you don’t do that, at sign dynamic just eliminates the warning, and it will Crash if an external call is made to the access method for that property.
  • The problem that instance variables cannot be added to a Category can be solved through associated objects: due to the restriction of the underlying structure of a Category, member variables cannot be added directly to a Category, but the effect of Category having member variables can be achieved indirectly through associated objects. See Link: OC Low-level Explorations – Association Of Associated Objects. It is important to note that the memory management semantics of storing associated objects need to be consistent with the properties. It is easy to overlook that if the memory management semantics of the properties change, then the association policy of the associated objects also changes.
  • Read-only properties can be used in the class, for which we implement getter methods. Since all the methods needed to implement the property (read-only properties only need to implement getter methods) are implemented, the compiler no longer automatically synthesizes instance variables for the property and no warnings are issued.
  • Class interfaces and class extensions are where you can really define instance variables, and attributes are just syntactic sugar for defining instance variables and their associated access methods, so you should follow the same rules as instance variables. Although you can add instance variables to a category by associating objects, the goal is to extend the functionality of the class, not to encapsulate data. Attributes are used to encapsulate data, so you can define access methods in the classification, but try not to define attributes.

Use class-continuation classes to hide implementation details

The main points of

  • Add instance variables to a class through a class-continuation class (class extension).
  • If an attribute is declared “read-only” on the main interface and is modified internally by a set method of the class, then it is extended to “read-write” in the class-continuation class.
  • Declare the prototype of a private method in a class-continuation class.
  • If you want to keep the protocol that the class follows unknown, you can declare it in a class-continuation class.

The author

  • Although the way OC’s messaging dynamic system Runtime [🚩 11] works makes it impossible to implement true private methods or variables, it is better to only expose those that do need to be exposed, and to declare methods, properties, and instance variables that do not need to be exposed in class extensions.
  • If an attribute needs to be stored internally in the class, but externally only the user is allowed to value it. You can then set the read/write permission on this property toreadonlyRead only, and declare this property again in the class extension and set toreadwriteRead/write.
  • If the protocol that the class follows should only be considered private, such as the delegate protocol and the data source protocol, then that protocol can be followed in the class extension.
  • In addition to declaring attributes, instance variables, and protocol compliance, class extensions can also declare private methods. Although now the compiler doesn’t force us to declare methods before we use them, we just implement them in class implementation. However, it is useful to declare a private method in the class extension, so that all related methods contained in the class are described in this way, making the code more readable.

28. Provide anonymous objects by protocol

The main points of

  • Protocols can provide anonymous types to some extent. Specific object types can be reduced to ID types that follow a protocol that specifies the methods the object should implement.
  • Use anonymous objects to hide the type name (or class name).
  • If the specific type is not important, but the object’s ability to respond to a particular method (defined in the protocol) is important, then anonymous objects can be used.

The author

  • You can hide the class name by providing an anonymous object through a protocol by declaring the object as an ID type that conforms to a protocol:id <protocol> object.
    1. For example, the delegate property is declared as:
      @property (nonatomic.weak) id <EOCDelegate> delegate;
      Copy the code

      The principal does not need to care about the specific type of agent, only the agent can abide by the commission agreement and implement the protocol method, so that the principal can send the protocol message to the agent.

    2. In dictionaries, the standard memory management semantics for keys and values are “copy-on-set” and “keep-on-set”, respectively. So NSMutableDictionary sets the key as follows:
      - (void)setObject:(id)object forKey:(id<NSCopying>)key;
      Copy the code

      The key argument is of type ID, so you can pass in any type of OC object that complies with the NSCopying protocol, so the dictionary can send copy messages to that object, and the key argument can be treated as an anonymous object.

  • You can find out what type an anonymous object belongs to at runtime, but this is not a good idea because the anonymous object already indicates that its specific type doesn’t matter, you just need to call protocol methods through it.
  • Using anonymous objects:
    • There are different implementation classes behind the interface, and you don’t want to specify which class to use. Because sometimes these classes may change, and sometimes they do not fit into the standard class inheritance system, they cannot be represented uniformly as a common base class. In this way, the methods of these objects can be defined in the protocol, referred to and adhered to by the ID type, and the methods in the protocol can be called, and at run time, the specific method implementation can be called, depending on the type of the object.
    • The specific type of the object is not important; the important thing is that the object can respond to specific methods (defined in the protocol). You can do this even if the object type is fixed, to show that the type is not important here.