series

  • Effective Objective-C 2.0 is a two-month tutorial on Effective Objective-C 2.0
  • Effective Objective-C 2.0
  • Effective Objective-C 2.0

Chapter 5: Memory Management

Objective-c manages memory with reference counting, which many beginners struggle with, especially if they’ve used a language that manages memory in a “garbage collector.” The “automatic reference counting” mechanism alleviates this problem, but there are important considerations when using it to ensure that the object model is correct and that memory leaks do not occur. This chapter warns readers of pitfalls in memory management.

29. Understand reference counting

The main points of

  • The reference counting mechanism manages memory by incrementing and decrementing counters. After an object is created, its retention count is at least 1. If the retention count is positive, the object continues to live. When the retention count drops to 0, the object is destroyed.
  • During the object’s life, the rest of the objects retain or release the object by reference. The retention and release operations increase and decrease the retention count, respectively.

The author

  • Apple deprecated the Garbage Collector (GC) mechanism in favor of ARC starting with MacOS X 10.8. In iOS 5, MRC was replaced by ARC. Although ARC has helped us implement automatic reference counting (also known as “retention counting”), it is important to master the details of memory management.
  • How reference counting works
    • The reference counting mechanism manages memory by incrementing and decrementing counters. After an object is created, its retention count is at least 1. If the retention count is positive, the object continues to live. When the retention count drops to 0, the object is destroyed.
    • During the object’s life, the rest of the objects retain or release the object by reference. The retention and release operations increase and decrease the retention count, respectively.
    • Reference counting related memory management methods (which are prohibited under ARC) :
      • Retain: Increments the retained reference count
      • Release: decrements the reserved reference count
      • Autorelease: Decrement the retention count when the automatic release pool is cleaned up later
      • RetainCount: View the retention count, which is not very useful, even when debugging, and is therefore not recommended. One reason is that the caller may delay releasing the object through autoRelease, so its retention count is not what we expect. 36 】 【 🚩
    • Tracing back through the reference tree, they are retained by a root object, UIApplication object in iOS. In MacOS are NSApplication objects, both of which are singletons created when the application is started.
    • When the reference count drops to 0, it should no longer be used, which can cause a Crash. When the reference count drops to 0, the object’s memory is “unallocated” and put back into the “available memory pool”. If the object is used and its memory is not overwritten, the Crash does not occur. If the object memory is overwritten, it crashes. To avoid inadvertently using an invalid object, the pointer is usually cleared after release. This ensures that there are no Pointers that might point to invalid objects, often referred to as “dangling Pointers.”
  • Memory management in property access methods

    The property declared as retain in MRC has the following implementation of setter methods:
    @property (nonatomic.retain) NSNumber *count;
    
    - (void)setCount:(NSNumber *)newCount {
        [newCount retain];
        [_count release];
        _count = newCount;
    }
    Copy the code

    You must retain the new value before releasing the old value, otherwise releasing the old and new object may cause its reference count to be reduced to 0 and destroyed if it is the same object. A subsequent retain operation failed to revive it, and the instance variable became a dangling pointer, causing a Crash.

  • Autorelease calls immediately decrement the reference count of the object, while autorelease calls delay the release to extend the life of the object, and the object is added to the autorelease pool. Usually you send a release message to an object on the next “event loop,” unless you add it to a manually created automatic release pool. Autorelease is often used when an object is returned in a method to keep it alive for a while beyond the bounds of a method call. With release, the object is reclaimed before the method returns.
  • Reserved ring If two objects strongly reference each other, or multiple objects, each strongly references the next object until it returns to the first, then a “reserved ring”, commonly known as a “circular reference”, is produced. The reference count of all objects in the loop will not drop to zero and will not be released, resulting in a memory leak. Therefore, we want to avoid the creation of retention rings. Retention rings are usually avoided or broken by “weak references” or by commands from the outside that one object in the loop no longer holds another object, thus avoiding memory leaks.

30. Simplify reference counting with ARC

The main points of

  • With ARC, programmers don’t have to worry about memory management. Using ARC to program saves a lot of boilerplate code in your classes.
  • ARC manages object lifetimes basically by inserting “hold” and “release” operations where appropriate. In an ARC environment, the memory-management semantics of variables can be indicated by modifiers, instead of manually performing “save” and “release” operations.
  • The memory-management semantics of objects returned by methods are always represented by method names. ARC has identified this as a rule that developers must follow.
  • ARC is only responsible for managing memory for Objective-C objects. In particular, Core Foundation objects are not managed by ARC and developers must call CFRetain/CFRelease when appropriate.

The author

  • ARC is a compiler feature that passes throughLLVMThe compiler andRuntimeCollaborate to automate memory management. The LLVM compiler inserts retain, release, and Autorelease code for OC objects at compile time, where appropriate, to automatically manage the object’s memory.
  • Retain, Release, Autorelease, dealloc, retainCount, and so on are prohibited under ARC because manual calls interfere with ARC’s efforts to analyze the object’s life.
  • ARC does not invoke these methods through the OC messaging mechanism (objc_msgSend), but directly through its underlying C language version. This is better because operations such as reserve and release need to be performed frequently, so calling the underlying function directly saves a lot of CPU cycles.

    Such as callingretainEquivalent underlying functionobjc_retain. This is why you cannot overwrite retain, release, or autorelease because these methods will not be called.
  • Optimizations brought by ARC:
    1. Optimization 1: Automatically call the “save” and “release” methods. In MRC, if a method name begins with alloc, new, copy, or mutableCopy, the returned object belongs to the caller. Therefore, the code that calls these four methods is responsible for releasing the object returned by the method. If the method name does not start with the above four words, the returned object does not belong to the caller and will be released automatically. Therefore, the caller should reserve the returned object before using it and release it after using it. We don’t have to deal with this ourselves in ARC, which standardizes memory management rules through naming conventions.
    2. Optimization 2: At compile time, ARC reduces the retain, release, and autorelease operations that cancel each other out. If you find that multiple “save” and “release” operations have been performed on the same object, ARC can sometimes remove the two operations in pairs. This is an optimization that is difficult or impossible to do manually.
    3. Optimization 3: If the calling method returns a script,autorelease) objects added to the auto-release pool, followed by (retain), ARC calls other functions instead of autorelease and retain that allow the object to be returned directly to the caller instead of being added to the automatic release pool to subtract unnecessary operations and improve performance. (For backward compatibility, the autorelease and retain operations cannot be removed and the autorelease cannot be discarded for this optimization to accommodate code that does not use ARC.)

      ARC checks at run time:
      1. Called when an automatically freed object is returned from a methodobjc_autoreleaseReturnValueStudent: Functions instead ofautorelease, which looks at the section of code to be executed after the current method returns. If that code is found to be performing a retain operation on the returned object, set a flag bit in the global data structure (the exact contents of which vary by processor) and return the object directly, otherwise autorelease and return.
      2. Is called if the method returns an automatically freed object and the calling method code wants to preserve the objectobjc_retainAutoreleasedReturnValueStudent: Functions instead ofretain. This function looks at that flag bit and does not retain if it is already set, otherwise it will retain.

        Setting and detecting identity bits is faster than calling autorelease and retain.

    4. Optimization 4: ARC also handles memory management for local and instance variables.

      ARC By default, each variable is a strong reference to an object.

      Under ARC, we can write setter methods just like this:
      - (void)setObject:(id)object {
          _object = object;
      }
      Copy the code

      When compiled, it will become:

      - (void)setObject:(id)object {
          [object retain];
          [_object release];
          _object = object;
      }
      Copy the code

      In addition to saving manual retain and release operations, it also avoids the problem that we may release the old object first and then retain the new one, which happens to be the same object, causing Crash.

    5. Optimization 5: Help us perform the release of instance variables during object destruction

      The following operations we must perform under MRC are handled automatically by ARC. ARC borrows a feature of Objcetive C++ to generate a cleanup routine. When an Objcetive C++ object is recycled, the object to be reclaimed calls the destructor of all C++ objects. If the compiler finds that the object contains C++ objects, it generates a method named.cxx_destruct. ARC borrows this feature to generate the code needed to clean up memory in this method.
      - (void)dealloc {
          [_foo release];
          [_bar release];
          [super release];
      }
      Copy the code

      However, if you have non-Objective-C objects, such as objects in CoreFoundation or memory allocated in the heap by malloc(), you need to clean them up manually. But it is not needed and cannot be called[super dealloc]ARC generates and executes the code that calls this method in the.cxx_destruct method.

      - (void)dealloc {
          CFRelease(_coreFoundationObject);
          free(_heapAllocatedMemoryBlob);
      }
      Copy the code
  • Ownership modifier
    • __strong: The default value is retained
    • __unsafe_unretained: This value is not retained, but it generates dangling Pointers, which is unsafe
    • __weak: This value is not retained, and the __weak pointer will be null when the object is destroyed, so it is safe
    • __autoreleasing: Used when passing objects “by reference” to methods. This value is automatically released when the method returns
  • Under MRC, we might override the memory management approach. For example, when implementing a singleton class we often overwrite the Release method, replacing it with a null operation, because singletons cannot be freed. Under ARC, the memory management methods cannot be called and overridden because it interferes with ARC’s efforts to analyze the object’s lifetime. And because you can’t do that, ARC can perform various optimizations. For example, ARC can optimize retain, release, and Autorelease operations so that they do not go through OC messaging and instead call C functions hidden in the runtime library objC4. Also, the aforementioned ARC optimizes the autorelease and retain operations in “if the object to be returned by a method command is later ‘automatically released’ and the method caller immediately ‘retains’ the returned object.”

31. In the dealloc method, only the reference is released and the listener is unlistener

The main points of

  • In the dealloc method, all you need to do is release references to other objects and unsubscribe from the previously subscribed KVO or NSNotificationCenter notifications, and do nothing else.
  • If the object holds system resources such as file descriptors, you should write a special method to release such resources. Such classes have a convention with their consumers that they must call the close method when they run out of resources.
  • Methods that perform asynchronous tasks should not be called in dealloc; Methods that can only be executed in normal state should also not be called in dealloc because the object is already in the recycling state.

The author

  • What should I do in the dealloc method?
    • Releases references owned by the object. ARC will automatically add release code in dealloc for us to release OC objects by generating the.cxx_destruct method, while non-OC objects such as CoreFoundation objects must be released manually.
    • Remove KVO observer. KVO does not strongly reference the observer after the KVO registration method is called, so be aware of the lifecycle of the observer. The KVO removal method must be called to remove the observer at least before the observer is destroyed. Otherwise, triggering the KVO listening method again after the observer is destroyed will cause Crash. Link: iOS – Some Summary of KVO
    • Remove notification observer. After removal, the notification center will not send the notification to the object that has been destroyed and recycled. If the notification is still sent to the object, it will inevitably cause Crash.

      Notifications don’t have to be removed manually after iOS9 because the notification center now uses __weak to retain the observer, as opposed to __unsafe_unretain so that dangling Pointers will be generated if the observer is not removed and sending notifications to the object will Crash. However, notifications registered through the following API still need to be removed manually.

      - (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void(^) (NSNotification *note))block;
      Copy the code
  • What should not be done in the dealloc method?
    • Resources that are expensive or scarce within the system, such as file descriptors, sockets, chunks of memory, and so on, should not be freed.

      The reason is:
      1. You can’t expect the dealloc method to be called when you expect it, because something unexpected might hold the object and not release scarce resources when you expect it.

        When the application terminates, all undestroyed objects are destroyed, and the system optimizes program efficiency by not calling the dealloc method for each object. If you must clean up some objects, you can call the cleaning methods for those objects in the following methods, which are called when the program terminates.

        / / iOS, UIApplicationDelegate
        - (void)applicationWillTerminate:(UIApplication *)application
        Mac OS X, NSApplicationDelegate
        - (void)applicationWillTerminate:(NSNotification *)notification
        Copy the code
      2. There is no guarantee that every object’s dealloc method will execute, as there may be a memory leak. The usual approach is to implement another cleanup method that is called to release resources when they run out. This makes the lifetime of the resource object more explicit.

        It’s also a good idea to call the cleanup method in dealloc in case the developer forgets to clean up these resources. And output an error message or throw an exception to remind the developer that the cleanup method must be called to free the resource before the object can be reclaimed.
        - (void)close {
            /* clean up resources */
            _closed = YES;
        }
        
        - (void)dealloc {
            if(! _closed) {NSLog(@"Error: close was not called before dealloc!"); // or @throw expression
                [selfclose]; }}Copy the code
    • You should not call any other method. Because in dealloc the object is already about to be destroyed for recycling. If the methods called here perform tasks asynchronously or continue to call methods of their own, by the time those tasks are completed, the system will have completely destroyed the current object to be reclaimed. If the object is used again in the callback, it crashes. Note also that the thread calling the dealloc method performs the “final release operation”, whereas some methods must be called in a specific thread (such as the main thread). If those methods are called in the dealloc method, there is no guarantee that the current thread is the one required for those methods.
    • Property access methods should not be called. Because someone might overwrite these methods and do things in them that cannot be safely done in the recycle phase. In addition, the property may be monitored by the KVO mechanism, and the observer of the property may retain or use the soon-to-be-reclaimed object if the property value changes. This can throw the state of the run-time system completely out of whack, leading to inexplicable errors.

32. Be aware of memory management issues when writing exception safe code

The main points of

  • When catching exceptions, it is important to clean up the objects created in the try block.
  • By default, ARC does not generate the cleanup code needed to safely handle exceptions. When the compiler flag is turned on, this code is generated, but it makes the application larger and less efficient.

The author

  • Write “exception-safe codetry-catch-finallyBe aware of memory management issues, or memory leaks can easily occur.
  • In general, exceptions should be thrown only if there is a serious error and the application must terminate because of the exception, and it does not matter if the program terminates or leaks memory.
  • Sometimes you still need to write code to catch and handle exceptions. For example, when you code in Objective-C++ (C++ and Objective-C exceptions are compatible), or when you code in a third-party library that throws exceptions outside your control.
  • When catching exceptions, it is important to clean up the objects created in the try block.
    • Under MRC, if the object release operation is executed in the try block and an exception is thrown in the code that precedes it, memory leaks will result if the release operation is not executed. The solution is to place the release operation ina finally block, and the object must be declared global. The downside is that if the logic in a try block is complex, it’s easy to forget to release an object and cause a memory leak, especially if the object is a scarce resource.
      EOCSomeClass *object;
      @try {
          object = [[EOCSomeClass alloc] init];
          [object doSomethingThatMayThrow];
      }
      @catch(...). {NSLog(@"Whoops, there was an error, Oh well...");
      }
      @finally {
          [object release];
      }
      Copy the code
    • If you are in ARC, in exception-safe code, ARC will not automatically insert release for you by default. The reason is that adding a lot of this code can seriously affect runtime performance, even when exceptions are not thrown, and can significantly increase the size of your application.
      1. You can generate this additional code (release) by turning on the compiler flag -fobjc-arc-exceptions. Its default opens the reasons for the above mentioned: in general, only when a serious error, the application must be due to abnormal termination should throw an exception, when will happen and if termination of the program is also a memory leak is irrelevant, add additional code used in the safety handle exceptions (such as release) also does not make sense. In addition, it will cause the application to become larger and reduce the operation efficiency.
      2. -fobjC-arc-exceptions is automatically turned on by the compiler if the file is objective-C ++.
  • When you find a large number of exception catching operations, consider refactoring your code to replace the exception with an NSError style error message passing method.

33. Avoid retaining rings with weak references

The main points of

  • Set some references to weak to avoid “retention rings”.
  • Weak references may or may not be cleared automatically. Automatic clearing (Autonilling) is a new feature introduced with ARC and implemented by run-time systems. On weak references with auto-cleaning, the data can be read at will because the reference does not point to the reclaimed object.

The author

  • Retaining ring:
    • Two objects form a reserved ring by strong references to each other.
    • Multiple objects, each of which strongly references the next object until it returns to the first, form a reserved ring.
  • Retaining rings causes memory leaks:

    If the last reference to an object in the reserved ring is removed, the objects in the reserved ring cannot be accessed by the outside world, and they are strongly referenced to each other and cannot be destroyed, resulting in a memory leak. (A memory leak is the failure to release allocated memory that is no longer being used.)

    Previously, Objective-C programs on the Mac OS X platform could enable a garbage Collector, which would detect reserved rings and recycle objects if they were no longer referenced by outsiders. Since Mac OS X 10.8, however, the garbage collection mechanism has been deprecated in favor of ARC. Neither MRC nor ARC mechanisms have ever supported automatic detection of reclaimed reserved rings, so we want to avoid them.

  • Avoid retaining rings with weak references
    • unsafe_unretained: As the name implies, no object is retained but also insecure. The reason is that: if the object to which the pointer points is destroyed, the pointer still points to the memory address of the object and becomes a dangling pointer. If the pointer is used again, it will cause Crash.
    • weak: Can be used only under ARC. Same as unsafe_unretained objects. The difference is that if the object to which a pointer points is destroyed, the pointer is set to nil, and it is safe to use it again.

34. Reduce peak memory with Auto Free Pool Block

The main points of

  • The autorelease pool is arranged in the stack, and when an object receives an AutoRelease message, it is placed in the top pool.
  • The proper use of automatic release pools can reduce memory spikes in your applications.
  • The new @autoreleasepool notation creates a lighter, automatic releasepool.

The author

  • Objects that call the autoRelease method are added to the auto-release pool,@autoreleasepool{}The autofree pool is created in the open curly bracket and destroyed in the right curly bracket. When automatic release pool destruction occurs, a release message is sent to all objects in it.
  • If you send an AutoRelease message to an object without creating an autorelease pool, the console prints the following information.
    MISSING POOLS: (0xabcd1234) Object 0xabcd0123 of class __NSCFString autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug
    Copy the code

    In general, however, there is no need to worry about the auto-release pool. Some threads, such as the main thread or GCD thread, are automatically created. These threads have auto-release pools by default, which are emptied every time the event loop is executed.

  • The proper use of automatic release pools can reduce peak memory usage in applications (peak memory refers to the maximum amount of memory used by an application system at a particular time). However, while the overhead of automatic release pools is small, it is still there. Before using it to optimize efficiency, you should first monitor the memory usage to determine whether it is necessary for automatic release pool, and do not abuse it.

    The autorelease pool is created and cleared in each of the following execution loops so that autoRelease objects generated in each loop can be released in time to reduce memory spikes.
    for (int i = 0; i < 100000; i++) {
        @autoreleasepool{[selfdoSomethingWithInt:i]; }}Copy the code
  • Under MRC, you can use NSAutoreleasePool or @Autoreleasepool to create automatic release pools, but only the latter can be used under ARC. The advantages of @autoreleasepool over NSAutoreleasePool are:
    • Lighter, apple says it’s about six times faster than NSAutoreleasePool. NSAutoreleasePool is more heavyweight and is often used to create automatic release pools that occasionally need to be emptied. The @Autoreleasepool can create and empty the auto-release pool on each execution loop.
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
      for (int i = 0; i < 100000; i++) {
          [self doSomethingWithInt:i];
          // Drain the pool only every 10 cycles
          if (++i == 10) { [pool drain]; }}// Also drain ai the end in case the loop is not a multiple of 10
      [pool drain];
      Copy the code
    • The scope is the left and right curly braces to avoid inadvertent misuse of objects that have been reclaimed by the system after the pool has been emptied. NSAutoreleasePool is unavoidable.
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
      id object = [self createObject];
      [pool drain];
      [self useObject:object];
      Copy the code
      @autoreleasepool {
          id object = [self createObject];
      }
      [self useObject:object]; // Cannot access object, cannot compile
      Copy the code
  • For a more detailed explanation of autorelease pools, see: Link: iOS – Talk about AutoRelease and @Autoreleasepool

35. Debug memory management problems with zombie objects

The main points of

  • When the system recycles an object, it can convert it to a zombie object instead of actually recycling it. You can enable this function by using the environment variable NSZombieEnabled.
  • The system changes the ISA pointer to the object to point to a special zombie class, making the object a zombie object. The zombie class can respond to all selectors by printing a message containing the message content and its recipient, and then terminating the application.

The author

  • It is not safe to send messages to already reclaimed objects, which are deallocated and just put back into the available memory pool. If the object’s memory has not been allocated to someone else, then the access is fine. If it has been allocated to someone else, the access will crash again.

    A more precise explanation is that if only part of the memory of the reclaimed object is reused, some binary data in the object is still valid, so it may not crash. If that chunk of memory happens to be occupied by another valid and living object. The run-time system sends a message to a new object, which may or may not respond. If it does, the program won’t crash, but the object receiving the message is not the one we expected; If it cannot respond, the program still crashes.

    This may not be convenient for debugging, but we can better debug memory management issues through “Zombie objects”.

  • Enabling zombie objects: Enable the zombie object function using the environment variable NSZombieEnabled.
  • How zombie objects work:

    Its implementation code is embedded in the Objective-C runtime library, the Foundation framework, and the CoreFoundation framework. If the system finds NSZombieEnabled == YES when it is about to reclaim an object, it converts the object to zombie rather than actually reclaim it. A message is then sent to the object (now a zombie object), which prints a message on the console containing the message content and its recipient (below), and terminates the application. This allows us to know when and where to send messages to already reclaimed objects.
    [EOCClass message] : message sent to deallocated instance 0x7fc821c02a00
    Copy the code
  • Zombie object implementation principle:
    1. After zombie objects are enabled, the runtime system swaps the Dealloc method implementation with Swizzle and creates one for each object as it is about to be reclaimed by the system_NSZombie_OriginalClassClass. (OriginalClassIs the name of the class to which the object belongs_NSZombie_Classes are copied instead of using less efficient inheritance and then given new names_NSZombie_OriginalClassTo remember the old class name. Remember the old class name so that when sending a message to a zombie object, the system will know which class the object originally belonged to. .

      The object to be reclaimed becomes a zombie by pointing the isa pointer to the zombie class. Since the dealloc methods are swapped, all free() functions are not executed and the object’s memory is not freed. This is a memory leak, but only for debugging purposes, so it doesn’t matter).
    2. Due to the_NSZombie_Class (and all classes copied from that class_NSZombie_OriginalClassNo method is implemented, so any message sent to a zombie object enters the “full message forwarding phase.” In the “complete message forwarding phase”,__forwarding__The function is the core. The first thing it does is check the name of the class to which the object receiving the message belongs. If the prefix for_NSZombie_A message is printed on the console (the information indicates the message received by the zombie object, the original class, the memory address, etc.).[OriginalClass message] : message sent to deallocated instance 0x7fc821c02a00), and then terminate the application.

Don’t use retainCount

The main points of

  • Retention counts for objects may seem useful, but they are not, because an “absolute retain count” at any given time does not capture the full picture of the object’s lifetime.
  • The retainCount method was officially deprecated after ARC was introduced, and calling it in ARC resulted in an error from the compiler.

The author

  • ARC has disabled calling the retainCount method. But even if it can be called under MRC, or just for debugging, we should not use retainCount, its value is imprecise.
  • Reasons not to use retainCount:
    1. The retention count it returns is only the value at a given point in time. It doesn’t take into account that some objects added to the auto-release pool by calling the autoRelease method are released late (retention count -1), whereas retainCount simply returns the current retention count. So the results often don’t meet expectations.

      For example, the following code has two errors. First, if object is in the automatic release pool, the program will crash due to excessive release of object when the automatic release pool is empty. Second, the retainCount may never return 0, because sometimes the system optimizes the release behavior of the object to reclaim it while the retention count is still 1. The while loop may still be executing after the object is recycled, causing the program to crash.
      while ([object retainCount]) {
          [object release];
      }
      Copy the code
    2. When the object is a string constant or stored using Tagged Point, the value of retainCount is too large to be viewed. The retention count of a singleton also stays the same, because singleton cannot be freed, so we might overwrite its memory management methods, replacing the reserve and free operations with null operations.

    Therefore, we should not always rely on the specific value of the retention count to code. If you add or subtract an NSNumber object based on its specific retention count, and the system implements it in Tagged Point, the code is wrong.

Chapter 6: Block and large center distribution

Apple introduced the concept of a “block” with a syntax similar to “closure” in the C language extension. In Objective-C, we often use blocks to do things that would otherwise require a lot of boilerplate code, and blocks also enable code separation. Grand Central Dispatch (GCD) provides a simple set of interfaces for multithreaded environments. Blocks can be viewed as GCD tasks that may be executed concurrently, depending on system resources. This chapter will teach you how to make the most of these two core techniques.

37. Understand the concept of “chunks.

The main points of

  • A block is a lexical closure in C, C++, and Objective-C.
  • Block can accept arguments and return values.
  • Blocks can be allocated on a stack or heap, or they can be global. Blocks allocated on the stack can be copied to the heap so that, like standard Objective-C objects, they have reference counts.

The author

  • A block is a lexical closure in C, C++, and Objective-C. The block itself is also an OC object.
  • If self is not explicitly used in the block to access the instance variable, then the block implicitly captures self, which can easily lead to inadvertent circular references. The following code, the compiler will give warning, recommended useself->_anInstanceVariableself.anInstanceVariableTo access.
    self.block = ^{
        _anInstanceVariable = @"Something";// ⚠️ Block declared retains' self '; Explicitly mention 'self' to indicate this is intended behavior
    }
    Copy the code
  • Block memory layout
    • The ISA pointer points to a Class object
    • The Invoke variable is a function pointer to the block’s implementation code
    • Descriptor variables are Pointers to structures that declare the overall size of block objects, and Pointers to copy and Dispose functions that retain and dispose captured objects
    • The block also makes a copy of all the variables it captures, after the descriptor variable

  • There are three types of blocks: stack blocks, heap blocks, and global blocks
    • Stack blocks

      When a block is defined, the area of memory it occupies is allocated on the stack. A block is valid only in the scope in which it is defined.
      void (^block)();
      if ( /* some condition */ ) {
          block = ^{
              NSLog(@"Block A");
          };
      } else {
          block = ^{
              NSLog(@"Block B");
          };
      }
      block();
      Copy the code

      There is a danger in the above code that the two blocks defined in the if and else are allocated in stack memory, and when out of the if and else range, the stack block may be destroyed. If the compiler overwrites the block’s memory, calling the block causes the program to crash.

      Under ARC, the upper block is automatically copied to the heap, so there should be no problem. But we want to avoid that under MRC.

    • lumps

      To solve this problem, a block object can be copied from the stack to the heap by sending a copy message. The heap block can be used outside of the scope in which it is defined. Heap blocks are reference-counted objects, so under MRC if heap blocks are no longer used you need to call Release to release them.
      void (^block)();
      if ( /* some condition */ ) {
          block = [^{
              NSLog(@"Block A");
          } copy];
      } else {
          block = [^{
              NSLog(@"Block B");
          } copy];
      }
      block();
      [block release];
      Copy the code
    • Global block

      A block is global if all the information needed to run it can be determined at compile time, including no access to automatic local variables. Global blocks can be declared in global memory and do not need to be created on the stack each time they are used.

      A copy operation for a global block is null because the global block can never be reclaimed by the system and is effectively a singleton.
      void (^block)() = ^{
          NSLog(@"This is a block");
      };
      Copy the code

