1.对象
1.alloc、init、new的区别
- alloc:
1. _objc_rootAlloc
2. callAlloc
3. instanceSize(计算对象大小) 算法:(x + 7) & ~7
4. calloc(申请开辟空间)
5. initInstanceIsa(指针关联对象)
- init
+ (id)init {
return (id)self;
}
init底层没有做什么操作,直接返回了obj。这样做是一种抽象工厂设计模式,让代码实现更加自由;我们可以在子类中重写init方法做一些初始化操作。
- new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
new = alloc + init
2.内存对齐
什么是内存对齐
在计算机中,内存大小的基本单位是字节,理论上来讲,可以从任意地址访问某种基本数据类型。
但是实际上,计算机并非按照字节大小读写内存,而是以2、4、8的倍数的字节块来读写内存。因此,编译器会对基本数据类型的合法地址作出一些限制,即它的地址必须是2、4、8的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐。
iOS编译器会自动的进行字节对齐,32位下采用4字节对齐,64位下采用8字节对齐
内存对齐原则
- 数据成员对齐原则:结构(struc)(或联合(union))的数据成员,第一个数据成员放在offset为0的位置,以后每个
数据成员M存储的起始位置要从成员M大小或者成员M的子成员大小(只要该成员有子成员,比如说是数组、结构体等)的整数倍开始(比如int为4的字节,则要从4的整数倍地址开始存储)。 - 结构体作为成员:如果一个结构内有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct A内存有struct B,B里有char、int、double等元素,B应该从double也就是8的整数倍开始存储)
- 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
struct StructA {
int a; // 4字节,0,1,2,3
char b; // 1字节,4, 补5,6,7
double c; // 8字节,8,9,10,11,12,13,14,15
int *p; // 8字节,16,17,18,19,20,21,22,23 大小24字节
} strA;
struct StructB {
int a; // 4字节,0,1,2,3, 补4,5,6,7
int *p; // 8字节,8,9,10,11,12,13,14,15
char b; // 1字节,16, 补17,18,19,20,21,22,23
double c; // 8字节,24,25,26,27,28,29,30,21,32 大小32字节
} strB;
struct StructC {
int a; // 4字节,0,1,2,3
char b; // 1字节,4, 补5,6,7
double c; // 8字节,8,9,10,11,12,13,14,15
int *p; // 8字节,16,17,18,19,20,21,22,23
struct StructB b; //32字节,sizeof(structB) = 32 大小56字节
} strC;
内存对齐原因
- 性能上的提升:从内存占用的角度讲,对齐后比未对齐有些情况反而增加了内存分配的开支,是为了什么呢?数据结构(尤其是栈)应该尽可能地在自然边界上对齐,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。最重要的是提高内存系统的性能。
- 跨平台:有些硬件平台并不能访问任意地址上的任意数据的,只能处理特定类型的数据,否则会导致硬件层级的错误。有些CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。
八进制对齐算法:对象的属性
WORD_MASK = 2^3-1 = 7
1.(x + 7) & ~7
2. (x + 7) >> 3 << 3 左移3 右移3
十六进制对齐算法:对象
WORD_MASK = 2^4-1 = 15
1.(x + 15) & ~15
2. (x + 15) >> 4 << 4 左移4 右移4
3.对比class_getInstanceSize,malloc_size,sizeOf
- class_getInstanceSize: 依赖于<objc/runtime.h>,获取实例对象中成员变量内存大小。
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK; // WORD_MASK = 7
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- malloc_size: 依赖于<malloc/malloc.h>,系统为该对象实际开辟的内存大小
malloc_size = (instanceSize + 16 - 1) >> 4 << 4
对象占用内存大小:8字节对齐,最小16(class_getInstanceSize)。
系统实际分配的内存大小:16字节对齐(malloc_size)
- sizeOf: sizeof是操作符,不是函数,它的作用对象是数据类型,主要作用于编译时,得到的结果是该数据类型占用空间大小。sizeof 只会计算类型所占用的内存大小,不会关心具体的对象的内存布局;
例如:在64位架构下,自定义一个NSObject对象,无论该对象有多少个成员变量,最后得到的内存大小都是8个字节。
- 面试题:一个NSObject对象占用多少内存?
在64位架构下, 系统分配了16个字节给NSObject对象(通过malloc_size函数获得);
但NSObject对象内部只使用了8个字节的空间(可以通过class_getInstanceSize函数获得)。
4.isa的结构
isa_t联合体有3个成员(Class cls、uintptr_t bits、位域ISA_BITFIELD),3个成员共同占用8字节的内存空间,通过ISA_BITFIELD里面的位域成员,可以对8字节空间的不同二进制位进行操作,达到节省内存空间的目的。
isa是一个联合体,8个字节,它的特性就是共用内存,或者说是互斥(比如说如果cls赋值了,再对bits进行赋值时会覆盖掉cls)。
union isa_t {
isa_t() { } // 初始化方法1
isa_t(uintptr_t value) : bits(value) { } // 初始化方法2
Class cls; // 成员1
uintptr_t bits; // 成员2
#if defined(ISA_BITFIELD)
struct { // 成员3
ISA_BITFIELD; // defined in isa.h // 位域宏定义
};
#endif
};
在isa_t联合体内使用宏ISA_BITFIELD定义了位域,我们进入位域内查看源码。
# if __arm64__
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
nonpointer:是否对isa指针开启指针优化
- 当
nonpointer = 0:不优化,纯isa指针,当访问isa指针时,直接通过isa.cls和类进行关联,返回其成员变量cls - 当
nonpointer = 1:优化过的isa指针,指针内容不止是类对象地址,还会使用位域存放类信息、对象的引用计数,此时创建newisa并初始化后赋值给isa指针。 如果没有,则可以更快的释放对象。
- has_assoc:是否有关联对象,0没有,1存在。
- has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls:存储类对象和元类对象的指针的值,在开启指针优化的情况下,在 arm64 架构中用 33 位用来存储类指针
newsisa.shiftcls = (uintptr_t)cls >> 3 // shiftcls >> 3 是因为他前面有3位,移动之后取的才是cls的值
5. magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
6. weakly_referenced:对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
7. deallocating:标志对象是否正在释放内存
8. has_sidetable_rc:当对象引用计数大于 10 时,则需要借用该变量存储进位(rc = retainCount)
9. extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下has_sidetable_rc。
- 联合体所有属性共用内存,内存长度等于其最长成员的长度,使代码存储数据高效率的同时,有较强的可读性;而位域可以容纳更多类型。
- isa是联合体,最长成员的长度是位域的8字节(64位/8=8字节,1字节byte=8位bit),所以isa在内存中占用8字节。
5.isa走位
- 1.对象:程序猿根据类实例化的。实例对象 和 类 通过isa关联。
- 2.类:本质上也是对象,通过元类实例化出来,内存中只有一份,一般是编译期系统创建的。类对象 和 元类通过isa关联。
- 3.元类:编译期系统创建的,便于方法的编译
- isa走位(虚线):实例对象 -> 类对象 -> 元类 -> 根元类 -> 根元类自身
- 继承关系(实线):子类 -> 父类 -> NSObject -> nil
- 根元类的父类为NSObject。NSObject的父类是nil
6.对象的本质
clang编译Person对象:
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
...
}
- 对象是一个结构体
- 属性:_成员变量、setter、getter
- 成员变量:底层编译不会生成相应的setter、getter
2.类
类的本质
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();
}
...
};
objc_class继承于objc_object,存储着isa
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
类的本质是objc_class类型的结构体,objc_class继承于objc_object,所以满足万物皆对象
继承于objc_object就满足万物皆对象呢?我们来查看下NSObject源码
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
NSObject是OC版本的objc_object,和objc_object的定义是一样的,在底层会编译成objc_object
类的结构
通过objc_class源码可以得出,类有4个属性:isa、superclass、cache、bits。
Class ISA:实例对象通过isa关联类,类对象通过isa关联元类。Class是个指针,占用8字节。Class superClass:类的父类,一般为NSObject。Class类型的结构体指针,占用8字节
typedef struct objc_class *Class
cache_t cache:cache主要是用来存储_buckets(方法缓存链表)和mask(分配用来缓存bucket的总数),还有当前实际占用的bucket个数。 cache是结构体,占用16字节。
struct cache_t {
struct bucket_t *_buckets; // 8 结构体指针
mask_t _mask; // 4
mask_t _occupied; // 4
...
};
class_data_bits_t bits:为我们提供了便捷方法用于返回其中的 class_rw_t * 指针;一个long类型的bits标志位,这个标志位存储了很多flags(比如:快速分配内存标志、是否有析构函数、是否有构造函数、是否有自定义控件等)
struct class_data_bits_t {
uintptr_t bits;
private:
bool getBit(uintptr_t bit)
{
return bits & bit;
}
...
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...
};
类的属性和方法
| 存储位置 | |
|---|---|
| 属性 | bit.data.rw.ro.property |
| 成员变量 | bit.data.rw.ro.ivar |
| 实例方法 | bit.data.rw.method |
| 类方法 | 实例方法的形式,存储在元类。元类又继承自NSObject,形成一个闭环 |
| ro是编译其生成的,运行时copy一份生成rw,所以属性、成员变量存在ro |
cache_t cache
struct cache_t {
struct bucket_t *_buckets; // 结构体指针
mask_t _mask; // 可以看出是一个无符号Int类型,在64位下为uint32_t
mask_t _occupied; // 容积
...
};
struct bucket_t {
MethodCacheIMP _imp;
cache_key_t _key;
};
并不是掉用一个方法就缓存,而是有一个特殊缓存策略
cache_t的作用
Class中的Cache主要是为了在消息发送的过程中,进行方法的缓存,加快调用效率。其中使用了动态扩容的方法,当容量达到最大值的0.75时,开始2倍扩容,扩容时会完全抹除旧的buckets,并且创建新的buckets代替,之后把最近一次临界的imp和key缓存进来。
为什么在0.75的时候进行扩容
在哈希这种数据结构里面,有一个概念用来表示空位的多少叫做装载因子——装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降 负载因子是3/4的时候,空间利用率比较高,而且避免了相当多的Hash冲突,提升了空间效率
bucket与mask、capacity、sel、imp的关系
类cls拥有属性cache_t,cache_t中的buckets有多个bucket,bucket里存储着方法实现imp和方法编号sel强转成的key值cache_key_t
- mask对于bucket,主要是用来在缓存查找时的哈希算法
- capacity对于bucket,可以获取到cache_t中bucket的数量
cache_t什么时候调用?
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
cache_fill_nolock的调用时机,我们在源码中可以看到,是在cache_fill中进行了调用。cache_fill的调用时机其实是在method lookup的过程中调用的,而方法查找则要牵扯到objc_msgSend,也就是消息发送机制。所以我们姑且可以认为,在消息发送的过程中,先通过缓存查找imp,如果查找到就直接调用,如果没有,那么就进行缓存。
3.方法
方法查找测试
- 对象方法:
objc_msgSend(student, sel_registerName("student_instanceMethed"));
- 对象的实例方法 - 自己有
- 对象的实例方法 - 自己没有 - 找父类
- 对象的实例方法 - 自己没有 - 父类没有 - 找父类的父类 - NSObject
- 对象的实例方法 - 自己没有 - 父类没有 - 找父类的父类 - NSObject也没有 - 崩溃
- 类方法
objc_msgSend(objc_getClass("AKStudent"), // objc_getClass获取元类
sel_registerName("student_ClassMethod"));
- 类方法 - 自己的元类有
- 类方法 - 自己的元类没有 - 找元类的父类
- 类方法 - 自己的元类没有 - 元类的父类没有 - 元类的父类的父类 - 根元类 - NSObject也没有 - 奔溃
- 类方法 - 自己的元类没有 - 元类的父类没有 - 元类的父类的父类 - 根元类 - NSObject也没有,但是有对象方法(类方法会以实例方法的形式存在元类中)
方法查找流程
objc_msgSend(obj, SEL)
快速查找:
cache_getImp
- 如果
cacheHit缓存命中,表示找到了sel对应的imp,直接返回imp。 - 如果
JumpMiss缓存未命中,进入慢速查找
慢速查找:
lookUpImpOrForward(id obj, SEL sel, Class cls)
- id obj:方法接受者(实例方法->实例对象,类方法->元类);SEL sel:方法名;Class cls:类
- 查找方法:二分查找
- 当前class命中,
cache_fill添加缓存,然后return imp; - 当前class未命中,便利
cls->superclass,重复步骤2 - NSObject也未找到,进入消息转发
objc_msgForward
动态方法决议
_class_resolveMethod
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) { //判断类不是元类
// 调用对象方法解析
_class_resolveInstanceMethod(cls, sel, inst);
} else {
// 调用类方法解析
_class_resolveClassMethod(cls, sel, inst);
// 再次查找下方法,如果没有的话,就再转发一下resolveInstanceMethod方法
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
2. 实例方法_class_resolveInstanceMethod
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
//发送SEL_resolveInstanceMethod消息,
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
//再次查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
lookUpImpOrNil:会调用lookupImpOrForward查找类、父类是否实现SEL_resolveInstanceMethod方法,并cache_fill- 再次
lookUpImpOrNil,查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了 - 结束动态方法决议,回到
lookUpImpOrForward方法,将triedResolve = YES并goto retry重新查找缓存和方法列表
- 类方法
类方法与实例方法基本一致,不同的是在结束
SEL_resolveClassMethod回到lookUpImpOrForward方法时,再次调用lookUpImpOrNil会查找sel的imp,若有imp则退出动态方法决议,若无则进入_class_resolveInstanceMethod。
else {
// 调用类方法解析
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
4. 解决方法
- 实例方法:重写
+resolveInstanceMethod添加imp - 类方法:重写
+resolveClassMethod往元类添加imp(实例方法存在类对象中,类方法存在元类对象中)
快速消息转发
forwardingTargetForSelector会返回一个对象,将自己处理不了的消息转发给该对象,询问该对象是否能够处理该消息。
- (id)forwardingTargetForSelector:(SEL)sel {
if ([AKTeacher respondsToSelector:sel]) {
return AKTeacher.class;
}
return nil;
}
慢速消息转发
如果快速转发还不能找到method,会来到慢速转发methodSignatureForSelector。
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
methodSignatureForSelector会返回一个方法签名NSMethodSignature- 根据
NSMethodSignature创建NSInvocation对象。 - 将
NSInvocation对象作为参数传给forwardInvocation方法.
-(void)forwardInvocation:(NSInvocation *)anInvocation
forwardInvocation方法类似于将消息当做事务堆放起来,方法内部将消息给能处理该消息的对象,就算不操作也不会崩溃,这里也是防崩溃的最后处理机会。
+ (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}
- 异常是在
doesNotRecognizeSelector方法里面抛出的,所以我们重写forwardInvocation方法后,如果不在里面执行父类的方法,程序是不会崩溃的
// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("+[%s %s]: unrecognized selector sent to instance %p",
class_getName(self), sel_getName(sel), self);
}
// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
3. 慢速转发补救
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (NSSelectorFromString(@"saySomething") == aSelector) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 把不能够处理的方法,返回一个方法签名
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 给不同的方法,传递给不同的能够处理的类去处理
SEL aSelector = [anInvocation selector];
if ([[AKTeacher alloc] respondsToSelector:aSelector]) {
// 此时 AKTeacher 能够处理 方法 aSelector
[anInvocation invokeWithTarget:[AKTeacher alloc]];
}
else {
[super forwardInvocation:anInvocation];
}
}
消息转发总结
当开发者调用了未实现的方法,苹果提供了三个解决途径:
1.动态方法决议:Method resolution
- runtime调用
+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。 - 如果你添加了函数并返回YES, 那运行时系统就会重启一次消息发送
lookupimporforward的过程。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
2.快速转发:Fast forwarding
- 如果目标对象实现了
-forwardingTargetForSelector:,runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 - 只要这个其他对象不是nii或者self,那运行时系统就会重启一次消息发送的过程。此时,发送消息的对象变成你返回的那个其他对象。
- (id)forwardingTargetForSelector:(SEL)aSelector;
这里叫fast,只是为了区别下一步的慢速转发。因为这一步不会创建新的对象,而Normal forwarding会创建一个NSInvocation对象。
3.慢速转发:Normal forwarding
这一步是runtime最后一次给你挽救的机会。
- 先调用
-methodSignatureForSelector返回一个方法签名NSMethodSignature - 根据
NSMethodSignature创建NSInvocation对象。 - 将
NSInvocation对象作为参数传给-forwardInvocation方法,在其内部做消息处理。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
4.为什么_class_resolveInstanceMethod会调用调用两次?
methodSignatureForSelector之后,forwardInvocation之前 调用第二次_class_resolveInstanceMethod`
类方法会以实例方法的形式存在元类中。这里的cls是元类,ins是类对象。在_class_resolveInstanceMethod会沿着isa走位图((类方法 -> [元类 - 根元类 - NSObject]))遍历查找有没有以实例方法形式存储的类方法