类结构、isa指针结构、引用计数、内存管理等总结

零 索引

  1. 内存管理
  2. 类结构窥探
  3. 类结构分解-isa结构、superclass、cache_t、class_data_bits_t
  4. 引用计数
  5. weak实现
  6. nonpointer和taggedpointer
  7. 自动释放池的结构和工作原理、autorelease与引用计数
  8. 内存分配 class_getInstanceSize以及内存对齐
  9. copy and mutablecopy

一 内存管理

  1. TaggedPointer(针对类似于 NSNumber 的小对象类型)

  2. NONPOINTER_ISA(64位系统下)

第一位的 0 或 1 代表是纯地址型 isa 指针,还是 NONPOINTER_ISA 指针。
第二位,代表是否有关联对象
第三位代表是否有 C++ 代码。
接下来33位代表指向的内存地址
接下来有 弱引用 的标记
接下来有是否 delloc 的标记....等等
  1. 散列表(引用计数表、weak表)
SideTables 表在 非嵌入式的64位系统中,有 64张 SideTable 表
每一张 SideTable 主要是由三部分组成。自旋锁、引用计数表、弱引用表。
全局的 引用计数 之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
引用计数表 中引入了 分离锁的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率

4:CF框架对象和OC相互转换

__bridge
CF和OC对象转化时只涉及对象类型不涉及对象所有权的转化
__bridge_retained
与__bridge_transfer 相反,常用在将OC对象转化成CF对象,且OC对象的所有权也交给CF对象来管理,即OC对象转化成CF对象时,涉及到对象类型和对象所有权的转化,作用同CFBridgingRetain()
_bridge_transfer
常用在CF对象转化成OC对象时,将CF对象的所有权交给OC对象,此时ARC就能自动管理该内存,作用同CFBridgingRelease()

二 类结构窥探

类的结构是什么?当问到类的本质是什么的时候,我们应该都知道是结构体。下面我们就通过编译源码来看一下,类的本质。

我们创建一个类:

LGPerson *person = [LGPerson alloc]
Class pClass     = object_getClass(person);
image.png

然后我们4gx打印pClass


image.png

接下来我们进行clang调试 我们OC代码被编译了这个样子,也是runtime的mesg


image.png

我们全局搜索LGPerson 可以看到LGPerson的具体是什么了,strut 结构体


image.png

我们想知道类的结构吗,所以我们需要继续查找 class 就是我们所说的类,这个源码可以很清楚的明白,class的真正类型是 objc_class

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

可见,Class是指向C的结构体objc_class的指针,我们再看一下objc_class的定义

在Objc2.0之前,objc_class源码如下:

struct objc_class {
Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

 #if !__OBJC2__
Class _Nullable super_class                              OBJC2_UNAVAILABLE;
const char * _Nonnull name                               OBJC2_UNAVAILABLE;
long version                                             OBJC2_UNAVAILABLE;
long info                                                OBJC2_UNAVAILABLE;
long instance_size                                       OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

在这里可以看到,在一个类中,有超类的指针,类名,版本的信息。
ivars是objc_ivar_list成员变量列表的指针;methodLists是指向objc_method_list指针的指针。methodLists是指向方法列表的指针。这里如果动态修改methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。

然后在2006年苹果发布Objc 2.0之后,objc_class的定义就变成下面这个样子了。

typedef struct objc_class *Class;
typedef struct objc_object *id;

@interface Object { 
    Class isa; 
}

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {
private:
    isa_t isa;
}

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

 struct objc_class : objc_object {
// Class ISA; // 8
Class superclass; // 8
cache_t cache;    // 16 不是8         // formerly cache pointer and vtable
class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() { 
    return bits.data();
}
void setData(class_rw_t *newData) {
    bits.setData(newData);
}

void setInfo(uint32_t set) {
    assert(isFuture()  ||  isRealized());
    data()->setFlags(set);
}

void clearInfo(uint32_t clear) {
    assert(isFuture()  ||  isRealized());
    data()->clearFlags(clear);
}
image.png

看这个结构内部含有一个class isa,注释了,说明这是个隐藏的类,所以isa 肯定是继承父类的.验证一下,jump 下就可以你看到.这个内部第一个是isa ,第二个就是superclass,第三个是cache,第四个是bit,这样我们就可以和4gx 打印的吻合了!我们po 出来的NSObject 就是父类

 /// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

属性和方法是存在哪里?

之前已经确定了 0x001d800100002389 为isa指针,0x0000000100b37140为superclass,0x00000001003da260这里为cache_t,最后为bit,我们先读取bit里面有什么,需要使用地址偏移
isa 占8个字节,superclass占8个字节,chache_t 内部代码

  struct cache_t {
  struct bucket_t *_buckets; // 8
  mask_t _mask;  // 4
  mask_t _occupied; // 4

所以chache_t 占16个字节,看下偏移结果

image.png

我们想看我们定义的属性在哪里,我们可以看见一个properties,那么久 p 下
image.png

发现了我们的属性,name
image.png

但是惊人的事情发生了,其实属性不在properlist里面,这个是个意外,其实是放在 ro 里面的!
image.png

image.png

其实这个就是我们设置属性的Nickname,实例变量是放在ivas里的 p 下
image.png

实例变量和属性是有区别的,存储位置不同 name我们可以看到ivars 里面的count 为2 ,其实我们添加的属性,编译进来了,_nickName;
image.png

image.png

是不是想到了runtime copyivarlist 获取的属性为带下划线的
属性存在当前的类里面,并且就在bits里面

方法在哪?正好在p 属性的时候,可以看见methodlist,方法是否在这个里面?验证一下


image.png

确实是我们定义方法,但是数量是4,因为属性,系统会默认添加set,get方法,但是数量还是差一个,我们p 一下


image.png

可以看出多出的那一个为系统的c++方法, 但是我们还添加了一个类方法,只在这里看到了对象方法,类方法没有找到呢,这里是找不到了!我们尝试用代码获取方法列表

void testIMP_classToMetaclass(Class pClass){

const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);

IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));

IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
NSLog(@"%s",__func__);
}

打印结果:
0x100002228-0x0-0x0-0x1000021c0

所以 对象方法在当前类返回了地址,类方法在元类返回了地址,所以类方法存在元类里面

三 类结构分解-isa结构、superclass、cache_t、class_data_bits_t

从一中,我们可以看到,Objective-C 对象都是 C 语言结构体实现的,在objc2.0中,所有的对象都会包含一个isa_t类型的结构体。

objc_object被源码typedef成了id类型,这也就是我们平时遇到的id类型。这个结构体中就只包含了一个isa_t类型的结构体。这个结构体在下面会详细分析。

objc_class继承于objc_object。所以在objc_class中也会包含isa_t类型的结构体isa。至此,可以得出结论:Objective-C 中类也是一个对象。在objc_class中,除了isa之外,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个这个类的实例方法链表。

object类和NSObject类里面分别都包含一个objc_class类型的isa。

上图的左半边类的关系描述完了,接着先从isa来说起

当一个对象的实例方法被调用的时候,会通过isa找到相应的类,然后在该类的class_data_bits_t中去查找方法。class_data_bits_t是指向了类对象的数据区域。在该数据区域内查找相应方法的对应实现。

但是在我们调用类方法的时候,类对象的isa里面是什么呢?这里为了和对象查找方法的机制一致,遂引入了元类(meta-class)的概念。

关于元类,更多具体可以研究这篇文章What is a meta-class in Objective-C?