38. Create a typedef for a common block type

The main points of

  • Redefining block types with typedefs makes block variables easier to use.
  • When defining a new type, follow existing naming conventions and do not allow the name to conflict with another type.
  • You may wish to define multiple type aliases for the same block signature. If the code to be refactored uses an alias for a block type, you only need to modify the block signature in the corresponding typedef without changing the other typedefs.

The author

  • Each block has its own “inherent type” (consisting of its arguments and return values) so that it can be assigned to a variable of the appropriate type.
  • Redefine block types with the typedef keyword:
    typedef int(^EOCSomeBlock)(BOOL flag, int value);
    EOCSomeBlock block = ^(BOOL flag, int value) {
        // Implementation
    }
    Copy the code
  • The benefits of redefining block types with the typedef keyword
    • The syntax for defining block variables is different from the syntax for defining other types of variables, and the block type syntax for defining method parameters is different from the block variable syntax. This syntax is difficult to remember, so redefining a block type with a typedef keyword makes it easier to use a block variable, giving it a more readable name to indicate the purpose of the block, while hiding the block type behind it.
    • When you modify a typedef type definition statement, everything that uses the type definition fails to compile and can be fixed one by one. If you write block types without a type definition, you have more changes, and it’s easy to forget one or two of them and cause bugs that are hard to troubleshoot.
  • It is best to define these typedefs in classes that use block types, and the new type name should also start with the class name to clarify the purpose of the block.
  • Multiple type aliases can be defined for the same block signature for different purposes. If the code to be refactored uses an alias for a block type, you only need to modify the block signature in the corresponding typedef without changing the other typedefs.

Use handler blocks to reduce code fragmentation

The main points of

  • When you create an object, you can use an inline handler block to declare the relevant business logic together.
  • When you have multiple instances to monitor, if you use delegate mode, you often need to switch based on incoming objects, whereas if you use handler blocks, you can place blocks directly with related objects.
  • When you design your API, if you use a handler block, you can add a parameter that allows the caller to determine on which queue the block should be scheduled to execute.

