前言
之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了
iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。因此我们决定
进一步探究iOS底层原理的任务,本文探索的底层原理围绕“OC对象的本质【底层实现、内存布局、继承关系”展开
一、概述
我们在日常开发中得知,OC语言中,所有的类都继承自 NSObject类。
那么,我们要探索OC对象的本质,首先就要围绕NSObject,通过几个维度入手:
-
- NSObject的底层实现
-
- 通过NSObject的继承关系,去进一步了解其NSObject对象的本质
二、NSObject的底层实现
1. 通过 Clang 转 源码格式查看
我们在上一篇文章:OC的本质 中得出了结论:
我们平时编写的Objective-C代码,底层实现其实都是C、C++代码、asm汇编代码,所以Objective-C的面向对象 都是基于C\C++的数据结构实现的
因此,我们可以写一份最简单的命令行项目,通过Clang编译器把源码文件 转成 C++的格式,进行查看,其底层的实现。
命令行:
格式:
clang -rewrite-objc OC源文件 -o 输出的CPP文件
clang -rewrite-objc main.m -o main.mm
我们打开代码可以看见:
NSObject的底层是一个结构体类型,内部有一个Class类型的成员名叫isa:
struct NSObject_IMPL {
Class isa;
};
2.查看源码
我们在上一篇文章:OC的本质 中了解到了,苹果官方已经对OC语言的底层实现有了一部分的开源!!
因此,我们可以通过阅读源码的形式了解NSObject的底层实现:
我们可以通过.h文件看到,NSObject内部确实只有一个Class类型的isa成员,其余皆是一些方法。
若我们了解内存分配就知道,我们探索一个类的内部结构的内存布局情况,关注其成员对象即可,不需要关注其方法(方法存储在公共内存位置,提供给所有的实例对象使用、类对象使用)。
因此,我们进一步去看看Class是什么东西即可:
我们查看源码得知,Class本质是一个 objc_class 类型的结构体指针
typedef struct objc_class *Class;
我们进一步去看一下 objc_class这个结构体:
我们发现
objc_class结构体继承自objc_object结构体,且objc_class内部有若干成员如下(忽略其函数、方法):
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
....
}
我们跳进去查看 objc_object发现,其本身也只有一个 isa_t类型的成员
struct objc_object {
private:
isa_t isa;
....
}
最终 isa_t类型的成员是一个联合体
3.总结
通过前面的介绍,我们可以将NSObject的定义简写为:
目前官方暴露的头文件中的格式:
@interface NSObject <NSObject> {
Class isa ;
}
@end
简写格式:
@interface NSObject <NSObject> {
objc_class isa ;
}
@end
结构体objc_class的实现:
struct objc_class {
private:
isa_t isa;
public:
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
....
}
OC对象的本质
- Objective-C的对象、类主要是基于C\C++的结构体实现的
- 凡是继承自NSObject的对象,都会自带一个类型是
Class的isa的成员变量-
将其转成C++,就可以看到NSObject本质上是一个叫做
NSObject_IMPL的结构体 -
其成员变量isa本质上也是一个指向
objc_class结构体的指针(objc_class继承自objc_object结构体,内部有一个isa成员)
-
4.其它|将Objective-C代码转换为C\C++代码
通过下面的命令可以将OC代码转换为C++代码来查看
clang -rewrite-objc OC源文件 -o 输出的CPP文件由于Clang会根据不同平台转换的C++代码有所差异,所以针对iOS平台用下面的命令来转换- 如果需要链接其他框架,使用-framework参数。比如-framework UIKit
// 意为:通过Xcode运行iPhone平台arm64架构,重写OC文件到C++文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
三、NSObject的内存布局
在iOS中查看对象内存布局的几种方式:
-
- 通过 lldb命令(若是同学们不熟悉lldb,可以参考我这篇文章入门:LLDB【命令结构、查询命令、断点设置、流程控制、模块查询、内存读写、chisel插件】
-
- 通过
Debug -> Debug Workfllow -> View Memory(Control+Option+Shift + Command + M)
- 通过
-
- 通过 底层函数API
1.通过 lldb命令 窥探NSObject内存布局
- 1、添加断点
- 2、打印内存地址: 通过
po命令打印出 对象的内存地址 - 3、打印内存布局: 通过
memory read + 内存地址值命令打印出 对象的内存布局情况:
- 我们从打印结果中可以得出结论:
一个NSObject对象,系统给其分配了16个字节
2.通过 View Memory 窥探NSObject内存布局
- 从截图上我们可以看到,地址
101323a20与101323a4F之间差48,刚好显示差了一行 - 而
101323a20的整个存储空间为绿色框框出来的一部分,蓝色框为101323a30开始的部分了 - 从内存布局中我们看到,
NSObject中有十六个字节,但是只用了八个字节来存储内容。与前面的方式是得到的结论是相符合的。
3.通过 底层函数API 窥探NSObject内存布局
我们通过阅读苹果官方开源的源码,我们可以看到runtime中有一个函数:
/**
* Returns the size of instances of a class.
*
* **@param** cls A class object.
*
* **@return** The size in bytes of instances of the class \e *cls,* or \c 0 if \e *cls* is \c Nil.
*/
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
我们通过调用可以看到结果:
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
结果是8。
为什么是8??跟我们前面得到的结论不一致!!!我们继续往下探索!!
我们知道,OC中创建对象分配内存是通过 alloc方法,我们直接去看官方开源的程序中alloc的实现即可(本质上最终alloc的实现就会调用allocWithZone:方法):
OBJC_SWIFT_UNAVAILABLE这段宏是限制Swift语言分配内存(因为目前存在OC+Swift混编的情况,这个不是本篇幅谈论的范畴,咱们只关注 纯OC 环境即可)
我们从
NSObject.mm文件可以看到其实现调用的函数:
从图上我们清晰看见,最终其调用的是objc里面的 _objc_rootAllocWithZone函数:
我们通过全局搜索找到函数的内部实现:
我们可以看到语言逻辑,在其首次分配内存的时候,调用了函数:
class_createInstance
class_createInstance函数最终也是调用了 class_createInstanceFromZone函数
obj = class_createInstance(cls, 0);
从函数的实现中我们也看到了,CF框架要求:
所有对象至少分配16个字节内存。这涉及到内存对齐的概念。
- 系统底层是早已开辟了16个长度、32个长度、48个长度、64个长度、128个长度....(16的倍数)的内存块的
- 当分配内存给对象时,是按照对象需要内存,能容纳其所需且最接近其的16的最小公倍数来分配内存块的
- 结合前面探索,我们不难猜出这段代码得出的
8个字节,是用来存储 isa 指针的,我们来验证一下:如图所示,我们无法直接访问isa私有成员变量(苹果设计不可以直接访问),但是我们窥探过其开源代码,知道其数据结构,我们可以自己在外部写一个类似的结构体对象进行调试打印!且通过验证得出结论,我们的猜想是正确的!
那么我们可以得出结论:class_getInstanceSize这个runtime函数是用来获取,创建一个对象的实例,至少得给其分配多少内存的!(也就是其本身的数据结构需要多少内存)
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
我们进一步去找一找源码,看看系统底层对内存对齐方面的处理:
我们可以看到红框框中框出来的部分,为分配内存的代码!在objc这份源码中已经看不到了,我们要重新去苹果的OpenSource去下载libmalloc这个开源文件进行探索:
我们查找到
_malloc_zone_calloc的实现:
从上图,我们不难得知,本质上还是调了
calloc函数
- 其中
zone->calloc传入的zone 就是 上一步中的default_zone - 这个关键代码的
目的就是申请一个指针,并将指针地址返回calloc这个函数的实现,苹果官方没有开源但是我们在系统暴露的
malloc.h头文件中找到了一个函数:
extern size_t malloc_size(const void *ptr); /* Returns size of given ptr */
其解释是指,创建对象时,分配多少内存,并把分配的内存地址返回给指针ptr。我们试着用一下这个函数去获取一下实际分配的内存:
从打印结果我们可以看到 实际分配了
16个字节,与前面的几种方式得到的结论一致!!
4.总结:
我们对前面两个函数做一个总结:
创建一个实例对象,至少需要多少内存?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
创建一个实例对象,实际上分配了多少内存?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);
我们前面通过三种方式,得出结论,创建一个NSObject对象:
至少需要8个字节的内存(用于存放isa指针)实际分配了16个字节的内存(因为系统开发者在设计的时候,规定了内存分配的规则)- OC对象的
内存对齐参数为 16- 若需要分配的内存不够16,则以给16个字节
- 若对象需要分配的内存超过16,则以能容纳对象的数据结构为前提,以最接近其的16最小公倍数为最终分配大小进行分配
- 一个OC对象在内存中的布局:
- 系统会在堆中开辟一块内存空间存放该对象
- 这块空间里还包含
成员变量和isa指针 - 然后栈里的 局部变量
指向这块存储空间的地址
四、通过继承关系进一步了解NSObject
随手写两个类:
Car继承自NSObjectBBA_BMW继承自Car
//
// main.m
// 窥探iOS底层原理
//
// Created by VanZhang on 2022/5/6.
//
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
struct {
Class isa;
}NSObject_ISA;
@interface Car :NSObject{
@public
int _year;//多少年了 4个字节
double _kilometres;//多少公里数 4个字节
}//int 8+ double 8 + isa 8 = 24 ;需要24
//内存对齐参数为16 ;总共分配了能容纳其需要的16的最小公倍数:32
-(void)run;
@end
@implementation Car
- (instancetype)init{
self = [super init];
if (self) {
_year = 1;
_kilometres = 2;
}
return self;
}
- (void)run{
NSLog(@"%s",__func__);
}
@end
@interface BBA_BMW :Car{
@public
NSString*_nameplate;//汽车铭牌 8个字节
}//int 8+ double 8+ isa 8 + _nameplate 8= 32 ;需要32
//内存对齐参数为16 ;总共分配了能容纳其需要的16的最小公倍数:32
-(void)runFaster;
@end
@implementation BBA_BMW
- (void)runFaster{
NSLog(@"%s",__func__);
}
@end
void testFunc(void){
Car *c = [[Car alloc]init];
c->_year = 18;
c->_kilometres = 890123;
NSLog(@"Car_class_getInstanceSize:%zd",class_getInstanceSize([c class]));
NSLog(@"Car_size:%zd",malloc_size((__bridge const void *)(c)));
BBA_BMW *bba = [[BBA_BMW alloc]init];
bba->_nameplate = @"宝马七系";
NSLog(@"BBA_BMW_class_getInstanceSize:%zd",class_getInstanceSize([bba class]));
NSLog(@"BBA_BMW_size:%zd",malloc_size((__bridge const void *)(bba)));
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSObject *obj = [[NSObject alloc]init];
// NSLog(@"%@",obj);
NSLog(@"class_getInstanceSize:%zd",class_getInstanceSize([obj class]));
// NSLog(@"isa:%zd",sizeof(obj->isa));
NSLog(@"isa:%zd",sizeof(NSObject_ISA));
NSLog(@"malloc_size:%zd",malloc_size((__bridge const void *)(obj)));
testFunc();
}
return 0;
}
1.运行项目,通过系统函数打印一下:
2.打断点,通过ViewMemory查看一下内存
car:
-
我们前面将
_year设置为18,在16进制中,0x12=十进制的18 -
实际分配了32,真正用到了24
bba:
-
我们前面将
_year默认设置为1,将_kilometres改为int类型 默认设置为 2 -
内存分布情况如下:
总结
通过通篇介绍,我们了解了 NSObject的底层实现、通过三种不同的方式窥探了NSObject对象的底层内存布局、写了两三个类关注继承关系下 的OC对象的内存布局情况
专题系列文章
1.前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数、枚举、可选项、结构体、类、闭包、属性、方法、swift多态原理、String、Array、Dictionary、引用计数、MetaData等Swift基本语法和相关的底层原理文章有如下几篇:
其它底层原理专题
1.底层原理相关专题
2.iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案