内存管理(1.基本知识)

@synthesize和@dynamic

如下,@property会自动生成成员变量_name,name的get方法name和set方法setName:的声明,不包括实现。

@property (nonatomic, strong) NSString *name;

在实现文件中可以通过@synthesize生成实现文件,可以通过@synthesize name = _userName;的方式给生成的成员变量重命名。在很远古的版本需要手动加上@synthesize,但后来系统会在编译时自动加上@synthesize。若不需要@synthesize,可以使用@dynamic name;

@synthesize name;
@synthesize name = _userName;
@dynamic name;
属性修饰符strong(retain),copy

@synthesize 会根据修饰符的不同生成不同的set方法。
需要注意的是,在MRC下,需要在dealloc中手动release(引用计数-1),放弃对属性的引用。

- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain]; // [name copy];
    }
}

- (void)dealloc {
    self.name = nil;
    [super dealloc];
}
NSCopying和NSMutableCopying协议

Foudation框架中很多类都实现了NSCopyingNSMutableCopying协议,可通过copy或者mutableCopy方法得到一个不可变对象或可变对象。也有一些类只支持NSCopying协议,比如NSDate。我们也可以在自定义的类中实现NSCopyingNSMutableCopying协议,支持copymutableCopy方法。
苹果对Foudation中的copy做了优化,对不可变的的对象执行copy操作,等价于执行retain操作。
如下:
str1指向常量区字符串123,因为常量区不可变,str2指向和str1相同。
str3因为是taggedPointer,str4存放的值和str3相同。
str5指向堆区字符串对象,str6进行copy操作,Apple为了避免内存浪费,对不可变对象执行copy相当于一次retain操作,因此str6的copy等价于一次retain
str7是使用stringWithString创建,和stringWithFormat不同的是,stringWithString判断传入参数是常量则会直接指向该常量,不开辟空间。
str8同理str7。
str9是taggedPointer
str10和str11看出stringWithString与stringWithFormat的不同,stringWithString会尽力避免开辟新空间,而stringWithFormat因为会将字符串内容重新组织,所以不关心源内容的地址。
str10和str5地址相同,str12中str13地址不同,因为str12是可变字符串的,str12值的变化不应该影响str13。可以看出stringWithString类似copy操作。
注:常量区的123和taggedPointer都不是oc对象,因此不存在引用计数的概念,对其进行retain或copy都不会执行任何操作,返回值是其本身。

NSString *str1 = @"123";
NSString *str2 = [str1 copy];
NSString *str3 = [NSString stringWithFormat:@"123"];
NSNumber *str4 = [str3 copy];
NSString *str5 = [NSString stringWithFormat:@"123123123123123123"];
NSNumber *str6 = [str5 copy];
NSString *str7 = [NSString stringWithString:@"123"];
NSString *str8 = [NSString stringWithString:str1];
NSString *str9 = [NSString stringWithString:str3];
NSString *str10 = [NSString stringWithString:str5];
NSString *str11 = [NSString stringWithFormat:@"%@", str5];
NSMutableString *str12 = [NSMutableString stringWithFormat:@"123123123123123123"];
NSString *str13 = [NSString stringWithString:str12];
NSLog(@"%p %p %p %p %p %p", str1, str2, str3, str4, str5, str6);
NSLog(@"%p %p %p %p %p %p %p", str7, str8, str9, str10, str11, str12, str13);
// 输出结果
0x10943e020 0x10943e020 0xc4349fdb850c51e4 0xc4349fdb850c51e4 0x600003435e90 0x600003435e90

0x10943e020 0x10943e020 0xc4349fdb850c51e4 0x600003435e90 0x600003435e60 0x600003435ec0 0x600003435dd0
深拷贝与浅拷贝

Foundation中,对不可变对象执行copy操作,都是浅拷贝。比如上面的例子中,str6对str5 copy操作后,指向的仍然为str5的对象,而并没有创建新的对象,因此是浅拷贝。
Foundation中,对不可变对象执行mutableCopy操作,或者可变对象执行copy或mutableCopy操作,都是深拷贝。

对于容器对象,拷贝方式分两种,一种容器本身拷贝方式(array),一种是容器内元素的拷贝方式(@"1", @"2", @"3")。在对容器执行mutableCopy操作时,元素是指针传递,并没有开辟新的对象。因此,容器执行copymutableCopy操作,容器内元素都是浅拷贝。若要实现容器元素的深拷贝,可以自己实现深拷贝方法,依次对每个元素重新创建赋值。另一种方式是通过归档NSKeyedArchiver,将对象转成NSData,再通过解档将data转成容器对象,但这种方式因为要将数据转换成中间对象NSData,效率不高。

NSArray *array = @[@"1", @"2", @"3"];
NSZone

NSZone是Apple用来分配和释放内存的一种方式,它是一个结构体,使用C结构存储着关于对象的内存管理信息。Cocoa Application使用了一个系统默认的NSZone来对应用中的对象进行管理。当默认的NSZone里面管理大量对象的时候,大量对象的释放可能会导致内存严重碎片化,Cocoa本身也有优化,每次Alloc的时候会尝试去填满内存的空隙,但是这样做的时间开销很大。于是,当代码中需要有大量的alloc请求时可以自定义创建一个NSZone,把alloc全部转移到这个NSZone里面,减少了大量时间的开销,而且使用NSZone可以一口气把zone里面的对象全部清除掉,省掉大量时间去一个一个dealloc对象。
但是从ARC时代起,Apple放弃了NSZone(自动忽略),可以从ARC发布声明中看到。

You cannot use memory zones.
There is no need to use NSZone any more—they are ignored by the modern Objective-C runtime anyway.
https://developer.apple.com/library/archive/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

原因是运行时的内存管理本身已极具效率,NSZone容易引起内存使用效率低下并增加代码复杂度。
NSObject的创建都会经过callAlloc函数。

+ (id)alloc {
    return _objc_rootAlloc(self);
}
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
id objc_allocWithZone(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}

从源码也可以看出,callAlloc函数传入参数allocWithZone并没有被使用。

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    //判断当前类是否有自定义的+allocWithZone实现。
    //hasCustomAWZ()返回NO即当前类未实现allocWithZone方法,则调用_objc_rootAllocWithZone;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    // ...
}

上面看到了slowpath(checkNil && !cls),关于slowpath与fastpath的宏定义如下:

#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

由于计算机并不是一次只读取一条指令,而是一次会读取多条指令,所以在读到 if 语句时也会把 if 代码块内的内容也读取进来。但是有可能 if 大概率判断都是false,那么就增加了cpu不必要的开销。
于是 GCC 提供了一个内置函数 __builtin_expect:

long __builtin_expect (long EXP, long C)

参数 C 代表预计的值,表示程序员知道 EXP 的值很可能就是 C,大概率就是C。
回到上面的两个宏
fastpath(x) (__builtin_expect(bool(x), 1)) 表明x为真的概率更大
slowpath(x) (__builtin_expect(bool(x), 0)) 表明x为假的概率更大
一方面提高了性能,一方面提供概率信息也增加了代码的可读性。

引用计数

通过对obj4源码分析,retainCount方法依次会进行如下调用。

retainCount
_objc_rootRetainCount
objc_object::rootRetainCount

rootRetainCount内部实现如下:

#if SUPPORT_NONPOINTER_ISA
inline uintptr_t 
objc_object::rootRetainCount()
{
    //如果是taggedPointer ,那么就返回自己
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED); //isa.bits其实就是isa本身内容
    //判断isa是否支持非指针类型(下面会分析)
    if (bits.nonpointer) {
        uintptr_t rc = bits.extra_rc; // 读取引用计数,注意这里没有+1(旧版本有+1),最新的ObjC中extra_rc存储的引用计数不再需要-1。
        if (bits.has_sidetable_rc) {//判断一下是否是存在sideTable里,如果为1的话,加上存储在sideTable中的引用计数,key是对象的地址。
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
#else
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;
    return sidetable_retainCount();
}
#endif

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

SUPPORT_NONPOINTER_ISA字面理解是 支持非指针isa。NONPOINTER_ISA 这个设计思想跟TaggetPointer类似,ISA其实并不单单是一个指针。其中一些位仍旧编码指向对象的类。但是实际上并不会使用所有的地址空间,Objective-C 运行时会使用这些额外的位去存储每个对象数据,比如它的引用计数和是否它已经被弱引用等。isa中使用nonpointer位标识是否支持非指针isa。
我们一般理解isa是个Class指针,指向objc_class结构体,而objc_class是类对象。事实上,isa同时也是union共同体结构,可以看到在objc-private.h中isa的另一种定义。

// objc.h
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
// objc-private.h
struct objc_object {
private:
    isa_t isa;
}

union isa_t {
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;//0代表普通指针,存贮着class、meta-class对象的内存地址;1代表优化过,使用位域存储着更多信息
        uintptr_t has_assoc         : 1;//是否有设置过关联对象,如果没有会释放更快
        uintptr_t has_cxx_dtor      : 1;// 是否有c++析构函数
        uintptr_t shiftcls          : 33; // 对应的类对象(元类对象)内存地址
        uintptr_t magic             : 6; 
        uintptr_t weakly_referenced : 1;//标记是否有被弱引用指向过,如果没有会释放的更快
        uintptr_t deallocating      : 1; // 是否正在释放中
        uintptr_t has_sidetable_rc  : 1; //标记引用计数是否过大,无法存在isa中,如果为1,则表示引用计数存在一个sideTable表中
        uintptr_t extra_rc          : 19;//里面存储的值是引用计数器减1(最新版本objc4-818.2不需-1)
    };
};