The author

  • When an asynchronous method needs some way to notify the code after it has completed a task, we can either use the delegate pattern or define the Completion Handler as a block type. Add the Handle block argument to the initialization method of the object, passing in the Handler block when the object is created. This reduces code fragmentation, makes the code cleaner, makes the API tighter, and makes it easier for developers to call.
    [fetcher startWithCompletionHandler:^(NSData *data) {
        self->_fetchedFooData = data;
    }];
    Copy the code
  • Disadvantages of the delegate pattern: If you use the delegate pattern when you have multiple instances to monitor, the code for the delegate callback method can become very long because you have to decide what to do based on the object passed in.
    - (void)networkFetcher:(EOCNetworkFetcher *)networkFetcher didFinishWithData:(NSData *)data {
        if (networkFetcher == _fooFetcher) {
            ...
        } else if (networkFetcher == _barFetcher) {
            ...
        }
        // etc.
    }
    Copy the code

    Use handler block to achieve, you can directly put the block and the related object together, no longer need to determine which object.

    [_fooFetcher startWithCompletionHandler:^(NSData *data) {
        ...
    }];
    [_barFetcher startWithCompletionHandler:^(NSData *data) {
        ...
    }];
    Copy the code
  • When asynchronous callbacks have both success and failure, there are two ways to design the API:
    • Having two handler blocks to handle successful and failed callbacks makes your code more readable, and you can pass nil for callback block arguments that you don’t want to handle as needed.
      [fetcher startWithCompletionHandler:^(NSData *data) {
          // Handle success
      } failureHander:^(NSError *error) {
          // Handle failure
      }];
      Copy the code
    • Use a handler block to handle both successful and failed callbacks, and add an error parameter to the block to handle the failure case.
      [fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
          if (error) {
              // Handle failure
          } else {
              // Handle success}}];Copy the code
      • Cons: All the logic put together can make blocks long and complex
      • Pros: More flexibility, successes and failures may sometimes be handled in unison
  • Sometimes the callback parameter needs to be executed at the relevant point in time, such as when downloading and you want to be notified every time a download progress is made, which can also be handled with handler blocks.
  • When you design your API, if you use a handler block, you can add a parameter that allows the caller to determine on which queue the block should be scheduled to execute. See the notification API:
    - (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void(^) (NSNotification *))block;
    Copy the code

40. Do not have a retention ring when referring to a block’s owning object

The main points of

  • If the object captured by the block directly or indirectly preserves the block itself, then beware of retention rings.
  • It is important to find an appropriate time to remove the reserved ring and not pass the buck to the API caller.

The author

  • This section deals with circular references to blocks. When using blocks, it is easy to form reserved rings. We need to analyze the reference relationship between them to avoid the generation of reserved rings. If a retention ring is formed, find the right time to remove the retention ring. And it should be handled internally, rather than leaving the task of breaking a loop to the API caller because there is no guarantee that the caller will do so.

41. Use more queues and less synchronous locks

The main points of

  • Dispatch queues can be used to express synchronization semantics, which is simpler than using @synchronized blocks or NSLock objects.
  • Combining synchronous and asynchronous dispatch enables the same synchronous behavior as normal locking without blocking the thread performing the asynchronous dispatch.
  • Using synchronous queues and fence blocks can make synchronous behavior more efficient.

The author

  • Using locks for synchronization is risky and not very efficient. GCD lets you lock your code in a simpler and more efficient way.
  • Lock synchronization
    • @synchronized
      - (void)synchronizedMethod {
          @synchronized(self) {
              // Safe}}Copy the code
      • How it works: @synchronized automatically creates a lock based on a given object, waits for the code in the block to complete, and then releases the lock.
      • Disadvantages: Overusing @synchronized(self) can reduce code efficiency because blocks that share the same lock must be executed sequentially, meaning that all @synchronized(self) blocks are synchronized with each other. If the self object is locked frequently, the program may have to wait for an unrelated piece of code to complete before continuing with the current code, which is unnecessary.
    • NSLock etc.
      _lock = [[NSLock alloc] init];
      
      - (void)synchronizedMethod {
          [_lock lock];
          // Safe
          [_lock unlock];
      }
      Copy the code
  • Synchronize with GCD
    • GCD serial synchronization queue

      Scheduling read and write operations in the same queue ensures data synchronization.
      _syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue".NULL);
      
      - (NSString *)someString {
          __block NSString *localSomeString;
          dispatch_sync(_syncQueue, ^{
              localSomeString = _someString;
          });
          return localSomeString;
      }
      
      - (void)setSomeString:(NSString *)someString {
          dispatch_sync(_syncQueue, ^{
              _someString = _someString;
          });
      }
      Copy the code

      The code for setter methods can also be executed asynchronously. Because getter methods need to return a value, they need to be executed synchronously to block the thread to prevent an early return, whereas setter methods do not need to return a value and can be executed asynchronously.

      Blocks need to be copied during asynchronous execution. Therefore, whether asynchronous execution can improve the execution speed depends on how heavy the block task is. If it takes longer to copy a block than to execute it, asynchronous execution reduces efficiency, whereas if the block task is heavy, execution speed can be increased.

      - (void)setSomeString:(NSString *)someString {
          dispatch_async(_syncQueue, ^{
              _someString = _someString;
          });
      }
      Copy the code
    • GCD fence function

      This ensures read and write security, but is not optimal because the read methods are executed synchronously.

      To ensure read and write security, the following conditions are met:
      1. Only one thread can write data at a time.
      2. Multiple threads are allowed to read data at the same time.
      3. Both read and write operations are not allowed at the same time.

      We can optimize for the second point so that the read methods can be executed concurrently. Using the GCD fence function:

      _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
      
      - (NSString *)someString {
          __block NSString *localSomeString;
          dispatch_sync(_syncQueue, ^{
              localSomeString = _someString;
          });
          return localSomeString;
      }
      
      - (void)setSomeString:(NSString *)someString {
          // Dispatch_barrier_async can also be selected depending on how heavy the block task is
          dispatch_barrier_sync(_syncQueue, ^{ 
              _someString = _someString;
          });
      }
      Copy the code

      For detailed instructions on the use of the GCD fence function, see:Link: iOS – Some Summary of GCD.

42. Use GCD more and performSelector less

The main points of

  • The performSelector family is prone to lapses in memory management. It cannot determine exactly what selector will be executed, so the ARC compiler cannot insert proper memory management methods.
  • The performSelector family of methods is very limited in the number of selectors they can handle, both in the type of value they return and in the number of arguments they can send to the method.
  • If you want to execute a task on another thread, it is better not to use the performSelector family of methods, but to encapsulate the task in a block and then call the relevant methods of the great central dispatch mechanism to do so.
  • The author
  • NSObject defines a series of performSelector methods that let the developer call any method at will, postpone the method call, specify the thread on which the method is executed, and so on.
    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
    // ...
    Copy the code
  • performSelector:What is the use of method?
    1. It’s a bit redundant if you just use it to call a method
    2. Usage one: The selector is determined at run time
      SEL selector;
      if ( /* some condition */ ) {
          selector = @selector(foo);
      } else if ( /* some other condition */ ) {
          selector = @selector(bar);
      } else {
          selector = @selector(baz);
      }
      [object performSelector:selector];
      Copy the code
    3. Usage 2: Save the selector until an event occurs
  • performSelector:Disadvantages of the method:
    1. Potential memory leakage:

      Because the selector is determined at run time, the compiler does not know what selector is being executed. If you are in ARC, the compiler will issue a warning that a memory leak may be caused.
      waring: PerformSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]
      Copy the code

      Since the compiler does not know what selector to perform is, and therefore does not know its method name, method signature, return value, etc., there is no way to use ARC’s memory-management rules to determine whether the return value should be freed. For this reason, ARC took the prudent step of not adding a release operation, which could result in a memory leak because the method might have already retained the object when it returned it.

      If it is a call to alloc/new/copy/mutableCopy method in the beginning, when created will hold objects, the compiler will insert ARC environment release method to release the object, and use the performSelector compiler would not add the release of the operation, This leads to a memory leak. Any other method that starts with a name, the object that you return is added to the auto-release pool, so you don’t have to insert release, and you don’t have a problem using performSelector.

    2. The return value can only be void or object type. If you want to return a primitive data type, you need to perform some complicated and error-prone conversion operations. If the return value type is C struct, then performSelector cannot be used.
    3. Parameter types and numbers also have limitations: Parameter types must be ID types, not basic data types. Number: A maximum of two selector arguments can be performed. If you use performSelector to defer execution or specify a method to be executed by a thread, then you can have at most one selector argument.
  • Use GCD instead of performSelector
    1. If you want to delay execution, you can use dispatch_after
    2. If you want to specify thread execution, then GCD can do that as well

43. Know when to use GCD and operation queue

The main points of

  • Dispatching queues is not the only solution to solve the problems of multithreading and task management.
  • Action queues provide a high-level Objective-C API that implements most of the functionality that GCD does, as well as more complex operations that GCD would require additional code to implement.

The author

  • When dealing with multithreading and task management, use GCD or NSOperation as the case may be, and if you choose the wrong tool, your code will be difficult to maintain. Here are the differences:
    Multithreaded scheme The difference between
    GCD GCD is iOS4.0 launched, mainly for multi-core CPU optimization, is C language technology;

    GCD is a pure C API;

    GCD adds blocks to queues (serial/concurrent/global/main queue) and executes them synchronously/asynchronously;

    GCD provides some features that NSOperation does not:

    1) queue group

    ② One-time execution

    ③ Delayed execution
    NSOperation NSOperation was released in iOS2.0, and it was rewritten in iOS4, and the underlying implementation is GCD;

    NSOperation is an OC object;

    NSOperation adds an operation (asynchronous task) to a queue (concurrent queue), and the specified operation is performed;

    NSOperation provides convenient operations:

    ① Maximum number of concurrent requests

    ② Suspend/continue/cancel the queue operation

    ③ Specify dependencies between operations (synchronization can be used in GCD)
  • Advantages of using NSOperation and NSOperationQueue:
    • Canceling an operation can be canceled by calling the CANCEL method of NSOperation before the operation is performed, but an ongoing operation cannot be canceled. After iOS8, GCD can use the dispatch_block_cancel function to cancel unexecuted tasks and ongoing tasks.
    • Specify dependencies between operations so that a particular operation cannot be executed until another operation has completed successfully.
    • The properties of the NSOperation object are monitored by KVO to be notified when an operation task changes its status, such as isCancelled or isFinished. The COMMUNIST Party does not.
    • Specify the priority of an operation Specify the priority relationship between an operation and other operations in the queue. Operations with higher priorities are executed first, and those with lower priorities are executed later. The GCD has no direct way of implementing this feature.
    • A reusable NSOperation object can use a system-provided NSOperation subclass (such as NSBlockOperation) or a custom subclass.
  • GCD tasks are represented by blocks, which are lightweight data structures, while NSOperation is the more heavyweight Objective-C object. That being said, the COMMUNIST Party is not the best solution. Sometimes the overhead of adopting an object is minimal, and its benefits greatly outweigh its disadvantages. In addition, it is not always true to say that you should use a high-level API whenever possible and only turn to a low-level API when you really need to. Some functionality can be done with a high-level API, but that doesn’t necessarily mean it’s better than the low-level implementation. To determine which solution is better, it’s best to test performance.

44. Perform tasks according to system resource status through the Dispatch Group mechanism

The main points of

  • A series of tasks can be grouped into a Dispatch group. Developers can be notified when this set of tasks is complete.
  • With dispatch groups, multiple tasks can be performed simultaneously in parallel dispatch queues. The GCD then schedules these concurrent tasks based on system resources. Developers would have to write a lot of code to implement this feature themselves.

The author

  • GCD queue group, also known as “scheduling group”, implements a unified callback after all tasks are completed. The GCD has a concurrent queue mechanism, so it can execute tasks concurrently based on available system resources. With queue groups, you can both execute a given set of tasks concurrently and be notified when all of these given tasks are completed. Queue groups can be used when you need to execute multiple asynchronous tasks concurrently before continuing with other tasks.
  • Create a queue group.
    dispatch_group_t dispatch_group_create(void);
    Copy the code
  • Executes a block asynchronously and associates it with the specified queue group.
    void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    Copy the code
  • You can also specify the queue group to which a task belongs by using the following two functions.

    Note that dispatch_group_leave must be followed by dispatch_group_leave, otherwise the queue group will never complete.
    void dispatch_group_enter(dispatch_group_t group);
    void dispatch_group_leave(dispatch_group_t group);
    Copy the code
  • Synchronization is returned only when all blocks added by dispatch_group_async have been executed or the specified timeout period has expired. You can call DISPATCH_TIME_FOREVER to indicate that the function will wait for the task to complete without timeout.

    Note: dispatch_group_wait blocks threads

    // @return long Returns 0 if the block completes within the specified timeout period; A non-zero is returned when timeout occurs.
    long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    Copy the code
  • After all blocks added by dispatch_group_async are executed, the blocks of dispatch_group_notify are sent to the specified queue.

    Dispatch_group_notify does not block threads

    void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    Copy the code

45. Use dispatch_once to execute thread-safe code that only needs to be run once

The main points of

  • It is often necessary to write “thread-safe single-code execution”. This is easily done with the dispatch_once function provided by GCD.
  • Tags should be declared in the static or global scope so that the same tags are passed to the dispatch_once function for blocks that only need to be executed once.

The author

  • Dispatch_once can be used to implement “thread-safe code that only needs to be executed once”.
  • Use dispatch_once to implement singletons, which is more efficient than @synchronized. Instead of heavyweight synchronization, it uses “atomic access” to query tags to see if the corresponding code has already been executed.
    // Common singleton, thread unsafe
    + (id)sharedInstance {
        static EOCClass *sharedInstance = nil;
        if (sharedInstance == nil) {
            sharedInstance = [[self alloc]init];
        }
        return sharedInstance;
    }
    // Lock, thread safe
    + (id)sharedInstance {
        static EOCClass *sharedInstance = nil;
        @synchronized(self) {
            if(! sharedInstance) { sharedInstance = [[selfalloc] init]; }}return sharedInstance;
    }
    // dispatch_once, thread safety, higher efficiency
    + (id)sharedInstance {
        static EOCClass *sharedInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
        });
        return sharedInstance;
    }
    Copy the code
  • For blocks that only need to be executed once, the tags passed in must be exactly the same each time the function is called, usually in the static or global scope.

