类的方法和分类的方法重名,执行的是哪一个方法?
首先如果重名方法不是Load
方法,那么先执行分类方法
那么如果重名方法是Load
方法,那么先执行主类再执行分类方法。
下面来解释一下,为什么重名方法是Load
方法时,它先执行的是主类再执行分类;
首先我们需要了解,类中方法的加载在objc
源码中的load_images
方法中;来看看源码实现:!
看上图方框中的代码,就是发现方法,而call_load_methods
就是调用方法;
下面去看看prepare_load_methods
方法实现:
在看到源码实现后,可以清楚的看到_getObjc2NonlazyClassList
非懒加载类的加载。然后去看看类加载的实现在方框中的schedule_class_load
方法(后面的方法是加载分类的非懒加载方法,其代码与下面主类方法的加载类似):
在这个方法中,使用了递归调用,知道cls
不存在,就找完了。接下来就是add_class_to_loadable_list
方法:
此方法的作用就是将所有非懒加载类的方法添加到loadable_classes
这张表中;
至于方法的发现就已经清楚了,首先发现主类方法添加到表中,在add_class_to_loadable_list
中有源码实现,然后加载分类方法,同样添加到表中,在add_category_to_loadable_list
方法有实现。
接下来就是对类中方法的加载了,同样的回答load_images
方法中的call_load_methods
方法:
看到这个方法,其实很简单,首先对主类的加载调用,然后对分类方法的加载调用,最后释放内存空间。
那么方法的加载就已经很清晰了,首先调用call_class_loads
加载主类的方法,然后调用call_category_loads
加载分类的方法,而所有方法的加载都在load_images
方法中。
这道面试题其实是有关方法的调用顺序的,下面做一个总结:
普通方法:包括initialize,因为分类方法是在类realize之后attach进去的,插在前面,所以优先调用分类的方法
注意 : 不是分类覆盖主类哦!!!!
Load方法:
1: 主类load
2: 分类load (分类之间 看编译的顺序)
方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
SEL : 方法编号
IMP : 函数指针地址
方法的本质:发送消息, 消息会有以下几个流程
1:快速查找 (objc_msgSend)~ cache_t 缓存消息
2:慢速查找~ 递归自己| 父类 ~ lookUpImpOrForward
3:查找不到消息: 动态方法解析 ~ resolveInstanceMethod
4:消息快速转发~ forwardingTargetForSelector
5:消息慢速转发~ methodSignatureForSelector & forwardInvocation
sel 是方法编号 ~ 在read_images 期间就编译进入了内存
imp 就是我们函数实现指针 ,找imp 就是找函数的过程
sel 就相当于书本的目录 tittle
imp 就是书本的⻚码 查找具体的函数就是想看这本书里面具体篇章的内容
1:我们首先知道想看什么 ~ tittle (sel)
2:根据目录对应的⻚码 (imp)
3:翻到具体的内容
能否向编译后的得到的类中增加实例变量?能否想运行时创建的类中添加实例变量
答案:
1:不能向编译后的得到的类中增加实例变量
2:只要内没有注册到内存还是可以添加
原因:我们编译好的实例变量存储的位置在 ro,一旦编译完成,内存结构就完全确定 就无法修改
可以添加属性 + 方法
[self class]和[super class]的区别以及原理分析
下面看一段代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Teacher *teacher = [[Teacher alloc] init];;
NSLog(@"%@",teacher);
}
return 0;
}
@implementation Teacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
当执行程序,打印的结果都会是Teacher
。
那么为什么会是这样一个结果呢?
首先[self class]
我们是可以理解的,他的底层实现是返回object_getClass(self)
,而object_getClass
方法获取的是该类的Isa
,那就很好理解了,所以打印的是Teacher
。
而[super class]
就比较难理解了,首先super
是一个关键字,而self
其实是init
方法的隐藏参数。那么现在就需要去了解super
是什么东西;
通过clang
转换cpp文件,找到init
方法实现:
static instancetype _I_Teacher_init(Teacher * self, SEL _cmd) {
self = ((Teacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Teacher"))}, sel_registerName("init"));
if (self) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_Teacher_d9115f_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")),
objc_msgSendSuper(
{(id)self, (id)class_getSuperclass(objc_getClass("Teacher"))},
sel_registerName("class")));
}
return self;
}
可以看到super
关键字转换的是class_getSuperclass
;
下图是objc_super
的结构,他的第一个参数就是receiver
,也就是一个消息接收者,而看cpp
文件中init
实现后,super
的第一个参数就是self
,那也就可以解释的通了,为什么打印的是Teacher
了。
结果是对的,但是过程不一定就对了;
首先我们需要明白,用self
和super
的区别在哪,那就是super
少走了一层方法,这个在类和isa的走位图中有详细解释,首先super
的调用,让系统节约了在Teacher
这一层找方法,而是直接从父类开始找方法。
而super
的底层实现并不是class_getSuperclass
而是objc_msgSendSuper2
,那是如何知道的呢?
在init
方法的if判断处打上断点,通过查看汇编的形式去看:
可以看到它在之前调用了objc_msgSendSuper2
方法,而不是走class_getSuperclass
,这就是运行时的处理。而objc_msgSendSuper2
的底层实现是用汇编写的,就不详细介绍了。
面试题,内存平移
首先看一段代码:
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [Person class];
void *kc = &cls; //
Person *person = [Person alloc];
[(__bridge id)kc saySomething];
[person saySomething];
}
- (void)saySomething{
NSLog(@"%s",__func__);
}
看上面的代码:kc
是指向cls
的内存地址,当使用cls
和kc
执行saySomething
方法,打印的结果都是一样的;
那么现在修改部分代码:
NSLog(@"%s - %@",__func__,self.name);
其中name
是Person
类的一个NSString
类型的属性,并没有对name
赋值;
那么执行代码后,结果却不一样了:
可以看到使用person
调用方法的结果为null
,这是什么愿意导致的?
下面打印一下类的地址信息:
Class cls = [Person class];
void *kc = &cls; //
Person *person = [Person alloc];
NSLog(@"%p - %p",&person,kc);
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i
上图打印的信息依次为:
self cmd (id)class_getSuperclass(objc_getClass("Person")) self cls kc person
这些信息都是改控制器中的内存地址信息,类似于栈的结构一样,先进后出。
而在kc
和person
调用方法时,首先由kc调用,打印的是栈顶的第一个元素0x7ffeece44058 :
,而当person调用方法时,它会先经过平移内存,去找到name
,平移8字节,得到的是0x7ffeece44050 : viewDidLoad
,而取的值与属性类型不匹配,因此打印的结果为null
,那么就很好理解了。
如果将name
类型改为int
型,再执行程序会出现问题,因为int
是4字节,无法识别读取的内容。