This is the first day of my participation in the More text Challenge. For details, see more text Challenge

IOS underlying principles article summary

This paper mainly explains the principle and realization of two kinds of wild pointer detection

Technical point: wild pointer detection

The main purpose of this paper is to understand the formation process of wild pointer and how to detect it

primers

Before introducing wild Pointers, let’s talk about the current types of exception handling, with a link to apple’s official website.)

Exception types

Abnormalities can be roughly divided into two categories:

  • 1. Software exceptions: mainly from kill(), pthread_kill(), uncaught NSException in iOS, absort, etc

  • 2. Hardware exception: The hardware signal starts from the processor trap, which is related to the platform. The wild pointer crash is mostly hardware exception

When handling exceptions, there are two concepts to focus on

  • The Mach abnormal:The Mach layercapture
  • UNIX signal:The BSD layerTo obtain

The POSIX API in iOS is implemented through the BSD layer on top of Mach, as shown in the figure below

  • Mach is a UniX-compatible system inspired by Accent.

  • The BSD layer is built on top of Mach and is an integral part of XNU. BSD is responsible for providing a reliable, modern API

  • POSIX stands for Portable Operating System Interface.

So, to sum up, there is a corresponding relationship between Mach exceptions and UNIX signals

  • 1, hardware exception process: hardware exception -> Mach exception -> UNIX signal
  • 2, software exception process: software exception -> UNIX signal

Mach exception and UNIX signal conversion

The following is the code for the conversion relationship between Mach exceptions and UNIX signals, from BSD /uxkern/ux_exception.c in XNU

switch(exception) {
case EXC_BAD_ACCESS:
    if (code == KERN_INVALID_ADDRESS)
        *ux_signal = SIGSEGV;
    else
        *ux_signal = SIGBUS;
    break;

case EXC_BAD_INSTRUCTION:
    *ux_signal = SIGILL;
    break;

case EXC_ARITHMETIC:
    *ux_signal = SIGFPE;
    break;

case EXC_EMULATION:
    *ux_signal = SIGEMT;
    break;

case EXC_SOFTWARE:
    switch (code) {

    case EXC_UNIX_BAD_SYSCALL:
    *ux_signal = SIGSYS;
    break;
    case EXC_UNIX_BAD_PIPE:
    *ux_signal = SIGPIPE;
    break;
    case EXC_UNIX_ABORT:
    *ux_signal = SIGABRT;
    break;
    case EXC_SOFT_SIGNAL:
    *ux_signal = SIGKILL;
    break;
    }
    break;

case EXC_BREAKPOINT:
    *ux_signal = SIGTRAP;
    break;
}
Copy the code
  • The corresponding relationships are summarized in a table, as shown below

  • The Mach exceptions are as follows
The Mach abnormal instructions
EXC_BAD_ACCESS Unreachable memory
EXC_BAD_INSTRUCTION Invalid or undefined instruction or operand
EXC_ARITHMETIC An arithmetic exception (such as dividing by 0). IOS is disabled by default, so we don’t usually encounter this
EXC_EMULATION Execute the instructions intended to support emulation
EXC_SOFTWARE Software generated exception, we do not normally see this type in the Crash log, apple’s log will be EXC_CRASH
EXC_BREAKPOINT Trace or breakpoint
EXC_SYSCALL UNIX system calls
EXC_MACH_SYSCALL Mach system call
  • There are several types of UNIX signals
UNIX signal instructions
SIGSEGV Segmentation fault. Access unallocated memory or write to memory that has no write permission.
SIGBUS Bus error. Such as memory address alignment, incorrect memory type access, etc.
SIGILL An invalid instruction was executed, usually an error occurred in the executable file
SIGFPE Deadly arithmetic operations. For example, numeric overflow, NaN values, and so on.
SIGABRT This is generated by calling abort() and sent via pthread_kill().
SIGPIPE The pipe broke. Usually occurs during interprocess communication. For example, if two processes communicate using FIFO(pipe) and write to the pipe without opening or accidentally terminating, the writing process will receive the SIGPIPE signal. According to Apple’s documentation, you can ignore this signal.
SIGSYS System call exception.
SIGKILL This signal indicates that the system has stopped the process. Crash reports contain code that represents the cause of the abort. Exit (), kill(9) and so on. The iOS system kills processes, such as a watchDog.
SIGTRAP Breakpoint instruction or other trap instruction is generated.

Wild pointer

The object to which it points is freed or reclaimed, but the pointer is not modified so that it still points to the reclaimed memory address. This pointer is the wild pointer

Wild pointer classification

This reference Tencent Bugly team summary, roughly divided into two categories

  • Memory is not overwritten
  • Memory overwritten

See the figure below

Why so many OC wild pointer crashes? Generally speaking, most crash should be covered. However, due to the randomness of wild pointer, crash often does not occur during the test, but occurs online, which is very fatal to the app experience

The randomness of wild Pointers can be roughly divided into two categories:

  • 1, can not run into the wrong logic, the execution of the wrong code, this can passImprove test scenario coverageTo solve the
  • 2. Running into faulty logic, but the address of the wild pointer does not necessarily cause crash, because:Wild pointerIts essence is a pointingObjects that have been deletedorRestricted memory regionthePointer to the. hereOC wild pointerRefers to theOC A wild pointer that is not set to null after the object is released. The reason it doesn’t have to be here is becausedeallocAfter execution just tell the system this pieceI didn't use the memory, and the system didn't make it inaccessible

Wild pointer solution

Here are two processing schemes in Xcode:

  • 1.Malloc Scribble, itsThe official explanationDo as follows: Apply for memoryallocWhen, fill in memory0xAA, free memorydeallocFill in the memory0x55.

  • 2,Zombie Objects, itsThe official explanationAn object that has been dereferenced and has been released, but is still able to accept messages, is calledZombie Objects(Zombie objects). The whole point of this isTurn the released objects into zombies

Compare the two schemes

  • 1, zombie objects compared to Malloc Scribble, do not need to consider the problem will not crash, as long as the wild pointer pointing to the zombie object, then again visiting the wild pointer will definitely crash

  • 2, zombie object this method is not as wide as Malloc Scribble coverage, you can hook free method to include C functions

1, Malloc Scribble

When the object memory is filled with 0xAA and 0x55, the program will appear abnormal

  • When alloc is applied for memory, enter 0xAA on the memory,

  • Free memory Dealloc fills 0x55 in memory.

The above application and release fill correspond to the following two cases respectively

  • Application: Accessed directly without initialization
  • Release: Access after release

So to sum up, for wild pointer, our solution is: when the object is released, the data is filled 0x55. About the release of the object process can refer to this article iOS – the underlying principle of 33: memory management (a) TaggedPointer/retain/release/dealloc/retainCount layer analysis

Wild pointer probe implementation 1

This implementation is mainly based on the sharing of Tencent Bugly engineer: Chen Qifeng, the main idea in its code is

  • 1, through thefishhookreplaceC functionsthefreeMethods are customsafe_free, similar to Method Swizzling
  • 2, insafe_freeMethod forThe memory of the variable has been freedFill in the0x55To cause the variable to have been freedCan not access, which changes the crash of some wild Pointers from not having to be installedWill now.
    • In order to prevent the memory filled with 0x55 from being filled with new data contents and making the wild pointer crash unnecessary, the strategy used here is that safe_free does not free the memory, but keeps it, that is, free is not actually called in the safe_free method.

    • In addition, to prevent excessive memory consumption (because memory needs to be reserved), you need to release some memory when the reserved memory is greater than a certain value to prevent it from being killed by the system. In addition, you also need to release some memory when you receive a memory warning

  • 3. When a crash occurs, the crash information is limited, which is not conducive to troubleshooting, so the proxy class is adopted here (inheriting fromNSProxy), overriding the three methods of message forwarding (see this articleIOS – Underlying Principle 14: Dynamic method resolution & Message forwarding for message flow analysis) and the instance method of NSObject to get the exception information. However, there is a problem with this. NSProxy can only be used as a proxy for OC objects, so you need to add the object type judgment in safe_free

Here is the complete wild pointer probe implementation code

  • The introduction of fishhook

  • Implement a proxy subclass of NSProxy
<! --1, mizombieproxy. h--> @interface MIZombieProxy: nsproxy@property (nonatomic, assign) Class originClass; @end <! --2, MIZombieProxy. M --> #import "MIZombieProxy. H "@implementation - (BOOL)respondsToSelector:(SEL)aSelector{ return [self.originClass instancesRespondToSelector:aSelector]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ return [self.originClass instanceMethodSignatureForSelector:sel]; } - (void)forwardInvocation: (NSInvocation *)invocation { [self _throwMessageSentExceptionWithSelector: invocation.selector]; } #define MIZombieThrowMesssageSentException() [self _throwMessageSentExceptionWithSelector: _cmd] - (Class)class{ MIZombieThrowMesssageSentException(); return nil; } - (BOOL)isEqual:(id)object{ MIZombieThrowMesssageSentException(); return NO; } - (NSUInteger)hash{ MIZombieThrowMesssageSentException(); return 0; } - (id)self{ MIZombieThrowMesssageSentException(); return nil; } - (BOOL)isKindOfClass:(Class)aClass{ MIZombieThrowMesssageSentException(); return NO; } - (BOOL)isMemberOfClass:(Class)aClass{ MIZombieThrowMesssageSentException(); return NO; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol{ MIZombieThrowMesssageSentException(); return NO; } - (BOOL)isProxy{ MIZombieThrowMesssageSentException(); return NO; } - (NSString *)description{ MIZombieThrowMesssageSentException(); return nil; } #pragma mark - MRC - (instancetype)retain{ MIZombieThrowMesssageSentException(); return nil; } - (oneway void)release{ MIZombieThrowMesssageSentException(); } - (void)dealloc { MIZombieThrowMesssageSentException(); [super dealloc]; } - (NSUInteger)retainCount{ MIZombieThrowMesssageSentException(); return 0; } - (struct _NSZone *)zone{ MIZombieThrowMesssageSentException(); return nil; } #pragma mark - private - (void)_throwMessageSentExceptionWithSelector:(SEL)selector{ @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"(-[%@ %@]) was sent to a zombie object at address: %p", NSStringFromClass(self.originClass),NSStringFromSelector(selector), self] userInfo:nil]; } @endCopy the code
  • Hook free method of concrete implementation
