iOS Runtime二: Tagged Pointer, is
Tagged Pointer Object
在objc中会有很多轻量的实例对象,比如NSNumber,NSDate,NSString等的实例,从64位开始,苹果使用了Tagged Pointer策略来优化这些轻量的对象的存储.
这个对象直接存储在指针里,指针实际上没有指向任何对象,整体只有8字节.
1.首先看看指向一个编译时常量的情况
定义几个NSNumber和NSString常量
NSNumber *num1 = @1;
NSNumber *num2 = @2;
NSNumber *num3 = @3;
NSNumber *num4 = @4;
NSString *str = @"aaa";
NSLog(@"Hello, World!");
用lld查看他们的地址,分别查看对象指针的地址,对象isa的地址,以及类的地址
(lldb) p/x num1
(NSConstantIntegerNumber *) $0 = 0x0000000100008110 (int)1
(lldb) p/x num2
(NSConstantIntegerNumber *) $1 = 0x0000000100008128 (int)2
(lldb) p/x num3
(NSConstantIntegerNumber *) $2 = 0x0000000100008140 (int)3
(lldb) p/x num1->isa
(Class) $4 = 0x00007ff85e83d228 NSConstantIntegerNumber
(lldb) p/x num2->isa
(Class) $5 = 0x00007ff85e83d228 NSConstantIntegerNumber
(lldb) p/x num3->isa
(Class) $6 = 0x00007ff85e83d228 NSConstantIntegerNumber
(lldb) p/x num1.class
(Class) $12 = 0x00007ff85e83d228 NSConstantIntegerNumber
(lldb) p/x 0x00007ff85e83d228 & 0x00007ffffffffff8
(long) $7 = 0x00007ff85e83d228
(lldb) p/x str
(__NSCFConstantString *) $8 = 0x0000000100004010 @"aaa"
(lldb) p/x str2
(__NSCFConstantString *) $9 = 0x0000000100004030 @"bbb"
(lldb) p/x str->isa
(Class) $10 = 0x00007ff85e8108d8 __NSCFConstantString
(lldb) p/x str2->isa
(Class) $11 = 0x00007ff85e8108d8 __NSCFConstantString
(lldb) p/x str.class
(Class) $14 = 0x00007ff85e8108d8 __NSCFConstantString
1.首先NSNumber对象num1,num2,num3的地址都间隔了24个存储单元,也就是24字节(换成十进制计算).
2.str和str2也是间隔24字节
3.num1,num2,num3的isa都是0x00007ff85e83d228,这个值与上掩码还是这个值,并且NSConstantIntegerNumber这个类的地址也是0x00007ff85e83d228,说明这个isa除了shiftcls其他位置都是0,而且shiftcls就是NSConstantIntegerNumber的地址.
实际上这时候不能说shiftcls了,因为它是一个Class指针形态,因为nonpointer是0,它的值就是class的地址.
4.同样的str和str2的isa的shiftcls也都是__NSCFConstantString的地址.
这说明常量的NSNumber和NSString是一个普通的对象,而且他们的isa的形态是Class指针,存在常量区.占用24个字节,
然后再来看看内存里都存了什么
(lldb) x/3gx num1
0x100008110: 0x00007ff85e83d228 0x0000000100003f82
0x100008120: 0x0000000000000001
(lldb) x/3gx num2
0x100008128: 0x00007ff85e83d228 0x0000000100003f82
0x100008138: 0x0000000000000002
注意现在输出的不是地址了,是内存单元里的内容,一个地址编号对应一个字节,也就是8位,
GBD调试中的内存读取命令,可以输出存储单元里的二进制,格式是:
x/<n/f/u><addr> n,f,u是可选的参数。
第一个x是command.
n表示输出几段.
f 表示显示的进制格式.x 16进制,t二进制等.
u是输出几个字节的内容,b一个字节,h两个字节,w四个字节,g八个字节.
另外u,f分别对应不同的字母,因此不讲究顺序,x/3gx和x/3xg是一样的.
addr表示开始的地址.
x/3gx num1表示输出num1的地址开始的, 3*8个字节的内容,用十六进制输出,分为三段,每段8个字节.
输出num1之后24个字节之后,第一段是NSConstantIntegerNumber的地址,也就是isa,第二段是对象的其他内容,第三段就是这个NSNumber的数值.
2.然后看看编译时没有指向常量的情况
定义一个类
@interface MyObjc : NSObject
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) NSNumber *number;
@end
my.text = [NSString stringWithFormat:@"123456789"];
my.number = @(my.text.length);
NSLog(@"%@",my.text);
lldb查看
(lldb) p/t my.text
(NSTaggedPointerString *) $0 = 0b0001010011000110111001001011110000100100100101101001011101101111 @"123456789"
(lldb) p/t my.number
(__NSCFNumber *) $1 = 0b0000101001100111000100111001011110010111100111000010111111001101 (long)9
(lldb) p my.text->isa
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
(lldb) p my.number->isa
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
报错说无法读取这部分内存,此时是TaggedPointer.
如何判断一个对象是否是tagged pointer
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
就是拿指针的值和_OBJC_TAG_MASK按位与算,这个方法只有一个地方调用,就是
inline bool
objc_object::isTaggedPointer()
{
return _objc_isTaggedPointer(this);
}
define _OBJC_TAG_MASK (1UL<<63)
所以是那自己和_OBJC_TAG_MASK与运算,
_OBJC_TAG_MASK的定义根据环境不同有的定义
arm64和MSB的macos是1UL<<63,也就是1跟着63个0,其他情况都是1.
当前环境是LBS的macos,x86_64,此时_OBJC_TAG_MASK=1,
注意this是一个指针,所以如果对象的地址最低位是1,就是tagged pointer object.与nonpointer没有直接关系.
Tagged Pointer的特性
想知道Tagged Pointer的特性,只要搜索isTaggedPointer()在哪些地方调用了就行.
inline Class
objc_object::ISA(bool authenticated)
{
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
}
比如Tagged Pointer获取isa的时候会报错,也就是上面报错的原因,因为这里有个断言.
这里其实有三个函数
// ISA() assumes this is NOT a tagged pointer object
Class ISA(bool authenticated = false);
// rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
Class rawISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
这是objc_object的三个getIsa,第一个必须是非tagged pointer object才能调用,最终返回的是isa_t的getClass(),返回类的地址.
第二个必须rawIsa能够调用,rawisa指的是纯指针,既不是tagged pointer也不是isa优化,isa的nonpointer位是0,返回isa_t的值.
第三个才是tagged pointer能够调用的,它是这样实现的:
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA(/*authenticated*/true);
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
如果是非TaggedPointer,直接走第一个ISA()函数,fastpath是一个编译器优化指令,这里表示大概率不走后面的代码.
后面就是按照预定的算法得到偏移,从一个数组中取出类返回.
TaggedPointer不会增加引用计数
inline id
objc_object::retain()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return sidetable_retain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
有一个常用的例子来观察tagged pointer object和NSObject
@interface MyObjc : NSObject
//这里用strong来修饰
@property (nonatomic, strong) NSString *text;
@end
//字符串是10位
for (int i = 0; i<10000; i++) {
dispatch_async(dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT), ^{
my.text = [NSString stringWithFormat:@"1234567890"];
});
}
/*
//字符串是9位
for (int i = 0; i<10000; i++) {
dispatch_async(dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT), ^{
my.text = [NSString stringWithFormat:@"123456789"];
});
}
*/
分别使用两端for循环,添加异步并发任务,上面for循环会crash,下面的不会
strong修饰的set方法的实现大概是这样的
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
并发去访问text,很可能会访问到坏地址.而tagged pointer不会,因为它不是一个对象,就是一个64位二进制.
tagged pointer的结构
image.png标志位的类型
// 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,
初始化tagged pointer 的函数是
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
这里面有两句
if (DisableTaggedPointers) {
disableTaggedPointers();
}
initializeTaggedPointerObfuscator();
DisableTaggedPointers是环境变量OBJC_DISABLE_TAGGED_POINTERS,环境变量在edit schema里添加
image.png
另外还有其他很多环境变量,定义在objc-env.h里
initializeTaggedPointerObfuscator()就是初始化TaggedPointer的函数.
static void
initializeTaggedPointerObfuscator(void)
{
if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)) {
// 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;
}
}
在这里做了混淆,获得 objc_debug_taggedpointer_obfuscator 将随机数据放入变量中,然后 ~_OBJC_TAG_MASK与运算移走所有非有效位.
isa的走向
struct objc_class : objc_object {
// Class ISA;
Class superclass;
类Class是objc_class的typedef,继承自objc_object,第一个成员是isa_t,第二个是Class指针,指向superclass,所以两个都是8字节.
元类就是类对象的类,元类的设计并非objc的独创,也不是首创,相较于追究元类的优越性,更像是采取了一种常用设计.
实例对象的成员属性方法添加在类对象上,向对象发送消息,对象会通过isa找到类,再去类的cache或者方法列表查找imp,同样的像类对象发送消息,就要去元类找.
static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
/* ... */
cls->initClassIsa(meta);
if (superclass) {
meta->initClassIsa(superclass->ISA()->ISA());
cls->setSuperclass(superclass);
meta->setSuperclass(superclass->ISA());
addSubclass(superclass, cls);
addSubclass(superclass->ISA(), meta);
} else {
meta->initClassIsa(meta);
cls->setSuperclass(Nil);
meta->setSuperclass(cls);
addRootClass(cls);
addSubclass(cls, meta);
}
这段代码是objc-runtime-new.mm中的,在动态创建一个类的时候,会执行这个函数,
第一行,initClassIsa()内部是setClass,把类的信息放在shiftcls,所以这里是把元类放在类的isa中,因此类对象的isa指向它的元类.
在看后面,如果传进来了superclass,也就是说这个类有父类,
则meta用superClass.getClass().getClass()初始化isa,也就是父类的元类的getClass(),姑且叫做A,
meta.initClassIsa()之后,meta.getClass()也就是这个A了,也就是元类的isa的shiftcls和父类的元类的isa的shiftcls是一样的,都是A,这个A就是根源类.
然后接下来设置superclass,meta的superclass指向superclass的meta.
以及设置subclass.
如果superclass是nil,meta就用自己初始化isa,shiftcls是自己,superclass是Nil,
并且这里有两个重要的事情,一是meta的父类是类对象本身,二是调用了一个addRootClass(cls).
除了NSObjct和NSProxy,还可以动态创建rootClass.
下面来验证一下:
@interface MyObjc : NSObject
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSObject *obj;
@end
MyObjc *my = [[MyObjc alloc]init];
一个继承自NSObject的类MyObjc.
运行,用lldb查看.
(lldb) p/x my.class
(Class) $0 = 0x0000000100008258 MyObjc
(lldb) x/4gx $0
0x100008258: 0x0000000100008230 0x00007ff85e983030
0x100008268: 0x0000000108d13590 0x0001802c00000007
0的内存,读取32个字节,每段8个字节,第一段是类对象的isa_t,第二段是superclass.
(lldb) p/x 0x0000000100008230
(long) $1 = 0x0000000100008230
(lldb) p/x $1 & 0x00007ffffffffff8
(long) $2 = 0x0000000100008230
(lldb) po $2
MyObjc
然后从第一段取出类的地址,输出,是MyObjc,这个MyObjc就是MyObjc的元类.
(lldb) p/x 0x00007ff85e983030
(long) $3 = 0x00007ff85e983030
(lldb) po $3
NSObject
然后是$0的第二段,这段是Myobjc的superclass,输出是NSObject类对象.
(lldb) x/4gx $3
0x7ff85e983030: 0x00007ff85e982fe0 0x0000000000000000
0x7ff85e983040: 0x0000000108f0a6c0 0x0001801000000003
(lldb) p/x 0x00007ff85e982fe0 & 0x00007ffffffffff8
(long) $4 = 0x00007ff85e982fe0
(lldb) po $4
NSObject
读取NSObject类对象的内存,从第一段取出类的信息,这个NSObject是NSObject的元类.
并且第二段是0x0,对应NSObject的父类是Nil.
(lldb) x/4gx $4
0x7ff85e982fe0: 0x00007ff85e982fe0 0x00007ff85e983030
0x7ff85e982ff0: 0x000000010fc040e0 0x0003e03100000007
(lldb) p/x 0x00007ff85e982fe0 & 0x00007ffffffffff8
(long) $5 = 0x00007ff85e982fe0
再读取NSObject的元类的内存,从第一段取出类的地址4是一样的,所以NSObject的元类的isa指向自己.
并且看到$4的第二段,0x00007ff85e983030是NSObject类对象,根元类的父类是根类.
(lldb) x/4gx $2
0x100008230: 0x00007ff85e982fe0 0x00007ff85e982fe0
0x100008240: 0x00007ff81d112770 0x0000e03500000000
读取MyObjc的元类的内存,第一段和$4相同,是根源类.所以任何一个元类的isa都指向根元类,包括根元类自己.
运行objc4源码
已经有很多人整理好了如何编译运行,比如这个项目GitHub
直接使用最新的objc-841,选择编译的target是KCObjcBuild.
m1可以选择Rosetta,运行起来可能会crash:
objc[66360]: task_restartable_ranges_register failed (result 0x6: (os/kern) resource shortage)
找到task_restartable_ranges_register这个函数调用的地方,这个暂时不重要,注释了就行
解决crash
然后就可以运行,可以试一下在+alloc的地方断点,能进断点就成功了.
测试断点
如果添加了新的文件,需要注意在build phases -> compile sources中让main.m保持在第一个的位置,否则不会进断点.
可以拖动