46. Do not use dispatch_get_current_queue

The main points of

  • The dispatch_get_CURRENT_queue function often behaves differently from what developers expect. This function is deprecated and should only be used for debugging.
  • Because dispatch queues are hierarchically organized, there is no single queue object to describe the concept of “current queue”.
  • The dispatch_get_CURRENT_queue function is used to resolve deadlocks caused by non-reentrant code, but problems that can be solved with this function can often be solved with queue-specific data instead.

The author

  • dispatch_get_current_queueThis function returns the queue of currently executing code, but has been officially deprecated since iOS6.0. We should only use this function for debugging purposes.
  • dispatch_get_current_queueA typical misuse of the function is to use it to detect whether the current queue is a particular queue in an attempt to avoid the deadlock problem that might occur when performing a synchronous dispatch, but it may still cause a deadlock.
  • Because there is a hierarchy between queues, it passesdispatch_get_current_queueFunction to “check whether the current queue is the one used to perform a synchronous dispatch” does not always work.

    What is “hierarchy between queues”?



    Blocks in a queue are executed in the upper queue (also called the parent queue), and the highest queue in the hierarchy is always the global concurrent queue.

    In the figure above, blocks in queue B and C are executed in order in queue A. So blocks in A, B, and C always stagger with each other. The block in D may be parallel to the block in A (including B and C), because the target queue of A and D is A concurrent queue.

    For example, a developer might assume that a block in queue C must be executed in C, passingdispatch_get_current_queueThe function determines that the current queue is C and assumes that it is safe to execute the block synchronously in A, whereas the block may execute in queue A, resulting in A deadlock.

  • dispatch_get_current_queueThe function is used to resolve deadlocks caused by non-reentrant code, but problems that can be solved with this function can often be solved with queue-specific data instead.
    dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA".NULL);
    dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB".NULL);
    dispatch_set_target_queue(queueB, queueA); // Set the target queue of B to A
    
    static int kQueueSpecific;
    CFStringRef queueSpecificValue = CFSTR("queueA");
    // Set queue-specific data for queue A
    dispatch_queue_set_specific(queueA, 
                        &kQueueSpecific, 
                        (void *)queueSpecificValue, 
                        (dispatch_function_t)CFRelease); 
    
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{ NSLog(@"NO deadlock"); };
    
        CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
        // According to queue-specific data, if the current queue is QUEUE A, execute the Block directly, otherwise commit the synchronized Block to queue A
        if (retrievedValue) { 
            block();
        } else {
            dispatch_sync(queueA, block); }});Copy the code

Chapter 7: System framework

People usually develop Mac OS X or iOS programs in Objective-C. In both cases, a complete system framework is available, named Cocoa in the former case and Cocoa Touch in the latter. This chapter provides an overview of these frameworks and delves into some of them.

Be familiar with the system framework

The main points of

  • Many system frameworks can be used directly. The most important of these are Foundation and CoreFoundation, which provide many of the core functions needed to build applications.
  • Many common tasks can be done with frameworks, such as audio and video processing, network communication, and data management.
  • Remember: Frameworks written in pure C are just as important as those written in Objective-C, and to be a good Objective-C developer, you need to understand the core concepts of C.

The author

  • This article mainly introduces some system frameworks, before writing a new tool class, it is best to check whether the system provides the required functionality of the framework can be directly used.
    • Foundation and CoreFoundation: basic framework
    • UIKit and AppKit: core UI frameworks
    • CFNetwork: provides C-level network communication capabilities
    • CoreAudio: Provides C API for manipulating audio hardware on devices
    • AVFoundation: Provides Objective-C objects for playback and recording of audio and video
    • CoreData: Provides an Objective-C interface to put objects into a database for easy persistence
    • CoreText: Provides a C language interface for efficient text typesetting and rendering operations
    • CoreAnimation: Used to render graphics and play animations
    • CoreGraphics: Provides the necessary data structures and functions for 2D rendering
    • MapKit: Provides map functions
    • Social: Provides Social networking functions
  • The Foundation and CoreFoundation frameworks are implemented in Objective-C and C, respectively, and can be toll-free bridging between them.
  • An API implemented in C can bypass the Objective-C runtime system, thereby increasing execution speed. But since ARC is only responsible for Objective-C objects, memory management is a particular concern when using these apis.

48. Use block enumerations more and use for loops less

The main points of

  • There are four ways to traverse a collection. The most basic method is for loop, followed by NSEnumerator traversal and fast traversal, and the latest and most advanced method is “block enumeration”.
  • The block enumeration method itself enables concurrent traversal via GCD without the need to write separate code. This cannot be easily achieved with other traversal methods.
  • If you know in advance what objects the collection to be traversed contains, you should modify the block signature to indicate the specific type of object.

The author

  • There are four ways to traverse a collection, and we should use block enumerations more often than for loops.
  • The for loop
    // Array
    NSArray *anArray = / *... * /;
    for (int i = 0; i < anArray.count; i++) {
        id object = anArray[i];
        // Do something with 'object'
    }
    
    // Dictionary
    NSDictionary *aDictionary = / *... * /;
    NSArray *keys = [aDictionary allKeys];
    for (int i = 0; i < keys.count; i++) {
        id key = keys[i];
        id value = aDictionary[key];
        // Do something with 'key' and 'value'
    }
    
    // Set
    NSSet *aSet = / *... * /;
    NSArray *objects = [aSet allObjects];
    for (int i = 0; i < objects.count; i++) {
        id object = objects[i];
        // Do something with 'object'
    }
    Copy the code

    Dictionaries and sets are unordered, so values cannot be accessed by subscripts, so an ordered array needs to be created to aid traversal, but creating and destroying the array incurs additional overhead. Other traversal methods do not require the creation of such mediation arrays.

    When performing a reverse traverse, using the for loop is much easier than the other way around.

  • Use objective-C 1.0 NSEumerator for traversal
    // Array
    NSArray *anArray = / *... * /;
    NSEnumerator *enumerator = [anArray objectEnumerator];
    // NSEnumerator *enumerator = [anArray reverseObjectEnumerator]; // Reverse traversal
    id object;
    while((object = [enumerator nextObject]) ! =nil) {
        // Do something with 'object'
    }
    
    // Dictionary
    NSDictionary *aDictionary = / *... * /;
    NSEnumerator *enumerator = [aDictionary objectEnumerator];
    id key;
    while((key = [enumerator nextObject]) ! =nil) {
        id value = aDictionary[key];
        // Do something with 'object'
    }
    
    // Set
    NSSet *aSet = / *... * /;
    NSEnumerator *enumerator = [aSet objectEnumerator];
    id object;
    while((object = [enumerator nextObject]) ! =nil) {
        // Do something with 'object'
    }
    Copy the code

    The nextObject method returns the nextObject in the enumeration (NSEnumerator). After all the objects in the enumeration have returned, the call will return nil, indicating that the end of the enumeration has been reached, that the traversal is complete.

    NSEnumerator compared to for loop:

    1. Too much code, can’t get the subscript
    2. The advantage is that traversal groups, dictionaries and sets are all written similarly
    3. For reverse traversal, using NSEnumerator makes the code read more smoothly
  • Fast traversal (objective-C 2.0 introduced)
    // Array
    NSArray *anArray = / *... * /;
    for (id object in anArray) {
        // Do something with 'object'
    }
    
    // Dictionary
    NSDictionary *aDictionary = / *... * /;
    for (id key in aDictionary) {
        id value = aDictionary[key];
        // Do something with 'key' and 'value'
    }
    
    // Set
    NSSet *aSet = / *... * /;
    for (id object in aSet) {
        // Do something with 'object'
    }
    
    NSEumerator implements THE NSFastEnumeration protocol, so it supports fast traversal
    NSArray *anArray = / *... * /;
    for (id object in [anArray reverseObjectEnumerator]) {
        // Do something with 'object'
    }
    Copy the code

    If you want objects of a class to support fast traversal, you simply comply with the NSFastEnumeration protocol and implement the unique methods defined in the protocol.

    - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;
    Copy the code

    The fast traversal syntax is cleaner and more efficient, with the in keyword for the for loop. The disadvantage is that you can’t easily get the index of the element currently traversed.

  • Block enumeration traversal
    // Array
    NSArray *anArray = / *... * /;
    [anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){
        // Do something with 'object'
        if (shouldStop) {
            *stop = YES; }}];// Dictionary
    NSDictionary *aDictionary = / *... * /;
    [aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
        // Do something with 'key' and 'object'
        if (shouldStop) {
            *stop = YES; }}];// Set
    NSSet *aSet = / *... * /;
    [aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
        // Do something with 'object'
        if (shouldStop) {
            *stop = YES; }}];Copy the code

    Block enumeration has advantages over other traversal methods. Although it has a large amount of code, it brings many benefits:

    1. Provides the subscript for which the traversal is targeted
    2. Dictionaries can be traversed to provide both keys and values without additional coding
    3. NSEnumerationOptions can be used to specify the traversal mode, such as NSEnumerationReverse, NSEnumerationConcurrent, etc. (GCD is the underlying implementation)
    4. You can terminate the traversal by setting the stop variable, or break for other traversal methods
    5. The ability to modify a block’s method signature (parameter type) to avoid type conversion operations

      This is possible because the ID type can be overridden by other types.

      By specifying the specific type of the object, the compiler can detect whether the developer is calling a method that the object does not have and report an error if it finds such a problem. If you know the specific type of the object in the collection, you should specify its type in this way.
      // Other traversal methods require type coercion
      for (NSString *key in aDictionary) {
          NSString *object = (NSString *)aDictionary[key];
          // Do something with 'key' and 'object'
      }
      
      // Block enumeration can directly modify the block's method signature to avoid type conversion operations
      NSDictionary *aDictionary = / *... * /;
      [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){
          // Do something with 'key' and 'obj'
      }];
      Copy the code

49. Use seamless bridging for collections that customize their memory management semantics

The main points of

  • Seamless bridging allows you to convert objective-C objects in the Foundation framework back and forth to C data structures in the CoreFoundation framework.
  • When you create a collection at the CoreFoundation level, you can specify a number of callback functions that indicate how the collection should handle its elements. This can then be converted into objective-C collections with special memory management semantics using seamless bridging techniques.

The author

  • There are__bridge__bridge_retained__bridge_transferThere are three bridging schemes, and their differences are as follows:
    1. __bridge: Does not change the memory management ownership of the object.
    2. __bridge_retainedARC memory management: used to disallow ARC memory management when Foundation objects are converted to Core Foundation objects.
    3. __bridge_transfer: used to transfer memory management when Core Foundation objects are converted to Foundation objects.

    For details, see:Link: iOS – The Old Story of Memory Management (3) : ARC Offering – Managing Toll-Free Bridging

  • Why use CoreFoundation framework objects and bridging techniques when we’re writing applications in pure Objective-C? Because the Objective-C classes in The Foundation framework have some features that the C data structures in the CoreFoundation framework don’t, and vice versa.
  • The author gives an example of using CoreFoundation objects: a big problem with dictionary objects in the Foundation framework, whose memory-management semantics are “copy” for keys and “preserve” for values. That is, when you add keys and values to the NSMutableDictionary, the dictionary automatically “copies” the keys and “preserves” the values. If the object used as a key does not support copy operations (to do so, you must comply with the NSCopying protocol and implement the copyWithZone: method), then the compiler will warn and Crash at runtime:
    NSMutableDictionary *mDict = [NSMutableDictionary dictionary];
    [mDict setObject:@ "" forKey:[Person new]]; // ⚠️ warning: Sending 'Person *' to parameter of incompatible type 'id
            
              _Nonnull'
            
    
    Runtime:
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
    reason: '-[Person copyWithZone:]: unrecognized selector sent to instance 0x60000230c210'
    Copy the code

    There is no way to modify the memory-management semantics of the keys and values of NSMutableDictionary directly. We can modify the memory management semantics by creating the CoreFoundation framework’s CFMutableDictionary C data structure, performing “save” instead of “copy” operations on keys, and then using seamless bridging techniques, Convert it to an NSMutableDictionary object.

    You can also use NSMapTable to specify memory management semantics for keys and values.

  • When you create a collection at the CoreFoundation level, you can specify a number of callback functions that indicate how the collection should handle its elements. This can then be converted into objective-C collections with special memory management semantics using seamless bridging techniques.

    Let’s take CFMutableDictionary as an example to solve the above problem.
    1. Let’s first look at the definition of a method that creates a dictionary:

      /** * create dictionary * @param allocator indicates the memory allocator to be used (data structures in CoreFoundation objects require memory, and the allocator allocates and reclaims this memory). Usually NULL is passed to indicate that the default allocator is used. * @param capacity Specifies the initial size of the dictionary, the same as NSMutableDictionary initWithCapacity: * @param keyCallBacks * @param valueCallBacks The last two arguments are Pointers to structures that define a number of callback functions that indicate what the keys and values in the dictionary should do when they encounter various events. Its definition see below CFDictionaryKeyCallBacks and CFDictionaryValueCallBacks * /
      CFMutableDictionaryRef CFDictionaryCreateMutable(
          CFAllocatorRef allocator, 
          CFIndex capacity, 
          const CFDictionaryKeyCallBacks *keyCallBacks, 
          const CFDictionaryValueCallBacks *valueCallBacks
      ); 
      
      @param retain, release, copyDescription, equal, hash are all Pointers to the function. Define which functions should be used to perform tasks when various events occur. Such as adding key/value pair to the dictionary is called retain function, whose definition see below CFDictionaryRetainCallBack * /
      typedef struct {
          CFIndex				version;
          CFDictionaryRetainCallBack		retain;
          CFDictionaryReleaseCallBack		release;
          CFDictionaryCopyDescriptionCallBack	copyDescription;
          CFDictionaryEqualCallBack		equal;
          CFDictionaryHashCallBack		hash;
      } CFDictionaryKeyCallBacks;
      
      /** * value of the callback function * as CFDictionaryKeyCallBacks */
      typedef struct {
          CFIndex				version;
          CFDictionaryRetainCallBack		retain;
          CFDictionaryReleaseCallBack		release;
          CFDictionaryCopyDescriptionCallBack	copyDescription;
          CFDictionaryEqualCallBack		equal;
      } CFDictionaryValueCallBacks;
      
      /** * retain function definition * @param allocator * @param Value Key to be added to dictionary * @return void* represents final value to be added to dictionary */
      typedef const void* (*CFDictionaryRetainCallBack) (CFAllocatorRef allocator, const void *value);
      Copy the code
    2. NSMutableDictionary: NSMutableDictionary:

      #import <Foundation/Foundation.h>
      #import <CoreFoundation/CoreFoundation.h>
      
      const void * EOCRetainCallBack(CFAllocatorRef allocator, const void *value) {
          return CFRetain(value);  // Keys or values added to the dictionary are returned after retain without copy
      }
      
      void EOCReleaseCallBack(CFAllocatorRef allocator, const void *value) {
          CFRelease(value);
      }
      
      CFDictionaryKeyCallBacks keyCallBacks = {
          0,
          EOCRetainCallBack,
          EOCReleaseCallBack,
          NULL.// copyDescription passes NULL, which means the default implementation is adopted
          CFEqual.// CFEqual eventually calls NSObject's isEqual: method
          CFHash   // CFHash will eventually call NSObject's hash method
      };
      
      CFDictionaryValueCallBacks valueCallBacks = {
          0,
          EOCRetainCallBack,
          EOCReleaseCallBack,
          NULL.CFEqual
      };
      
      CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(
          NULL.0,
          &keyCallBacks,
          &valueCallBacks
      );
      // Make seamless bridge
      NSMutableDictionary *aNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFDictionary;
      Copy the code

50. Use NSCache instead of NSDictionary to build the cache

The main points of

  • Use NSCache instead of NSDictionary objects when implementing caching. Because NSCache provides elegant auto-delete, it’s “thread-safe,” and unlike dictionaries, it doesn’t copy keys.
  • An NSCache object can be capped to limit the total number of objects in the cache and the “total cost,” and these scales define when the cache removes objects from it. But never think of these scales as reliable “hard limits”; they are only guidelines for NSCache.
  • When NSPurgeableData is used with NSCache, it automatically clears data. That is, when an NSPurgeableData object is discarded by the system, the object itself is removed from the cache.
  • If caching is used properly, the response time of your application can be improved. Only data that is “cumbersome to recalculate” is worth putting in the cache, such as data that needs to be fetched from the network or read from disk.