<! H --> @interface MISafeFree: NSObject void free_safe_mem(size_t freeNum); @end <! Misafefree. m--> #import "misafefree. h" #import "queue. H" #import "fishhook. H "#import "MIZombieProxy <dlfcn.h> #import <objc/runtime.h> #import <malloc/malloc.h> // Save zombie static Class kMIZombieIsa; Static size_t kMIZombieSize; Static void(* orig_free)(void *p); Static CFMutableSetRef registeredClasses = nil; // registeredClasses = nil; */ struct DSQueue *_unfreeQueue = NULL */ struct DSQueue *_unfreeQueue = NULL; Int unfreeSize = 0; int unfreeSize = 0; #define MAX_STEAL_MEM_SIZE 1024*1024*100 #define MAX_STEAL_MEM_NUM 1024*1024*10 // Number of Pointers to be freed on each release #define BATCH_FREE_NUM 100 @implementation MISafeFree #pragma mark - Public Method Void free_safe_mem(size_t freeNum){#ifdef DEBUG // Obtain queue length size_t count = ds_queue_length(_unfreeQueue); FreeNum = freeNum > count? count : freeNum; For (int I = 0; i < freeNum; Void *unfreePoint = ds_queue_get(_unfreeQueue); Size_t memSize = malloc_size(unfreePoint); __sync_fetch_and_sub(&unfreeSize, (int)memSize); __sync_fetch_and_sub(&unfreeSize, (int)memSize); / / release orig_free (unfreePoint); } #endif } #pragma mark - Life Circle + (void)load{ #ifdef DEBUG loadZombieProxyClass(); init_safe_free(); #endif} #pragma mark-private Method void safe_free(void* p){int unFreeCount = ds_queue_length(_unfreeQueue); / / reserved memory is greater than a certain value, release of part of the if (unFreeCount > MAX_STEAL_MEM_NUM * 0.9 | | unfreeSize > MAX_STEAL_MEM_SIZE) { free_safe_mem(BATCH_FREE_NUM); }else{// create memory size for P; size_t memSize = malloc_size(p); If (memSize > kMIZombieSize) {// obj = (id)p; OrigClass = object_getClass(obj); Char *type = @encode(typeof(obj)); /* -strcmp -cfsetContainsValue -cfsetContainsValue If (STRCMP ("@", type) == 0 && CFSetContainsValue(registeredClasses, OrigClass)) {// fill memory 0x55 memset(obj, 0x55, memSize); Memcpy (obj, &kmizombieisa, sizeof(void*)); // Set class object_setClass(obj, [MIZombieProxy class]) for obj; // retain obj's original class ((MIZombieProxy*)obj). OriginClass = origClass; // Multithread fetch_and_add(&unfreeSize, (int)memSize); // Multithread fetch_and_add(&unfreeSize, (int)memSize); Ds_queue_put (_unfreeQueue, p); }else{ orig_free(p); } }else{ orig_free(p); }}} void loadZombieProxyClass(){registeredClasses = CFSetCreateMutable(NULL, 0, NULL); Unsigned int count = 0; unsigned int count = 0; Class *classes = objc_copyClassList(&count); For (int I = 0; int I = 0; i < count; i++) { CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i])); } free(classes); classes = NULL; kMIZombieIsa = objc_getClass("MIZombieProxy"); kMIZombieSize = class_getInstanceSize(kMIZombieIsa); } // Initialize and rebind the free symbol to bool init_safe_free(){// initialize the queue used to hold memory _unfreeQueue = ds_queue_CREATE (MAX_STEAL_MEM_NUM); Orig_free = (void(*)(void*)) dlSYm (RTLD_DEFAULT, "free"); Struct rebinding {const char *name; /* rebindings (struct rebinding {const char *name; Void *replacement; Void **replace; // Replace symbol value (address value) void **replace; // To store the original symbol value (address value)}; - Parameter 2: Rebindings_nel describes the length of the array */ // rebind the free symbol, Make it point to the custom safe_free function rebind_symbols((struct rebinding[]){{"free", (void*)safe_free}}, 1); return true; } @endCopy the code
  • test
- (void)viewDidLoad { [super viewDidLoad]; id obj = [[NSObject alloc] init]; self.assignObj = obj; // [MIZombieSniffer installSniffer]; } - (IBAction)mallocScribbleAction:(id)sender { UIView* testObj = [[UIView alloc] init]; [testObj release]; for (int i = 0; i < 10; I++) {UIView* testView = [[UIView alloc] initWithFrame:CGRectMake(0,200,CGRectGetWidth(self.view.bounds), 60)]; [self.view addSubview:testView]; } [testObj setNeedsLayout]; }Copy the code

The print result is as follows

2, Zombie Objects

The zombie object

  • Can be used to detect memory errors (EXC_BAD_ACCESS), which can catch any calls that interpret access to bad memory

  • If you send a message to a zombie object, it is still responsive, and then crashes with an error log showing the name of the class and method that the wild pointer object called

First of all, let’s look at how to implement Zombie Objects in Xcode. The specific operation steps can refer to this article to explore the principle of iOS Zombie Objects

  • fromdeallocIn the source code, we can see"Replaced by NSZombie", i.e.,Object to releaseWhen,NSZombie is going to be a replacement in Dealloc, as shown below

So zombie object generation process pseudocode is as follows

Class CLS = object_getClass(self); //3, const char *zombieClsName = "_NSZombie_" + clsName; //3, const char *zombieClsName = "_NSZombie_" + clsName; //4. Create Class zombieCls = objc_lookUpClass(zombieClsName); if (! ZombieCls) {//5, get the zombie object Class _NSZombie_ Class baseZombieCls = objc_lookUpClass(" _NSZombie_"); //6, create zombieClsName class zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0); } //7. Destroy the object's member variables and associated references if the object's memory is not freed. objc_destructInstance(self); //8, modify object isa pointer to point to the special zombie class objc_setClass(self, zombieCls);Copy the code
  • When the zombie object is accessed again, the message forwarding process starts to process the access of the zombie object, output logs, and crash occurs

So the zombie object triggers the process pseudocode as follows

Class class CLS = object_getClass(self); Const char *clsName = class_getName(CLS); //2, const char *clsName = class_getName(CLS); _NSZombie_ if (string_has_prefix(clsName, Const char *originalClsName = substring_from(clsName, 10); const char *originalClsName = substring_from(clsName, 10); Const char *selectorName = sel_getName() const char *selectorName = sel_getName(); Log(' *** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self); //7, abort();Copy the code

So to sum up, the idea behind this nakanfield pointer probe is that instead of the dealloc method, the key is to call objc_destructInstance to unreference the object

Wild pointer probe implementation 2

The main idea for this is the source sindrilin, and the main idea is:

  • Wild pointer detection process
    • 1. Open the wild pointer detection

    • 2, set the callback block when monitoring to the wild pointer, print information in the block, or store the stack

    • 3. Whether the wild pointer crashes is detected

    • 4. Maximum memory usage

    • 5, whether to record dealloc call stack

    • 6. Monitoring policy

      • 1) Monitor only custom objects

      • 2) Whitelist policy

      • 3) Blacklist policy

      • 4) Monitor all objects

    • Swap NSObject’s dealloc method

  • Trigger wild pointer
    • 1. Start processing objects

    • 2. Whether the replacement conditions are met

      • 1) According to the monitoring policy, whether it belongs to the class to be detected
      • 2) Whether there is enough space
    • 3. If the conditions are met, the object is obtained and dereferencedent. If not, the object is normally freed, that is, the original dealloc method is called

    • 4. Populate objects with data

    • 5. Replace ISA with class Pointers to zombie objects

    • 6, object +dealloc call stack, saved in zombie objects

    • 7. Determine whether to clean up memory and objects

The realization idea through zombie object detection

  • Swap root class NSObject and NSProxy’s dealloc methods as custom dealloc methods via Mehod Swizzling in OC

  • 2. In order to avoid the problem of wild pointer caused by overwriting the memory space after release, we store the freed objects through the dictionary, and set the dealloc method to release the objects stored in the dictionary after 30s to avoid memory increase

  • In order to get more crash information, we need to create NSProxy subclass again

The specific implementation

  • Create a subclass of NSProxy that implements exactly the same as MIZombieProxy

  • 2. Implementation of Hook dealloc function