简单来说,isa即是Class指针,也是isa_t共用体,通过这种方式,可以更好地利用存储空间。但这样会导致一个问题,isa存储了一些标志位,会导致指针的值不正确。这就是Apple设计巧妙之处,isa需要通过 & ISA_MASK得到最终的地址值

 #   define ISA_MASK        0x0000000ffffffff8ULL)

,可以看到ISA_MASK中最低三位为0,而对象在内存分配地址遵循内存对齐原则,地址值最低三位一定为0;而shiftcls看出,地址最大空间为33位,前面28位也可用来做标志位。

retain,release的内部实现与此类似。

weak指针

weak相对__unsafe_unretained的区别是,当指向对象被销毁时,所有指向对象的指针都会改为指向nil。通过检查weak指针是否为nil,可以判断被赋值的对象是否已废弃。
weak详细分析:https://www.jianshu.com/p/cb7d9a4b85ba

自动释放池。

对象调用autorelease方法会将该对象添加到自动释放池中,当自动释放池销毁(或倾倒)时,会对池内的所有对象发送release消息。在MRC下的Foundation框架中,使用类方法(非alloc,new,copy,mutableCopy)创建的对象一般内部已经加上了autorelease,比如[NSMutableArray array](注意像[NSArray array]`这种不会加上autorelease,因为触发了TaggedPointer机制,没有开辟对象空间,也不存在引用计数)。
@autoreleasepool创建的自动释放池,会在代码块开头{创建,在代码块结尾}释放。若没有通过@autoreleasepool加入到自动释放池,会在Runloop进入休眠前对自动释放池进行释放,同时创建一个新的自动释放池。(仅限主线程)
通过以下代码输出主线程runloop内部情况。

NSLog(@"%@", [NSRunLoop mainRunLoop]);

可以看到两个与autorelease有关的observers。

    observers = (
    "{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f821d003038>\n)}}",
// ...
    "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f821d003038>\n)}}"
),

可以看到主线程runloop监听了kCFRunLoopBeforeTimers和kCFRunLoopBeforeWaiting活动状态,并回调_wrapRunLoopWithAutoreleasePoolHandler方法,实现自动释放池的管理。

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

AutoreleasePool创建和销毁如下:

// NSAutoreleasePool方式在ARC中被抛弃
 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release]; // [pool drain];
// 现在使用
@autoreleasepool {
}

通过llvm将代码转换成c/c++,可以看到@autoreleasepool {}内部实现是下面的代码。

@autoreleasepool {
}
// 转换后
{ __AtAutoreleasePool __autoreleasepool;
}

可以看到在代码块开始的位置,创建了__AtAutoreleasePool类型的结构体变量。

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

从__AtAutoreleasePool结构看出,__AtAutoreleasePool在构造函数中执行了objc_autoreleasePoolPush(),并返回atautoreleasepoolobj指针,在析构函数中执行了objc_autoreleasePoolPop(atautoreleasepoolobj)。因为局部变量在代码块结束时释放,因此@autoreleasepool {}实现也可以理解成如下:

{ 
void *atautoreleasepoolobj = objc_autoreleasePoolPush();

objc_autoreleasePoolPop(atautoreleasepoolobj);
}

从苹果官方文档看到:
1.每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构
2.新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除
3.对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池。
4.主线程的RunLoop在每次事件循环之前,会自动创建一个autoreleasePool
5.并且会在事件循环结束时,执行drain操作,释放其中的对象
6.如果您的应用程序在事件循环中创建了大量临时自动释放对象,那么创建自己的自动释放池可能会有助于最小化峰值内存占用。
7.当线程终止时候,所有与该线程相关的自动释放池都会执行drain操作。

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.
(https://developer.apple.com/documentation/foundation/nsautoreleasepool/)

通过objc4源码可以看到,objc_autoreleasePoolPush和objc_autoreleasePoolPop方法内部都是通过AutoreleasePoolPage管理自动释放池。
AutoreleasePoolPage

每个线程通过一组AutoreleasePoolPage来管理内存自动释放,这一组AutoreleasePoolPage组成一个双向链表,通过parent和child指向上一个或下一个节点,每一个节点是个AutoreleasePoolPage对象。
每个AutoreleasePoolPage对象占4096字节内存,除了存放内部成员变量,剩下都是存放autorelease对象的地址。如果新的对象地址装不下,会在双向链表开辟一个新的AutoreleasePoolPage节点用来存放。

  • pthread:page所属的线程
  • parent:父page(前一个page)
  • child:子page(下一个page)
  • next:下一个可存放元素的地址。
  • depth:深度(节点编号)

AutoreleasePoolPage类中提供begin()和end()方法,分别返回page中可容纳存放autorelease对象指针的起始地址和结束地址(4096字节结尾地址)。

AutoreleasePoolPage的存放空间除了可以存放autorelease对象的地址,也可以存放POOL_BOUNDARY(一个固定的值,用于做标记,图表中叫POOL_SENTINEL)。因为AutoreleasePoolPage链表入栈(Push)或出栈(Pop)对象指针遵循FILO,AutoreleasePool可以嵌套调用,因此用POOL_BOUNDARY是用来标识某个自动释放池创建起始位置,为了后续的autoreleasePool执行drain操作。
每次执行objc_autoreleasePoolPush()函数,会将一个新的POOL_BOUNDARY插入到AutoreleasePoolPage链表中,返回值是POOL_BOUNDARY所在的指针地址。
每次对象调用autorelease方法,就会将该对象的内存地址插入到AutoreleasePoolPage链表中。
每次执行objc_autoreleasePoolPop(atautoreleasepoolobj)(atautoreleasepoolobj就是POOL_BOUNDARY所在的指针地址),会对AutoreleasePoolPage链表中的指针元素进行出栈并调用release方法,直到到达atautoreleasepoolobj内存地址。

可以通过在代码中引用私有函数进行自动释放池的测试(私有函数不能上架AppStore)。

extern void _objc_autoreleasePoolPrint(void);

4 releases pending 代表POOL_BOUNDARY数量加上对象指针数量为4。
................ PAGE 代表一个AutoreleasePoolPage
################ POOL代表POOL_BOUNDARY
hot代表当前使用的PAGE(最后一页),cold代表第一页,full代表该页已满。

objc[73600]: ##############
objc[73600]: AUTORELEASE POOLS for thread 0x115797dc0
objc[73600]: 4 releases pending.
objc[73600]: [0x7fbe0880c000]  ................  PAGE  (hot) (cold)
objc[73600]: [0x7fbe0880c038]  ################  POOL 0x7fbe0880c038
objc[73600]: [0x7fbe0880c040]    0x600002458050  Person
objc[73600]: [0x7fbe0880c048]  ################  POOL 0x7fbe0880c048
objc[73600]: [0x7fbe0880c050]    0x600002645ce0  Car
objc[73600]: ##############

obj4中,autorelease方法依次会进行如下调用。可以看到最终调用了AutoreleasePoolPage::autorelease((id)this);,将自身指针传递给AutoreleasePoolPage用来管理。

autorelease
_objc_rootAutorelease
objc_object::rootAutorelease // 如果是taggedPointer直接返回
objc_object::rootAutorelease2
AutoreleasePoolPage::autorelease((id)this);

注:objc_object::rootAutorelease()内部调用了prepareOptimizedReturn()方法,该方法判断是否可以采用一种优化方案来代替autoreleasePool,如果可以则采用。
并不是所有对象在调用 autorelease 后都会被放进自动释放池的,一种情况是TaggedPointer,另一种是触发了autorelease优化。
后文会进一步分析prepareOptimizedReturn()

inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

通过下面代码可以看到,在autoreleaseFast中,将对象指针加入到page中。

static inline id autorelease(id obj)
{
    ASSERT(!obj->isTaggedPointerOrNil());
    id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
#endif
    return obj;
}

static inline id *autoreleaseFast(id obj)
{
    // 取到当前使用页(hotPage)
    AutoreleasePoolPage *page = hotPage();
    // page没满直接入栈
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) { // page满了
        return autoreleaseFullPage(obj, page);
    } else { // 没有page
        return autoreleaseNoPage(obj);
    }
}

主线程的RunLoop在每次事件循环之前,会自动创建一个autoreleasePool,并且会在事件循环结束时,执行drain操作,释放其中的对象。若主线程的autorelease的对象没有加入到@autoreleasepool{}中,则会在主线程Runloop进入休眠前(事件循环结束)进行释放。
主线程Runloop在初始化时默认使用runloopObserver监听了很多状态,并进行相关处理。
其中在收到kCFRunLoopEntry事件时,会调用objc_autoreleasePoolPush()。kCFRunLoopEntry指进入Runloop,Runloop初始化成功。
在收到kCFRunLoopBeforeWaiting事件时,会依次调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()。kCFRunLoopBeforeWaiting值Runloop即将进入休眠。
在收到kCFRunLoopExit事件时,调用objc_autoreleasePoolPop()。kCFRunLoopExit指Runloop退出。

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

注:当对象连续调用autorelease函数时,对象会被放进自动释放池多次,当自动释放池 pop 时会对对象调用对应次数的 release 操作,此时极有可能导致对象过度释放而使程序 crash。

···
ARC会对自动补充内存管理相关代码,那么具体做了哪些呢?
实现以下三个函数,看下llvm在ARC下补充了那些代码。

void testARC1() {
    [[NSMutableArray alloc] init];
}

void testARC2() {
    NSObject *object = [[NSMutableArray alloc] init];
}

void testARC3() {
    [NSMutableArray array];
}

若使用llvm的rewrite指令将代码重写为c++,并不会补充ARC相关代码,所以试图转换成编译中真实的中间代码查看。
使用以下指令,转换成ll格式中间代码。(-fobjc-arc一定要带上,告诉llvm这段代码是ARC格式。)

xcrun -sdk iphoneos clang -S -arch arm64 -fobjc-arc -emit-llvm main.m

通过中间代码可以看出, [[NSMutableArray alloc] init];转成中间代码后是调用了objc_alloc_init函数。
另外,testARC1()中由ARC补充了release()函数的调用,testARC2()中由ARC补充了objc_storeStrong()的调用。

define void @testARC1() #0 {
  %1 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %2 = bitcast %struct._class_t* %1 to i8*
  %3 = call i8* @objc_alloc_init(i8* %2)
  %4 = bitcast i8* %3 to %0*
  %5 = bitcast %0* %4 to i8*
  call void @llvm.objc.release(i8* %5) #1, !clang.imprecise_release !9
  ret void
}

define void @testARC2() #0 {
  %1 = alloca %1*, align 8
  %2 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %3 = bitcast %struct._class_t* %2 to i8*
  %4 = call i8* @objc_alloc_init(i8* %3)
  %5 = bitcast i8* %4 to %0*
  %6 = bitcast %0* %5 to %1*
  store %1* %6, %1** %1, align 8
  %7 = bitcast %1** %1 to i8**
  call void @llvm.objc.storeStrong(i8** %7, i8* null) #1
  ret void
}

define void @testARC3() #0 {
  %1 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8
  %2 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
  %3 = bitcast %struct._class_t* %1 to i8*
  %4 = call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %3, i8* %2)
  call void asm sideeffect "mov\09fp, fp\09\09// marker for objc_retainAutoreleaseReturnValue", ""()
  %5 = call i8* @llvm.objc.unsafeClaimAutoreleasedReturnValue(i8* %4) #1
  %6 = bitcast i8* %5 to %0*
  ret void
}

objc_alloc_init函数实现。

id objc_alloc_init(Class cls)
{
    return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}

在testARC1()中补充了release(),用来释放对象,这很好理解。

MRC与ARC下 autorelease的一些差异

在ViewController编译文件中设置-fno-objc-arc,使用mrc方式编译该文件。这里不使用weak指针而使用_objc_autoreleasePoolPrint来调试autorelease,因为weak指针会改变autorelease关系,后文会介绍。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSArray *array = [NSArray arrayWithObject:@"123"];
    NSLog(@"%s", __func__);
    _objc_autoreleasePoolPrint();
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%s", __func__);
    _objc_autoreleasePoolPrint();
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%s", __func__);
    _objc_autoreleasePoolPrint();
}

@end

通过下面的输出可以看到,ViewController在加载时的某个Runloop循环中,有大量的autorelease对象加入到自动释放池中,并得出以下结论。

  1. viewDidLoad和viewWillAppear处于同一个Runloop循环中,而viewDidAppear处于另一个Runloop循环。
  2. MRC中UIKit中非alloc copy new mutableCopy类方法创建的对象都在内部调用了autorelease。viewDidLoad的最后一项__NSSingleObjectArrayI正对应[NSArray arrayWithObject:@"123"];,__NSSingleObjectArrayI 是NSArray类簇中最终创建的对象类型,SingleObject代表只有一个元素,I代表immutable不可变。
2021-08-01 09:16:55.441588+0800 ObjCTest3[12877:843002] -[ViewController viewDidLoad]
objc[12877]: ##############
objc[12877]: AUTORELEASE POOLS for thread 0x11290a5c0
objc[12877]: 568 releases pending.
objc[12877]: [0x7fe53b004000]  ................  PAGE (full)  (cold)
objc[12877]: [0x7fe53b004038]  ################  POOL 0x7fe53b004038
objc[12877]: [0x7fe53b004040]  ################  POOL 0x7fe53b004040
objc[12877]: [0x7fe53b004048]  ################  POOL 0x7fe53b004048
objc[12877]: [0x7fe53b004050]    0x600001f69a00  UIApplicationSceneSettings
objc[12877]: [0x7fe53b004058]    0x600002466080  NSConcreteValue
// ...
objc[12877]: [0x7fe53b034220]    0x7fe539608730  ViewController
objc[12877]: [0x7fe53b034228]    0x600003370690  __NSSingleObjectArrayI
objc[12877]: ##############

2021-08-01 09:16:55.526984+0800 ObjCTest3[12877:843002] -[ViewController viewWillAppear:]
objc[12877]: ##############
objc[12877]: AUTORELEASE POOLS for thread 0x11290a5c0
objc[12877]: 786 releases pending.
objc[12877]: [0x7fe53b004000]  ................  PAGE (full)  (cold)
objc[12877]: [0x7fe53b004038]  ################  POOL 0x7fe53b004038
objc[12877]: [0x7fe53b004040]  ################  POOL 0x7fe53b004040
objc[12877]: [0x7fe53b004048]  ################  POOL 0x7fe53b004048
objc[12877]: [0x7fe53b004050]    0x600001f69a00  UIApplicationSceneSettings
objc[12877]: [0x7fe53b004058]    0x600002466080  NSConcreteValue
// ...
objc[12877]: [0x7fe53b034228]    0x600003370690  __NSSingleObjectArrayI
// ...
objc[12877]: [0x7fe53b0348f0]    0x7fe539506690  UITransitionView
objc[12877]: [0x7fe53b0348f8]    0x7fe539609730  UIWindow
objc[12877]: ##############

2021-08-01 09:16:55.687487+0800 ObjCTest3[12877:843002] -[ViewController viewDidAppear:]
objc[12877]: ##############
objc[12877]: AUTORELEASE POOLS for thread 0x11290a5c0
objc[12877]: 2 releases pending.
objc[12877]: [0x7fe53b004000]  ................  PAGE  (hot) (cold)
objc[12877]: [0x7fe53b004038]  ################  POOL 0x7fe53b004038
objc[12877]: [0x7fe53b004040]    0x7fe539609730  UIWindow
objc[12877]: ##############

那么MRC中非Foundation中对象,使用非alloc copy new mutableCopy类方法创建的对象,内部有没有调用autorelease呢?或者说Foundation中的autorelease,是在Foundation源码的类方法中显式调用,还是编译器通过识别函数名是否包括alloc copy new mutableCopy字段,自动补充autorelease。
我经过测试,非Foundation中对象,类方法中若没有显示调用autorelease,并不会加入到自动释放池,那么可以理解为Foundation源码的类方法中显式调用了autorelease。

ARC中使用__autoreleasing修饰符代替autorelease,当对象被赋值给__autoreleasing修饰的指针时,会将该对象加入到自动释放池中。
ARC中应该避免同一对象多次赋值给__autoreleasing修饰的指针(同一或不同指针),这样会导致该对象被多次加入到自动释放池中;MRC中同理,避免同一对象多次调用autorelase。这样会导致autoreleasePool在drain操作时对该对象多次release。

上面MRC同样的测试在ARC环境下进行,得出以下结论:

  1. 与MRC不同,ARC中UIKit中非alloc copy new mutableCopy类方法创建的对象不会在内部加入自动释放池。
  2. ARC中对象加上__autoreleasing修饰符,会被添加到自动释放池中。
  3. 无论MRC和ARC,都没有编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是自动将返回值的对象注册到autoreleasepool的机制,这和一些早期书籍上写的不符,可能是iOS在版本更新中取消该机制。
weak 与autoreleasePool
id obj = [NSArray arrayWithObject:@"123"];
id __weak obj1 = obj;
[obj1 class];
[obj1 count];
NSLog(@"%@", [obj1 class]);
_objc_autoreleasePoolPrint();

以上代码在MRC中执行,通过测试发现NSArray对象一共被四次加入autoreleasePool。一次是arrayWithObject方法内部调用autorelease,三次对obj1的使用都调用了autorelease。因此,在MRC环境中,每次对弱引用对象使用都会触发一次加入autoreleasePool。

objc[15067]: [0x7f89da80f228]    0x7f89d9c08b90  ViewController
objc[15067]: [0x7f89da80f230]    0x600001a9baf0  __NSSingleObjectArrayI
objc[15067]: [0x7f89da80f238]    0x600001a9baf0  __NSSingleObjectArrayI
objc[15067]: [0x7f89da80f240]    0x600001a9baf0  __NSSingleObjectArrayI
objc[15067]: [0x7f89da80f248]    0x600001a9baf0  __NSSingleObjectArrayI
objc[15067]: ##############

同样的代码在ARC中执行,可以看到NSArray对象一次也没有加入到autoreleasePool中。

objc[15353]: [0x7f8ad1829220]    0x7f8ad16072f0  ViewController
objc[15353]: ##############

但是如果将执行代码改成如下,__weak obj1放在代码块外部。

id __weak obj1;
{
    id obj = [NSArray arrayWithObject:@"123"];
    obj1 = obj;
    [obj1 class];
    [obj1 count];
    NSLog(@"%@", [obj1 class]);
    _objc_autoreleasePoolPrint();
}

输出结果看到,NSArray对象只有一次加入到autoreleasePool中。

objc[15418]: [0x7feb8f815220]    0x7feb8ed07f90  ViewController
objc[15418]: [0x7feb8f815228]    0x600000145f90  __NSSingleObjectArrayI
objc[15418]: ##############

那是那行代码引起加入autoreleasePool呢?将代码改成如下:

id __weak obj1;
{
    id obj = [NSArray arrayWithObject:@"123"];
    obj1 = obj;
    _objc_autoreleasePoolPrint();
}

输出结果仍然是NSArray对象只有一次加入到autoreleasePool中。
另外测试发现,如果obj1生命周期小于obj,也不会触发加入autoreleasePool。

objc[15468]: [0x7fa3d1016220]    0x7fa3cf607540  ViewController
objc[15468]: [0x7fa3d1016228]    0x60000247bea0  __NSSingleObjectArrayI
objc[15468]: ##############

由此可得出结论:

  1. MRC环境中,对weak指针对象每次使用都会触发一次加入autoreleasePool。
  2. ARC环境中,对weak指针对象的使用不会出发加入autoreleasePool。
  3. 当weak指针比对象指针生命周期大的时候,或者说对象指针被释放,weak指针仍然未被释放时,obj1 = obj;将对象指针赋值操作会触发一次加入autoreleasePool(如果有多个weak指针或多次赋值也只会触发一次)。

你可能感兴趣的:(内存管理(1.基本知识))