003-OC对象原理探究(下)

003-OC对象原理探究(下)

IOS小彩虹2021-08-24 22:10:56270A+A-

通过这篇文章可以获得什么:

什么是对象?

探究方式:

在底层探索的过程中,由于Objective-c语言是C语言和C++的超集,那么可以通过clang轻量级编译器的代码还原,还原OC代码的基层实现,或者说用来查看OC代码的底层结构以及过程结构。clang可以将.m文件编译成.cpp文件。

Clang与xcrun

什么是Clang:

Clang是C语言、C++、Objective-c语言的轻量编译器。源代码发布于BSD协议下。 Clang将支持lambad表达式,返回类型的简单处理以及更好的处理constexpr关键字。 Clang有Apple主导编写,基于LLVM的C/C++/Objective-c编译器

什么是xcrun

Xcode安装的时候一起安装了xcrun命令,xcrunclang的基础上进行了封装,使用更加方便

准备阶段:

将main.m编译成main.cpp文件

clang编译:

clang -rewrite-objc main.m -o main.cpp

会出现的问题:

In file included from ViewController.m:9:
./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
1 error generated.

解决方法:

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

xcrun编译:

//模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o 
main-arm64.cpp

//真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o 
mainarm64.cpp

分析阶段:

main.m文件源码,声明了一个FFPerson的class:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface FFPerson : NSObject
@property (nonatomic, strong) NSString *bblvName;

@end

@implementation FFPerson

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

main.cpp文件:main.cpp

关注点一struct:在main.cpp文件看到FFPerson对应的地方:

struct FFPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_bblvName;
};

结论一:针对上面声明与.cpp文件源码的对比,可以得出结论对象在底层的本质就是结构体

关注点二isa:在FFPerson_IMPL结构体中看到了struct NSObject_IMPL NSObject_IVARS;在.cpp文件中查找NSObject_IMPL

struct NSObject_IMPL {
	Class isa;
};

结论二:NSObject_IVARS为成员变量isa

关注点三objc_object:FFPerson继承的是NSObject,但是在.cpp文件中确是objc_object类型

typedef struct objc_object NSObject;

结论三:NSObject对象在底层的对象是objc_object

关注点四Class:通过类比关注点三,在cpp文件中搜索objc_class

typedef struct objc_class *Class;

结论四:我们通常声明一个类时使用的Class的类型是objc_class *,是一个结构体指针

关注点五:id类型,通常代表任何类型的id是什么

typedef struct objc_object *id;

结论五:id的类型是objc_object *,所以可以定义任何类型的变量

关注点六getter setter 隐藏参数:在.cpp文件中查找FFPersonget set方法

//getter
static NSString * _I_FFPerson_bblvName(FFPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_FFPerson$_bblvName)); }
//setter
static void _I_FFPerson_setBblvName_(FFPerson * self, SEL _cmd, NSString *bblvName) { (*(NSString **)((char *)self + OBJC_IVAR_$_FFPerson$_bblvName)) = bblvName; }

结论六:在Get与Set方法中看到了两组参数FFPserson * selfSEL _cmd这是隐藏参数,我们通常创建的方法默认携带这两个参数,这也就是问什么我们在每一个方法里面都可以使用self的原因

那么关于get、set方法是如何在内存中找到对一个的值的呢?

结构体、联合体、位域

案例一(struct):

#import <Foundation/Foundation.h>

struct FFPerson {
    bool left;
    bool right;
    bool front;
    bool back;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct FFPerson ffPerson;
        NSLog(@"---%lu---",sizeof(ffPerson));
    }
    return 0;
}

打印sizeof结果为:ffPerson占用4字节空间

2021-06-12 19:46:51.463279+0800 001-联合体位域[98512:2394618] ---4---

案例二(struct):

struct FFPerson {
    bool left: 1;
    bool right: 1;
    bool front: 1;
    bool back: 1;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct FFPerson ffPerson;
        NSLog(@"---%lu---",sizeof(ffPerson));
    }
    return 0;
}

打印sizeof结果为:ffPerson占用1字节空间

2021-06-12 19:51:51.061432+0800 001-联合体位域[98541:2398442] ---1---

案例三(struct):

struct FFPerson {
    NSString * name;
    int age;
    float height;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct FFPerson ffPerson;
        ffPerson.name = @"BBlv";
        ffPerson.age = 27;
        ffPerson.height = 180.0;
        
        NSLog(@"---%lu---",sizeof(ffPerson));
    }
    return 0;
}

对ffPerson的赋值过程ffPerson.name = @"BBLv"ffPerson.age设置了断点,分别打印了在当下状态下的ffPerson的信息

(lldb) p ffPerson
(FFPerson) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p ffPerson
(FFPerson) $1 = (name = @"BBlv", age = 0, height = 0)
(lldb) p ffPerson
(FFPerson) $2 = (name = @"BBlv", age = 27, height = 0)
2021-06-12 20:14:28.321207+0800 001-联合体位域[98575:2402057] ---16---

