iOS底层探索--内存管理

本文深入探讨了iOS内存管理的各个方面,包括内存的五大分区,TaggedPointer的优化机制,NONPOINTER_ISA如何提升效率,以及retain, release, retainCount, dealloc的细节分析。特别关注了Block对全局变量的影响,以及在循环引用场景下,特别是Timer造成的循环引用问题的解决方案。此外,还详细剖析了自动释放池AutoreleasePool的工作原理,包括AutoreleasePoolPage的结构和对象添加的数量限制,以及objc_autoreleasePoolPush和objc_autoreleasePoolPop的执行流程。最后,文章阐述了RunLoop的基础知识和运行原理,展示了其在事件处理中的核心角色。" 131709975,14706596,SPSS回归分析在问卷信效度中的应用,"['数据挖掘', '统计分析', '机器学习', '预测模型', 'SPSS工具']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 五大分区

在一个4G内存的移动设备中,内核区约占1GB
内存分区:代码段、数据段、BSS段,栈区,堆区。栈区地址一般为0x7开头,堆区地址一般为0x6开头。数据段一般0x1开头。

0x70000000对其进行转换,刚好为3GB

  • 栈区:存储函数,方法,快速高效,
  • 堆区:通过alloc分配的对象,block copy,灵活方便,数据适应面广泛,
  • BSS段:未初始化的全局变量,静态变量,程序结束后有系统释放。
  • 数据段:初始化的全局变量,静态变量,程序结束后有系统释放。
  • 代码段:程序代码,加载到内存中

栈的内存是访问寄存器直接访问其内存空间,堆里的对象的访问是通过存在栈区的指针存储的地址,再找到堆区对应的地址。

全局变量和局部变量在内存中是否有区别?有什么区别?
  • 存储位置不同全局变量存在相应的全局存储区域。局部变量定义在局部的空间,存储在栈中
Block中是否可以直接修改全局变量

Block中可以修改全局变量。

全局静态变量的修改

LGPerson中,定义一个全局变量personNum,并定义两个方法,对全局变量++,然后在ViewController调用打印,结果是什么样的?

static int personNum = 100;

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

- (void)run;
+ (void)eat;
@end

#import "LGPerson.h"

@implementation LGPerson
- (void)run{
    personNum ++;
    NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}

+ (void)eat{
    personNum ++;
    NSLog(@"LGPerson内部:%@-%p--%d",self,&personNum,personNum);
}

- (NSString *)description{
    return @"";
}

@end

NSLog(@"vc:%p--%d",&personNum,personNum); // 100
personNum = 10000;
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson new] run]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[LGPerson eat]; // 102
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LGPerson alloc] cate_method];

打印结果如下:

static修饰的静态变量,只针对文件有效。在vc中和LGPerson中的两个全局变量的地址不相同。

2. TaggedPointer

使用TaggedPointer存储小对象NSNumber、NSDate,优化内存管理。

首先,看下面的代码,能正常执行么?点击屏幕时会有什么问题?

- (void)taggedPointerDemo {
  
    self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"cooci"];
             NSLog(@"%@",self.nameStr);
        });
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    

    
    NSLog(@"来了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"和谐学习不急不躁"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

通过测试,上述代码,能正常执行,当点击屏幕时,发生崩溃。那么为什么在点击屏幕是,会发生崩溃呢?

其实在多线程代码块中赋值,打印,是调用的settergetter,当settergetter加入多线程时,就会不安全。

setter方法底层是retian newvalue,然后realase oldvalue。多加入多线程时,就会出现多次释放,造成野指针。

那么,为什么第一段能够正常执行呢?

通过上面的断点调试,发现第一段代码中_nameStr的类型并不是NSString而是taggedPointer类型,而第二段中是NSString类型。

接下来看一下objc_releaseobjc_retain的源码:

void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

release时,先判断是否是isTaggedPointer,是,则直接返回,并没有真的进行release操作,而在retain时也是同样的操作,先判断isTaggedPointer,并没有进行retain,这也就解释了为什么第一段代码能正常执行,因为其底层并没有retainrelease,即使搭配多线程,也不会出现多次释放的问题,也就不会出现野指针,也不会崩溃。

其实在read_images中的的initializeTaggedPointerObfuscator()中,会初始化一个objc_debug_taggedpointer_obfuscator,在构造TaggedPointer时,通过对这个值的^操作,进行编码和解码。

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

initializeTaggedPointerObfuscator中,在iOS之前低版本时,objc_debug_taggedpointer_obfuscator = 0,之后的版本objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK

在构建TaggedPointer时,会进行编码,在获取TaggedPointer时,解码。

_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
    // They are reversed here for payload insertion.

    // ASSERT(_objc_taggedPointersEnabled());
    if (tag <= OBJC_TAG_Last60BitPayload) {
        // ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        // ✅ 返回一个编码的值
        return _objc_encodeTaggedPointer(result);
    } else {
        // ASSERT(tag >= OBJC_TAG_First52BitPayload);
        // ASSERT(tag <= OBJC_TAG_Last52BitPayload);
        // ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}

_objc_getTaggedPointerTag(const void * _Nullable ptr) 
{
    // ASSERT(_objc_isTaggedPointer(ptr));
    // ✅ 解码,然后进行一系列偏移运算,返回
    uintptr_t value = _objc_decodeTaggedPointer(ptr);
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t extTag =   (value >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return (objc_tag_index_t)(extTag + OBJC_TAG_First52BitPayload);
    } else {
        return (objc_tag_index_t)basicTag;
    }
}

其实编码和解码的操作就是与上objc_debug_taggedpointer_obfuscator

_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

/**
 1000 0001
^0001 1000
 
 1001 1001
^0001 1000
 1000 0001
 */

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

我们可以调用解码方法,来打印一下具体的TaggedPointer

    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"b"];

    NSLog(@"%p-%@",str1,str1);
    NSLog(@"%p-%@",str2,str2);
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));

    NSNumber *number1 = @1;
    NSNumber *number2 = @1;
    NSNumber *number3 = @2.0;
    NSNumber *number4 = @3.2;
    
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));

打印结果:

上图打印结果中,0xb000000000000012b表示数字,1就是变量的值。

不同类型的标记:

{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再 是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储 在堆中,也不需要mallocfree,也不用retainrelease

在内存读取上有着3倍的效率,创建时比以前快106倍,一般一个变量的位数在8-10位时,系统默认会使用Tagged Pointer

3.NONPOINTER_ISA的优化

通过对NONPOINTER_ISA64个字节位置的存储,来内存管理。
isa结构:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

  • nonpointer:表示是否对 isa 指针开启指针优化

    0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数当对象引用技术大于 10 时,则需要借用该变量存储进位等

  • has_assoc:关联对象标志位,0没有,1存在

  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象

  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。

  • magic :用于调试器判断当前对象是真的对象还是没有初始化的空间

  • weakly_referenced:标志对象是否被指向或者曾经指向一个 ARC 的弱变量,
    没有弱引用的对象可以更快释放

  • deallocating:标志对象是否正在释放

  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc

3. retain & release & retainCount & dealloc分析

retain 和 release 分析

首先我们看一下retain的源码:

objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

从源码中可以看出,在retain时,先判断是否是isTaggedPointer,是则直接返回,不是,则开始retain

最终进入到rootRetain方法中。

objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    // retain 引用计数处理
    // 
    do {
        transcribeToSideTable =
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值