iOS ------ tagged Pointer 内存对齐

一,tagged Pointer

为了节省内存和提高执行效率,苹果在64bit程序中引入了Tagged Pointer计数,用于优化NSNumber, NSDate, NSString等小对象的存储。一个指针或地址区域,除了放对象地址之外,也可以放其他额外的信息,并将其中的一些bit位作为tag标记区分,这就叫做Tagged Pointer

从占用内存来看

指针类型的大小通常也是与 CPU 位数相关,一个指针所在 32 bit 下占用 4 个字节,在 64 bit 下占用 8 个字节。

NSNumber等对象的指针中存储的数据变成了Tag+Data形式(Tag为特殊标记,用于区分NSNumber、NSDate、NSString等对象类型;Data为对象的值)。这样使用一个NSNumber对象只需要 8 个字节指针内存。当指针的 8 个字节不够存储数据时,才会在将对象存储在堆上。

在 64 bit 下,如果没有使用Tagged Pointer的话,为了使用一个NSNumber对象就需要 8 个字节指针内存和 32 个字节对象内存。

objectivec 复制代码
    NSInteger i = 0xFFFFFFFFFFFFFF;
    NSNumber *number = [NSNumber numberWithInteger:i];
    NSLog(@"%zd", malloc_size((__bridge const void *)(number))); // 32
    NSLog(@"%zd", sizeof(number)); // 8

使用了Tagged Pointer且指针的8歌字节够存储数据,NSNumber对象的值直接存储在了指针上,不会在堆上申请内存。则使用一个NSNumber对象只需要指针的 8 个字节内存就够了,大大的节省了内存占用。

objectivec 复制代码
NSInteger i = 1;
 NSNumber *number = [NSNumber numberWithInteger:i];
 NSLog(@"%zd", malloc_size((__bridge const void *)(number))); // 0
 NSLog(@"%zd", sizeof(number)); // 8

从效率上来看

为了使用一个NSNumber对象,需要在堆上为其分配内存,还要维护它的引用计数,管理它的生命周期,影响执行的效率

NSNumber

objectivec 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *number4 = @(0xFFFFFFFFFFFFFFFF);
    
        NSLog(@"%p %p %p %p", number1, number2, number3, number4);
    }
    return 0;
}
// 关闭 Tagged Pointer 数据混淆后:0x127 0x227 0x327 0x600003a090e0
// 关闭 Tagged Pointer 数据混淆前:0xaca2838a63a4fb34 0xaca2838a63a4fb04 0xaca2838a63a4fb14 0x600003a090e0

number1~number3指针为Tagged Pointer类型,可以看到对象的值都存储在了指针中,对应0x1、0x2、0x3。而number4由于数据过大,指针的8个字节不够存储,所以在堆中分配了内存。

0x127 中的 2 和 7 表示什么?

我们先来看这个7,0x127为十六进制表示,7的二进制为0111。最后一位1是Tagged Pointer标识位 ,代表这个指针是Tagged Pointer。前面的011是类标识位,对应十进制为3,表示NSNumber类。

可以在Runtime源码objc4中查看NSNumber、NSDate、NSString等类的标识位

objectivec 复制代码
// objc-internal.h
{
    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,
    ......
}

0x127 中的 2(即倒数第二位)又代表什么呢?

倒数第二位用来表示数据类型。

Tagged Pointer倒数第二位对应数据类型:

0: char

1: short

2: int

3: long

4: float

5: double

NSString

objectivec 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *a = @"a";
        NSMutableString *b = [a mutableCopy];
        NSString *c = [a copy];
        NSString *d = [[a mutableCopy] copy];
        NSString *e = [NSString stringWithString:a];
        NSString *f = [NSString stringWithFormat:@"f"];
        NSString *string1 = [NSString stringWithFormat:@"abcdefg"];
        NSString *string2 = [NSString stringWithFormat:@"abcdefghi"];
        NSString *string3 = [NSString stringWithFormat:@"abcdefghij"];
    }
    return 0;
}
a: 0x100002038, __NSCFConstantString, 18446744073709551615
b: 0x10071f3c0, __NSCFString, 1
c: 0x100002038, __NSCFConstantString, 18446744073709551615
d: 0x6115, NSTaggedPointerString, 18446744073709551615
e: 0x100002038, __NSCFConstantString, 18446744073709551615
f: 0x6615, NSTaggedPointerString, 18446744073709551615
string1: 0x6766656463626175, NSTaggedPointerString, 18446744073709551615
string2: 0x880e28045a54195, NSTaggedPointerString, 18446744073709551615
string3: 0x10071f6d0, __NSCFString, 1 */

为Tagged Pointer的有d、f、string1、string2指针。它们的指针值分别为0x6115、0x6615 、0x6766656463626175、0x880e28045a54195。

其中0x61、0x66、0x67666564636261分别对应字符串的 ASCII 码。

最后一位5的二进制为0101,最后一位1是代表这个指针是Tagged Pointer,010对应十进制为2,表示NSString类。

倒数第二位1、1、7、9代表字符串长度

NSString的类型NSString类型

注意: MacOS与iOS平台下的Tagged Pointer有差别:

MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位

iOS下则采用MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。

