主要看下底层面试巩固下所学。
1.+load方法在什么时候调用?
在load_images的时候调用,通过prepare_load_methods方法准备,递归的方式schedule_class_load进行添加add_class_to_loadable_list加入实现+load方法的表里。分类要实现+load方法的分类加入add_category_to_loadable_list表里。准备好数据后开始调用call_load_methods()来实现+load方法。该方法分为主类call_class_loads和分类call_category_loads来实现。过程如下图:
2. 关于+load和+initialize以及C++的静态构造函数调用顺序?
- +load调用时通过
load_images的时候进行调用,通过call_load_methods实现类或者分类的的+load方法。 - +initialize实现是实现它的类或者分类
第一次发送消息的时候调用。 - C++的静态构造函数通常是
类初始化完成后调用,但是在底层源码中objc_init()会先调用static_init自己调用。 +load方法调用顺序先主类后分类,分类之间看编译顺序调用先后;主类和分类方法同名的时候,因为分类方法是在主类实现后加入的,所以优先调用分类的方法。
3.runtime是什么?
Runtime时数据类型的确定由编译时推迟到运行时,runtime是由C和C++汇编实现的一套API,为OC语言加入了面向对象,运行时功能。
例如:通常正常写的extension拓展就是编译时,编译的时候就确定了方法和属性加入到了类的ro中,而category分类则是通常情况下调用的时候才去添加到类的rw_e的方法列表
里。
平时写的oc代码,在程序运行过程中,最终会转换成Runtime的C语言代码,runtime就是oc的幕后实现者。
4.方法的本质,sel,IMP是什么?有什么关系?
- 方法本质是
发送消息objc_msgSend,消息的发送有一下几步:
快速查找,缓存中查找。- 没找到进入慢速查找
lookupImpOrForward,递归自己的父类链。 - 还是没找到进入方法动态决议:查看
resolveInstanceMethod或resolveClassMethod是否实现。 - 依然没找到进入消息快速转发,
forwardingTargetForSelctor去看看有没有别的类帮他实现。 5.没找到进入慢速转发,实现方法签名methodSignatureForSelector和forward Invocation找人实现。
sel是方法编号,在read_images期间编译进内存;imp就是具体实现的指针,找imp就是找函数的过程。- 它们关系通常
一一对应,method就是由sel和imp组成。比如我们要读一本书的具体内容,我们查看目录,sel就是目录的标题,imp就是具体的页数也就是实现地址。我们先要知道读什么内容(sel就是标题),通过标题找到对应的页数(imp),根据页数去读具体内容(方法实现)。
5.能否向编译后得到类中增加实例变量?能否向运行时创建的类中添加实例变量?
不能向编译后的类中增加实例变量;可以向运行时创建的类添加实例变量。
原因:只要还没有注册到内存就可以修改,编译好的实例变量储存在ro,一旦编译完成就不会改变了,但是可以运行时添加属性和方法。
6. [self class]和[super class]的区别以及原理分析
- [self class]就是发送消息
objc_msgSend,消息的接受者是self,方法编号:class。 - [super class]本质是
objc_msgSendSuper,消息的接受者还是self,方法编号:class。
最终都是
self的类。就向上面所说的本质都是发送消息,objc_msgSendSuper只是去父类中查找class的方法,调用还是self。super只是编译器一个特殊字符,并不代表父类的一个实例化对象。调用的主体还是self。
[super class]在编译的时候是
objc_msgSendSuper,在运行的时候是objc_msgSendSuper2(这里可以通过汇编跟踪一下),通过objc_msgSendSuper的汇编查看,在里面有跳转到objc_msgSendSuper2的代码。
objc_msgSendSuper2会去查询当前传入的class,而不是superclass。
7.Runtime是如何实现weak的?为神马可以置为nil?
1.通过SideTable找到我们的weak_table
2.weak_table根据referent找到或创建weak_entry_t
3.然后append_refereer(entry, referrer)将我们新的弱引用的对象加进去entry
4.最后weak_entry_insert把entry加入我们的weak_table.
8.Runtime Associate方法关联的对象,需要在dealloc中释放吗?
当我们释放对象的时候dealloc
1.c++函数释放:object_cxxDestruct。
2.移除关联属性:_object_remove_associations
3.将弱引用自动设置nil:weak_clear_no_lock(&table.weak_table, (id)this)
4.引用计数处理:table.refcnts.erase(this)
5.销毁对象:free(obj)
因此关联对象不需要我们手动释放,将在dealloc中释放。
源码中dealloc实现进入_objc_rootDealloc,看下rootDealloc
里面有关联属性设置的
bool值,当有这些条件的时候进入else流程。
object_dispose主要是销毁实例对象
在
objc_destructInstance中就有进行关联属性的移除操作
具体的移除操作,和我们之前探索的
关联对象属性类似,只是过程是相反的。
9.内存平移问题
- (void)saySomething{
NSLog(@"%s,__func__);
}
Class cls = [LGPerson class];
void *kc = &cls;
LGPerson *person = [LGPerson alloc];
[person saySomething];
[(__bridge id)kc saySomething];
通过上面的方法是否可以调用实例方法?为什么?
结果是可以调用的,我们知道方法的本质是消息发送
objc_msgSend。我们之前探究知道实例对象的isa指向该对象的类,实例方法就是先进行在当前类的cache_t中快速查找,没找到进行慢速查找,也就是我们方法查找流程。
而我们通过 &cls获取当前类的地址,相当于实例对象根据它的isa获取类的地址,之后方法查找一样的流程,找到方法的imp,进行消息发送。
方法saySomething里面有属性 self.kc_hobby 的打印
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString * kc_name;
@property (nonatomic, copy) NSString *kc_hobby; // 12
- (void)saySomething;
@end
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.kc_hobby);
}
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
LGPerson *person = [LGPerson alloc];
[person saySomething];
打印结果
并没有给
kc_hobby赋值?为什么实例调用有值,但是却是viewController,而类方法中没有?
我们知道self.kc_hobby,通过getter方法获取,实例对象是由isa和成员变量组成,获取属性的值的时候是进行实例对象进行偏移得到的,我们clang下自定义的文件得到:
我们在源码中看下
objc_getProperty的实现,和我们clang编译的一样的。
因此我们在
[person saySomething]中调用self.kc_hobby是LGPerson实例对象开辟的内存进行偏移获取值的。而[(__bridge id)kc saySomething] 中kc只是伪装的对象,并没有实际开辟内存空间,因此获取不是当前对象的属性。
kc是一个指针,存在栈中的,栈遵循先进先出,因此参数传入就是一个不断压栈的过程。
1.其中隐藏参数会压入栈,每个函数都有2个隐藏参数(id self,sel _cmd)
2.栈中,地址是衰减的,从高地址到底地址进行分配。因此在栈中,参数会从前往后进行压栈
3.super我们知道消息发送是通过objc_msgSendSuper或者objc_msgSendSuper2其中第一个结构体,结构体是怎么压栈的
结构体objc_super:
我们写一个
结构体,打印一下里面地址
在结构体内部在堆中,地址是从低到高,0x00007ffee3635438<--0x00007ffee3635440即
10<-20;而外部地址是高到低:0x00007ffee3635448->0x00007ffee3635438即person<——>str
因此栈中顺序self-->_cmd-->class_getSuperclass-->self-->cls-->kc-->person
我们把它打印出来看下
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls;
LGPerson *person = [LGPerson alloc];
NSLog(@"%p - %p",&person,kc);
// 隐藏参数 会压入栈帧
void *sp = (void *)&self;//取当前开始的栈地址
void *end = (void *)&person;//结束
long count = (sp - end) / 0x8;//指针8字节压栈
for (long i = 0; i<count; i++) {
void *address = sp - 0x8 * i;
if ( i == 1) {
NSLog(@"%p : %s",address, *(char **)address);//cmd
}else{
NSLog(@"%p : %@",address, *(void **)address);
}
}
[(__bridge id)kc saySomething];
[person saySomething];
打印一下看下结果,因为kc是伪装的实例对象,因此系统也将它平移0x8获取属性,结果获取的是self(viewcontroller)
大致流程
10. Method-Swizzling方法交换的应用和坑点
method-swizzling是方法交换,主要作用是运行时替换方法,把一个方法换成另一个方法实现。通常所知的iOS黑魔法。
- oc中就是利用方法交换实现
AOP面向切面编程。AOP时进行方法封装,提取公共部分,提高复用率。比如我们方法崩溃处理,埋点处理等都是AOP的体现。 OOP倾向业务逻辑的封装,面向对象编程 方法Method时由sel和imp组成,方法交换就是把2个方法原本对应的sel-imp断开重新组合正常情况:
交换后
坑点1:method-swizzling使用过程中的一次性问题
method-swizzling方法交换写在+load中,但是+load方法可能会主动调用多次,就会重复交换,可能会使方法还原,导致交换失败。
解决方法:使用单例模式,只走一次 dispatch_once_t实现
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
坑点2:子类没有实现,父类实现了
定义父类LGPseosn
@interface LGPerson : NSObject
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
+ (void)personClassMethod{
NSLog(@"person类方法:%s",__func__);
}
@end
定义子类LGStudent继承LGPerson,并在它的分类中实现方法交换
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp
- (void)lg_studentInstanceMethod{
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}
@end
******************************************
@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
@end
在ViewController中调用
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
运行报错,方法没找到
- student调用父类方法没问题,但是在分类
+load中进行了方法交换替换成了lg_studentInstanceMethod,所以实际上调用的lg_studentInstanceMethod而在该方法中我们又调用了[self lg_studentInstanceMethod],但是没有递归。因为此时lg_studentInstanceMethod方法的imp交换后指向了personInstanceMethod的实现。 - 当我们父类调用它自己的方法
personInstanceMethod由于方法交换imp实现实际上是lg_studentInstanceMethod的imp,父类没法继承子类的方法,因此imp找不到就方法报错。 - 解决:我们子类替换了父类的方法的imp,所以想
不影响父类的情况又想子类方法交换到父类。我们可以先判断子类是否有父类的方法,没有的话我自己把父类方法添加一份到自己的方法列表,这个时候在进行交换,就不会影响父类了。
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
// oriSEL personInstanceMethod
// swizzledSEL lg_studentInstanceMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 尝试添加。相当于当前的添Student添加lg_studentInstanceMethod
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
坑点3:子类没有实现,父类也没有实现,下面的调用有什么问题?
- (void)viewDidLoad {
[super viewDidLoad];
LGStudent *s = [[LGStudent alloc] init];
[s helloword];
******************分类****************
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
- (void)lg_studentInstanceMethod{
NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
[self lg_studentInstanceMethod];
}
@end
我们运行会进行死循环,最终会导致内存溢出。
helloword我们没有实现因此没有imp,交换的时候始终找不到oriMethod,交换了寂寞。当我们把helloword的imp换成了lg_studentInstanceMethod的,lg_studentInstanceMethod自己的imp却是空的了。当进入lg_studentInstanceMethod中没有指向oriMethod,所以就会自己掉自己造成死循环。
- 解决:如果方法没有实现就
没有交换的必要了,我们可以自己指向一个imp就行操作。
+ (void)lg_bestMethodSwizzlingWithClass:(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) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :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);
}
}