Easily overlooked problems in ARC memory management

Easily overlooked problems in ARC memory management

Directory: 1. String 1.1, String creation 1.2, String isa 2. Copy 2.1, Immutable object copy 2.2, Mutable object copy 2.3, Shallow copy and deep copy 2.4, Single layer Deep copy 3. Collections 3.1, NSMapTable 3.2, NSHashTable 3.3, NSPointerArray

1. String

I have seen several articles talking about this interview question. Strings are almost necessary for every high-level language, and they are indeed one of the most used types in actual projects. This article starts our discussion of memory management with this topic.

 //First
   NSString *name1 = [NSString stringWithFormat:@"stringTestOne"];
    __weak NSString *name2 = name1;
    NSLog(@"name1:%@", name1);
    NSLog(@"name2:%@", name2);
    name1 = nil;
    NSLog(@"name1:%@", name1);
    NSLog(@"name2:%@", name2);
   //the second
    NSString *a1 = [[NSString alloc] initWithFormat:@"stringTestTwo"];
    __weak NSString *a2 = a1;
    NSLog(@"a1:%@", a1);
    NSLog(@"a2:%@", a2);
    a1 = nil;
    NSLog(@"a1:%@", a1);
    NSLog(@"a2:%@", a2);
    //The third
    NSString *b1 = @"stringTestThree";
    __weak NSString *b2 = b1;
    NSLog(@"b1:%@", b1);
    NSLog(@"b2:%@", b2);
    b1 = nil;
    NSLog(@"b1:%@", b1);
    NSLog(@"b2:%@", b2);

Print result:
  name1:stringTestOne
  name2:stringTestOne
  name1:(null)
  name2:stringTestOne
  a1:stringTestTwo
  a2:stringTestTwo
  a1:(null)
  a2:(null)
  b1:stringTestThree
  b2:stringTestThree
  b1:(null)
  b2:stringTestThree

For the time being, I will not spray the interviewer to ask whether there is a big X component in it, but I really think this question is of little significance and there are loopholes.

1.1, the creation of a string

   NSString *string1 = [NSString stringWithFormat:@"TestString1_BAXIANG"];
    NSLog(@"%p",string1);
    NSString *string2 = [[NSString alloc] initWithFormat:@"TestString2_BAXIANG"];
    NSLog(@"%p",string2);
    NSString *string3 = @"TestString3_BAXIANG";
    NSLog(@"%p",string3);
Print result:
 0x7fb6e7d58a40
 0x7fb6e7d1c3a0
 0x10e6a7280

(1) Regarding the difference between stringWithFormat and initWithFormat, it’s easy to understand if students come from MRC developers all the way, but ARC has completely dominated the development of today, and the concept of reference counting does not need to be so harsh. StringWithFormat is actually created It is an autoreleased object that is added to the autorelease pool. The main purpose is to delay the release, and the object of initWithFormat needs to follow the golden rule of memory management that we often nagging. Who creates it and releases it. That is, the release in MRC.

Use po _objc_autoreleasePoolPrint() to print the Autorelease pools of the current autorelease pool. The string object 0x7fa65a50fdc0 created by stringWithFormat just now is in the current release pool.

objc[24294]: ##############
objc[24294]: AUTORELEASE POOLS for thread 0x10e6533c0
objc[24294]: 798 releases pending.
objc[24294]: [0x7fa65b800000] ................ PAGE (full) (cold)
objc[24294]: [0x7fa65b800038] ################ POOL 0x7fa65b800038
objc[24294]: [0x7fa65b800040] 0x7fa65a702820 __NSCFString
objc[24294]: [0x7fa65b800048] ################ POOL 0x7fa65b800048
........
//
objc[24294]: [0x7fa65b010958] 0x7fa65a50fdc0 __NSCFString

So the first character name1 = nil just removes the pointer to the current character 0x7fa65a50fdc0. name2 still points to the 0x7fa65a50fdc0 string, so name2 can also print out the current character data. Regarding printing the memory address, you will find that the string 3 (0x10e6a7280) will be significantly smaller than the above two, because it is created in the string constant area, and our first and second strings are created in the heap area. So b2 can still print out the string. But if we modify the content of the second string to string and debug it on a 64-bit Apple device, the printed result becomes the same as our first and second string results:

  NSString *a1 = [[NSString alloc] initWithFormat:@"string"];
    __weak NSString *a2 = a1;
    NSLog(@"a1:%@", a1);
    NSLog(@"a2:%@", a2);
    a1 = nil;
    NSLog(@"a1:%@", a1);
    NSLog(@"a2:%@", a2);
Print result:
a1:string
a2:string
a1:(null)
a2:string

You may think that seeing this result will subvert our above theory, but we may see the screenshot below and you may be even more strange:

The character content is: stringTestTwo

Character content is: string

We just shortened the length of the string, and the class of the current string has changed. What is even more strange is that the shortened object is not empty without isa. That is, the current string object has no class. This involves Apple's Tagged Pointer .