在引入元类之后,类对象和对象查找方法的机制就完全统一了。

对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

对应关系的图如下图,下图很好的描述了对象,类,元类之间的关系:


image.png

图中实线是 super_class指针,虚线是isa指针。

  1. Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
  2. 每个Class都有一个isa指针指向唯一的Meta class
  3. Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
  4. 每个Meta class的isa指针都指向Root class (meta)。

我们其实应该明白,类对象和元类对象是唯一的,对象是可以在运行时创建无数个的。而在main方法执行之前,从 dyld到runtime这期间,类对象和元类对象在这期间被创建。具体可看sunnyxx这篇iOS 程序 main 函数之前发生了什么

(1)isa_t结构体的具体实现

接下来我们就该研究研究isa的具体实现了。objc_object里面的isa是isa_t类型。通过查看源码,我们可以知道isa_t是一个union联合体。

struct objc_object {
private:
    isa_t isa;
public:
    // initIsa() should be used to init the isa of new objects only.
    // If this object already has an isa, use changeIsa() for correctness.
    // initInstanceIsa(): objects with no custom RR/AWZ
    void initIsa(Class cls /*indexed=false*/);
    void initInstanceIsa(Class cls, bool hasCxxDtor);
private:
    void initIsa(Class newCls, bool indexed, bool hasCxxDtor);
}

那就从initIsa方法开始研究。下面以arm64为例。

inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}

inline void
objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
    if (!indexed) {
        isa.cls = cls;
    } else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}

initIsa第二个参数传入了一个true,所以initIsa就会执行else里面的语句。

union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__ // arm64架构
#   define ISA_MASK        0x0000000ffffffff8ULL //用来取出33位内存地址使用(&)操作
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; //0:代表普通指针,1:表示优化过的,可以存储更多信息。
        uintptr_t has_assoc         : 1; //是否设置过关联对象。如果没设置过,释放会更快
        uintptr_t has_cxx_dtor      : 1; //是否有C++的析构函数
        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; //引用计数器是否过大无法存储在ISA中。如果为1,那么引用计数会存储在一个叫做SideTable的类的属性中
        uintptr_t extra_rc          : 19; //里面存储的值是引用计数器减1

#       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
image.png

ISA_MAGIC_VALUE = 0x000001a000000001ULL转换成二进制是11010000000000000000000000000000000000001,结构如下图:


image.png

关于参数的说明:

第一位index,代表是否开启isa指针优化。index = 1,代表开启isa指针优化。

在2013年9月,苹果推出了iPhone5s,与此同时,iPhone5s配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于64位程序,引入Tagged Pointer后,相关逻辑能减少一半的内存占用,以及3倍的访问速度提升,100倍的创建、销毁速度提升。

在WWDC2013的《Session 404 Advanced in Objective-C》视频中,苹果介绍了 Tagged Pointer。 Tagged Pointer的存在主要是为了节省内存。我们知道,对象的指针大小一般是与机器字长有关,在32位系统中,一个指针的大小是32位(4字节),而在64位系统中,一个指针的大小将是64位(8字节)。

假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示:

image.png

苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:
image.png

关于Tagged Pointer技术详细的,可以看上面链接那个文章。

  • has_assoc
    对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存

  • has_cxx_dtor
    表示该对象是否有 C++ 或者 Objc 的析构器

  • shiftcls
    类的指针。arm64架构中有33位可以存储类指针。
    源码中isa.shiftcls = (uintptr_t)cls >> 3;
    将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。具体可以看从 NSObject 的初始化了解 isa这篇文章里面的shiftcls分析。

  • magic
    判断对象是否初始化完成,在arm64中0x16是调试器判断当前对象是真的对象还是没有初始化的空间。

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

  • deallocating
    对象是否正在释放内存

  • has_sidetable_rc
    判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。

  • extra_rc
    存放该对象的引用计数值减一后的结果。对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc的值就为 9。

ISA_MAGIC_MASK 和 ISA_MASK 分别是通过掩码的方式获取MAGIC值 和 isa类指针。


inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
    return (Class)(isa.bits & ISA_MASK);
}

关于x86_64的架构,具体可以看从 NSObject 的初始化了解 isa文章里面的详细分析。

(2)cache_t的具体实现

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

typedef unsigned int uint32_t;
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

typedef unsigned long  uintptr_t;
typedef uintptr_t cache_key_t;

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}
image.png

据源码,我们可以知道cache_t中存储了一个bucket_t的结构体,和两个unsigned int的变量。

mask:分配用来缓存bucket的总数。
occupied:表明目前实际占用的缓存bucket的个数。

bucket_t的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。

cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。

Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找

(3)class_data_bits_t的具体实现

源码实现如下:

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
}

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
image.png

在 objc_class结构体中的注释写到 class_data_bits_t相当于 class_rw_t指针加上 rr/alloc 的标志。

class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

它为我们提供了便捷方法用于返回其中的 class_rw_t *指针:

class_rw_t *data() {
    return bits.data();
}

Objc的类的属性、方法、以及遵循的协议在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一个指向常量的指针,存储来编译器决定了的属性、方法和遵守协议。rw-readwrite,ro-readonly

在编译期类的结构中的 class_data_bits_t *data指向的是一个 class_ro_t *指针:


image.png

在运行时调用 realizeClass方法,会做以下3件事情:

  1. 从 class_data_bits_t调用 data方法,将结果从 class_rw_t强制转换为 class_ro_t指针
  2. 初始化一个 class_rw_t结构体
  3. 设置结构体 ro的值以及 flag

最后调用methodizeClass方法,把类里面的属性,协议,方法都加载进来。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

方法method的定义如上。里面包含3个成员变量。SEL是方法的名字name。types是Type Encoding类型编码,类型可参考Type Encoding,在此不细说。

IMP是一个函数指针,指向的是函数的具体实现。在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数。

整个运行时过程可以描述如下:

image.png

更加详细的分析,请看@Draveness 的这篇文章深入解析 ObjC 中方法的结构
到此,总结一下objc_class 1.0和2.0的差别。
image.png

image.png

四 引用计数

有些对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回;如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;否则 Runtime 会使用一张散列表来管理引用计数。

散列表来存储引用计数具体是用 DenseMap 类来实现,这个类中包含好多映射实例到其引用计数的键值对,并支持用 DenseMapIterator 迭代器快速查找遍历这些键值对。接着说键值对的格式:键的类型为 DisguisedPtr<objc_object>,DisguisedPtr 类是对 objc_object * 指针及其一些操作进行的封装,目的就是为了让它给人看起来不会有内存泄露的样子(真是心机裱),其内容可以理解为对象的内存地址;值的类型为 __darwin_size_t,在 darwin 内核一般等同于 unsigned long。其实这里保存的值也是等于引用计数减一。使用散列表保存引用计数的设计很好,即使出现故障导致对象的内存块损坏,只要引用计数表没有被破坏,依然可以顺藤摸瓜找到内存块的位置。

之前说引用计数表是个散列表,这里简要说下散列的方法。有个专门处理键的 DenseMapInfo 结构体,它针对 DisguisedPtr 做了些优化匹配键值速度的方法:

struct DenseMapInfo<DisguisedPtr<T>> {
  static inline DisguisedPtr<T> getEmptyKey() {
    return DisguisedPtr<T>((T*)(uintptr_t)-1);
  }
  static inline DisguisedPtr<T> getTombstoneKey() {
    return DisguisedPtr<T>((T*)(uintptr_t)-2);
  }
  static unsigned getHashValue(const T *PtrVal) {
      return ptr_hash((uintptr_t)PtrVal);
  }
  static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
      return LHS == RHS; 
  }
};

