关键字 super
关键字super
,在调用[super init]
的时候,super
会转化成结构体__rw_objc_super
1 | struct __rw_objc_super { |
[super init]
使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 Student.m
转化成cpp
打开cpp
大概在底部的位置找到
1 | (Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init")) |
简化之后是
1 | (void *)objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init")) |
void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
其实是向父类发送消息,参数是struct objc_super *super, SEL op, ...
,我们源码中找到了该函数的实现在objc-msg-arm64.s
1 | ENTRY _objc_msgSendSuper |
将self
和superclass
赋值给 p0, p16
调用CacheLookup NORMAL
1 | .macro CacheLookup //.macro 是一个宏 使用 _cmd&mask 查找缓存中的方法 |
汇编比较多,只看到第二行p1 = SEL, p16 = isa
,查找缓存是从p16
,也就是superclass
开始查找,后边的都和objc_msgSend
一样。
大致上比较清楚了,super
本质上调用了objc_msgSendSuper
,objc_msgSendSuper
是查找从父类开始查找方法。
[super init]
就是self
直接调用父类init
的方法,但是objc_msgSend
接受者是self
,假如是[self init]
则会产生死循环。[super test]
则是执行父类的test
。
使用Debug Workflow->Always Show Disassemdly
发现super
其实调用了汇编的objc_msgSendSuper2
,进入objc_msgSendSuper2 objc-msg-arm64.s 422 行
发现和objc_msgSendSuper
其实基本一致的
1 | //_objc_msgSendSuper 开始 |
也可以使用LLVM
转化成中间代码来查看,clang -emit-llvm -S FYCat.m
查看关键函数
1 | define internal void @"\01-[FYCat forwardInvocation:]"(%1*, i8*, %2*) #1 { |
这是forwardInvocation
函数的调用代码,简化之后是objc_msgSendSuper2(self,struct._objc_super i8*,%2*)
,就是objc_msgSendSuper2(self,superclass,@selector(forwardInvocation),anInvocation)
。
验证
1 | @interface FYPerson : NSObject |
test
是执行父类的方法,[super age]
获取父类中固定的age
,[self name]
从父类开始寻找name
的值,但返回的是self.name
的值。
isMemberOfClass & isKindOfClass
1 | + (BOOL)isMemberOfClass:(Class)cls { |
- (BOOL)isMemberOfClass
和- (BOOL)isKindOfClass:(Class)cls
比较简单,都是判断self.class
和cls
,+ (BOOL)isMemberOfClass:(Class)cls
是判断self.class->isa
是否和cls
相等,+ (BOOL)isKindOfClass:(Class)cls
判断cls->isa
和cls->isa->isa
有没有可能和cls
相等?只有基类是,其他的都不是。
验证 实例方法
1 | Class cls = NSObject.class; |
p
是pcls
的子类,obj
是cls
的子类,在明显不过了。
验证 类方法
1 |
|
堆栈 对象本质 class本质实战
网上看到了一个比较有意思的面试题,今天我们就借此机会分析一下,虽然网上很多博文已经讲了,但是好像都不很对,或者没有讲到根本的东西,所以今天再来探讨一下究竟。
其实这道题考察了对象在内存中的布局,类和对象的关系,和堆上的内存布局。基础知识不很牢固的同学可以看一下我历史的博文obj_msgsend基础、类的本质、对象的本质。
1 | @interface FYPerson : NSObject |
问题一 能否编译成功?
当大家看到第二个问题的时候,不傻的话都会回答能编译成功,否则还问结果干嘛。我们从之前学的只是来分析一下,调用方法成功需要有id self
和SEL sel
,现在cls
和obj
都在栈区,obj
指针指向cls
的内存地址,访问obj
相当于直接访问cls
内存存储的值,cls
存储的是Person.class
,[obj print]
相当于objc_msgSend(cls,@selector(print))
,cls
是有print
方法的,所以会编译成功。
输出什么?
fix/cls/obj
这三个对象都是存储在栈上,fix/cls/obj
地址是连续从高到低的,而且他们地址相差都是8
字节,一个指针大小是8
字节。他们三个地址如下所示:
使用图来表示fix
和obj
:
对象 | 地址 | 地址高低 |
---|---|---|
fix | 0x7ffeec3df920 | 高 |
cls | 0x7ffeec3df918 | 中 |
obj | 0x7ffeec3df910 | 低 |
寻找属性先是寻找isa
,然后再在isa
地址上+8
则是属性的值,所以根据obj
寻找cls
地址是0x7ffeec3df918
,然后cls
地址+8字节则是_name
的地址,cls
地址是0x7ffeec3df918
,加上8
字节正好是fix
的地址0x7ffeec3df920
,因为都是指针,所以都是8
字节,所以最后输出是结果是fix
对象的地址的数据。
情况再复杂一点,FYPerson
结构改动一下1
2
3
4
5
6@interface FYPerson : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *name2;
@property (nonatomic,copy) NSString *name3;
- (void)print;
@end
则他们的_name
、_name2
、_name3
则在cls
的地址基础上再向上寻找8*1=8/8*2=16/8*3=24
字节,就是向上寻找第1个,第2个,第3个指向对象的指针。
测试代码:
1 | @interface FYPerson : NSObject |
再变形:
1 | - (void)viewDidLoad { |
_name1
是cls
地址向上+8字节,_name2
是向上移动16字节,[super viewDidLoad]
本质上是objc_msgSuperSend(self,ViewController.class,sel)
,self
、ViewController.class
、SEL
是同一块连续内存,布局由低到高,看了下图的内存布局就会顿悟,
结构体如下图所示:
对象 | 地址高低 |
---|---|
self | 低 |
ViewController.class | 中 |
SEL | 高 |
常用的runtimeAPI
method | desc |
---|---|
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) | 动态创建一个类(参数:父类,类名,额外的内存空间 |
void objc_registerClassPair(Class cls)) | 注册一个类 |
void objc_disposeClassPair(Class cls) | 销毁一个类 |
Class objcect_getClass(id obj) | 获取isa指向的class |
Class object_setClass (id obj,Class cls) | 设置isa指向的class |
BOOL object_isClass(id class) | 判断oc对象是否为Class |
BOOL class_isMetaClass(Class cls) | 是否是元类 |
Class class_getSuperclass(Class cls) | 获取父类 |
Ivar class_getInstanceVariable(Class cls ,const char * name | 获取一个实例变量信息 |
Ivar class_copyIvarList(Class cls,unsigned int outCount) | 拷贝实例变量列表,需要free |
void object_setIvar(id obj,Ivar ivar,id value | 设置获取实例变量的值 |
id object_getIvar(id obj,Ivar ivar) | 获取实例变量的值 |
BOOL class_addIvar(Class cls,const cahr name ,size_t size,uint_t alignment,const char types) | 动态添加成员变量(已注册的类不能动态添加成员变量) |
const char * ivar_getName(Ivar v) | 获取变量名字 |
const char * ivar_getTypeEncoding(Ivar v) | 变量的encode |
objc_property_t class_getProperty(Class cls,const char* name) | 获取一个属性 |
objc_property_t _Nonnull _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int _Nullable outCount) | 拷贝属性列表 |
objc_property_t _Nullable class_getProperty(Class _Nullable cls, const char * _Nonnull name) | 获取属性列表 |
BOOL class_addProperty(Class _Nullable cls, const char _Nonnull name,const objc_property_attribute_t _Nullable attributes,unsigned int attributeCount) | 添加属性 |
void class_replaceProperty(Class _Nullable cls, const char _Nonnull name,const objc_property_attribute_t _Nullable attributes, unsigned int attributeCount) | 替换属性 |
void class_replaceProperty(Class cls, const char name, const objc_property_attribute_t attributes,unsigned int attributeCount) | 动态替换属性 |
const char * _Nonnull property_getName(objc_property_t _Nonnull property) | 获取name |
const char * _Nullable property_getAttributes(objc_property_t _Nonnull property) | 获取属性的属性 |
IMP imp_implementationWithBlock(id block) | 获取block的IMP |
id imp_getBlock(IMP anIMP) | 通过imp 获取block |
BOOL imp_removeBlock(IMP anIMP) | IMP是否被删除 |
… | … |
在业务上有些时候需要给系统控件的某个属性赋值,但是系统没有提供方法,只能靠自己了,那么我们
获取class
的所有成员变量,可以获取Ivar
查看是否有该变量,然后可以通过KVC
来赋值。
1 |
|
大家常用的一个功能是JsonToModel
,那么我们已经了解到了runtime
的基础知识,现在可以自己撸一个JsonToModel
了。
1 | @interface NSObject (Json) |
然后自己定义一个字典,来测试一下这段代码
1 | @interface FYCat : NSObject |
hook钩子(method_exchangeImplementations)
由于业务需求需要在某些按钮点击事件进行记录日志,那么我们可以利用钩子来实现拦截所有button的点击事件。
1 | @implementation UIButton (add) |
可以在code here
添加需要处理的代码,一般记录日志和延迟触发都可以处理。[self fy_sendAction:action to:target forEvent:event];
不会产生死循环,原因是在+load
中已经将m1
和m2
已经交换过了IMP
。我们进入到method_exchangeImplementations
内部:
1 | void method_exchangeImplementations(Method m1, Method m2) |
m1
和m2
交换了IMP
,交换的是method_t->imp
,然后刷新缓存(清空缓存),等下次调用IMP
则需要在cls->rw->data->method
中去寻找。
数组越界和nil处理
1 | @implementation NSMutableArray (add) |
NSMutableArray
是类簇,使用工厂模式,NSMutableArray
不是数组实例,而是生产数组对象的工厂。
真实的数组对象是__NSArrayM
,然后给__NSArrayM
钩子,交换objectAtIndexedSubscript:(NSUInteger)idx
和insertObject:(id)anObject atIndex:(NSUInteger)index
方法,实现崩溃避免。
字典nil处理
1 | @interface NSMutableDictionary (add) |
利用类别+load
给__NSDictionaryM
添加方法,然后交换IMP
,实现给NSMutableDictionary setObject:Key:
的时候进行nil
校验,+load
虽然系统启动的自动调用一次的,但是为防止开发者再次调用造成IMP
和SEL
混乱,使用dispatch_once
进行单次运行。
总结
super
本质上是self
调用函数,不过查找函数是从sueprclass
开始查找的+isKandOfClass
是判断self
是否是cls
的子类,+isMemberOfClass:
是判断self
是否和cls
相同。- 了解
+load
在Category
是启动的时候使用运行时编译的,而且只会加载一次,然后利用objc/runtime.h
中method_exchangeImplementations
实现交换两个函数的IMP
,可以实现拦截nil
,降低崩溃率。 NSMutableDictionary
、NSMutableArray
是类簇,先找到他们的类然后再交换该类的函数的IMP
。
资料参考
- 小码哥视频
资料下载
- 学习资料下载
- demo code
- runtime可运行的源码
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间