对象,类,元类关系图
一个 NSObject 对象占用多少内存空间?
NSObject 对象都会分配 16byte:实际上是 8 字节,但是会因为字节对齐的原因返回16字节的倍数
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
//字节对齐
return word_align(unalignedInstanceSize());
}
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;
}
这个方法中,判断size是在字节对齐后是否小于16,如果小于16,就返回16
字节对齐
即字节大小肯定为某个字节的倍数:以8字节对齐为例:7->8,13->16
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL //在arm64架构中是一个常数7
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
/////////////////////////////////////////////////////
static inline uint32_t word_align(uint32_t x) {
// 7+8 = 15
// 0000 1111
// 0000 1000
//&
// 1111 1000 ~7
// 0000 1000 8
// 0000 0111
//
// x + 7
// 8
// 8 二阶
// 拓展 >> 3 << 3右移左移和(x + 7)功能一样
return (x + WORD_MASK) & ~WORD_MASK; //这段代码的作用就是返回出去的字节大小一定是8的倍数(升对齐)
}
内存对齐
内存对齐是为了更好的节省内存空间:
//int 类型只需要占用4字节 char类型只需要占用1字节,如果进行8字节对齐,那么
// int 4 + 4 = 8
// char 1 + 7 = 8
而内存对齐后,就会将这两个数据放在同一内存段中,共用一个8字节
- 8字节对齐----对象属性的内存空间优化;
- 16字节对齐----对象的内存空间优化;
isa
- isa_t 结构
union isa_t {
isa_t() { }//初始话方法
isa_t(uintptr_t value) : bits(value) { }
Class cls;//用于绑定关联类Class
uintptr_t bits;
#if defined(ISA_BITFIELD) //结构体位域
struct {
ISA_BITFIELD; // defined in isa.h,
};
#endif
};
isa_t 是一个联合体
- ISA_BITFIELD宏定义
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; //表示是否对isa指针开启指针优化;0表示纯指针,1表示不仅仅是类的对象地址,还包含了类的信息,比如对象的引用计数 \
uintptr_t has_assoc : 1; //是否有关联对象 \
uintptr_t has_cxx_dtor : 1; //是都有c++析构函数,有的话则需要做析构逻辑,没有的话释放对象速度更快 \
uintptr_t shiftcls : 44;// 存储类指针的值。在开启指针优化的情况下,在arm64架构中有33位用来存储类指针/*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; //用于调试判断当前对象是真的对象还是没有初始化的空间 \
uintptr_t weakly_referenced : 1; //是否被弱引用过,没有释放更快 \
uintptr_t deallocating : 1; //对象是否正在释放 \
uintptr_t has_sidetable_rc : 1; //当对象引用计数大于10时,则需要借用改变量储存进位 \
uintptr_t extra_rc : 8//当前对象引用数据,实际上是引用计数-1;例如当对象引用计数为10时,extra_rc=9。大于10时就要用到上面的has_sidetable_rc
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
- 联合体 & 位域 用联合体和位运算节省空间
源码查看网址
openSource.apple.com/tarballs
clang
//通过clang命令将person.m文件编译成person.cpp文件
$ clang -rewrite-objc Person.m -o Person.cpp
方法名
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
{{(struct objc_selector *)"tureName", "@16@0:8", (void *)_I_Person_tureName},
{(struct objc_selector *)"setTureName:", "v24@0:8@16", (void *)_I_Person_setTureName_}}
};
_I_Person_tureName : 函数名(函数指针)用于找到函数的具体实现 tureName: 方法名 @16@0:8 方法签名:
- @:返回值类型 - id类型
- 16:总共的量(偏移量)
- @:参数一类型 - id类型 参数偏移范围0-7
- : 参数二类型 - sel类型 参数偏移范围8-15
method_t
method_t是对方法\函数的封装
struct method_t {
SEL name; //函数名
const char *types; //编码(返回值类型、参数类型)
IMP imp;//指向函数的指针(函数地址)
};
类的结构
NSObjectd定义
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
Class定义
typedef struct objc_class *Class;
objc_class定义
struct objc_class : objc_object {
// Class ISA; //8个字节
Class superclass;//8个字节
cache_t cache; //16个字节 pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
void setInfo(uint32_t set) {
assert(isFuture() || isRealized());
data()->setFlags(set);
}
...
...
...
objc_object定义
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
上述源码总结:
- Class是objc_class结构体的指针(注意看*)
- objc_class 又是继承于objc_object的结构体
- objc_object结构体中只有isa 一个属性
class_rw_t
- rw代表可读可写
- 类的属性、方法、协议打等信息存放于class_rw_t
class_ro_t
存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
类的结构图解
isa
isa类型:
- 纯指针,指向内存地址
- NON_POINTER_ISA,除了内存地址,还存有一些其他信息
isa_t联合体
cache_t
cache_t结构
bucket_t结构
cache_t是结构体,里面有bucket_t散列表,每个bucket_t存储的 SEL 和 IMP 的键值对
cache散列表查找过程,在 objc-cache.mm 文件中
上面是查询散列表函数,其中cache_hash(k, m)是静态内联方法,将传入的key和mask进行&操作返回uint32_t 索引值。do-while 循环查找过程,当发生冲突 cache_next 方法将索引值减 1。所以是无序的;
- LRU算法
LRU - (Least Recently Used):最近最少使用策略——先淘汰最近最少使用的内容,在方法缓存中也用到了这种算法
cache_t图解
实例对象的数据结构
本质上 objc_object 的私有属性只有一个 isa 指针。指向 类对象 的内存地址。
类对象的数据结构
什么时候会报 unrecognized selector 的异常
objc 在向一个对象发送消息时,runtime 库会根据对象的 isa 指针找到该对象实际所属的类,然后在该类中 的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会 进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常 unrecognized selector sent to XXX
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量? 为什么?
不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量; 原因
- 1.因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setvarlayout 或 class_setWeaklvarLayout 来 处理 strong weak 引用.所以不能向存在的类中添加实例变量。
- 2.运行时创建的类是可以添加实例变量,调用 class_addIvar 函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
objc 中向一个 nil 对象发送消息将会发生什么?
如果向一个 nil 对象发送消息,首先在寻找对象的 isa 指针时就是 0 地址返回了,所以不会出现任何错误。 也不会崩溃
Category & initialize
分类的底层结构
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 对象方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
Category 在编译过后,是在什么时机与原有的类合并到一起的
-
- 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
-
- 然后会 map_images。
-
- 接下来调用 map_images_nolock。
-
- 再然后就是read_images,这个方法会读取所有的类的相关信息。
-
- 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。
-
- 在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体 中。
- 在运行时,新添加的方法,都被以倒序插入到原有方法列 表的最前面,所以不同的 Category,添加了同一个方法,执行的实际上是最后一个。
分类方法加载顺序
- load方法
- 主类的load方法会先于分类load方法
- 父类的load方法会先于子类的load方法
- 非load方法(子类/父类/分类)
- 主类和分类拥有相同方法时,分类会被调用,因为每个分类的方法会通过attachlist方法以数组的形式插入到二维数组的前面,所以会造成分类覆盖主类的假象。
- 如果子类也拥有相同的方法,会覆盖主类和分类的方法。
load方法加载时机
load方法在程序启动装载类信息的时候就会调用
load_images(const char *path __unused, const struct mach_header *mh) {
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
//准备非懒加载类load方法,将其放在一个数组中
prepare_load_methods((const headerType *)mh);
}
//遍历加载load方法
call_load_methods();
}
如何修改分类load方法顺序
target - Bulid Phases - Complies Sources,shuoebi 就是调整编译顺序
initialize在什么时候调用
- 晚于load方法调用
- +initialize方法通过消息机制调用(objc_msgSend),即类第一次接收到消息的时候调用。
- 每一个类只会initialize一次(父类的initialize方法可能会被调用多次),当子类没有实现+initialize方法时,会调用父类的initialize方法
- 先调用父类的+initialize方法,后调用子类的+initialize方法
- 如果一个类有分类,那么会调用最后编译的分类实现的。即:如果分类实现了+initialize,就覆盖类本身的+initialize调用。
消息发送和转发
消息发送
即_objc_msgSend函数的实现
上图核心 _class_lookupMethodAndLoadCache3函数
_class_lookupMethodAndLoadCache3 实现(下图)
如果消息发送阶段没有找到方法,就会进入消息转发机制。
消息转发
处理消息的一个机制:防止程序崩溃
OC的运行时在程序崩溃前提供了三次拯救程序的机会。当向someObject发送某消息,但runtime sys在本类和其父类都找不到对应的方法时runtime并不会马上报错:
- 动态方法解析:向当前类发送
resolveInstanceMethod:检查是否动态添加方法 - 备援接收者:检查该类是都实现了
forwardingtargetForSelector:方法 看看能不能把选择子转发给其他接收者处理 - 完整的消息转发
nsinvocation :runtime发送methodSignatureForSelector消息获取Selector对应的方法签名。返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出。
isKindOfClass、isMemberOfClass
BOOL re1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [[Person class] isKindOfClass:[Person class]];
BOOL re4 = [[Person class] isMemberOfClass:[Person class]];
NSLog(@"\nre1:%hhd\nre2:%hhd\nre3:%hhd\nre4:%hhd",re1,re2,re3,re4);
BOOL re5 = [[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [[Person alloc] isKindOfClass:[Person class]];
BOOL re8 = [[Person alloc] isMemberOfClass:[Person class]];
NSLog(@"\nre5:%hhd\nre6:%hhd\nre7:%hhd\nre8:%hhd",re5,re6,re7,re8);
2020-02-11 15:44:25.843470+0800 Test[44114:837672]
re1:1
re2:0
re3:0
re4:0
2020-02-11 15:44:25.844011+0800 Test[44114:837672]
re5:1
re6:1
re7:1
re8:1
解析:
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
####
object_getClass((id)self)
这个方法中:如果self是对象,那返回类,如果self是类对象,那返回元类
1-4是类方法,5-8是对象方法
method swizzling 方法交换
改变selector和imp的映射关系
- 利用 method_exchangeImplementations 交换两个方法的实现
- 利用 class_replaceMethod 替换方法的实现
- 利用 method_setImplementation 来直接设置某个方法的 IMP
使用注意点
- 使用dispatch_once_t 防止他人手动有调用
+ (void)load,导致方法又被交换回来了
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[RuntimeTool best_MethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(studentInstanceMethod)];
});
}
- 交换之前判断原始方法有没有实现,如果没有那就添加一个空的block实现,你可以在block中,进行bug上传
- 如果交换的方法是自己已经有的,那么就需要替换(更新)方法
- 防止子类没有实现,而父类实现。相当于交换了父类的方法,交换后就会崩溃,因为父类没有实现被交换的方法。所以要给本类先添加
class_addMethod(),添加如果成功,那就class_replaceMethod();如果添加失败,就证明存在方法,那就进行正常的交换
+ (void)best_MethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {//判断原始方法有没有实现,如果没有那就添加一个空的block实现,你可以在block中,进行bug上传
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
//这里面进行bug上传
}));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
//防止子类没有实现,而父类实现,相当于交换了父类的方法
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}