当然这里的哈希算法会根据是否为 64 位平台来进行优化,算法具体细节就不深究了,我总觉得苹果在这里的 hardcode 是随便写的:

#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
    key ^= key >> 4;
    key *= 0x8a970be7488fda55;
    key ^= __builtin_bswap64(key);
    return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
    key ^= key >> 4;
    key *= 0x5052acdb;
    key ^= __builtin_bswap32(key);
    return key;
}
#endif

再介绍下 SideTable 这个类,它用于管理引用计数表和 weak 表,并使用 spinlock_lock 自旋锁来防止操作表结构时可能的竞态条件。它用一个 64*128 大小的 uint8_t 静态数组作为 buffer 来保存所有的 SideTable 实例。并提供三个公有属性:

spinlock_t slock;//保证原子操作的自选锁
RefcountMap refcnts;//保存引用计数的散列表
weak_table_t weak_table;//保存 weak 引用的全局散列表

还提供了一个工厂方法,用于根据对象的地址在 buffer 中寻找对应的 SideTable 实例:

static SideTable *tableForPointer(const void *p)

weak 表的作用是在对象执行 dealloc 的时候将所有指向该对象的 weak 指针的值设为 nil,避免悬空指针。这是 weak 表的结构:

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。

4.2 ARC

是由clang(LLVM编译器)+运行时库共同实现的。

4.3 ARC的所有权修饰符号

4.3.1
  • __strong id类型和对象类型的默认修饰符(copy、strong、retain)
  • __weak 弱引用(weak)
  • __unsafe_unretained 既不是强引用也不是弱引用(assign、unsafe_unretained)
  • __autoreleasing ARC中@autoreleasepool中使用 类似于之前的release

Tips:
1:实际上在使用__weak修饰的对象,由于无法取得相应的对象的所有权,必定会注册到对应的autoreleasepool中,防止在作用域内被释放,导致crash。非显式使用了__autoreleasing。
2:id的指针或对象的指针 也会隐式的使用了__autoreleasing

4.3.2
  • __bridge,
  • __bridge_retained
  • __bridge_transfer

__bridge可以用于OC对象和CF对象互转,例如

NSObject *obj = [[NSObject alloc] init];    //retain count 1
CFTypeRef cfObj1 = (__bridge CFTypeRef)obj; //retain count 1
NSObject *obj1 = (__bridge id)cfObj1;       //retain count 2

在这种转换方式下,如果是OC对象转换成CF对象,引用计数不变。如果是CF对象转换成OC对象,因为OC对象的默认修饰符是__strong,引用计数会+1,即以下两种写法是一样的。

NSObject *obj1 = (__bridge id)cfObj1;
NSObject __strong *obj1 = (__bridge id)cfObj1;

__bridge_retained用于OC对象转换为CF对象,例如

NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj1 = (__bridge_retained CFTypeRef)obj;    //retain count 2

//等价写法
NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj1 = (CFTypeRef)CFBridgingRetain(obj);    //retain count 2

这种情况下,obj的引用计数会+1,obj的释放不会影响到cfObj1的使用

__bridge_transfer用于CF对象转换为OC对象,例如

NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj1 = (__bridge_retained CFTypeRef)obj;    //retain count 2
NSObject *obj1 = (__bridge_transfer id)cfObj1;          //retain count 2

//等价写法
NSObject *obj = [[NSObject alloc] init];                  //retain count 1
CFTypeRef cfObj1 = (__bridge_retained CFTypeRef)obj;      //retain count 2
NSObject *obj1 = (NSObject *)CFBridgingRelease(cfObj1);   //retain count 2

五 weak实现

在iOS开发过程中,会经常使用到一个修饰词weak,使用场景大家都比较清晰,避免出现对象之间的强强引用而造成对象不能被正常释放最终导致内存泄露的问题。weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。

1、weak 初探

下面的一段代码是我们在开发中常见的weak的使用

Person *object = [Person alloc];
id __weak objc = object;
复制代码

如果在此打断点跟踪汇编信息,可以发现底层库调了objc_initWeak函数 [图片上传失败...(image-99a1be-1615882971235)]

那么我们来看一下objc_initWeak 方法的实现代码是怎么样的呢?

1、objc_initWeak方法

如下是objc_initWeak 方法的底层源码

id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}
复制代码

该方法的两个参数locationnewObj

  • location__weak指针的地址,存储指针的地址,这样便可以在最后将其指向的对象置为nil。
  • newObj :所引用的对象。即例子中的obj 。

从上面的代码可以看出objc_initWeak方法只是一个深层次函数调用的入口,在该方法内部调用了storeWeak 方法。下面我们来看下storeWeak 方法的实现代码。

2、storeWeak方法

如下是storeWeak方法的实现代码。

// Template parameters.
enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems.
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) { // 如果weak ptr之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTable
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil; // 如果weak ptr之前没有弱引用过一个obj,则oldTable = nil
    }
    if (haveNew) { // 如果weak ptr要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTable
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil; // 如果weak ptr不需要引用一个新obj,则newTable = nil
    }

    // 加锁操作,防止多线程中竞争冲突
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    // location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&
            !((objc_class *)cls)->isInitialized())  // 如果cls还没有初始化,先初始化,再尝试设置weak
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls; // 这里记录一下previouslyInitializedClass, 防止改if分支再次进入

            goto retry; // 重新获取一遍newObj,这时的newObj应该已经初始化过了
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); // 如果weak_ptr之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址
    }

    // Assign new value, if any.
    if (haveNew) { // 如果weak_ptr需要弱引用新的对象newObj
        // (1) 调用weak_register_no_lock方法,将weak ptr的地址记录到newObj对应的weak_entry_t中
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // (2) 更新newObj的isa的weakly_referenced bit标志位
        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        // (3)*location 赋值,也就是将weak ptr直接指向了newObj。可以看到,这里并没有将newObj的引用计数+1
        *location = (id)newObj; // 将weak ptr指向object
    }
    else {
        // No new value. The storage is not changed.
    }

    // 解锁,其他线程可以访问oldTable, newTable了
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj; // 返回newObj,此时的newObj与刚传入时相比,weakly-referenced bit位置1
}
复制代码

storeWeak 方法的实现代码虽然有些长,但是并不难以理解。下面我们来分析下该方法的实现。

  1. storeWeak方法实际上是接收了5个参数,分别是haveOld、haveNew和crashIfDeallocating ,这三个参数都是以模板的方式传入的,是三个bool类型的参数。 分别表示weak指针之前是否指向了一个弱引用,weak指针是否需要指向一个新的引用,若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。
  2. 该方法维护了oldTablenewTable分别表示旧的引用弱表和新的弱引用表,它们都是SideTable的hash表。
  3. 如果weak指针之前指向了一个弱引用,则会调用weak_unregister_no_lock 方法将旧的weak指针地址移除。
  4. 如果weak指针需要指向一个新的引用,则会调用weak_register_no_lock 方法将新的weak指针地址添加到弱引用表中。
  5. 调用setWeaklyReferenced_nolock 方法修改weak新引用的对象的bit标志位

那么这个方法中的重点也就是weak_unregister_no_lockweak_register_no_lock 这两个方法。而这两个方法都是操作的SideTable 这样一个结构的变量,那么我们需要先来了解下SideTable

3、SideTable

先来看下SideTable的定义。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}
复制代码

SideTable的定义很清晰,有三个成员:

  • spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable。
  • RefcountMap refcnts :用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。

3.1、weak_table_t

先来看下weak_table_t 的底层代码。

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
复制代码
  • weak_entries: hash数组,用来存储弱引用对象的相关信息weak_entry_t
  • num_entries: hash数组中的元素个数
  • mask:hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
  • max_hash_displacement:可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)

weak_table_t是一个典型的hash结构。weak_entries是一个动态数组,用来存储weak_entry_t类型的元素,这些元素实际上就是OC对象的弱引用信息。

3.2、weak_entry_t

weak_entry_t的结构也是一个hash结构,其存储的元素是弱引用对象指针的指针, 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。

#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被弱引用的对象

    // 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
    union {
        struct {
            weak_referrer_t *referrers;                      // 弱引用该对象的对象指针地址的hash数组
            uintptr_t        out_of_line_ness : 2;           // 是否使用动态hash数组标记位
            uintptr_t        num_refs : PTR_MINUS_2;         // hash数组中的元素个数
            uintptr_t        mask;                           // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
            uintptr_t        max_hash_displacement;          // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent) // 构造方法,里面初始化了静态数组
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

可以看到在weak_entry_t 的结构定义中有联合体,在联合体的内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers两种方式来存储弱引用对象的指针地址。通过out_of_line()这样一个函数方法来判断采用哪种存储方式。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用定长数组。当超过WEAK_INLINE_COUNT时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。

到这里我们已经清楚了弱引用表的结构是一个hash结构的表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。那么接下来看看这个弱引用表是怎么维护这些数据的。

4、weak_register_no_lock方法添加弱引用

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    // 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // 确保被引用的对象可用(没有在析构,同时应该支持weak引用)
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }
    // 正在析构的对象,不能够被弱引用
    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    // 在 weak_table中找到referent对应的weak_entry,并将referrer加入到weak_entry中
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) { // 如果能找到weak_entry,则讲referrer插入到weak_entry中
        append_referrer(entry, referrer);   // 将referrer插入到weak_entry_t的引用数组中
    } 
    else { // 如果找不到,就新建一个
        weak_entry_t new_entry(referent, referrer);  
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

这个方法需要传入四个参数,它们代表的意义如下:

  • weak_tableweak_table_t 结构类型的全局的弱引用表。
  • referent_id:weak指针。
  • *referrer_id:weak指针地址。
  • crashIfDeallocating :若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。

从上面的代码我么可以知道该方法主要的做了如下几个方便的工作。

  1. 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作。
  2. 如果对象正在析构,则抛出异常。
  3. 如果对象不能被weak引用,直接返回nil。
  4. 如果对象没有再析构且可以被weak引用,则调用weak_entry_for_referent 方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer 方法向其中插入weak指针地址。否则新建一个weak_entry。

4.1、weak_entry_for_referent取元素

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    size_t begin = hash_pointer(referent) & weak_table->mask;  // 这里通过 & weak_table->mask的位操作,来确保index不会越界
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries); // 触发bad weak table crash
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) { // 当hash冲突超过了可能的max hash 冲突时,说明元素没有在hash表中,返回nil 
            return nil;
        }
    }

    return &weak_table->weak_entries[index];
}

4.2、append_referrer添加元素

static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line()) { // 如果weak_entry 尚未使用动态数组,走这里
        // Try to insert inline.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // 如果inline_referrers的位置已经存满了,则要转型为referrers,做动态数组。
        // Couldn't insert inline. Allocate out of line.
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[I];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    // 对于动态数组的附加处理:
    assert(entry->out_of_line()); // 断言: 此时一定使用的动态数组

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) { // 如果动态数组中元素个数大于或等于数组位置总空间的3/4,则扩展数组空间为当前长度的一倍
        return grow_refs_and_insert(entry, new_referrer); // 扩容,并插入
    }

    // 如果不需要扩容,直接插入到weak_entry中
    // 注意,weak_entry是一个哈希表,key:w_hash_pointer(new_referrer) value: new_referrer

    // 细心的人可能注意到了,这里weak_entry_t 的hash算法和 weak_table_t的hash算法是一样的,同时扩容/减容的算法也是一样的
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask); // '& (entry->mask)' 确保了 begin的位置只能大于或等于 数组的长度
    size_t index = begin;  // 初始的hash index
    size_t hash_displacement = 0;  // 用于记录hash冲突的次数,也就是hash再位移的次数
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;  // index + 1, 移到下一个位置,再试一次能否插入。(这里要考虑到entry->mask取值,一定是:0x111, 0x1111, 0x11111, ... ,因为数组每次都是*2增长,即8, 16, 32,对应动态数组空间长度-1的mask,也就是前面的取值。)
        if (index == begin) bad_weak_table(entry); // index == begin 意味着数组绕了一圈都没有找到合适位置,这时候一定是出了什么问题。
    }
    if (hash_displacement > entry->max_hash_displacement) { // 记录最大的hash冲突次数, max_hash_displacement意味着: 我们尝试至多max_hash_displacement次,肯定能够找到object对应的hash位置
        entry->max_hash_displacement = hash_displacement;
    }
    // 将ref存入hash数组,同时,更新元素个数num_refs
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

这段代码首先确定是使用定长数组还是动态数组,如果是使用定长数组,则直接将weak指针地址添加到数组即可,如果定长数组已经用尽,则需要将定长数组中的元素转存到动态数组中。

5、weak_unregister_no_lock移除引用

如果weak指针之前指向了一个弱引用,则会调用weak_unregister_no_lock方法将旧的weak指针地址移除。

void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;

    if ((entry = weak_entry_for_referent(weak_table, referent))) { // 查找到referent所对应的weak_entry_t
        remove_referrer(entry, referrer);  // 在referent所对应的weak_entry_t的hash数组中,移除referrer

        // 移除元素之后, 要检查一下weak_entry_t的hash数组是否已经空了
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }

        if (empty) { // 如果weak_entry_t的hash数组已经空了,则需要将weak_entry_t从weak_table中移除
            weak_entry_remove(weak_table, entry);
        }
    }
  1. 首先,它会在weak_table中找出referent对应的weak_entry_t
  2. 在weak_entry_t中移除referrer
  3. 移除元素后,判断此时weak_entry_t中是否还有元素 (empty==true?)
  4. 如果此时weak_entry_t已经没有元素了,则需要将weak_entry_t从weak_table中移除

到这里为止就是对于一个对象做weak引用时底层做的事情,用weak引用对象后引用计数并不会加1,当对象释放时,所有weak引用它的指针又是如何自动设置为nil的呢?

6、dealloc

当对象的引用计数为0时,底层会调用_objc_rootDealloc方法对对象进行释放,而在_objc_rootDealloc方法里面会调用rootDealloc方法。如下是rootDealloc方法的代码实现。

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
  1. 首先判断对象是否是Tagged Pointer,如果是则直接返回。
  2. 如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced、没有关联对象!isa.has_assoc 、没有自定义的C++析构方法!isa.has_cxx_dtor、没有用到SideTable来引用计数!isa.has_sidetable_rc则直接快速释放。
  3. 如果不能满足2中的条件,则会调用object_dispose 方法。

6.1、object_dispose

object_dispose 方法很简单,主要是内部调用了objc_destructInstance方法。

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
复制代码

上面这一段代码很清晰,如果有自定义的C++析构方法,则调用C++析构函数。如果有关联对象,则移除关联对象并将其自身从Association Manager的map中移除。调用clearDeallocating 方法清除对象的相关引用。

6.2、clearDeallocating

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

clearDeallocating中有两个分支,先判断对象是否采用了优化isa引用计数,如果没有的话则需要清理对象存储在SideTable中的引用计数数据。如果对象采用了优化isa引用计数,则判断是否有使用SideTable的辅助引用计数(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合这两种情况中一种的,调用clearDeallocating_slow 方法。

6.3、clearDeallocating_slow

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this]; // 在全局的SideTables中,以this指针为key,找到对应的SideTable
    table.lock();
    if (isa.weakly_referenced) { // 如果obj被弱引用
        weak_clear_no_lock(&table.weak_table, (id)this); // 在SideTable的weak_table中对this进行清理工作
    }
    if (isa.has_sidetable_rc) { // 如果采用了SideTable做引用计数
        table.refcnts.erase(this); // 在SideTable的引用计数中移除this
    }
    table.unlock();
}

在这里我们关心的是weak_clear_no_lock 方法。这里调用了weak_clear_no_lock来做weak_table的清理工作。

6.4、weak_clear_no_lock

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); // 找到referent在weak_table中对应的weak_entry_t
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;

    // 找出weak引用referent的weak 指针地址数组以及数组长度
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }

    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i]; // 取出每个weak ptr的地址
        if (referrer) {
            if (*referrer == referent) { // 如果weak ptr确实weak引用了referent,则将weak ptr设置为nil,这也就是为什么weak 指针会自动设置为nil的原因
                *referrer = nil;
            }
            else if (*referrer) { // 如果所存储的weak ptr没有weak 引用referent,这可能是由于runtime代码的逻辑错误引起的,报错
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }

    weak_entry_remove(weak_table, entry); // 由于referent要被释放了,因此referent的weak_entry_t也要移除出weak_table
}

7、总结

  • 1、weak的原理在于底层维护了一张weak_table_t结构的hash表,key是所指对象的地址,value是weak指针的地址数组。
  • 2、weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。
  • 3、对象释放时,调用clearDeallocating函数根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
  • 4、文章中介绍了SideTable、weak_table_t、weak_entry_t这样三个结构,它们之间的关系如下图所示。
    image.png

六 tagged pointer

1、Tagged Pointer 介绍

1.1、什么是 Tagged Pointer

Tagged Pointer是一个特别的指针,它分为两部分:

  • 一部分直接保存数据 ;
  • 另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址;

因此,我们说Tagged Pointer是一个伪指针!

1.2、 Tagged Pointer出现的背景

对于一个NSNumber对象,如果存储NSInteger的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位CPU下占4个字节。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在32位CPU下为4个字节。

在2013年9月,苹果推出了iPhone 5s,该款机型首次采用64位架构的A7双核处理器。此时,对于一个NSNumber对象,如果存储NSInteger的普通变量,那么它将占 8 个字节;一个指针也将占用 8 个字节。

对于一个普通的程序而言,从 32 位机器迁移到 64 位机器中后,虽然逻辑没有任何变化,但对于 NSNumberNSDateNSString等类型的实例所占用的内存会翻倍、浪费了稀有的内存资源!!同时维护程序中的对象需要也分配内存,维护引用计数,管理生命周期,使用对象给程序的运行增加了负担!!

为了节省内存和提高执行效率,苹果提出了 Tagged Pointer的概念。苹果将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。

1.3、苹果对 Tagged Pointer的介绍

苹果对于 Tagged Pointer特点的做出了介绍:

  • Tagged Pointer被设计的目的是用来存储较小的对象,例如NSNumberNSDateNSString 等;
  • Tagged Pointer的值不再表示地址,而是真正的值;
  • 在内存读取上有着3倍的效率,创建时比以前快106倍 ;
1.4、Tagged Pointer的使用

在一个程序中运行下述代码,获取输出日志:

NSNumber *number =  @(0);
NSNumber *number1 = @(1);
NSNumber *number2 = @(2);
NSNumber *number3 = @(9999999999999999999);
NSString *string = [[@"a" mutableCopy] copy];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];

NSLog(@"number ---- %@, %p", [number class], number);
NSLog(@"number1 --- %@, %p", [number1 class], number1);
NSLog(@"number2 --- %@, %p", [number2 class], number2);
NSLog(@"number3 --- %@, %p", [number3 class], number3);
NSLog(@"NSString -- %@, %p", [string class], string);
NSLog(@"indexPath - %@, %p", indexPath.class,indexPath);

/********************* 输出日志 *********************
number ---- __NSCFNumber, 0xb000000000000002
number1 --- __NSCFNumber, 0xb000000000000012
number2 --- __NSCFNumber, 0xb000000000000022
number3 --- __NSCFNumber, 0x600003b791c0
NSString -- NSTaggedPointerString, 0xa000000000000611
indexPath - NSIndexPath, 0xc000000000000016
 */

分析日志:

  • 1、NSNumber存储的数据不大时,NSNumber *指针是伪指针Tagged Pointer
  • 2、NSNumber存储的数据很大时,NSNumber *指针一般指针,指向NSNumber实例的地址,如 number3
  • 3、NSTaggedPointerString 经常遇见,它就是Tagged Pointer对象;

对于Tagged Pointer,是系统实现的,无需开发者操心!但是作为开发者,也要知道NSTaggedPointerString等是什么东西!

1.5、Tagged Pointer的思考

通过上文的概念 + 应用,相信大家对 Tagged Pointer 有了一定的了解;那么大家有没有一些疑问呢?

  • 系统是如何将 NSNumberNSDateNSString等类型的数据存储在 Tagged Pointer 上?
  • 既然Tagged Pointer是一个伪指针,不再指向实例对象,那么isa指针就不能再调用!不通过isa指针获取它所属的类,系统是如何知道它存储的数据结构的?
  • 数据展示在Tagged Pointer,可以被人通过Tagged Pointer获取,明显不再安全,那么苹果是如何加密Tagged Pointer上的数据的?

2、Tagged Pointer的底层探究

苹果设计的Tagged Pointer技术,是在 Runtime 库 中实现的。

2.1、Tagged Pointer技术的启用与禁用

苹果预留了环境变量 OBJC_DISABLE_TAGGED_POINTERS,通过设置该变量的布尔值,可以将Tagged Pointer技术的启用与关闭的决定权交给开发者!
如果禁用Tagged Pointer,只需设置环境变量 OBJC_DISABLE_TAGGED_POINTERSYES 即可!

2.1.1、Tagged Pointer的禁用函数

Runtime 库objc-runtime-new.mm文件中有一个禁用Tagged Pointer的函数:

static void disableTaggedPointers(){
    objc_debug_taggedpointer_mask = 0;
    objc_debug_taggedpointer_slot_shift = 0;
    objc_debug_taggedpointer_slot_mask = 0;
    objc_debug_taggedpointer_payload_lshift = 0;
    objc_debug_taggedpointer_payload_rshift = 0;

    objc_debug_taggedpointer_ext_mask = 0;
    objc_debug_taggedpointer_ext_slot_shift = 0;
    objc_debug_taggedpointer_ext_slot_mask = 0;
    objc_debug_taggedpointer_ext_payload_lshift = 0;
    objc_debug_taggedpointer_ext_payload_rshift = 0;
}

在该函数内部,将一些列变量全部设置为 0 !至于这些变量都有什么用处,我们后文用到了再解释!

