内存布局
- 栈区(stack):局部变量,方法,参数,函数,方法指针,一般0x7开头,访问---通过寄存器获取
- 堆区(heap): 需要开辟空间的地方,通过alloc分配的对象,block,copy。一般0x6开头,访问---通过对象->堆区地址->存在栈区的指针
- 程序加载到内存 一般0x1开头
- 未初始化数据(.bss)
- 已初始化数据(.data)
- 代码段(.text) 程序代码,加载到内存中
- 内核区:系统调用
static 修饰的成员变量不占内存,怎么理解?
static修饰的在全局区,占用全局区的内存,只是不占用结构体所在的堆区
内存管理方案-TaggedPointer
首先我们要知道什么是Tagged Pointed
- 1:TaggedPointer专⻔⽤来存储⼩的对象,例如NSNumber和NSDate,NSString/NSIndexPath
- 2:TaggedPointer指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也不需要malloc和free
- 3.在内存读取上(objc_msgSend)有着3倍的效率,创建时⽐以前快106倍。
arm64上普通的对象指针的结构以“0x00000001003041e0”为例,分解成二进制是:
我们有64位,然而我们并没有真正的使用到所有的这些位,我们只在一个真正的对象指针中使用了中间那些位。由于对齐要求的存在,低位始终是0,对象必须总是位于指针大小倍数的一个地址中。由于地址空间有限,所以高位始终为0,我们实际上并不会用到2^64,这些高位和低位总是0.
在这些高位和低位始终为0的位置中,选择一个位,并把它设置为1,这可以让我们知道这不是一个真正的对象指针,此时就可以给其他位置赋予一些其他的意义。我们称这种指针为Tagged Pointed,例如我们可以在其他位中塞入一些数值
其中的101010转换为10进制是42.尾部加了一个标识符1表明这个是Tagged Pointed,此时系统就可以把它这些东西当做对象指针来处理,这样可以不用为每一个类似情况分配一个小数字对象,
这些值实际上是通过与进程启动时初始化的随机值而被混淆的,这个安全措施使得很难去伪造一个Tagged Pointed
Tagged pointers on Intel
下面是在Inter上Tagged pointer的完整格式,把低位设置为1标识这是一个tagged pointer这样可以把它与真正的指针区分开来,接下来的3位是标签号,这个表示tagged pointer的类型,例如3表示它是一个NSNumber,6表示NSDate,由于这里有3个位的标签所以这里有8种可能的标签类型,剩下的是有效载荷payload
这个是特定类型可以随意使用的数据。
现在标签7有一个特殊的情况,它表示一个扩展的标签,扩展便签可以使用接下来的8位来编码类型,也就是说可以多出256个标签类型,但是代价是减少了有效载荷Payload
针对以上的特性,可用于存储一些用户界面UIColor和NSIndexSets
如果你是一个Swift开发者,你可以创建自己的Tagged pointers,如果你曾经使用过一个具有关联值的枚举,那就是一个类似Tagged pointer类,swift运行时将枚举判别器存储在关联值有效载荷的备用位中,而且swift对于值类型的使用,实际上使得Tagged pointer显得没有那么重要了。因为值不再需要完全是指针大小,例如Swift UUID类型可以是两个字并且保持内联,而不是分配一个单独的对象,因为它不适合在一个指针里面。以上就是Inter上的Tagged pointer
Tagged pointers on ARM64
在ARM64上这些是反过来的,最高位设置为1而不是最低位用来表示
Tagged pointer,然后在接下来的3位是标签位,为什么ARM64如此设计呢?
实际上这是对objc_msgSend的一个小优化,我们希望objc_msgSend的路径尽可能快,而最常见的路径是一个普通的指针,我们有两种不大常见的情况是Tagged pointer和nil
事实证明当我们使用最高位时,我们可以一次性的对这两种情况来进行检查,相对于分开检查Tagged pointer和nil,这就为msgSend中的常见情况节省了一个条件分支。
和Inter上一样,接下来的8位被用作扩展标签,剩下的是有效负载,这其实是iOS13使用的旧格式,在2020年的iOS14时候苹果做了一些改动:将标签位保持在最高位,因为msgSend的优化还是非常有用的,将标签号tag移动到了最下面的3个位,如果正在使用扩展标签,那么它会占据标签位后的高8位
为什么要这么做呢?我们看看正常的指针,我们现有的工具比如动态链接会忽略指针的前8位,这个是ARM的特性(Top Byte Ignore),而我们会把扩展标签放在Top Byte Ignore位,对于一个对齐指针,后面3位总是0,我们可以改变这一点,只需要在指针上添加一个小数字,7这是一个扩展标签把低位变成了1
这意味着我们实际上可以将上面的指针放入一个扩展标签指针的有效载荷中,这个结果就成了一个Tagged pointer其有效载荷中包含一个正常指针,为什么这很有用呢?它开启了Tagged pointer的能力,引用二进制文件中的常量数据的能力,比如字符串或者其他数据结构。否则他们将不得不占用dirty Memory
像之前(iOS13)这样检查没有问题,但是在iOS14的时候却不可行,正确的方式是使用APIisKindOfClass等,这些Tagged pointer中的东西都可以通过标准的API去检索,这也适用于CF类型
x86-64下的Tagged Pointer结构
由上面我们知道taggedPointer会被混淆,我们在libObjc源码中找到了taggedPointer加密的代码objc_debug_taggedpointer_obfuscator,同一个数异或两次得到的还是它本身
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return (void *)ptr;
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
return (void *)value;
}
所以上面的代码我们再异或一个objc_debug_taggedpointer_obfuscator就能得到真正的值
uintptr_t
kc_objc_decodeTaggedPointer(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
我们分别打印输出NSString``NSNumber以及NSIndexPath
对照着
OBJC_TAG类型表,刚好对应上。
ARM64下的Tagged Pointer结构
就像上面说的一样,在iOS14以及以后,tag位在最后面3位了,而不是前面
那么,在真机上我们是如果打印出上面的代码的呢?我们在混淆代码的时候看到,如果
!DisableTaggedPointerObfuscation就关闭混淆,全局搜索DisableTaggedPointerObfuscation
static void
initializeTaggedPointerObfuscator(void)
{
if (!DisableTaggedPointerObfuscation) {
// 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;
#if OBJC_SPLIT_TAGGED_POINTERS
// The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
// Shuffle the first seven entries of the tag permutator.
int max = 7;
for (int i = max - 1; i >= 0; i--) {
int target = arc4random_uniform(i + 1);
swap(objc_debug_tag60_permutations[i],
objc_debug_tag60_permutations[target]);
}
#endif
} else {
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
objc_debug_taggedpointer_obfuscator = 0;
}
}
libObjc全局搜索定位到了这里:
OPTION( DisableTaggedPointerObfuscation, OBJC_DISABLE_TAG_OBFUSCATION, "disable obfuscation of tagged pointers")
所以我们在Xcode的scheme配置上
此时运行之后,就没有混淆了,
NSString和NSNumber等的地址前后都是一样的了(参考上图控制台的打印输出)
TaggedPointer相关面试题
以下那个代码会崩溃?
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<100; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
// 多线程 读和写
// setter -> retian release
for (int i = 0; i<100; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci_和谐学习不急不躁"];
NSLog(@"%@",self.nameStr);
});
}
}
在多次操作(多次点击屏幕)的过程中,我们发现第二个代码会崩溃,这个是因为第一种的nameStr的类型是NSTaggedPointerString
而第二次的nameStr的类型是**NSCFString**
这里的原因是当字符长度>=10的时候,这个类型就从TP变成了OC对象
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
if (slowpath(isTaggedPointer())) return (id)this;
// ...
}
多线程读取就是一个retain和release的过程,我们找到源码可以看到,如果是小对象的话处理的时候直接返回,所以不存在野指针的情况。也就说明小对象根本不受ARC内存的管理
retain和release
retain流程,我们打开libObjc的源码定位到这个方法
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减1,
如果需要该位置存储满了,则需要使⽤到下⾯的散列表has_sidetable_rc。
- 首先判断
isTaggedPointer - 判断
nonpointer,如果是一个nonpointer则是对isa位的操作,- 不是
nonpointer散列表 操作引用计数(主要是因为没有extra_rc,在nonpointe_isa中改位置存储的是对象的引用计数) - 是否正在释放
- 是
nonpointer,直接对isa.bits位运算找出extra_rc来进行加减(retain/release)操作newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ - 如果加满了,真机情况下
uintptr_t extra_rc : 8可以存2^8=256个大小。如果存储满 了,就找到一个新的地方存储,这个地方就是散列表,散列表存1半
- 不是
release流程:
跟上面的流程一样,唯一一点需要注意的就是,在散列表里面也减完之后,就执行dealloc操作
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc))