OC对象原理之对象的本质

1,311 阅读11分钟

前两篇文章重点分析了OC创建对象时的alloc流程分析内存对齐,这篇文章继续探索OC创建对象的过程中是如何与类做关联的?对象的本质是什么?

基础知识介绍

  • 在分析重点过程前,我们先一起了解下一些基础知识,这更有利于我们接下来的探索和分析

联合体

  • 联合体(union):也可以叫做共用体
  • 在前面的文章我们介绍过结构体的内存对齐,这里不熟悉的可以参考前面的文章(OC对象原理之内存对齐)
  • 联合体的语法和结构体非常类似,但是它们占用内存的情况却有很大的不同,主要表现在:
    • 结构体的成员之间是共存的:各个成员占用不同的内存,它们互相之间没有影响。
    • 联合体的成员之间是互斥的:所有成员共用同一段内存,修改一个成员的值,会影响其余所有成员。
    • 结构体占用的内存:大于等于所有成员占用内存的总和(需要内存对齐)
    • 联合体占用的内存:等于最大的成员占用的内存,同一时刻只能保存一个成员的值
  • 示例:
union LGUnion {
    char    a;
    int     b;
    double  c;
} union1;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    union1.a = 'a';
    union1.b = 10;
    union1.c = 5.5;
}
  • 未赋值时内存情况:
    • p union1:输出union1的值
    • p/t union1:以二进制形式输出union1的值

image.png

  • a赋值后内存情况:

image.png

  • 对b赋值后内存情况:

image.png

  • 对c赋值后内存情况:

image.png

  • 总结: 从上面的示例可以看出,联合体union1占用的内存大小即为最大成员double c占用的内存大小,占8字节,成员char a占总内存的低8位,成员int b占总内存的低32位;

image.png

位域

  • 有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
  • 定义:在定义结构体或联合体时,成员变量后面加: 数字,用来限定成员变量占用的位数,这就是位域;例如:
struct LGScruct {
    bool a: 1;
    bool b: 1;
    bool c: 1;
    bool d: 1;
}struct1;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    struct1.a = 1;
    struct1.b = 0;
    struct1.c = 1;
    struct1.d = 1;
    
    NSLog(@"%ld", sizeof(struct LGScruct));
}

image.png

  • 从上面的打印结果可以看出:
    • 首先打印的是结构体变量struct1占用的内存空间大小,为1字节;如果没有位域的限制,此结构的大小为4字节
    • x/1bt &struct1:以单字节二进制形式打印变量struct1内存地址中存放的数据
    • 低4位(从低到高,即从右到左)分别对应的是abcd的值;共占4个二进制位,即0.5个字节大小,再进行结构体的内存对齐,总大小为最大成员变量的整数倍,即bool类型(占1字节)的整数倍,为1字节;
  • 位域还有一些其他规则:(这个比较简单,有兴趣的同学可以自己验证一下)
    • 位域的宽度不能超过它所依附的数据类型的宽度,否则编译时会报错;
    • 当相邻的成员类型相同时,如果他们的位宽之和小于类型的sizeof大小,那么后面的成员紧邻前面的成员存储,否则从新的存储单元开始存储,偏移为类型大小的整数倍。

Clang介绍

  • ClangC语言、C++Objective-C语言的轻量级编译器
  • Clang可以将目标文件编译成C++文件:clang -rewrite-objc mian.m -o main.cpp
  • Clang编译UIKit报错(fatal error: 'UIKit/UIKit.h' file not found)问题解决办法:
    • 指定SDK路径:clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m -o main.cpp
    • 使用xcrun
      • 模拟器:xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m -o main-arm64.cpp
      • 手机:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

Clang分析对象的本质

  • main.m定义:
#import <Foundation/Foundation.h>

@interface LGPerson : NSObject
@property (nonatomic) NSString *lgName;
@end

@implementation LGPerson
@end

int main(int argc, const char * argv[]) {
    LGPerson *p = [LGPerson alloc];
    NSLog(@"p:%@", p);
    return 0;
}
  • 使用clangmain.m编译成main.cppclang -rewrite-objc main.m -o main.cpp
  • main.cpp中关键代码:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_lgName;
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_lgName;
};

// @property (nonatomic) NSString *lgName;
/* @end */


// @implementation LGPerson

static NSString * _I_LGPerson_lgName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_lgName)); }
static void _I_LGPerson_setLgName_(LGPerson * self, SEL _cmd, NSString *lgName) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_lgName)) = lgName; }
// @end

int main(int argc, const char * argv[]) {
    LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_1d__07nngks0bl536_p0nvqhjkm0000gn_T_main_06b9c6_mi_0, p);
    return 0;
}
  • 可以看到,LGPerson实际上是struct objc_object类型,objc_object的定义如下,默认就有一个Class类型的isa变量
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};
  • LGPerson的实现struct LGPerson_IMPL中也可以看出,第一个成员变量为struct NSObject_IMPL类型,定义如下:
struct NSObject_IMPL {
	Class isa;
};
  • LGPerson_IMPL中的另一个成员变量即为我们自定义的属性lgNameNSString *_lgName;
  • OC中的属性会自动生成对应的gettersetter方法,在这里对应的分别是_I_LGPerson_lgName_I_LGPerson_setLgName_
    • 这两个方法的前两个参数都是self_cmd,这两个参数是默认的隐式参数
    • getter方法和setter方法中都是是通过self + OBJC_IVAR_$_LGPerson$_lgName去获取或改变变量lgName的值的,OBJC_IVAR_$_LGPerson$_lgName的定义:extern"C"unsignedlongintOBJC_IVAR_$_LGPerson$_lgName__attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct LGPerson, _lgName);,这里是通过位移找到变量的内存地址,然后进行取值或赋值的
  • 总结:
    • 类的定义在底层会变编译成结构体,第一个成员变量为isa;
    • 对象的本质struct objc_object类型的结构体指针;
    • OC定义的属性,会自动生成成员变量、getter方法和setter方法;

objc源码分析isa初始化流程

  • 前两篇文章介绍到了创建对象最终调用的方法为_class_createInstanceFromZone,其中初始化isa会调用obj->initInstanceIsa(cls, hasCxxDtor);obj->initIsa(cls);
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}
  • 接下来就探究一下初始化isa过程做了什么

动态调试

  • initInstanceIsainitIsa分别添加断点,运行调试
  • obj赋值前打印:

image.png

  • obj赋值后打印:

image.png

  • 从两次的打印结果可以看出,obj赋值后,内存地址0x000000010065dc30的类型从id变成了LGPerson *,即此过程中内存地址和类LGPerson做了关联,所以可以打印出内存地址对应的对象类型为LGPerson *

调用流程分析

  • 不同的架构处理调用的流程可能会有所差别,但是最终都会调用到initInstanceIsainitIsa中,而这两个方法最终都会调用objc_object::initIsa,具体流程如下:

image.png

  • 从上述流程可以看出最终都会调用objc_object::initIsa,并在此方法中对isa进行赋值,下面重点分析下该方法
  • 在分析该方法前,先看下这里涉及到的几个重点知识:
    • ISA_BITFIELD
    • isa_t

ISA_BITFIELD分析

  • 这里进行了架构的判断,不同架构对应的宏定义是不一样的,调试objc源码过程中,是运行在MacOS系统上,使用的是x86_64架构,下面是对应的定义:(其他架构的定义类似,在补充中会详细分析)
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      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 unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

  • x86_64架构中各个字段含义:
    • nonpointer:表示是否对isa指针开启指针优化;0:纯isa指针,1:isa中包含了类信息、对象的引用计数等信息;
    • has_assoc:关联对象标志;0:没有,1:存在
    • has_cxx_dtor:该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
    • shiftcls:存储类指针的值
    • magic:用于调试器判断当前对象是真的对象,还是没有初始化的空间
    • weakly_referenced:标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放
    • unused:标志是否未被使用过
    • has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
    • extra_rc:表示该对象的引用计数值,实际上是引用计数值减1。 例如,如果对象的引用计数为10,那么extra_rc为9。如果引用计数大于10, 则需要使用到has_sidetable_rc

isa_t分析

  • isa_t定义:
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};
  • 从上面的源码可以看出:
    • isa_t是一个联合体,而我们前面介绍过了,联合体的成员变量存储是互斥的,成员变量bits和结构体使用同一块内存;
    • 最上面是两个构造函数,第一个构造函数没有做任何处理,第二个构造函数使用传入的参数value对成员变量bits进行赋值;
    • 下面结构体中的ISA_BITFIELD为宏定义,实际上这里使用了位域,此结构体和成员变量bits共用同一块内存空间;

objc_object::initIsa分析

  • objc_object::initIsa实现:
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    isa = newisa;
}
  • 查看isa变量类型:
struct objc_object {
private:
    isa_t isa;
   	// ...省略
}
  • 这里的isa就是isa_t类型的变量,isaobjc_object结构体的第一个成员,而类在底层编译时会自动转化成struct objc_object类型
  • 这里对isa赋值使用的是变量newisa
  • 从最上面可以看出,newisaisa_t类型,newisa(0)即为调用isa_t中的第二个构造函数进行初始化,这里赋值为0的作用是对isa_t中的成员变量bits进行赋值,避免脏数据的影响。
  • 判断是否是nonpointer,即非纯isa指针:
    • 否:调用newisa.setClass(cls, this);,直接赋值
    • 是:开启isa指针优化,对isa指针中的bits和位域对应的变量赋值;