2.1.2、何时调用disableTaggedPointers()函数?

何时调用disableTaggedPointers()函数禁用Tagged Pointer呢?

还是在 objc-runtime-new.mm文件中,在大名鼎鼎的_read_images() 函数中有一处关键代码:

//注意:由于_read_images() 函数中完成了大量的初始化操作,我们在此处省略大量无关代码,
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
        //禁用NSNumber等的 Tagged Pointer 指针优化
        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }

        //初始化 TaggedPointer 混淆器:用于保护 Tagged Pointer 上的数据
        initializeTaggedPointerObfuscator();
}

可以看到,首先判断Tagged Pointer是否被禁用,如果被禁用,才调用disableTaggedPointers()函数将Tagged Pointer相关变量全部设置为 0。

OPTION( DisableTaggedPointers,  OBJC_DISABLE_TAGGED_POINTERS,  "disable tagged pointer optimization of NSNumber et al.") 

判断Tagged Pointer时,使用的是预定义的宏 OPTION!本质是获取环境变量 OBJC_DISABLE_TAGGED_POINTERS的值,设置该变量为 YES,则Tagged Pointer被禁用。

2.1.3、判断是否禁用的函数
static inline bool  _objc_taggedPointersEnabled(void){
    extern uintptr_t objc_debug_taggedpointer_mask;
    return (objc_debug_taggedpointer_mask != 0);
}

  • DisableTaggedPointers 通过环境变量 OBJC_DISABLE_TAGGED_POINTERS来判断是否使用Tagged Pointer
  • 函数 _objc_taggedPointersEnabled() 通过全局变量objc_debug_taggedpointer_mask判断是否使用Tagged Pointer
2.1.4、能否禁用?

可能有的读者去测试OBJC_DISABLE_TAGGED_POINTERSYES时,发现程序报错:

objc[3658]: tagged pointers are disabled
Message from debugger: Terminated due to signal 9

既然苹果给了OBJC_DISABLE_TAGGED_POINTERS这个环境变量让我们设置,为何程序还无法启动呢?分析下图错误堆栈:

image

在调用栈中发现了Runtime 库 杀手 _objc_fatal()函数,该函数一言不合就杀死进程!可以看到,是_objc_registerTaggedPointerClass() 函数请求_objc_fatal()杀死程序的!

那么_objc_registerTaggedPointerClass() 函数是什么呢?它凭什么一言不合就杀死程序?我们下节分析!

2.2、注册成为Tagged Pointer

为什么NSNumberNSDateNSString等类型可以自动转为Tagged Pointer?而UIViewController就不能自动转化呢?这就要说到上节遗留的_objc_registerTaggedPointerClass() 函数了!!
加载程序时,从 dyld 库_dyld_start()函数开始,经历了多般步骤,开始调用_objc_registerTaggedPointerClass() 函数!

在讲解_objc_registerTaggedPointerClass() 函数之前先说说题外话:classSlotForBasicTagIndex() 函数与classSlotForTagIndex() 函数!

2.2.1、classSlotForBasicTagIndex() 函数
static Class *classSlotForBasicTagIndex(objc_tag_index_t tag){
    uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK);
    uintptr_t obfuscatedTag = tag ^ tagObfuscator;
    // objc_tag_classes 数组的索引包含标记的位本身

#if SUPPORT_MSB_TAGGED_POINTERS//高位优先
    return &objc_tag_classes[0x8 | obfuscatedTag];
#else
    return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}

classSlotForBasicTagIndex() 函数的主要功能就是根据指定索引 tag 从数组 objc_tag_classes中获取类指针;该函数要求索引tag是个有效的索引!

注:可能大家会对objc_debug_taggedpointer_obfuscator产生疑惑,这个东西是什么?是用来干嘛的?别急,在此处不影响该函数的功能,后文用到了我们再讲!

a、这里有两个重要的全局变量数组:
#if SUPPORT_TAGGED_POINTERS //支持 Tagged Pointer

extern "C" {
    extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT*2];
    extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
#define objc_tag_classes objc_debug_taggedpointer_classes
#define objc_tag_ext_classes objc_debug_taggedpointer_ext_classes
#endif

  • 数组objc_tag_classes:存储苹果定义的几个基础类;
  • 数组objc_tag_ext_classes:存储苹果预留的扩展类;
b、数组objc_debug_taggedpointer_classesobjc_debug_taggedpointer_ext_classes的初始化

笔者在 Runtime 库objc-msg-armXX.s文件发现了相关代码:

#if SUPPORT_TAGGED_POINTERS
    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0
    .globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
    .fill 256, 8, 0
#endif

注:在别的地方没有关于这两个数组的代码,笔者怀疑这俩个数组在此处被初始化的!如果有大神认为笔者错了,这俩个数组的初始化另有地方,还望告知!

2.2.2、classSlotForTagIndex() 函数
static Class *classSlotForTagIndex(objc_tag_index_t tag){
    if (tag >= OBJC_TAG_First60BitPayload && tag <= OBJC_TAG_Last60BitPayload) {
        return classSlotForBasicTagIndex(tag);
    }

    if (tag >= OBJC_TAG_First52BitPayload && tag <= OBJC_TAG_Last52BitPayload) {
        int index = tag - OBJC_TAG_First52BitPayload;
        uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator >> _OBJC_TAG_EXT_INDEX_SHIFT)& _OBJC_TAG_EXT_INDEX_MASK);
        return &objc_tag_ext_classes[index ^ tagObfuscator];
    }
    return nil;
}

该函数的主要功能就是根据指定索引 tag 获取类指针:

  • 当索引tag为基础类的索引时,去数组objc_tag_classes中取数据;
  • 当索引tag为扩展类的索引时,去数组objc_tag_ext_classes中取数据;
  • 当索引tag无效时,返回一个 nil
2.2.3、索引objc_tag_index_t

classSlotForBasicTagIndex() 函数与classSlotForTagIndex() 函数的本质就是获取数组objc_tag_classes与数组objc_tag_ext_classes中的数据!
那么它的索引 objc_tag_index_t 是何方神圣呢?

Runtime 库objc-internal.h文件中找到了 objc_tag_index_t的定义:

//有关于标志位的枚举如下:
#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位净负荷
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, //表示这是一个NSString对象
    OBJC_TAG_NSNumber          = 3, //表示这是一个NSNumber对象
    OBJC_TAG_NSIndexPath       = 4, //表示这是一个NSIndexPath对象
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,//表示这是一个NSDate对象

    OBJC_TAG_RESERVED_7        = 7, //60位净负荷: 索引 7 被保留

    // 52 位净负荷
    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_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, // 52 位净负荷的开始处
    OBJC_TAG_Last52BitPayload  = 263, // 52 位净负荷的结束处

    OBJC_TAG_RESERVED_264      = 264 // 52 位净负荷: 索引 264 被保留
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

objc_tag_index_t 就是个枚举变量,存储在 Tagged Pointer的特殊标记的部位!

Runtime 就是根据 objc_tag_index_t的枚举值判断Tagged Pointer存储的是 NSString对象或者 NSNumber对象...

2.2.4、_objc_registerTaggedPointerClass() 函数

现在知道了classSlotForBasicTagIndex() 函数与classSlotForTagIndex() 函数的本质就是获取数组objc_tag_classes与数组objc_tag_ext_classes中的数据!而索引 objc_tag_index_t 又用来在Tagged Pointer对象标记存储的类!

