关于Objective-C / Swift
首先大部分开发流程基本都是
编写代码 -> 编译代码(连接) -> 运行程序
- 先来看看 C / C++
它是静态语言,大意就是 "代码写的是什么,编译完运行程序的时候就是什么了" - OC / Swift
这是动态语言, "写的是什么,编译完了运行的时候不一定就会运行写的代码"
(可以在程序运行时动态的修改代码)
这就是Runtime
ISA指针
相信有点基础的同学肯定都知道了:
实例对象可以通过 isa 查到到类对象( Class );
类对象可以通过 isa 查到到元类对象( MetaClass);
32位架构
在以前的32位架构中 isa 本身就是 类对象( Class ) / 元类对象( MetaClass )的指针地址;
Class isa
ARM64位架构
- 在 ARM64架构下,对 isa 进行了优化,需要使用
&ISA_MASK
进行位运算后得到值才是真实的 类对象 / 元类对象的地址值 - 这里可能会有疑问了 "64位下这么一搞那不是更麻烦了么"?, 从直观上来看确实变麻烦了,还需要位运算一次 才能拿到真实的地址值,不过如果你真正理解 isa 之后再来回答这个问题就会给出相反的答案, 这样做的确是一个优化方案
isa_t isa
--> isa_t
首先你可能需要了解一下 <共用体> 这个概念,
大概的意思就是通过"位域'的技术,使用"位(bit)"在存储相关的值(通过位运算来取值)
这样使用 "位(bit)"来存储而不是字节(Byte)可以更加节省空间, 从而使内存进一步优化,这也证明了为什么说ARM64位架构下 isa 是一个优化方案
比如 我有3个属性要保存 在代码中直接写的话 可能就需要 12个字节(假设一个变量占4个字节);
但是如果用 二进制来存储的话 只需要占用3位(每个变量占一位),连一个字节都不到;
这样通过二进制位来存储比普通的字节来存储要节省空间多的多
1字节(Byte) = 8位(bit)
tips
知识点1: 位域(点击跳转百度百科) 、共用体(点击跳转百度百科)
知识点2: 类对象 / 元类对象的地址值最后3位永远都是0
struct objc_object {
private:
isa_t isa;
}
struct isa_t {
// 右边为最低位
nopointer: 是否为非指针类型( isa 经过优化的, 引用计数存储在 extra_rc 中)
has_assoc: <曾经>是否有设置过关联对象, 如果为 0 释放的会更快
has_cxx_dtor: 是否有 cpp 析构函数(类似OC的dealloc 释放内存函数), 如果为 0 释放的会更块
shiftcls: 类对象 / 元类对象 的指针地址
magic: 在调试时候用于分辨 是否已经初始化
weakly_referenced : <曾经>是否有弱引用指向 如果为0 释放的会更快
deallocating: 是否正在被释放
has_sidetable_rc : (值为1的话)如果引用计数 在 extra_rc 放不下(大于19位) 就会放到 sidetable 中
extra_rc: 在 isa 指针中存放的引用计数(存储的值是 引用计数器值 - 1)
}
Class 对象结构
// 元类对象可以看做特殊的类对象
objc_class
struct objc_class : objc_object {
// Class ISA; isa在父类中
Class superclass;
cache_t cache // 方法缓存
class_data_bits_t bits // 用于存放具体的类信息 通过bits & FAST_DATA_MASK 可以得到 class_rw_t
}
上面bits & FAST_DATA_MASK
得到 class_rw_t
struct class_rw_t{
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // 类的原始信息(只读)
// methods, properties, protocols 都是二维数组 包含了 类本身的 和分类添加的 都在里面
method_array_t methods; // 方法列表(二维数组 [ method_list_t ][ method_t ])
property_array_t properties; // 属性列表(二维数组)
protocol_array_t protocols; // 协议列表(二维数组)
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
上面class_rw_t
中的class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // 内存中占用的空间
uint32_t reserved;
const uint8_t * ivarLayout;
const char * name; // 类名
method_list_t * baseMethodList; // 这里直接就是一维数组 [ method_t ]
protocol_list_t * baseProtocols; // 这里直接就是一维数组
const ivar_list_t * ivars; //成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
方法列表(二维数组)的结构
method_array_t -> [method_list_t]
method_list_t -> [method_t]
method_t
方法的结构
struct method_t {
//*** 不同类中的相同方法名的 SEL地址其实是一个, 唯一的一个
SEL name // 函数名 SEL类型 可以看做char *类型,就是一个字符串
const char *type // 编码 type codeing ( v : @ balabala...)
IMP imp //指向函数实现方法的指针(函数指针)
}
在回到 objc_class
中的 cache_t
结构
cache_t 这个东西也引出来了很重要的一个面试内容: 消息传递机制
先来看看 cache_t 是什么?
方法缓存, 会缓存曾经调用过的方法, 使用 散列表 的结构存储
散列表又叫哈希表,那么什么是散列表(点击跳转百度百科)
struct cache_t {
struct bucket_t * _buckets; // 散列表
mask_t _mask; // 值为 散列表的长度 - 1
mask_t _occupied; // 已经缓存的方法数量
}
struct bucket_t {
cache_key_t _key; // SEL内存址作为key, 上面提到过 相同方法名的SEL内存地址是一样的
IMP _imp; // 方法实现函数的内存地址
}
Q: 为什么要用 SEL作为key存在散列表里面?
A: 因为 objc_msgSend(objc, @selector(func))
发送消息 是通过 @selector()
给对象发送消息的
Q:为什么要用散列表当做cache_t的数据结构?
A: 因为散列表可以利用算法 生成对应的索引 key的地址 & _mask(cache_t结构里的属性) = 对应的索引
(这也是散列表的核心概念, 散列表 之所以快是牺牲内存空间换效率)
tips: key的地址 & _mask 永远小于_mask值
(知识点: 位运算)
objc_msgSend
OC中的方法调用在底层都是转为
objc_msgSend
函数的调用
白话文 : 给方法调用者发送消息
objc_msgSend( id , _cmd_ )
_cmd_
传入参数(sel_registerName()
等价于 @selector()
)
objc_msgSend执行流程(底层实现)
1. 消息发送(消息传递)
2. 消息转发
3.如果能走到这里就会报错 unrecignized selector sent to instance
_1. 消息传递机制流程(方法调用 / 消息发送流程)
-
objc_msgSend(objc, @selector(func))
开始 - 会通过
objc
的isa
到对应的Class / mateClass
中的cache_t
缓存列表中查找是否hit(命中)- isHit 直接返回
- noHit 进入3
- 在查找当前类的
methods
方法列表中查找- methods中查找方法分 有序数组(二分查找)和 无序数组(遍历)
- isHit 将方法缓存到当前类的
cache_t
并返回 - noHit 进入4
- 根据
superclass
指针到父类的cache_t
方法列表中查找- isHit 将方法缓存到当前类(上面的
objc
的类)的cache_t
并返回 - noHit 进入5
- isHit 将方法缓存到当前类(上面的
- 根据
superclass
指针到父类的methods
方法列表中查找- isHit 将方法缓存到当前类(上面的
objc
的类)的cache_t
并返回 - noHit 进入6
- isHit 将方法缓存到当前类(上面的
- 继续沿着
superclass
指针向上查找直至 根类 / 根元类的cache_t
和methods
- isHit 将方法缓存到当当前类(上面的
objc
的类)的cache_t
并返回 - noHit 则进入6
- isHit 将方法缓存到当当前类(上面的
6.进入消息转发流程
_2. 消息转发流程
- 先会进入 resolveInstanceMethod / resolveClassMethod 方法进行动态解析
-
第一次进入
a. 查看triedResolver
标记 (第一次进入为false
)
b1. 通过class_addMethod
添加方法 return true (其实这里return true
或者false
都一样 只要有处理 就不会进入下一步了,不过苹果官方 推荐return true
)
b1.1.class_addMethod
会将方法动态添加到class_rw_t
的methods
中
or b2. return[super resolveInstanceMethod]
c.triedResolver
标记为true
c. 然后 回到消息传递的 1. 步骤重新走一遍 -
非第一次进入(说明第一次进入没有动态添加方法实现)
a.查看triedResolver
标记(此时标记已经为 true)进入 2
-
第一次进入
- 如果上面1步骤没有得到处理 则会进入
fowardingTargetForSelector
方法 注意!!这里有对应的- 对象方法
与+ 类方法
- 可以将消息(
SEL
)转发给其他对象去处理;___forwarding___(不开源)
通过汇编大概可以追溯到 拿到return [[objc alloc] init];
之后objc_msgSend(objc, sel)
(这里表述的过于简单了,详细细节可以自行google) -
return nil
则进入3
- 可以将消息(
- 这是会进入
methodSignatureForSelector
方法来创建一个方法签名 注意!!这里有对应的- 对象方法
与+ 类方法
- 如果创建了一个有效的方法签名则进入 4
- 如果
return nil
或仍未处理则进入5
- 会进入
forwardInvocation
注意!!这里有对应的- 对象方法
与+ 类方法
- 对
anIvocation.target
指定方法调用对象(谁来处理这个消息,和 2. 转发对象意义相同) - 其实能进到这个方法 就可以随意处置消息了 哪怕是随便打印个NSLog 甚至 不写代码 也不会进入5 进而报错
- 对
- 此时会进入
doesNotRecognizeSelector
方法- 抛出异常
unrecignized selector sent to instance
app崩溃
- 抛出异常
网友评论