1.2, the isa of the string

(1) NSTaggedPointerString NSTaggedPointerString uses the surplus bits of the pointer address to store the current variable value. If the least significant bit of the object pointer is 1 (that is, an odd number), the pointer is a tagged pointer. This kind of pointer does not get its class by dereferencing isa, but by the index of a class table of the next three bits. The index is used to find which class of Tagged Pointer belongs to. The remaining 60 bits store data. For NSNumber, integers smaller than 2^60-1 are all stored by Tagged Pointer. For strings that require less than 60 bits of memory, it can create a Tagged Pointer, so NSTaggedPointerString is a disguised object that is stored inside It is not a pointer address but a string value, so that there is no need for a real object memory allocation and no indirect value. At the same time, the reference count can be a null instruction, because there is no memory to release, so performance will be significantly improved.

(2) __NSCFConstantString

A string constant is a compile-time constant. Its retainCount value is very large. The value printed on the console is 18446744073709551615==2^64-1. The test proves that even if the release operation is performed on it, retainCount will not Make any changes. The addresses of __NSCFConstantString objects with the same content are the same, which means that constant string objects are a singleton. This kind of object generally uses the literal value @"...", CFSTR("...") or stringWithString: method (it should be noted that this method is already called redundant in the iOS6 SDK, and this method will generate a Compiler warning. This method is equivalent to the literal creation method). Such objects are stored in the string constant area. (3) __NSCFString objects are stored on the heap. The __NSCFString object is a subclass of NSString created at runtime, it is not a string constant. So, like other objects, it gets a reference count of 1 when it is created. NSString objects created by methods such as NSString stringWithFormat are generally of this type.

2. Copy

2.1, copy of immutable objects

Will sending a copy message to an immutable object always get a new object? In the following test demo, copy messages are sent to immutable NSString, NSArray, NSDictionary and NSSet objects, and new immutable objects are obtained, but the question is: Is the copy a deep copy or a shallow copy?

    NSString *testStr = @"abc";
    NSString *copyStr = [testStr copy];
    NSLog(@"testStr = %p", testStr);
    NSLog(@"copyStr = %p", copyStr);
   
    NSArray *testArray = @[@1, @2, @3];
    NSArray *copyArray = [testArray copy];
    NSLog(@"testArray = %p", testArray);
    NSLog(@"copyArray = %p", copyArray);
  
    NSSet *testSet = [NSSet setWithObjects:@1,@2,@3,nil];
    NSSet *copySet = [testSet copy];
    NSLog(@"testSet = %p", testSet);
    NSLog(@"copySet = %p", copySet);
    
    NSDictionary *testDict = @{@"testKey":@"testValue"};
    NSDictionary *copyDict= [testDict copy];
    NSLog(@"testDict = %p", testDict);
    NSLog(@"testDict = %p", copyDict);

Print result:

testStr = 0x10442f220
copyStr = 0x10442f220
testArray = 0x7f9e7b60c3a0
copyArray = 0x7f9e7b60c3a0
testSet = 0x7f9e7b515e00
copySet = 0x7f9e7b515e00
testDict = 0x7f9e7b799140
testDict = 0x7f9e7b799140

Send copy messages to immutable objects, these objects will directly return to themselves instead of returning a newly created object.

-(id)copy {
    return [(id)self copyWithZone:nil];
}
-(id)mutableCopy {
    return [(id)self mutableCopyWithZone:nil];
}
-(id)copyWithZone:(NSZone *)zone{
 if (NSStringClass == Nil) 
    NSStringClass = [NSString class]; 
 return RETAIN(self); 
//return [[NSStringClass allocWithZone:zone] initWithString:self];
}

/* NSMutableCopying methods */
-(id)mutableCopyWithZone:(NSZone*)zone{ 
      return [[NSMutableString allocWithZone:zone] initWithString:self];
}

2.2, copy of mutable objects

  NSMutableArray *array = [NSMutableArray arrayWithObjects:@"baxianga",@"baxiang1",@"12345",nil];
    NSArray *copyArray = [array copy];
    NSMutableArray *mCopyArray = [array mutableCopy];
    NSLog(@"array = %p", array);
    NSLog(@"copyArray = %p", copyArray);
    NSLog(@"mCopyArray = %p", mCopyArray);

The memory addresses of copyArray, mCopyArray and array are different, indicating that copyArray and mCopyArray all copy the contents of array.

2.3, shallow copy and deep copy

There are two ways to copy objects: shallow copy (pointer copy) and deep copy (content copy). Shallow copy does not copy the content of the object, but only copies the pointer to the object; deep copy directly copies the entire object content to another block of memory in. Copying an immutable object is a shallow copy, and mutableCopy is a deep copy; copying a mutable object and mutableCopy are both a deep copy. Expressed as follows:

[immutableObject copy]//Shallow copy
[immutableObject mutableCopy]//Deep copy
[mutableObject copy]//Deep copy
[mutableObject mutableCopy]//Deep copy

2.4 Single deep copy

The deep copy of the collection object is limited to the object itself, and the object element is still a shallow copy. Apple called this kind of copy (This kind of copy is only capable of producing a one-level-deep copy), a real deep copy is Deep Copies

NSArray* trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

3. Collections

The collection stores an object, which is just a reference to this object, which is what we call a single deep copy, so that the retainCount of the object is +1, and when the object is removed from the array, the reference count is retainCount-1. The retainCount is not available under ARC. Although the three methods of obtaining the reference count (retain count) are not very accurate, you can still identify the current memory (1) private method

 OBJC_EXTERN int _objc_rootRetainCount(id);
 NSLog(@"before----%d",_objc_rootRetainCount(fo));

(2) Use KVC

 NSLog(@"%@",[fo valueForKey:@"retainCount"]);

(3) Core Foundation

 NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(fo)));

Adding an object to the iOS collection class will increase the reference count of the object by one and be held by the array. But sometimes you don't want the collection object to count the stored objects. At this time, you can use NSMapTable/NSHashTable/NSPointerArray.

    Foo *fo = [[Foo alloc] init];
    NSLog(@"before----%d",_objc_rootRetainCount(fo));
    NSMutableArray *array = [NSMutableArray array];
    [array addObject:fo];
    NSLog(@"after----%d",_objc_rootRetainCount(fo));
 //Print the result:
   before----1
   after----2

3.1, NSMapTable

NSMapTable is similar to NSDictionary, NSDcitionary or NSMutableDictionary. The key and value memory management is that the key is copied and the value is strongly referenced.

+ (instancetype)dictionaryWithObject:(ObjectType)object forKey:(KeyType <NSCopying>)key;

为了保证这个特性在NSDcitionary中对key的内存管理为copy,在复制的时候需要考虑对系统的负担,因此key应该是轻量级的,所以通常我们都用字符串和数字来做索引,但这只能说是key-to-object映射,不能说是object-to-object的映射。

- (instancetype)initWithKeyOptions:(NSPointerFunctionsOptions)keyOptions
                      valueOptions:(NSPointerFunctionsOptions)valueOptions
                          capacity:(NSUInteger)initialCapacity
static const NSPointerFunctionsOptions NSMapTableStrongMemory ;
static const NSPointerFunctionsOptions NSMapTableZeroingWeakMemory;
static const NSPointerFunctionsOptions NSMapTableCopyIn;
static const NSPointerFunctionsOptions NSMapTableObjectPointerPersonality;
static const NSPointerFunctionsOptions NSMapTableWeakMemory;
Person *p1 = [[Person alloc] initWithName:@"jack"];
Favourite *f1 = [[Favourite alloc] initWithName:@"ObjC"];

Person *p2 = [[Person alloc] initWithName:@"rose"];
Favourite *f2 = [[Favourite alloc] initWithName:@"Swift"];

NSMapTable *MapTable = [NSMapTable mapTableWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableWeakMemory];
//设置对应关系表
//p1 => f1;
//p2 => f2
[MapTable setObject:f1 forKey:p1];
[MapTable setObject:f2 forKey:p2];

NSLog(@"%@ %@", p1, [MapTable objectForKey:p1]);
NSLog(@"%@ %@", p2, [MapTable objectForKey:p2]);

3.2、NSHashTable

NSHashTable类似于NSSet和NSMutableSet合体,NSHashTable是可变的,可以使用 NSHashTableWeakMemory ,此选项使用weak存储对象,当对象被销毁的时候自动将其从集合中移除。NSHashTableObjectPointerPersonality :和 NSPointerFunctionsObjectPointerPersonality 相同,此选项是直接使用指针进行isEqual: 和 hash,提高查找效率,为了防止循环引用,创建一个delegate管理器,运营的就是NSHashTable。

- (NSHashTable *)delegates
{
    if (!_delegates) {
        _delegates = [NSHashTable weakObjectsHashTable];
    }
    
    return _delegates;
}

- (void)addDelegate:(id<UserAuthNotifierDelegate>)delegate
{
    if ([self.delegates containsObject:delegate]) {
        return;
    }
    [self.delegates addObject:delegate];
}
- (void)removeDelegate:(id<UserAuthNotifierDelegate>)delegate
{
    [self.delegates removeObject:delegate];
}

3.3、NSPointerArray

类似与NSArray ,NSPointerArray可以默认成 mutable的,而且可以插入空值nil,我们可以设置存储对象是否引用

[NSPointerArray strongObjectsPointerArray];//强引用
[NSPointerArray weakObjectsPointerArray];//弱引用
Reference:https://cloud.tencent.com/developer/article/1438308 ARC内存管理中容易忽略的问题 - 云+社区 - 腾讯云