准备工作已经做足,现在去分析_objc_registerTaggedPointerClass()函数! 在 Runtime 库objc-runtime-new.mm文件中找到该函数的实现:

void _objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls){
    if (objc_debug_taggedpointer_mask == 0) {
        _objc_fatal("tagged pointers are disabled");
    }

    Class *slot = classSlotForTagIndex(tag);//根据索引获取指定的类指针
    if (!slot) {
        _objc_fatal("tag index %u is invalid", (unsigned int)tag);
    }

    Class oldCls = *slot;//取出指针指向的类
    if (cls  &&  oldCls  &&  cls != oldCls) {
        //指定的索引被用于两个不同的类,终止程序
        _objc_fatal("tag index %u used for two different classes (was %p %s, now %p %s)", tag,oldCls, oldCls->nameForLogging(),cls, cls->nameForLogging());
    }
    *slot = cls;//将入参 cls 赋值给该类指针指向的地址

    if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
        Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
        if (*extSlot == nil) {
            extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
            *extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;//表示 TaggedPointer 类
        }
    }
}

分析该函数的实现:

  • 1、首先判断 objc_debug_taggedpointer_mask是否为 0 ,也就是判断开发者是否把 OBJC_DISABLE_TAGGED_POINTERS 设置为 YES;如果禁用了 Tagged Pointer,那么不好意思,直接调用 _objc_fatal()函数终止该程序,不让该程序启动!
    只有启用 Tagged Pointer,程序才有执行下去的意义!
  • 2、根据索引 tag去取出数组objc_tag_classes或数组objc_tag_ext_classes中指定的类指针classSlotForTagIndex(tag)
    如果传递无效的索引 tag,获取一个 nil,还是要调用_objc_fatal()终止该程序;
  • 3、尝试着去获取该指针指向的类Class oldCls = *slot:如果要注册的类和该处的类不是同一个?不好意思,_objc_fatal()终止程序!
    只有类指针 slot指向的位置为 NULL,或者类指针 slot指向的位置就是存储着我们要注册的类,系统才能安稳的运行下去;
  • 4、将入参cls赋值给类指针 slot指向的位置*slot = cls;到此,该函数的功能经过重重考验就已经实现了!
  • 5、假如注册的不是基础类,而是第一次注册扩展类,该函数还有个额外功能:在OBJC_TAG_RESERVED_7出存储占位类 OBJC_CLASS_$___NSUnrecognizedTaggedPointer
2.2.5、打印系统注册的Tagged Pointer

不妨在_objc_registerTaggedPointerClass()函数中插入一条 print()语句,打印出系统注册的Tagged Pointer,如下图所示:

image

观察打印日志,可以得知NSNumberNSString等确实在启动程序时,被系统注册!

2.2.6、获取_objc_registerTaggedPointerClass()函数调用栈

_objc_registerTaggedPointerClass()函数打一个断点,打印该线程栈信息:

(lldb)  thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100382213 libobjc.A.dylib`::_objc_registerTaggedPointerClass(tag=OBJC_TAG_NSNumber, cls=__NSCFNumber) at objc-runtime-new.mm:6668
    frame #1: 0x00007fff433a0b5b CoreFoundation`__CFNumberGetTypeID_block_invoke + 65
    frame #2: 0x0000000100d087c3 libdispatch.dylib`_dispatch_client_callout + 8
    frame #3: 0x0000000100d0a48b libdispatch.dylib`_dispatch_once_callout + 87
    frame #4: 0x00007fff433a0b17 CoreFoundation`CFNumberGetTypeID + 39
    frame #5: 0x00007fff433a0103 CoreFoundation`__CFInitialize + 715
    frame #6: 0x0000000100020a68 dyld`ImageLoaderMachO::doImageInit(ImageLoader::LinkContext const&) + 316
    frame #7: 0x0000000100020ebb dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 29
    frame #8: 0x000000010001c0da dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 358
    frame #9: 0x000000010001c06d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 249
    frame #10: 0x000000010001c06d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 249
    frame #11: 0x000000010001c06d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 249
    frame #12: 0x000000010001b254 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 134
    frame #13: 0x000000010001b2e8 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 74
    frame #14: 0x000000010000a756 dyld`dyld::initializeMainExecutable() + 169
    frame #15: 0x000000010000f78f dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6237
    frame #16: 0x00000001000094f6 dyld`dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) + 1154
    frame #17: 0x0000000100009036 dyld`_dyld_start + 54

具体的调用信息如上所示,这里不再过多啰嗦!

至此,笔者已经讲完了注册为Tagged Pointer的流程!

2.3、判断一个指针是否是 Tagged Pointer

Tagged Pointer之所以特殊,是因为它有个标记位表明它是特殊指针,那么该如何判断这个特殊的标记呢?

Runtime 库objc-internal.h文件中找到了 _objc_isTaggedPointer()的实现:

static inline bool _objc_isTaggedPointer(const void * _Nullable ptr){
    //将一个指针地址和 _OBJC_TAG_MASK 常量做 & 运算:判断该指针的最高位或者最低位为 1,那么这个指针就是 Tagged Pointer。
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

该函数将 指针ptr_OBJC_TAG_MASK 做了按位与 运算,这个运算有什么意义呢?我们先看下_OBJC_TAG_MASK 的定义:

#if OBJC_MSB_TAGGED_POINTERS //MSB 高位优先
#   define _OBJC_TAG_MASK (1UL<<63) //Tagged Pointer 指针
#else //LSB 低位优先
#   define _OBJC_TAG_MASK 1UL //Tagged Pointer 指针
#endif

对于 iOS 系统而言,遵循 MSB 规则(高位优先)!因此_OBJC_TAG_MASK的值为 0x8000000000000000:一个64 位的二进制,最左边一位是 1,其余位全是 0!

在 64 位系统中,使用指针很难将有限的 CPU 资源耗尽;因此 64 位还有很大的剩余! 苹果将64中的最左边一位(MSB 时)标记是 1 ,或者最右边一位(LSB 时)标记是 1 ,以此来表示这个指针是 Tagged Pointer

因此 ptr & _OBJC_TAG_MASK) 按位与 运算后可以判断它的标志位是否是 1,即是否是 Tagged Pointer

2.4、在伪指针 Tagged Pointer存储数据

讲了半天:又是禁用 Tagged Pointer、又是注册 Tagged Pointer、又是判断 Tagged Pointer 的 ! 那么到底怎么存储NSNumberNSDateNSString等类型的数据呢?

Runtime 库objc-internal.h文件中找到了 _objc_makeTaggedPointer() 函数的实现:

static inline void * _Nonnull _objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value){
    if (tag <= OBJC_TAG_Last60BitPayload) {//如果是基础的索引
        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 {//如果是扩展的索引
        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);
    }
}

该函数将 value 位运算,然后再编码 _objc_encodeTaggedPointer(),就是指针的值!