图解补充:

结构体存储过程.png 案例四(union):

union FFPerson {
    char * name;
    int age;
    float height;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        union FFPerson ffPerson;
        ffPerson.name = "BBlv";
        ffPerson.age = 27;
        ffPerson.height = 180.0;
        
        NSLog(@"---%lu---",sizeof(ffPerson));
    }
    return 0;
}

对联合体ffPerson对象的赋值过程ffPerson.nameffPerson.age设置了断点,分别打印了在当下状态下的ffPerson的信息

(lldb) p ffPerson
(FFPerson) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p ffPerson
(FFPerson) $1 = (name = "BBlv", age = 16284, height = 2.28187442E-41)
(lldb) p ffPerson
(FFPerson) $2 = (name = "", age = 27, height = 3.78350585E-44)
(lldb) 

图解补充:

联合体存储过程.png

联合体位域Demo

结论:

  1. 通过案例一结构体内声明了4个bool类型,占用4字节内存
  2. 通过案例二对结构体内声明的4个bool类型进行指定位域,指定每一个bool类型的成员变量使用1bit的内存空间,那么4个bool类型最终占用4bit空间,即0.5字节的空间,最终占用了1字节内存,为对象指定位域是内存优化的方式
  3. 通过案例三与案例四,也就是structunion的区别,struct内成员变量的存储互不影响union内的对象存储是互斥
  4. 结构体(struct)中所有的变量是共存的,优点是可以存储所有的对象的值,比较全面。缺点是struct内存空间分配是粗放的,不管是否被使用,全部分配
  5. 联合体(union)中所有的变量是互斥的,优点是内存使用更加精细灵活,也节省了内存空间,缺点也很明显,就是不够包容

nonpointerIsa初探

如何找到isa?

找到isa指针内存了什么

通过宏定义ISA_BITFIELD找到了isa指针内都存放了什么,分为两种模式 arm64:

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;       //C++的析构函数                               \
        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)

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;       //C++的析构函数                                \
      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)

图形分析:

isa的64bit-001.jpeg isa的64bit-002.jpeg isa的64bit-003.jpeg isa的64bit-004.jpeg

isa的位运算,还原类信息

案例代码:

#import <Foundation/Foundation.h>
#import "FFPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        FFPerson * person = [FFPerson alloc];
        NSLog(@"%@",person);
        
    }
    return 0;
}

位运算测试结果:

(lldb) x/4gx person
0x10064d420: 0x011d8001000080e9 0x0000000000000000
0x10064d430: 0x566265574b575b2d 0x74696e6920776569
(lldb) p 0x011d8001000080e9 >> 3
(long) $1 = 10045138768236573
(lldb) p/x 0x011d8001000080e9 >> 3
(long) $2 = 0x0023b0002000101d
(lldb) p/x 0x0023b0002000101d << 20
(long) $3 = 0x0002000101d00000
(lldb) p/x 0x0002000101d00000 >> 17
(long) $4 = 0x00000001000080e8
(lldb) po 0x00000001000080e8
FFPerson
(lldb) 

图形分析:

isa的位运算还原FFPerson.png

测试所用架构为x86_64的架构,初始化的对象的isa的bit位信息第3号标志位至47号标志位为FFPerson的信息,还原方式为:

  1. 通过x/4gx person,格式化输出person对象的内存地址,首地址为isa指针0x011d8001000080e9
  2. isa的前三个bit位移除,即0x011d8001000080e9向右移3位,通过p/X 得到新的isa指针0x0023b0002000101d
  3. isa指针的后17个bit位移除,由于刚刚向右移了3个bit位,那么现在需要向左移20个bit位,即0x0002000101d00000向左移17+3位,通过p/x 得到新的isa指针0x0002000101d00000
  4. 最后将isa内代表FFPerson信息的33个bit位还原,将0x0002000101d00000向右移17个bit位
  5. 最后输出的结果为FFPerson

isa的位运算Demo

补充:

getter方法图解

getter方法.jpeg

setter方法图解

setter方法.jpeg

如何找到Isa

在对象alloc过程中执行_class_createInstanceFromZone方法中,会执行initIsa方法将objclass进行绑定,这里删除了跟isa部分无关的代码,只保留了isa相关的代码

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)
{
    //移除跟isa部分无关代码......
    
    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);
    }
    
    //移除跟isa部分无关代码......
}

然后可能进入initInstanceIsa函数或者initIsa函数,但是initInstanceIsa函数执行后依然会进入到initIsa

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

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);

    //删除了部分无关代码
    
    isa = newisa;
}

可以看到源码内声明isa的类型是isa_t,而isa_t的类型是联合体(union)

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查找流程.png

只此一生,必须热情

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示 | 支付宝红包
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们