这篇文章将分析苹果的内存管理的相关知识点,本文章都是基于arm64架构下的输出。分为以下几点内容
1.内存的布局
2.引用计数
3.弱引用/强引用
4.自动释放池
内存布局
关于内存布局,这里放两张图,尊重原创,图片的出处来自于 作者对内存布局的不同区域都做了总结。
打印一下看看,栈区,堆区,常量区,静态区的地址有没有什么差异:
x86_64架构下:发现栈的地址是0x7开始,堆的内存地址0x6开始,静态常量0x1
arm_64架构下:栈的地址 静态常量 是0x1开始,堆的地址从0x2开始
**2021-09-10 14:41:54.922932+0800 001---五大区Demo[3358:229699] obj栈的地址=0x7ffeeb3e04d8, 堆的内存地址=0x60000396c550**
**2021-09-10 14:41:54.923074+0800 001---五大区Demo[3358:229699] a == 0x7ffeeb3e04d4**
**2021-09-10 14:41:54.923140+0800 001---五大区Demo[3358:229699] b == 0x7ffeeb3e04d0**
**2021-09-10 14:41:54.923221+0800 001---五大区Demo[3358:229699] ************静态区**************
**2021-09-10 14:41:54.923317+0800 001---五大区Demo[3358:229699] clA == 0x10482045c**
**2021-09-10 14:41:54.923398+0800 001---五大区Demo[3358:229699] bssA == 0x104820460**
**2021-09-10 14:41:54.923470+0800 001---五大区Demo[3358:229699] bssStr1 == 0x104820468**
**2021-09-10 14:41:54.923570+0800 001---五大区Demo[3358:229699] ************常量区**************
**2021-09-10 14:41:54.923654+0800 001---五大区Demo[3358:229699] clB == 0x104820440**
**2021-09-10 14:41:54.923724+0800 001---五大区Demo[3358:229699] bssB == 0x104820444**
**2021-09-10 14:41:54.923792+0800 001---五大区Demo[3358:229699] bssStr2 == 0x104820448**
复制代码
2021-09-10 14:47:46.683094+0800 001---五大区Demo[515:92250] obj栈的地址=0x16d5f8f48, 堆的内存地址=0x281d150e0
2021-09-10 14:47:46.683186+0800 001---五大区Demo[515:92250] a == 0x16d5f8f44
2021-09-10 14:47:46.683226+0800 001---五大区Demo[515:92250] b == 0x16d5f8f40
2021-09-10 14:47:46.683267+0800 001---五大区Demo[515:92250] ************静态区************
2021-09-10 14:47:46.683302+0800 001---五大区Demo[515:92250] clA == 0x10280d45c
2021-09-10 14:47:46.683334+0800 001---五大区Demo[515:92250] bssA == 0x10280d460
2021-09-10 14:47:46.683367+0800 001---五大区Demo[515:92250] bssStr1 == 0x10280d468
2021-09-10 14:47:46.683407+0800 001---五大区Demo[515:92250] ************常量区************
2021-09-10 14:47:46.683589+0800 001---五大区Demo[515:92250] clB == 0x10280d440
2021-09-10 14:47:46.683727+0800 001---五大区Demo[515:92250] bssB == 0x10280d444
2021-09-10 14:47:46.683841+0800 001---五大区Demo[515:92250] bssStr2 == 0x10280d448
复制代码
知识点:静态变量只有文件拥有者才有权限修改,其它文件只可读取。
引用计数
引用计数存在ISA的extra_rc下,如果extra_rc存满了,大于10的时候就需要使用到has_sidetable_rc散列表
define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
复制代码
我们用strong修饰的属性,在赋值的时候set方法会触发objc_object::rootRetain进行应用计数处理
通过生成中间层代码后我们发现,set方法编译器增加了obj_storeStrong方法,
clang -S -fobjc-arc -emit-llvm main.m -o main.ll。
; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[LGTeacher setPerson:]"(%0* %0, i8* %1, %2* %2) #1 {
%4 = alloca %0*, align 8
%5 = alloca i8*, align 8
%6 = alloca %2*, align 8
store %0* %0, %0** %4, align 8
store i8* %1, i8** %5, align 8
store %2* %2, %2** %6, align 8
%7 = load %2*, %2** %6, align 8
%8 = load %0*, %0** %4, align 8
%9 = bitcast %0* %8 to i8*
%10 = getelementptr inbounds i8, i8* %9, i64 16
%11 = bitcast i8* %10 to %2**
%12 = bitcast %2** %11 to i8**
%13 = bitcast %2* %7 to i8*
call void @llvm.objc.storeStrong(i8** %12, i8* %13) #3
ret void
复制代码
断点走下去可以看到几个核心的函数,如果不是nopointisa会发现SideTable里面的refcntStorage做了一个+2的操作,直接操作散列表。
objc_object::sidetable_retain(bool locked)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
if (!locked) table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
复制代码
如果是nonpointerisa,先判断是否在执行析构函数。不是在析构会对newisa.bits的extra_rc+1,多了放到carry中。extra_rc在StoreExclusive方法中对引用计数进行了+1操作。
slowpath(newisa.isDeallocating())
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
newisa.extra_rc = RC_HALF;//extra_rc存一半
sidetable_addExtraRC_nolock(RC_HALF);//散列表存一半
StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)) // extra_rc++
复制代码
下面看一下弱引用oc是怎么处理的。当我们用weak修饰一个变量的时候,可以看到调用了storeWeak函数
@property(nonatomic, weak) LGPerson *person;
; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[LGTeacher setPerson:]"(%0* %0, i8* %1, %2* %2) #1 {
%4 = alloca %0*, align 8
%5 = alloca i8*, align 8
%6 = alloca %2*, align 8
store %0* %0, %0** %4, align 8
store i8* %1, i8** %5, align 8
store %2* %2, %2** %6, align 8
%7 = load %2*, %2** %6, align 8
%8 = load %0*, %0** %4, align 8
%9 = bitcast %0* %8 to i8*
%10 = getelementptr inbounds i8, i8* %9, i64 16
%11 = bitcast i8* %10 to %2**
%12 = bitcast %2** %11 to i8**
%13 = bitcast %2* %7 to i8*
%14 = call i8* @llvm.objc.storeWeak(i8** %12, i8* %13) #3
ret void
复制代码
进入到storeWeak中可以看到SideTabble结构体
struct SideTable {
spinlock_t slock; //锁
RefcountMap refcnts;//散列表 存储引用计数
weak_table_t weak_table;//散列表 存储弱引用计数
复制代码
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);//操作弱引用计数表
if ((entry = weak_entry_for_referent(weak_table, referent))) { //如果散列表存在就追加
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);//创建一个new_entry实体
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
struct weak_entry_t {//结构体
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;//存放弱引用指针
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
entry->inline_referrers[i] = new_referrer;
复制代码
weak_table弱引用表里面存对象的弱引用,每个对象的属性也存在弱引用表。
如下图的弱引用处理办法,就算object实际被销毁了,weakobj3不会收到影响,weakobj3并不管理object对象。弱引用表和对象相对独立,各自管理。
自动释放池
在main函数入口,会用一个@autoreleasepool自动释放池去管理UIApplicationMain应用的运行。我们通过clang看看汇编内容是什么。再通过汇编方式看看调用的什么函数。libobjc.A.dylib`objc_autoreleasePoolPush:
int main(int argc, char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_1y_3mygr0nx0c5dnvbk_r66nhtc0000gn_T_main_677664_mi_0);
}
}
复制代码
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
复制代码
看到autoreleasepoolpush的说明,首先是1.有一个POOL_BOUNDARY边界管理,2有区分hot page和cool page,对象使用的情况,3.自动释放池就是一个双向列表。找到了自动释放池的夫类,主要包含以下属性
magic_t const magic; // 16 校验page是否完整
__unsafe_unretained id *next; // 8 最新添加的autorelease对象的下一个位置
pthread_t const thread; // 8 当前线程
AutoreleasePoolPage * const parent; // 8 第一个节点的parent nil
AutoreleasePoolPage *child; // 8 最后一个节点的child nil
uint32_t const depth; // 4
uint32_t hiwat; // 4 入栈最大数量标记
复制代码
通过一个简单例子,我们看一下自动释放池管理的对象的情况
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char *argv[]){
@autoreleasepool {
NSObject *obj = [[[NSObject alloc] init] autorelease];
_objc_autoreleasePoolPrint();
}
return 0;
}
objc[3237]: ##############
objc[3237]: AUTORELEASE POOLS for thread 0x1000ebe00
objc[3237]: 2 releases pending.
objc[3237]: [0x10880a000] ................ PAGE (hot) (cold)
objc[3237]: [0x10880a038] ################ POOL 0x10880a038 //哨兵对象
objc[3237]: [0x10880a040] 0x101505760 NSObject //管理对象
objc[3237]: ##############
复制代码
接下来进入到push函数里面,定位到autoreleaseFast,判断当前hotpage是否满了,满了就创建autoreleaseNoPage,不满就add
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
复制代码
autoreleaseNoPage函数的部分逻辑
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //创建page
setHotPage(page);//设置为hotpage
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY); //增加边界
}
//AutoreleasePoolPage的构造函数
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(),
newParent,
newParent ? 1+newParent->depth : 0,
newParent ? newParent->hiwat : 0)
{
if (objc::PageCountWarning != -1) {
checkTooMuchAutorelease();
}
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
复制代码
思考每一个AutoreleasePoolPage对象到底存多少数据,到达多少需要开辟新的page,我们用for循环的方式增加对象,看一下输出结果
计算一页的空间504*8 + 56(自身) + 8(哨兵对象) = 4096 4k。
一个自动释放池中只需要一个哨兵对象。
一个知识点 alloc new copy mutablecopy都不会加入自动释放池。