The author

  • Use NSCache instead of NSDictionary to build the cache. The advantages of NSCache are:
    1. When the system is about to run out of resources, it can gracefully automatically delete the cache, and the oldest unused cache will be deleted first. You can do it yourself with NSDictionary, but it’s complicated.
    2. NSCache does not copy the key, but preserves it. This can be done using NSDictionary, but it is more complicated. See 🚩 49.
    3. NSCache is thread-safe. Multiple threads can access NSCache at the same time without writing locking code. And NSDictionary is not thread-safe.
  • You can control when NSCache deletes the cache
    1. totalCostLimitLimit the total overhead of all objects in the cache
    2. countLimitLimit the total number of objects in the cache
    3. - (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;When an object is added to the cache, you can specify its overhead value

    The timing of the cache object may be truncated:

    1. When the total number of objects or total overhead exceeds the upper limit
    2. When available system resources tend to be stretched

    Note that:

    1. Just because something might be cut doesn’t mean it will be cut
    2. The order in which objects are deleted is determined by the implementation
    3. It is not recommended to force the cache to delete objects first by tweaking overhead values. Never consider these metrics as reliable “hard limits”; they are only guidelines for NSCache.
  • use- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;You can specify the cost of an object when it is added to the cache. But this is only true if the overhead can be calculated quickly, because caching is meant to increase the speed at which the application responds to user actions.
    1. For example, if you need to access the disk or database to determine the file size when calculating overhead, this approach is not appropriate.
    2. If you want to add an NSData object to the cache, its data size is known and you can access the properties directlydata.length.
  • NSPurgeableData
    1. NSPurgeableData is derived from NSMutableData. It is used with NSCache to automatically clear data. It implements the NSDiscardableContent protocol (if the memory occupied by an object can be discarded at any time according to data needs, the interface defined by this protocol can be implemented). After adding an object to NSCache, when the object is discarded by the system, it will be automatically cleared from the cache. Can be opened or closed by NSCache evictsObjectWithDiscardedContent attribute this functionality.
    2. NSPurgeableData is used in much the same way as reference counting, which is called when you need to access an NSPurgeableData objectbeginContentAccessMakes a “hold” and is called when it runs outendContentAccessRelease. NSPurgeableData is created with a “reference count” of 1, so it doesn’t need to be calledbeginContentAccess, only need to be called after useendContentAccessWill do.
      • beginContentAccess: tells it that it should not discard its occupied memory just yet
      • endContentAccess: tells it to discard its occupied memory if necessary
  • Example of code for NSPurgeableData to implement caching with NSCache:
    // Network fetcher class
    typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
    
    @interface EOCNetworkFetcher : NSObject
    
    - (id)initWithURL:(NSURL*)url;
    - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
    
    @end
    
    // Class that uses the network fetcher and caches results
    @interface EOCClass : NSObject
    @end
    
    @implementation EOCClass {
        NSCache *_cache;
    }
    
    - (id)init {
    
        if ((self = [super init])) {
            _cache = [NSCache new];
    
            // Cache a maximum of 100 URLs
            _cache.countLimit = 100;
    
            /** * The size in bytes of data is used as the cost, * so this sets a cost limit of 5MB. */
            _cache.totalCostLimit = 5 * 1024 * 1024;
        }
        return self;
    }
    
    - (void)downloadDataForURL:(NSURL*)url { 
    
        NSPurgeableData *cachedData = [_cache objectForKey:url];
    
        if (cachedData) {
            [cachedData beginContentAccess];
            // Cache hit: there is Cache, read
            [self useData:cachedData];
            [cachedData endContentAccess];
        } else {
            // Cache miss: no Cache, download
            EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];      
    
            [fetcher startWithCompletionHandler:^(NSData *data){
                NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
                [_cache setObject:purgeableData forKey:url cost:purgeableData.length];    
                [selfuseData:data]; [purgeableData endContentAccess]; }]; }}@end
    Copy the code

51. Simplify the initialize and Load implementation code

The main points of

  • During the load phase, the system calls the load method if the class implements it. This method can also be defined in a class, where the load method is called first. Unlike the other methods, the LOAD method does not participate in an override mechanism.
  • The system sends an Initialize message to a class before it is first used. Since this method follows normal overwrite rules, it is usually in there to determine which class is currently being initialized.
  • Both the Load and Initialize methods should be implemented in a streamlined manner to help keep the application responsive and reduce the chance of introducing an interdependency cycle.
  • Global constants that cannot be set at compile time can be initialized in the initialize method.

The author

  • Know when, how, and in what order load and Initialize methods are called. See Link: OC Bottom Exploration – Load and Initialize
  • Problems and considerations with using the load method:
    1. It is not safe to use other classes in the load method. For example, there is no inheritance relationship between class A and class B, and the order of execution of the load methods between them is uncertain, and you instantiate B in class A’s load method, while class B may perform some important operations in its load method before instantiating B. The load method of class B may not be executed at this point, so it is not safe.
    2. The load method must be implemented in a compact manner, minimizing the number of operations it performs, and not tasks that take too long or require locks, because the entire application would block while executing the load method.
    3. If the task does not need to be executed when the class is loaded into memory, but can be executed when the class is initialized, use initialize instead of load.
  • Initialize differs from Load in terms of when, how, and in what order it is called. The Initialize method is still safe. The runtime system is normal when initialize is executed, because it is safe to use and call any method in any class. The system also ensures that initialize is executed in a thread-safe environment. Only the initialize thread can manipulate the class or instance of the class, while all other threads block until Initialize completes.
  • If a subclass does not implement the initialize method, then the parent class will be called, so the initialize implementation usually makes some judgments about the message receiver:
    + (void)initialize {
        if (self == [EOCBaseClass class]) {
            NSLog(@"%@ initialized".self); }}Copy the code
  • The initialize implementation is also kept lean for the following reasons:
    1. If you initialize a class in the main thread, it will block for the duration of initialization.
    2. There is no control over the initialization timing of a class. You can’t write your code so that it depends on a specific point in time, or it can be dangerous if a later run-time system update changes the way classes are initialized.
    3. If you send messages to other classes in Initialize, it forces them all to be initialized. If other classes rely on data from that class when Initialize is executed, and data from that class is completed in Initialize, the problem will be “circular dependencies.” Therefore, the initialize method should only be used to set internal data, such as global constants that cannot be set at compile time. No other method should be called, even if it is the class’s own method.
  • When implementing the Load and Initialize methods, be sure to pay attention to these issues and simplify your code. If there are other things to do besides initializing the global state, you can create a special method to perform these operations and require that the consumer of the class must call this method before using the class. For example, if the singleton class has to perform some operations before it can be used for the first time, you can use this approach.

52. Don’t forget that NSTimer keeps its target objects

The main points of

  • The NSTimer object retains its target until the timer itself expires, which can be invalidated by calling the invalidate method, and one-time timers expire after the task is triggered.
  • Repeating timers are easy to introduce retention loops, which are guaranteed to result if the target object of such a timer also preserves the timer itself. This circular reservation may occur directly or indirectly through other objects in the object diagram.
  • You can extend the functionality of NSTimer to break retention rings with “blocks”. However, unless NSTimer provides this functionality in a public interface in the future, you will have to create a category to include the relevant implementation code. (Author’s note: NSTimer now offers this feature)

The author

  • If you create a timer by passing the target parameter, be aware that the target is reserved for the timer. If used improperly, memory leaks can easily occur.
  • In the following code, a retained loop is formed between the EOCClass instance and the _pollTimer. The loop can be broken in two ways.
    1. The instance is reclaimed by the system and the timer is deactivated in the dealloc method. However, since the timer retains the instance, it is not recycled until the timer is effective, so this approach is not feasible and must cause a memory leak.
    2. Calling the stopPolling method actively breaks the reserved ring, but it’s not a good idea to actively call a method to avoid memory leaks.
      #import <Foundation/Foundation.h>
      
      @interface EOCClass : NSObject
      - (void)startPolling;
      - (void)stopPolling;
      @end
      
      @implementation EOCClass {
          NSTimer *_pollTimer;
      }
      
      - (id)init {
          return [super init];
      }
      
      - (void)dealloc {
          [_pollTimer invalidate];
      }
      
      - (void)stopPolling {
          [_pollTimer invalidate];
          _pollTimer = nil;
      }
      
      - (void)startPolling {
          _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                        target:self
                                                      selector:@selector(p_doPoll)
                                                      userInfo:nil
                                                       repeats:YES];
      }
      
      - (void)p_doPoll {
          // Poll the resource
      }
      
      @end
      Copy the code
  • There are many ways to solve this problem, such as:
    1. Make the timer valid in viewWillDisappear. This method is not recommended because the VC does not destroy the push, but it also calls this method. The following code also has limitations.
      - (void)viewWillDisappear:(BOOL)animated {
          [super viewWillDisappear:animated];
          if ([self.navigationController.viewControllers indexOfObject:self] = =NSNotFound) {[self.timer invalidate];
              self.timer = nil; }}Copy the code
    2. DidMoveToParentViewController. The disadvantage is that it cannot be used with multiple controllers.
      - (void)didMoveToParentViewController:(UIViewController *)parent {
          if (parent == nil) {[self.timer invalidate];
              self.timer = nil; }}Copy the code
    3. Create a timer using a block
      __weak typeof(self) weakSelf = self;
      self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
          [weakSelf message];
      }];
      Copy the code

      Then make the timer valid in self’s destructor method.

    4. Encapsulate a layer of NSTimer
    5. Use the intermediate variable NSProxy
      1. Timer holds a strong reference to the proxy
      2. The proxy holds a weak reference to self and forwards the message to self
      3. Release the timer normally in self’s destructor method
  • NSTimer is passed as a target. Since the class object is a singleton, it doesn’t matter if the timer keeps it. Since Apple now provides a method to create NSTimer by passing blocks, there is no need to wrap a method like this one, so there is no need to post code here.