OC底层原理八:剖析isa & clang的使用

OC底层原理 学习大纲

对象的本质

1. Clang探索

  • Clang 是一个由Apple主导编写,基于LLVMC/C++/Objective-C轻量级编译器。源代码发布于LLVM BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
  • 它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC

2. 操作指令:

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

3. 探索

  • 构建测试代码
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 打开终端cdmain.m的文件夹。

  • 输入clang指令: clang -rewrite-objc main.m -o main.cpp 将objc语言的main.m文件重写为cpp格式的文件

    image.png

  • 忽略warnings警告,查看main.m文件夹。发现已生成main.cpp文件
    cpp的意思是 c plus plus

    image.png

  • 打开main.cpp文件。 内容很长。我们搜索自定义类HTPerson

image.png
  • 可以发现,对象在底层已经编译成struct结构体
  • _I_HTPerson_name是属性name的get方法
  • _I_HTPerson_setName_是属性name的set方法。 set方法内调用objc_setProperty方法。

同时我们发现,HTPerson_IMPL struct 中有一个NSObject_IMPL struct。 这就是表明HTPerson继承自NSObject

不相信?

我们在main.m中创建一个HTCar类继承自HTPerson

@interface HTCar : HTPerson
@end

@implementation HTCar
@end

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

使用clangmain.m编译为main.cpp

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

main.cpp中搜索HTCar:

image.png

服不服? 😼

结论

  • OC结构体没有继承关系,但是CC++语言中,结构体是有继承关系的。

  • OC对象在底层被C语言编程成结构体。而C语言结构体的继承方式,是每一个结构体第一个属性都包含父结构体的所有信息。如此,实现了OC类的继承关系

  • OC对象的本质就是结构体,结构体中包含了所有属性方法和父类信息

4. 探究属性get、set方法

  • 对象核心功能就是信息的存取,即getset方法。

  • 我们没有独立实现getset方法,工程中也找不到属性的getset方法,但这2个方法为什么可以直接使用呢?

其实上面已经有答案了。对象在底层将属性都进行了记录。并自动实现了他们的getset方法。

image.png
  • get方法我们看懂,就是直接访问指针地址,返回指针地址的值

  • set方法调用了objc_setProperty函数进行赋值。

我们打开objc4源码。搜索objc_setProperty

image.png
  • 点击进入内部,发现所有objc_setProperty方法都调用了reallySetProperty

    image.png

  • 所有值变更(set),都是reallySetProperty在处理。(同时管理引用计数)

image.png

所有外层属性的set方法。都会来到objc_setProperty方法,调用了reallySetProperty实现set功能。

  • 外部set方法: 个性化定制层(例如setName、setAge等)
  • objc_setProperty:接口隔离层 (将外界信息转化为对内存地址和值的操作)
  • reallySetProperty:底层实现层 (赋值和内存管理)

这是一个典型的封装设计思维。


isa

  • oc对象的本质是结构体,我们在main.cpp的文件中了解到。HTPerson_IMPL继承自NSObject_IMPL

  • 我们搜索struct NSObject_IMPL

image.png
  • 发现NSObject对象在底层就是编译为 isa, 其类型为Class

到这里,我们可以肯定对象在底层,是通过继承isa来继承父类信息

现在,让我们揭开isa的神秘面纱。

1. union联合体位域

首先了解union联合体位域,isa的类型结构就是union。

小案例
如果我们创建Car对象,我们需要控制它的前后左右4个方向。我们可以这样定义:

@interface Car : NSObject

@property(nonatomic, assign) BOOL front;   // 2字节
@property(nonatomic, assign) BOOL back;   // 2字节
@property(nonatomic, assign) BOOL left;    // 2字节
@property(nonatomic, assign) BOOL right;    // 2字节

@end
  • BOOL类型占用2字节, 每个字节是8位(Bit)。
  • Car对象所有属性占用内存大小: 4属性 * 2字节 * 8位 = 64位

系统层面,我们会考虑极致的性能。用4位就实现前后左右的处理,每1位记录一个方向的信息。极大的节约内存空间

image.png

  • 2位更节省,每1位可记录2个信息.
  • 但使用4位存储,每1位独立记录1个信息。可以使用位运算来高效处理,在性能上更有优势)
    image.png

这就是我们要介绍的union联合体位域。

image.png
  • 结构体的类型大小大于等于内部所有变量的类型大小总和(参考结构体内存优化)
  • 联合体类型大小等于最大成员类型大小
  • 位域: 每一个二进制位均表示不同信息

2. isa结构

我们在objc4源码中找到initIsa

image.png

发现isa的赋值是isa_t结构,进入查看:

image.png

发现isa_t就是使用的union联合体结构。

通常来说,isa指针占用内存大小是8字节,即64位。对于系统来说已经足够了。

  • 我们知道union联合体内部属性是互斥关系。 所以clsbits不共存。

进入ISA_BITFILED宏定义,可以看到isa全部结构。 庐山真面目揭开了。

isa结构图
  • nonpointer: 表示是否对 isa 指针开启 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等

  • has_assoc: 关联对象标志位,0没有,1存在

  • has_cxx_dtor: 该对象是否有 C++Objc的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象

  • magic:用于调试器判断当前对象是真对象还是未初始化的空间

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

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

  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位

  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。
    如: 如果对象的引用计数为 10,那么extra_rc 为 9。如果引用计数大于 10, 则需要使用到has_sidetable_rc

  • isa中最重要的是shiftcls,它存储了类指针的值。但是除了这个信息,isa还存储了很多其他标志性信息。

现在我们了解了isa的结构,让我们运行objc4源码来完整了解信息

3. 检验isa

  • main.m中测试代码,在HTPerson初始化一行加入断点
#import "HTPerson.h"

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

进入alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone-> _class_createInstanceFromZone,加入断点:

image.png

继续进入initInstanceIsa->initIsa,加断点:

image.png

需要确保clsHTPerson

SUPPORT_INDEXED_ISA宏定义:

image.png

我们现在是电脑端,所以是这个条件为false,宏的值为0

  • isa_t是上面讲到的isa默认初始化的方法

  • 所以isa_t newisa(0)后,newisa已经完成默认初始化,但还未赋值,我们打印p newisa

    image.png

  • 进入ISA_MAGIC_VALUE宏,看到

    image.png

  • 继续断点,打印:


    image.png
  • 为何magci是59?
    打开计算器,将显示改为编程器,选择16进制,粘贴cls的地址

    image.png

我们看到1 1101 1

image

  • 将计算器改为10进制,输入59,看二进制结果:
    image.png

这个59是在默认值中设定的。

  • 接着往下走,断点设在shiftcls后一行,打印:

    image.png

  • 此时,我们已将HTPerson类信息完整的存到isashiftcls中。isaHTPerson完成绑定。

为何要右移3位?
因为 (uintptr_t)cls是将cls初始化为uintptr_t格式。但是初始化时,前3位是标记符,shiftcls是从第四位才开始。所以要移除前三位。

image.png

我回到上一层_class_createInstanceFromZone,加断点。继续走。

image.png

  • 打印 objx/4gx查看地址信息。 首地址就是isa
  • 我们取isa地址。按照isa的初始化格式,我们&mask偏移值(查上面isa结构图)。就得到了shiftcls
  • shiftcls存储的就是类信息。 所以直接打印出了类信息。
    image.png
  • 当我们对isa的结构完全熟悉后。就能理解为什么首地址符有时候打印不出类名了

  • 因为标记符可能存在数据,影响了地址的读取。类的信息只存储在isa的shiftcls中。

  • 我们可以手动左移右移,将前3后17位置的信息全部移除。这样就可以直接读取了。

image.png

拓展

runtime运行时object_getClass(perosn)返回的也是isa地址。

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

梳理了一份👉 【对象、类、isa 的逻辑关系】

拓展答疑:

  • 属性修饰符strongweakretaincopyassign:
    clang编译文件,打开cpp文件,可以发现:
  1. retaincopy都是调用了objc_setProperty。 不同的是objc_setProperty内部实现不同
    (详看objc4源码中的objc_setProperty代码)
  • copymutableCopy:是新开辟空间,旧值release;
  • 其他修饰类型:是新值retain,旧值release。
  1. strongassign类型都是直接使用地址进行赋值(通过对象地址偏移相应字节找到属性地址)

  2. 如果在set方法后加入断点,可以在汇编层看到所有属性赋值后,会调用objc_storeStrong

image.png

image.png

运行代码,进入断点,可以看到:
image.png

(在所有赋值完成后,objc_storeStrong在最后执行一次)

  • objc4源码中查看objc_storeStrong代码。可以发现它内部就是对对象进行了retainrelease
    image.png

下一节:OC底层原理九:类的原理分析

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