总结

  • isaisa_t联合体类型,他们的成员bits和结构体位域共用同一块内存
  • isa进行初始化时,先判断是否是nonpointer,即是否开启了指针优化
    • 否:直接赋值
    • 是:对联合体中的位域进行赋值

补充:ISA_BITFIELD分析

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                      \
        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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      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 unused            : 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
  • 从上面的源码中可以看出,这里首先对架构进行了判断,主要分为arm64x86_64两种架构
  • x86_64架构中的内容很清晰,前面也已经分析过了,下面重点看下arm64架构的处理
  • arm64架构:
    • 这里又做了条件判断,主要是__has_feature(ptrauth_calls)TARGET_OS_SIMULATOR
    • 其中TARGET_OS_SIMULATOR是模拟器,在这里代表arm64架构的模拟器设备
    • 那么__has_feature(ptrauth_calls)是什么呢?
    • 下面重点介绍下__has_feature(ptrauth_calls)的作用

__has_feature(ptrauth_calls)介绍

  • __has_feature:此函数的功能是判断编译器是否支持某个功能
  • ptrauth_calls:指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XSiPhone XS MaxiPhone XR或更新的设备)支持arm64e架构
  • 参考链接:developer.apple.com/documentati…
  • 下面分别使用真机iPhone 12iPhone 8进行验证

验证方法一:通过isa存储数据验证

  • 因为arm64架构中if分支和else分支存储的数据结构是不一样的,这里根据weakly_referenced值做验证
  • 测试代码
LGPerson *p = [LGPerson alloc];
__weak typeof(p) weakP = p;
NSLog(@"p:%@", p);
  • __weak typeof(p) weakP = p;处添加断点,这行代码执行前weakly_referenced的值为0,执行后会变为1

  • 测试iPhone 8

    • 执行前

    image.png

    • 执行后:

    image.png

  • 测试iPhone 12

    • 执行前

    image.png

    • 执行后

    image.png

  • 验证结果: 从上面两个设备的验证结果可以看出,执行__weak typeof(p) weakP = p;前后,iPhone 8修改的是第43位(从右往左),而iPhone 12修改的是第3位(从右往左),分别对应的是arm64架构中的else分支和if分支的存储结构

验证方法二:通过断点和汇编验证

  • [LGPerson alloc]处设置断点
  • 执行到断点处后,添加符号断点_objc_rootAllocWithZone,并继续运行
  • iPhone 8

image.png

  • iPhone 12

image.png

  • 验证结果: iPhone 8中使用了0xffffffff8iPhone 12中使用了0x7ffffffffffff8,在objc源码的isa.h中搜索发现,这两个值分别对应的是arm64架构中else分支的ISA_MASK值和if分支的ISA_MASK

总结

  • __has_feature(ptrauth_calls)的作用是判断编译器是否支持指针身份验证功能

  • iPhone X系列以上的设备(arm64e架构,使用Apple A12或更高版本A系列处理器的设备)支持指针身份验证

  • 对于arm64架构

    • iPhone X系列以上(包含)设备使用如下结构:
    #     define ISA_MASK        0x007ffffffffffff8ULL
    #     define ISA_MAGIC_MASK  0x0000000000000001ULL
    #     define ISA_MAGIC_VALUE 0x0000000000000001ULL
    #     define ISA_HAS_CXX_DTOR_BIT 0
    #     define ISA_BITFIELD                                                      \
            uintptr_t nonpointer        : 1;                                       \
            uintptr_t has_assoc         : 1;                                       \
            uintptr_t weakly_referenced : 1;                                       \
            uintptr_t shiftcls_and_sig  : 52;                                      \
            uintptr_t has_sidetable_rc  : 1;                                       \
            uintptr_t extra_rc          : 8
    #     define RC_ONE   (1ULL<<56)
    #     define RC_HALF  (1ULL<<7)
    
    • iPhone X以下(不包含)设备使用如下结构:
    #     define ISA_MASK        0x0000000ffffffff8ULL
    #     define ISA_MAGIC_MASK  0x000003f000000001ULL
    #     define ISA_MAGIC_VALUE 0x000001a000000001ULL
    #     define ISA_HAS_CXX_DTOR_BIT 1
    #     define ISA_BITFIELD                                                      \
            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 unused            : 1;                                       \
            uintptr_t has_sidetable_rc  : 1;                                       \
            uintptr_t extra_rc          : 19
    #     define RC_ONE   (1ULL<<45)
    #     define RC_HALF  (1ULL<<18)