下图是iOS下NSNumber的Tagged Pointer位视图: Tagged Pointer 位视图

下图是iOS下NSString的Tagged Pointer位视图:

相关题目

执行以下两段代码,有什么区别?

objectivec 复制代码
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abcdefghij"];
        });
    }
objectivec 复制代码
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abcdefghi"];
        });
    }

第一段代码会报错

第一段代码中self.name__NSCFString类型,而第二段代码中为NSTaggedPointerString类型。__NSCFString存储在堆上,它是个正常对象,需要维护引用计数的。self.name通过setter方法为其赋值。而setter方法的实现如下:

objectivec 复制代码
- (void)setName:(NSString *)name {
    if(_name != name) {
        [_name release];
        _name = [name retain]; // or [name copy]
    }
}

我们异步并发执行setter方法,可能就会有多条线程同时执行[_name release],连续release两次就会造成对象的过度释放,导致Crash。

解决办法:

  • 使用atomic属性关键字。
  • 加锁

而第二段代码中的NSString为NSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash,因为根本就没执行释放操作。

objectivec 复制代码
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

二,内存对齐

在iO64位系统中,采用8字节对齐(计算属性内存空间大小总和),最小内存大小为16个字节,实际分配空间是16字节对齐。

在计算机中,内存大小的基本单位是字节,理论上可以在任意地址在访问某种基本数据类型。而计算机并非按早字节大小读写内存,而是以2,4,8的字节块来读写内存。因此,编译器会对基本数据类型的合法地址做出一些限制,地址必须是2,4,8的倍数。那么就要求各种数据类型按早一定的规则在空间上排列,这就是内存对齐

对象的属性内存布局遵循下面规则:

  • 结构体变量的首地址是其最长基本类型成员的整数倍
  • 结构体的总大小为结构体最大基本类型成员变量的整数倍
  • 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足
  • 如果一个结构体内部成员变量包括其他结构体成员,则结构体成员要从其内部成员最大元素大小的整数倍地址开始储存
  • 结构体中的成员变量都是分配在连续的内存空间中
  • 结构体成员顺序不同,会导致所占内存空间不一样;对象经过编译器优化,就不会有这个问题

实例:

objectivec 复制代码
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //person 有name,age属性
        //如果对象创建了没去赋值属性,它会是内存假地址
        Person* person = [[Person alloc] init];
        person.name = @"111";
        person.age = 20;
        //class_getInstanceSize依赖于<ojc/runtime.h>返回创建一个实例对象的内存大小就是获取对象的全部属性的大小
        NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([Person class]));//输出24
        //malloc_size依赖于<malloc/malloc.h>返回给系统分配给对象的内存大小,而且最小是16字节。就是获取对象的全部属性的大小总和,然后按8位对齐获得,不足8位补齐8位。        
        NSLog(@"malloc_size = %zd", malloc_size((__bridge  const void*)person));//输出32
        //最后 sizeOf 得到的内存大小都是8个字节, 是因为 sizeOf获取的是类型所分配内存,所传参数为指针类型,所以最后得到的都是8
        NSLog(@"sizeof  = %zd", sizeof(person));//输出8
        }
    return 0;
}
  • class_getInstance 获取实例对象在内存对齐的情况下,所占大小
  • malloc_size 获取的是实际系统所分配的内存大小
  • sizeOf 获取类型所占字节大小,如果传的是对象,永远都是8;

内存对齐的原因

  • 性能上的提升
    从内存的占用的角度来讲,对齐后比未对齐有些情况反而增加了内存分配的开支。数据结构(尤其是栈)应该静可能在自然边界对齐,为了访问为对齐的内存,处理器会进行两次的内存访问;而对齐的内存访问仅需要一次的访问,最重要提高了内存系统的性能。
  • 跨平台
    某些硬性的平台不能访问任意地址上的任意数据的,只能 处理特定类型的数据,否则会导致硬件基基层的错误。

注意:

如果给类添加方法,类实例对象内存大小是不会变化的,为什么那?

创建对象的时候并不会给对象的方法分配内存,只会给属性,成员变量分配内存。一个类可能创建多个实例,每个实例的方法都一样,没有差异性,所有对象共用这块存储方法的内存,实际上方法都存储在类实例里面了,一个类只有一个类实例,由系统创建。这么设计的好处就是节省空间,加快初始化速度等

相关推荐
iFlyCai2 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
郝晨妤11 小时前
HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
android·ios·harmonyos·鸿蒙
Hgc5588866611 小时前
iOS 18.1,未公开的新功能
ios
CocoaKier13 小时前
苹果商店下载链接如何获取
ios·apple
zhlx283515 小时前
【免越狱】iOS砸壳 可下载AppStore任意版本 旧版本IPA下载
macos·ios·cocoa
XZHOUMIN1 天前
网易博客旧文----编译用于IOS的zlib版本
ios
爱吃香菇的小白菜1 天前
H5跳转App 判断App是否安装
前端·ios
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
hxx2211 天前
iOS swift开发--- 加载PDF文件并显示内容
ios·pdf·swift
B.-2 天前
在 Flutter 应用中调用后端接口的方法
android·flutter·http·ios·https