<! H --> @interface MIZombieSniffer: NSObject /*! * @method installSniffer * start zombie detection */ + (void)installSniffer; / *! * @method uninstallSnifier * stop zombie detection */ + (void)uninstallSnifier; / *! * @method appendIgnoreClass * Add whitelist Class */ + (void)appendIgnoreClass: (Class) CLS; @end <! M --> #import "MIZombieSniffer. H "#import "MIZombieProxy. H" #import <objc/runtime.h> // typedef void (*MIDeallocPointer) (id objc); // Whether the wild pointer detector is enabled static BOOL _enabled = NO; Static NSArray *_rootClasses = nil; Static NSDictionary<id, NSValue*> *_rootClassDeallocImps = nil; Static NSMutableSet *__mi_sniffer_white_lists(){static NSMutableSet *mi_sniffer_white_lists (); Static dispatch_once_t onceToken; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ mi_sniffer_white_lists = [[NSMutableSet alloc] init]; }); return mi_sniffer_white_lists; } static retained void __mi_dealloc(__unsafe_unretained ID obj){// Retained object Class currentCls = [obj Class]; Class rootCls = currentCls; // Get a non-nsobject and NSProxy class while (rootCls! = [NSObject class] && rootCls ! = [NSProxy class]) {// Obtain the parent class of rootCls and assign the value rootCls = class_getSuperclass(rootCls); } NSString *clsName = NSStringFromClass(rootCls); MIDeallocPointer deallocImp = NULL; MIDeallocPointer deallocImp = NULL; [[_rootClassDeallocImps objectForKey:clsName] getValue:&deallocImp]; if (deallocImp ! = NULL) { deallocImp(obj); }} hook dealloc static inline IMP __mi_swizzleMethodWithBlock(Method Method, Void *block){/* imp_implementationWithBlock: Take a block argument, copy it to the heap, and return a trampoline that makes the block an implementation of any class's method, */ IMP blockImp = imp_implementationWithBlock((__bridge ID _Nonnull)(block)); Method_setImplementation (method, blockImp) return method_setImplementation(method, blockImp); } @implementation MIZombieSniffer + (void)initialize {_rootClasses = [@[NSObject class], [NSProxy class]] retain]; } #pragma mark - public + (void)installSniffer{ @synchronized (self) { if (! _enabled) {//hook root class dealloc method [self _swizzleDealloc]; _enabled = YES; }}} + (void)uninstallSnifier{@synchronized (self) {if (_enabled) {// restore the dealloc method [self _unswizzleDealloc]; _enabled = NO; }}} + (void)appendIgnoreClass (Class) CLS {@synchronized (self) {NSMutableSet *whiteList = __mi_sniffer_white_lists(); NSString *clsName = NSStringFromClass(cls); [clsName retain]; [whiteList addObject:clsName]; } } #pragma mark - private + (void)_swizzleDealloc{ static void *swizzledDeallocBlock = NULL; // Define block as method of IMP static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{swizzledDeallocBlock = (__bridge void *)[^void(id obj) {currentClass = [obj Class] If ([__mi_sniffer_white_lists() containsObject: ClsName]) {// release the object __mi_dealloc(obj) if it is in the whitelist; else {// modify the isa pointer to the object, Pointer to MIZombieProxy /* valueWithBytes:objCType creates and returns an NSValue object with a given value, which will be interpreted as a given NSObject - parameter 1: the value of the NSValue object - parameter 2: */ NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))]; // set object_setClass(obj, [MIZombieProxy class]); // Retain the object's original class ((MIZombieProxy *)obj). OriginClass = currentClass; Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), [objVal getValue:] ^{__unsafe_unretained ID deallocObj = nil; // Set the object's class to the original class objlosetClass (deallocObj, currentClass); // set __mi_dealloc(deallocObj); }); // Swap root NSObject with NSProxy dealLoc method originalDeallocImp NSMutableDictionary *deallocImps = [NSMutableDictionary dictionary]; For (Class rootClass in _rootClasses) {// Get the dealloc Method in the specified Class Method oriMethod = class_getInstanceMethod([rootClass class], NSSelectorFromString(@"dealloc")); IMP originalDeallocImp = __mi_swizzleMethodWithBlock(oriMethod, swizzledDeallocBlock); // Set IMP implementation [deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)]; } //_rootClassDeallocImps dictionary store swap after IMP implementation _rootClassDeallocImps = [deallocImps copy]; } + (void) _unswizzleDealloc {/ / reduction dealloc exchange IMP [_rootClasses enumerateObjectsUsingBlock: ^ (Class rootClass, NSUInteger idx, BOOL * _Nonnull stop) {IMP originDeallocImp = NULL; NSString *clsName = NSStringFromClass(rootClass); [[_rootClassDeallocImps objectForKey:clsName] getValue:&originDeallocImp]; NSParameterAssert(originDeallocImp); oriMethod = class_getInstanceMethod([rootClass class]], NSSelectorFromString(@"dealloc")); // Reverts the implementation of dealloc to method_setImplementation(oriMethod, originDeallocImp);}]; // Release [_rootClassDeallocImps release]; _rootClassDeallocImps = nil; } @endCopy the code
  • 3, test,
@interface ViewController ()

@property (nonatomic, assign) id assignObj;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id obj = [[NSObject alloc] init];
    self.assignObj = obj;
    
    [MIZombieSniffer installSniffer];
}
- (IBAction)zombieObjectAction:(id)sender {

    NSLog(@"%@", self.assignObj);
    
}
Copy the code

The crash information is displayed as follows

Refer to the article

  • Quality control – wild pointer positioning
  • IOS wild pointer processing
  • IOS wild pointer positioning: wild pointer sniffer
  • Summary of iOS wild pointer positioning
  • IOS Zombie Objects Principle exploration