作为一个老生常谈的话题,基本被网上的各种博客说尽了。但是前段时间朋友通过某些手段对
- ARC
进行了优化,提高了大概 1/3 左右的效率,在观赏过他改进的源码之后我又重新看了一遍
- YYModel
相关的实现源码,主要体现
- ARC
机制的几个方法分别是
- ARC
、
- retain
以及
- release
,主要与
- dealloc
和
- strong
两者相关
- weak
来看看一段
环境下的代码
- ARC
- (void)viewDidLoad {
NSArray * titles = @[@"title1", @"title2"];
}
在编译期间,代码就会变成这样:
- - (void)viewDidLoad {
- NSArray * titles = @[@"title1", @"title2"];
- [titles retain];
- /// .......
- [titles release];
- }
简单来说就是
在代码编译阶段,会自动在代码的上下文中成对插入
- ARC
以及
- retain
,保证引用计数能够正确管理内存。如果对象不是强引用类型,那么
- release
的处理也会进行相应的改变
- ARC
下面会分别说明在这几个与引用计数相关的方法调用中发生了什么
强引用有
、
- retain
以及
- strong
三种修饰,默认情况下,所有的类对象会自动被标识为
- __strong
强引用对象,强引用对象会在上下文插入
- __strong
以及
- retain
调用,从 处可以下载到对应调用的源代码。在
- release
调用的过程中,总共涉及到了四次调用:
- retain
对传入对象进行非空断言,然后调用对象的
- id _objc_rootRetain(id obj)
方法
- rootRetain()
断言非
- id objc_object::rootRetain()
环境,如果对象是
- GC
指针,不做处理。
- TaggedPointer
是苹果推出的一套优化方案,具体可以参考 一文
- TaggedPointer
增加引用计数,具体往下看
- id objc_object::sidetable_retain()
增加引用计数,具体往下看
- id objc_object::sidetable_retain_slow(SideTable& table)
在上面的几步中最重要的步骤就是最后两部的增加引用计数,在
中可以看到函数的实现。这里笔者剔除了部分不相关的代码:
- NSObject.mm
- #define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
- #define SIDE_TABLE_DEALLOCATING (1UL<<1)
- #define SIDE_TABLE_RC_ONE (1UL<<2)
- #define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
- typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
- struct SideTable {
- spinlock_t slock;
- RefcountMap refcnts;
- weak_table_t weak_table;
- }
- id objc_object::sidetable_retain()
- {
- // 获取对象的table对象
- SideTable& table = SideTables()[this];
- if (table.trylock()) {
- // 获取 引用计数的引用
- size_t& refcntStorage = table.refcnts[this];
- if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
- // 如果引用计数未越界,则引用计数增加
- refcntStorage += SIDE_TABLE_RC_ONE;
- }
- table.unlock();
- return (id)this;
- }
- return sidetable_retain_slow(table);
- }
这个类包含着一个自旋锁
- SideTable
来防止操作时可能出现的多线程读取问题、一个弱引用表
- slock
以及引用计数表
- weak_table
。另外还提供一个方法传入对象地址来寻找对应的
- refcnts
对象
- SideTable
对象通过散列表的结构存储了对象持有者的地址以及引用计数,这样一来,即便对象对应的内存出现错误,例如
- RefcountMap
异常,也能定位到对象的地址信息
- Zombie
后以后引用计数的值实际上增加了
- retain
而不是我们所知的
- (1 << 2) == 4
,这是由于引用计数的后两位分别被
- 1
以及
- 弱引用
两个标识位占领,而第一位用来表示计数是否越界。
- 析构状态
由于引用计数可能存在越界情况(
位的值为 1),因此散列表
- SIDE_TABLE_RC_PINNED
中应该存储了多个引用计数,
- refcnts
函数也证明了这一点:
- sidetable_retainCount()
- #define SIDE_TABLE_RC_SHIFT 2
- uintptr_t objc_object::sidetable_retainCount()
- {
- SideTable& table = SideTables()[this];
- size_t refcnt_result = 1;
- table.lock();
- RefcountMap::iterator it = table.refcnts.find(this);
- if (it != table.refcnts.end()) {
- refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
- }
- table.unlock();
- return refcnt_result;
- }
引用计数总是返回
这个数值,这也是为什么经常性的当对象被释放后,我们获取
- 1 + 计数表总计
的值总不能为
- retainCount
。至于函数
- 0
的实现和
- sidetable_retain_slow
几乎一样,就不再介绍了
- sidetable_retain
调用有着跟
- release
类似的四次调用,前两次调用的作用一样,因此这里只放上引用计数减少的函数代码:
- retain
- uintptr_t objc_object::sidetable_release(bool performDealloc)
- {
- #if SUPPORT_NONPOINTER_ISA
- assert(!isa.indexed);
- #endif
- SideTable& table = SideTables()[this];
- bool do_dealloc = false;
- if (table.trylock()) {
- RefcountMap::iterator it = table.refcnts.find(this);
- if (it == table.refcnts.end()) {
- do_dealloc = true;
- table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
- } else if (it->second < SIDE_TABLE_DEALLOCATING) {
- // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
- do_dealloc = true;
- it->second |= SIDE_TABLE_DEALLOCATING;
- } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
- it->second -= SIDE_TABLE_RC_ONE;
- }
- table.unlock();
- if (do_dealloc && performDealloc) {
- ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
- }
- return do_dealloc;
- }
- return sidetable_release_slow(table, performDealloc);
- }
在
中决定对象是否会被
- release
有两个主要的判断
- dealloc
状态,然后执行完成后发送
- 正在析构
消息释放对象
- SEL_dealloc
函数照样会返回
- sidetable_retainCount
的值。这时计数小于宏定义
- 1
,就不进行减少计数的操作,直接标记对象
- SIDE_TABLE_DEALLOCATING == 1
- 正在析构
看到
的代码就会发现在上面代码中宏定义
- release
体现出了苹果这个
- SIDE_TABLE_DEALLOCATING
的用心之深。通常而言,即便引用计数只有
- 心机婊
位的占用,在剔除了首位
- 8
标记以及后两位后,其最大取值为
- 越界
位。通常来说,如果不是项目中
- 2^5-1 == 31
不加限制的引用,是很难达到这么多的引用量的。因此占用了
- block
位不仅减少了额外占用的标记变量内存,还能以作为引用计数是否归零的判断
- SIDE_TABLE_DEALLOCATING
最开始的时候没打算讲
这个修饰,不过因为
- weak
方法本身涉及到了弱引用对象置空的操作,以及
- dealloc
过程中的对象也跟
- retain
有关系的情况下,简单的说说
- weak
的操作
- weak
- bool objc_object::sidetable_isWeaklyReferenced()
- {
- bool result = false;
- SideTable& table = SideTables()[this];
- table.lock();
- RefcountMap::iterator it = table.refcnts.find(this);
- if (it != table.refcnts.end()) {
- result = it->second & SIDE_TABLE_WEAKLY_REFERENCED;
- }
- table.unlock();
- return result;
- }
和
- weak
共用一套引用计数设计,因此两者的赋值操作都要设置计数表,只是
- strong
修饰的对象的引用计数对象会被设置
- weak
位,并且不参与
- SIDE_TABLE_WEAKLY_REFERENCED
函数中的计数计算而已
- sidetable_retainCount
- void objc_object::sidetable_setWeaklyReferenced_nolock()
- {
- #if SUPPORT_NONPOINTER_ISA
- assert(!isa.indexed);
- #endif
- SideTable& table = SideTables()[this];
- table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
- }
另一个弱引用设置方法,相比上一个方法去掉了自旋锁加锁操作
是重量级的方法之一,不过由于函数内部调用层次过多,这里不多阐述。实现代码在
- dealloc
的
- objc-object.h
行,可以自行到官网下载源码后研读
- 798
其实写了这么多,终于把本文的主角给讲出来了。在 iOS5 的时候,苹果正式推出了
机制,伴随的是上面的
- ARC
、
- weak
等新修饰符,当然还有一个不常用的
- strong
- __unsafe_unretained
,但是只能修饰对象
- assign
在机器上保证应用能保持在
帧以上的速率会让应用看起来如丝绸般顺滑,但是稍有不慎,稍微降到
- 55
之间都有很大的可能展现出卡顿的现象。这里不谈及图像渲染、数据大量处理等耳闻能详的性能恶鬼,说说
- 50~55
所造成的损耗。
- Model
如前面所说的,在
环境下,对象的默认修饰为
- ARC
,这意味着这么一段代码:
- strong
- @protocol RegExpCheck
- @property (nonatomic, copy) NSString * regExp;
- - (BOOL)validRegExp;
- @end
- - (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
- for (id<RegExpCheck> item in params) {
- if (![item validRegExp]) { return NO; }
- }
- return YES;
- }
把这段代码改为编译期间插入
和
- retain
方法后的代码如下:
- release
- - (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
- for (id<RegExpCheck> item in params) {
- [item retain];
- if (![item validRegExp]) {
- [item release];
- return NO;
- }
- [item release];
- }
- return YES;
- }
遍历操作在项目中出现的概率绝对排的上前列,那么上面这个方法在调用期间会调用
次
- params.count
和
- retain
函数。通常来说,每一个对象的遍历次数越多,这些函数调用的损耗就越大。如果换做
- release
修饰对象,那么这部分的调用损耗就被节省下来,这也是笔者朋友改进的手段
- __unsafe_unretained
首先要承认,相比起其他性能恶鬼改进的优化,使用
带来的收益几乎微乎其微,因此笔者并不是很推荐用这种高成本低回报的方式优化项目,起码在性能恶鬼大头解决之前不推荐,但是去学习内存管理底层的知识可以帮助我们站在更高的地方看待开发。最后送上
- __unsafe_unretained
来源: