iOS --浅谈内存管理方式

1,125 阅读9分钟

1. 内存布局

内存布局.001.jpeg

2.ARC和MRC

MRC

系统是通过对象的引用计数来判断一个对象是否需要销毁,

1.对象被创建时引用计数为1

2.当对象被其他指针引用时,需要手动调用[objc retain],使对象的引用计数+1

3.当指针变量不再使用对象时,需要手动调用[objc release]来释放对象,使对象的引用计数-1

4.当对象的引用计数为0时,系统就会销毁这个对象

所以MRC的规则是:谁创建,谁释放,谁引用,谁管理

ARC

ARC是苹果引入的自动计数管理机制。是LLVMRuntime配合的结果,ARC中禁止手动调用retain/release/retainCount/dealloc/autorelease,编译器会在适当的位置插入这些方法。

3.内存管理方案

除了上面提到的 MRCARC,还有三种内存管理方案,分别是:Tagged PointerNonpointer_isaSideTables.

NSString 内存

Tagged Pointer专⻔用来存储小的对象,例如NSNumberNSDate,以及由数字、英文字母组合且长度小于等于9的NSString 下面通过NSString的内存管理来引出Tagged Pointer

    //初始化方式一:通过 WithString + @" "方式
    NSString *str1 = @"123";
    NSLog(@"%@ -- %p -- %@ -- %ld",str1,str1,[str1 class],(long)CFGetRetainCount((__bridge CFTypeRef)str1));
    NSString *str2 = [[NSString alloc] initWithString:@"1234"];
    NSLog(@"%@ -- %p -- %@ -- %ld",str2,str2,[str2 class],(long)CFGetRetainCount((__bridge CFTypeRef)str2));
    NSString *str3 = [NSString stringWithString:@"12345"];
    NSLog(@"%@ -- %p -- %@ -- %ld",str3,str3,[str3 class],(long)CFGetRetainCount((__bridge CFTypeRef)str3));
    
    
    //初始化方式二:通过 WithFormat
    //字符串长度在9以内
    NSString *str4 = [NSString stringWithFormat:@"hello"];
    NSLog(@"%@ -- %p -- %@ -- %ld",str4,str4,[str4 class],(long)CFGetRetainCount((__bridge CFTypeRef)str4));
    NSString *str5 = [[NSString alloc] initWithFormat:@"hello"];
    NSLog(@"%@ -- %p -- %@ -- %ld",str5,str5,[str5 class],(long)CFGetRetainCount((__bridge CFTypeRef)str5));
        
    //字符串长度大于9
    NSString *str6 = [NSString stringWithFormat:@"helloworld!!!"];
    NSLog(@"%@ -- %p -- %@ -- %ld",str6,str6,[str6 class],(long)CFGetRetainCount((__bridge CFTypeRef)str6));
    NSString *str7 = [[NSString alloc] initWithFormat:@"helloworld!!!!!!"];
    NSLog(@"%@ -- %p -- %@ -- %ld",str7,str7,[str7 class],(long)CFGetRetainCount((__bridge CFTypeRef)str7));
}

打印结果如下:

2021-03-24 14:41:23.439734+0800 TestDemo[28026:5804455] 123 -- 0x10a87e1b0 -- __NSCFConstantString -- 1152921504606846975
2021-03-24 14:41:23.439887+0800 TestDemo[28026:5804455] 1234 -- 0x10a87e1f0 -- __NSCFConstantString -- 1152921504606846975
2021-03-24 14:41:23.439978+0800 TestDemo[28026:5804455] 12345 -- 0x10a87e210 -- __NSCFConstantString -- 1152921504606846975
2021-03-24 14:41:23.440078+0800 TestDemo[28026:5804455] hello -- 0xc4c10d8f01d0ee6c -- NSTaggedPointerString -- 9223372036854775807
2021-03-24 14:41:23.440199+0800 TestDemo[28026:5804455] hello -- 0xc4c10d8f01d0ee6c -- NSTaggedPointerString -- 9223372036854775807
2021-03-24 14:41:23.440307+0800 TestDemo[28026:5804455] helloworld!!! -- 0x600003c8a8c0 -- __NSCFString -- 2
2021-03-24 14:41:23.440396+0800 TestDemo[28026:5804455] helloworld!!!!!! -- 0x600003290930 -- __NSCFString -- 1

从结果可以看出:

__NSCFConstantString:字符串常量,是一种编译时的常量,存储在字符串常量区。其retainCount值很大,所以对其操作,不会引起引用计数变化.

NSTaggedPointerString:是苹果在64位环境下对NSStringNSNumber等对象做的优化。对于NSString对象来说当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区,其retainCount值很大。

__NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上,同样的当字符串的长度大于9时,也会创建成为__NSCFString类型。

Tagged Pointer 小对象类型

从上面的例子分析发现,Tagged Pointer的小对象的引用计数都比较大,那么对于Tagged Pointer来说,其引用计数的处理是怎样的呢?下面通过objc_retainobjc_release方法的源码来看一下:

__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}


__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

源码发现,如果对象是TaggedPointer,则直接返回,说明TaggedPointer类型的小对象不会retainrelease,那么其引用计数也就不会改变。

对于一般的对象而言,其指针指向的是对象的地址,而Tagged Pointer指针的值不再是地址了,而且包含真正的值。 下面看一下Tagged Pointer的源码是怎么做的处理:

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;
    }
}

iOS14之后,系统对Tagged Pointer小对象进行了混淆,通过与操作_OBJC_TAG_MASK混淆。下面通过objc_debug_taggedpointer_obfuscator来查找Tagged Pointer的编码和解码的过程。

//编码
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//解码
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

在编码和解码的源码中,看到Tagged Pointer小对象经过了两层异或(相同为0,不同为1),然后得到小对象自己。为了可以看到小对象的真实的地址,现在把解码的源码拷贝出来,通过上面的NSString小对象的例子,来看下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSLog(@"%p-%@",str1,str1);
    NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));
    NSNumber *number1 = @1;
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
}

uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

这里分别打印了对象的指针地址,值,经过解码后的真实的地址。打印结果

image.png 可以看出,对于0xa000000000000611,61代表的就是aASCII码,也就是str1的值,对于0xb000000000000012,数字1代表的就是当前number1的值。这就说明了Tagged Pointer小对象指针地址确实存储了对象的值。

那么对于这两个地址前面的0xa0xb代表什么呢? 来到_objc_isTaggedPointer的源码:

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

其中define _OBJC_TAG_MASK (1UL<<63),所以源码((uintptr_t)ptr & _OBJC_TAG_MASK)表示;ptr & 1左移63,即2^63,相当于除了64位,其他位都为0,只是保留了最高位的值,也即是判断最高位是否有值。所以判断是否为Tagged Pointer,就是判断第64位是否有值且值为1.

0xa转换成二进制为 1010,第64位为1,后三位表示tagType类型为2, 将0xb转换为二进制为 1011,第64位为1,后三位表示tagType类型为3, 下面通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入tagType的枚举

static inline void * _Nonnull
  _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);
    }
}
#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 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,
    
    ...部分代码省略

此时可以看到,2表示NSString类型,3表示NSNumber类型。所以0xa表示NSString类型,0xb表示NSNumber类型。 然后通过一个NSDate类型的对象来验证一下,NSDate类型是否为6

image.png 打印出来,date1的真实地址为0xe2d30c25fc861d86,其高位是0xe,转换为二进制为1110,第64位为1,其余三位为6,完全符合上述枚举值的情况。

总结:

Tagged Pointer小对象类型(存储NSNumberNSDate、小的NSString(不超过9位)),小对象指针不再是简单的地址,而是指针地址加上值,即真正的值,所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以可以直接进行读取。优点是占用空间小节省内存。

Tagged Pointer小对象 不会进行retainrelease操作,而是直接返回,意味着不需要ARC进行管理,所以可以直接被系统自主的释放和回收。

Tagged Pointer的内存并不存储在堆中,而是在常量区中,也不需要mallocfree,所以可以直接进行读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右。

所以,综合来说,Tagged Pointer的内存管理方案,比常规的内存管理,要快很多Tagged Pointer64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值。

NONPOINTER_ISA

objc_objcet对象中isa指针分为指针型isa与非指针型isa(NONPOINTER_ISA),我们都知道isa是纯指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;从 arm64架构开始,对isa进行了优化,用NONPOINTER_ISA表示

NONPOINTER_ISA不单单是指针,除了指向objc_class外,Objective-C 在运行时还会使用一些额外的数据位去存储引用计数,是否被弱引用等相关信息。将64位中的33位用来存储classmeta-class对象的内存地址信息。而且要通过位运算将isa的值& ISA_MASK掩码,才能得到classmeta-class对象的内存地址。下面通过源码来查看一下它的结构:


union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__ // arm64架构
#   define ISA_MASK        0x0000000ffffffff8ULL 
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1; 
        uintptr_t has_cxx_dtor      : 1; 
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6; 
        uintptr_t weakly_referenced : 1; 
        uintptr_t deallocating      : 1; 
        uintptr_t has_sidetable_rc  : 1; 
        uintptr_t extra_rc          : 19;
 
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
 
# elif __x86_64__ // arm86架构,模拟器是arm86
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        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 deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };
 
# else
#   error unknown architecture for packed isa
# endif
}
  • nonpointer:表示是否对isa指针开启指针优化。0表示纯isa指针,1表示不止是类对象地址,isa中包含了类信息、对象的引用计数等。
  • 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

SideTables 散列表

通过上面的内容,我们知道当引用计数存储到一定值时,将不会再存储到Nonpointer_isa的位域的extra_rc中,而是会存储到SideTables 散列表中。

void 
objc_object::sidetable_lock()
{
    SideTable& table = SideTables()[this];
    table.lock();
}

void 
objc_object::sidetable_unlock()
{
    SideTable& table = SideTables()[this];
    table.unlock();
}

SideTables 散列表的使用是全局的,通过上面的源码可以看出SideTables并不只有一张表,而是多张表。事实上SideTables是能够存储一系列SideTablehash数组。SideTableshash键值是通过要存储对象的地址计算而来。 一个对象对应一个SideTable。但是一个SideTable,会存储多个对象的引用计数。因为SideTable的数量有限,所以会有很多对象共用同一个SideTable的情况。

static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}

可以看到,SideTables的实质类型为模板类型StripedMap。下面来看一下StripedMap的定义:

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

可以看到当为真机类型时,SideTables最多有8SideTable。 接着来看一下SideTable的定义:

struct SideTable {
    spinlock_t slock;//自旋锁
    RefcountMap refcnts;//引用计数表
    weak_table_t weak_table;//弱引用表

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

可以看到包含三个成员:

  • spinlock_t slock : 自旋锁,用于上锁和解锁SideTable
  • RefcountMap refcnts :引用计数表,用来存储OC对象的引用计数
  • weak_table_t weak_table : 存储对象弱引用指针的hash表。 weak功能实现的核心数据结构。

下面就引出了一个问题:为什么SideTable在内存中会存在多张?

通过上面SideTable的定义,我们知道,SideTable里面有一个自旋锁,现在假如当前的SideTable在内存中只存在一张,意味着全局所有的对象都会存在当前这一张SideTable里,当每一次对对象的操作,都会对当前SideTable进行解锁,那么这一张表就不安全。那么假如给每一个对象都开一张SideTable,彼此分割独立,这样会导致操作效率和查询效率都很低。所以SideTable在内存中存在多张,而且最多8张,同时一个对象对应一个SideTable。但是一个SideTable,会存储多个对象。

image.png 注解:图片来源于github.com/tbfungeek/i… 感谢!