本文属笔记性质,主要针对自己理解不太透彻的地方进行记录。
推荐系统直接学习小码哥iOS底层原理班---MJ老师的课确实不错,强推一波。
简介
- 支撑Object-C的动态性
- C语言提供结构,C\C++\汇编编写实现
isa
isa作为runtime底层中最常用的一个数据结构。
class的isa
在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
union的isa
从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,使用位运算来获得更多的信息,但依旧是8位字符。
共用体与结构体类似,但内部所有成员将会共用(首元素的)内存。对整块内存通过位运算来进行扩展操作。
union isa_t
{
Class cls;
uintptr_t bits; //决定了共用体所占的内存大小。可以使用位运算
# if __arm64__ //位运算用掩码 bits & MASK \\ bits | MSAK
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
// 位域负责可读性展示.从右向左
struct {
uintptr_t nonpointer : 1; //占1位
uintptr_t has_assoc : 1;//占1位
uintptr_t has_cxx_dtor : 1;//占1位
//由于这种运算除了中间33位都是0,所以arm64下,所有类对象地址的64位中除了这33位一定都是0
uintptr_t shiftcls : 33; //占33位 //class对象的地址 bits&ISA_MASK
uintptr_t magic : 6;//占6位
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
.....
};
class_wr_t
存放类信息的可读写列表
基本介绍可以查阅OC对象本质中关于class_wr_t
的部分。
有一些东西不太重要的东西可以补充:
从源码上看,类在初始化时最开始的data() -> ro_t
,在初始化过程中才将data() -> rw_t
并且将ro_t
赋值给rw_t -> ro_t
method_t
方法(函数)封装
SEL name; //选择器(函数名 )
const char *types; //编码(返回值,参数类型)
IMP imp; //函数指针,存放函数地址
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
SEL
方法名,基本等同于C语言字符串 char*
typedef struct objc_selector *SEL;
不同类中的同名方法,其SEL相同
types
函数返回值,参数类型
可以通过@encode(type)函数查看类型对应的编码
// "i24@0:8i16f20" 数字代表参数/返回值所占字节
// 0id 8SEL 16int 20float == 24
- (int)test:(int)age height:(float)height;
需要注意的是,在构建方法签名(NSMethodSignature
)时,即使简化成i@:if
也是没有问题的。
cache_t
Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存
曾经调用过
的方法,可以提高方法的查找速度
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表长度-1
mask_t _occupied; //已缓存的方法数量
}
struct bucket_t {
cache_key_t _key; //SEL
IMP _imp; //IMP
}
查找的顺序
本类 -> 父类
在其中任一位置查找到IMP,都将会写入本类(在查找到的那一刻,直接通过cls
写入)缓存中。(父类本身则不会被缓存)
//LLDB中
p (IMP)0x0000123 可以打印内存地址所指的方法
objc_msgSend
OC的消息机制。内部会经历消息发送、动态方法解析、消息转发三个阶段
OC中的方法调用,其实都是转换为objc_msgSend函数的调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
[person personTest];
// 在编译是将会转化成
objc_msgSend(person, @selector(personTest));
}
return 0;
}
objc_msgSend底层,由汇编实现
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
//x0:寄存器 消息接收者 receiver
cmp x0, #0 // nil check and tagged pointer check
//一旦消息接收者为0 跳转(b.le)到 LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
//查找缓存
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
//返回0
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
objc_msgSend的消息机制,(在查找方法时)可以分为三个阶段:
消息发送、动态方法解析、消息转发。最后崩溃unrecognized selector sent to instance
消息发送阶段
在本类以及父类中查找方法
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
....
....
....
retry:
runtimeLock.assertReading();
// 查找本类缓存
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 查找本类方法列表
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 去查找父类
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
....
....
....
}
几个源码方面的点
- objc_msgSend本身由汇编编写
在未查找到缓存后进入正常代码lookUpImpOrForward
进行方法查找以及后续阶段操作。
- 本类会经历两次缓存查找
第一次是在汇编中直接查找缓存,第二次是在lookUpImpOrForward
方法中查找本类re_t之前再查找一次。
- 方法列表的查找分为两种
已经排序的,二分查找。没有排序的,遍历查找
-
只要查询到方法存在,就会写入调用者类缓存。
-
所有父类查找完毕仍未找到方法。进入动态方法解析。
动态方法解析
如果未查找到指定方法,runtime允许开发者进行一次方法动态添加。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
...
...
...
// 进行一次动态解析`triedResolver`
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
//内部会根据cls是否是元类调用不同的逻辑
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// 不会进行缓存
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
//重新走一遍方法查找,但如果还没有也不会再次被动态解析了。
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
//消息转发
....
....
done:
runtimeLock.unlockRead();
return imp;
}
resolveInstanceMethod
的返回值其实没什么用
runtime里只做了打印返回值的工作而已
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;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
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 {
// Method resolver didn't add anything?
_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));
}
}
}
- 每次发送消息,未实现的都有一次动态解析的机会
消息转发
如果经历动态解析依旧没有确定方法。runtime允许开发者重新指定target,甚至修改方法调用的各种参数再次调用。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
....
....
....
//查找失败,动态解析失败
//进行消息转发.内部由汇编实现
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
- forwardingTargetForSelector
如果forwardingTargetForSelector
指定了一个新的target
,会对其调用objc_msgSend,重新走一遍之前的逻辑。
- methodSignatureForSelector
有一种简便的调用方式,不需要自己编写type
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test:)) {
// return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
// return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
- forwardInvocation
如果一个方法调用可以进入forwardingTargetForSelector
,那么你可以对他进行任何操作。即使不invoke
也不会出现崩溃。
- 类方法 OC中没有直接暴露消息转发的+方法,但是编写是可以调用,并且对类方法进行操作的。
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MJPerson test];
}
return 0;
}
//因为是objc_msgSend是对MJPerson对象发送的消息,所以只有+方法才能被调用
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"1123");
}
- 让类方法被实例对象调用
只要将target对象修改成实例对象即可
objc_msgSend并不区分类方法与对象方法,二者只有消息接受者的区别而已。
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) return [[MJCat alloc] init];
return [super forwardingTargetForSelector:aSelector];
}
super
super
@implementation MJStudent
- (void)run {
[super run];
}
/cpp
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};
static void _I_MJStudent_run(MJStudent * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))}, sel_registerName("run"));
//化简一下
objc_msgSendSuper((__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("MJStudent"))},
sel_registerName("run"));
//继续化简
struct objc_super arg = {self, [MJPerson class]};
objc_msgSendSuper(arg, @selector(run));
}
可见在进行super调用时,使用的是objc_msgSendSuper
方法。在objc_super
结构体内部的receiver
依旧是self,只是在查找IMP时,从父类开始查找IMP实现而已。
isMemberOfClass && isKindOfClass
对象方法是比对
[self class]
,类方法是比对object_getClass(self)
所以在对类对象使用isMemberOfClass
或者isKindOfClass
时,需要对元类进行对比。
不过对于元类NSObject,其父类是NSObject所以会有点不同结果。
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
@end
结果如下:
// 这句代码的方法调用者不管是哪个类(只要是NSObject体系下的),都返回YES
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0
NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0
方法交换
交换两个方法的IMP,并且清空cache
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
// RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally?
flushCaches(nil);//清空类对象的方法缓存
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
LLVM编译器的中间代码
现在的LLVM编译器在将代码转成汇编之前,不再由CPP进行过度。而改为通过一种LLVM编译器专属的语言。
可以使用以下命令行指令生成中间代码
// 不需要指定架构
clang -emit-llvm -S main.m
之后会生成一个.ll文件。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[super forwardInvocation:anInvocation];
int a = 10;
int b = 20;
int c = a + b;
test(c);
}
define internal void @"\01-[MJPerson forwardInvocation:]"(%0*, i8*, %1*) #1 {
%4 = alloca %0*, align 8
%5 = alloca i8*, align 8
%6 = alloca %1*, align 8
%7 = alloca %struct._objc_super, align 8
%8 = alloca i32, align 4
%9 = alloca i32, align 4
%10 = alloca i32, align 4
store %0* %0, %0** %4, align 8
store i8* %1, i8** %5, align 8
store %1* %2, %1** %6, align 8
%11 = load %0*, %0** %4, align 8
%12 = load %1*, %1** %6, align 8
%13 = bitcast %0* %11 to i8*
%14 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 0
store i8* %13, i8** %14, align 8
%15 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
%16 = bitcast %struct._class_t* %15 to i8*
%17 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 1
store i8* %16, i8** %17, align 8
%18 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*, %1*)*)(%struct._objc_super* %7, i8* %18, %1* %12)
store i32 10, i32* %8, align 4
store i32 20, i32* %9, align 4
%19 = load i32, i32* %8, align 4
%20 = load i32, i32* %9, align 4
%21 = add nsw i32 %19, %20
store i32 %21, i32* %10, align 4
%22 = load i32, i32* %10, align 4
call void @test(i32 %22)
ret void
}
一些语法
@ - 全局变量
% - 局部变量
alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存
i32 - 32位4字节的整数
align - 对齐
load - 读出,store 写入
icmp - 两个整数值比较,返回布尔值
br - 选择分支,根据条件来转向label,不根据条件跳转的话类似 goto
label - 代码标签
call - 调用函数
一些实操注意点
利用关联对象(AssociatedObject)给分类添加属性
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
交换方法实现(交换系统的方法)
利用消息转发机制解决方法找不到的异常问题
如何给int类型的变量赋值
MJPerson *person = [[MJPerson alloc] init];
object_setIvar(person, nameIvar, @"123");
//先将10,转化为指针类变量指向10这个值。再将指针转化成id
object_setIvar(person, ageIvar, (__bridge id)(void *)10);
NSLog(@"%@ %d", person.name, person.age);