a、尝试计算
#if OBJC_MSB_TAGGED_POINTERS //MSB 高位优先
#   define _OBJC_TAG_MASK (1UL<<63) //Tagged Pointer 指针
#   define _OBJC_TAG_INDEX_SHIFT 60
#   define _OBJC_TAG_SLOT_SHIFT 60
#   define _OBJC_TAG_PAYLOAD_LSHIFT 4
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK (0xfUL<<60)
#   define _OBJC_TAG_EXT_INDEX_SHIFT 52
#   define _OBJC_TAG_EXT_SLOT_SHIFT 52
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#else //LSB 低位优先
#   define _OBJC_TAG_MASK 1UL //Tagged Pointer 指针
#   define _OBJC_TAG_INDEX_SHIFT 1
#   define _OBJC_TAG_SLOT_SHIFT 0
#   define _OBJC_TAG_PAYLOAD_LSHIFT 0
#   define _OBJC_TAG_PAYLOAD_RSHIFT 4
#   define _OBJC_TAG_EXT_MASK 0xfUL
#   define _OBJC_TAG_EXT_INDEX_SHIFT 4
#   define _OBJC_TAG_EXT_SLOT_SHIFT 4
#   define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
#   define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#endif

我们以 1.4 节 中的NSNumber为例尝试计算一下:当存储整数0 时Tagged Pointer的值为0xb000000000000002!(以MSB 规则计算)

由于NSNumber为基础类,它的索引为 OBJC_TAG_NSNumber=3,所以 tag <= OBJC_TAG_Last60BitPayload

  • 1、_OBJC_TAG_MASK 取值 0x8000000000000000
  • 2、tag << _OBJC_TAG_INDEX_SHIFT 转为0x3 << 60,位运算结果是 0x3000000000000000;该运算的目的是将类的标识符存储在标志位里;
  • 3、_OBJC_TAG_MASK | ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) 转为 0x8000000000000000 | 0x3000000000000000 ,位运算结果是0xb000000000000000;该结果就是Tagged Pointer的标记;
  • 4、(value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT)
  • 5、先来说说(value << _OBJC_TAG_PAYLOAD_RSHIFT),将value左移 4 位,为何是 4 位?因为有 4 位标识符,最右边一位的1Tagged Pointer的标志,剩余三位是存储类的识别!
  • 6、再来说说>> _OBJC_TAG_PAYLOAD_LSHIFT),将第 5 步的位运算结果右移 _OBJC_TAG_PAYLOAD_LSHIFT
    遵循 MSB 时右移 4 位,因为4 位标识符在最左边,即最左边 4 位不能用来表示值!
    遵循 LSB 时右移 0 位,因为4 位标识符在最右边,即最右边 4 位不能用来表示值!
  • 7、第4步公式转为(0<<4)>>0计算结果是:0
  • 8、最终 result结果是 0xb000000000000000!!

疑问:程序运行的是 0xb000000000000002,但为何计算的是0xb000000000000000?该处问题笔者暂时没有找到答案,如果有大神知道为何不同,怎么计算,还望告知!!

b、编码与解码 Tagged Pointer 指针的函数
/* 编码 TaggedPointer 指针
 */
static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr){
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

/* 解码 TaggedPointer 指针
 * @param ptr 编码后的 TaggedPointer 指针
 */
static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr){
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

注:此处又出现了变量objc_debug_taggedpointer_obfuscator,在该处的取值为 0 ,至于作用,后文再讲!

通过一步步计算可知,Tagged Pointer指针上存储的数据我们完全能够计算出来,此时数据暴露在外,及其危险!苹果为了数据安全问题,设计了数据混淆!

2.5、Tagged Pointer的数据混淆

在 2.1.2 节的_read_images()函数中还遗留了 initializeTaggedPointerObfuscator()没有讲解,现在来说说它的作用。
Runtime 库objc-runtime-new.mm文件中找到该函数的实现:

static void initializeTaggedPointerObfuscator(void){
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) || DisableTaggedPointerObfuscation) {
        // 对于链接到旧sdk的应用程序,如果它们依赖于tagged pointer表示,将混淆器设置为0,
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // 将随机数据放入变量中,然后移走所有非净负荷位。
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

该函数的主要功能:

  • 如果SDK版本过低,或者禁用混淆,则设置 objc_debug_taggedpointer_obfuscator 为 0;
  • 否则为objc_debug_taggedpointer_obfuscator 设置一个随机数;

前文遗留的objc_debug_taggedpointer_obfuscator是什么,到此终于有了明确的定义:该变量就是一个随机生成的数字,通过编码函数、或者解码函数,用来与 value做位运算,这样控制台就看不出是否是Tagged Pointer 指针,该指针存储的数据也是安全的!

2.5.1、Tagged Pointer数据混淆功能的开启与关闭
OPTION( DisableTaggedPointerObfuscation, OBJC_DISABLE_TAG_OBFUSCATION,    "disable obfuscation of tagged pointers")

通过设置环境变量OBJC_DISABLE_TAG_OBFUSCATIONYES,可以关闭Tagged Pointer的数据混淆,方便我们调试程序!

2.5.2、objc_debug_taggedpointer_obfuscator的使用

除了上文的编码解码函数使用了objc_debug_taggedpointer_obfuscator来混淆指针,还有最前文提到的通过指定索引tag获取类指针时使用objc_debug_taggedpointer_obfuscator

2.6、获取Tagged Pointer的数据

前文讲解了如何存储Tagged Pointer的数据,如何加密Tagged Pointer的数据!现在我们来看下如何获取Tagged Pointer的数据!

Runtime 库objc-internal.h文件中找到相关函数的实现:

/* 获取 Tagged Pointer 指针上存储的数据
 * @note 存储的数据是 zero-extended
 * @note 前提条件:假设启用了 tagged pointer 功能
 */
static inline uintptr_t _objc_getTaggedPointerValue(const void * _Nullable ptr){
    // assert(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer(ptr);// 解码 TaggedPointer 指针
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
}

/* 获取 Tagged Pointer 指针上存储的数据
 * @note 存储的数据是 sign-extended
 * @note 前提条件:假设启用了 tagged pointer 功能
 */
static inline intptr_t _objc_getTaggedPointerSignedValue(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;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
    } else {
        return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
    }
}

2.7、Tagged Pointer其他函数

/* 获取一个 Tagged Pointer 指针的索引
 * @param ptr 指定的指针
 */
static inline objc_tag_index_t _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;
    }
}

/* 根据指定的索引获取 Tagged Pointer 表示的类
 * @param tag 指定的索引
 * @return 如果该索引还没有使用 或 该索引超出范围,则返回nil。
 */
Class _objc_getClassForTag(objc_tag_index_t tag){
    Class *slot = classSlotForTagIndex(tag);
    if (slot) return *slot;
    else return nil;
}

至此,关于 伪指针Tagged Pointer,笔者已经讲完!

七 自动释放池的结构和工作原理、autorelease与引用计数

http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

八 内存对齐

https://www.jianshu.com/p/3294668e2d8c

九 copy及mutablecopy

首先来说,copy及mutablecopy/alloc/new都会获取对象所有权进行引用计数的+1。
两者的区别是

深拷贝:对象拷贝 -> 直接拷贝内容。
浅拷贝:指针拷贝 -> 将指针中的地址值拷贝一份。
  • 不可变对象(NSString、NSArray)的copy都是浅拷贝;
  • 可变对象的(NSMutableString、NSMutableArray) copy都是深拷贝;
  • 无论可变对象还是不可变对象的 mutableCopy都是深拷贝;
  • 自定义对象需要实现NSCopying协议

参考资料:
1: https://juejin.cn/post/6844904030246797319
2: https://halfrost.com/objc_runtime_isa_class/
3: https://www.jianshu.com/p/3176e30c040b

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,841评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,415评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,904评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,051评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,055评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,255评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,729评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,377评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,517评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,420评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,467评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,144评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,735评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,812评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,029评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,528评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,126评论 2 341

推荐阅读更多精彩内容