前言
这篇文章会简单介绍OC的几道底层相关的面试题。
load方法什么时候调用?
我们在objc源码中,搜下load_images方法(该方法在前几篇文章有介绍),如图所示:
load_images在dyld在调用map_images和load_imges调用的。
dyld会注册_dyld_objc_notify_register这个回调,这个回调在_objc_init这个函数中调用的时候,会有两个参数map_images和load_imges,这个时候会调起load_images。在iOS底层-dyld加载分析中有介绍。
prepare_load_methods这个函数是load方法的准备,我们看下它的代码:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, I;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[I];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
** schedule_class_load**这个函数扫描所有类的load方法,它的代码如下:
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->getSuperclass());
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
这里递归获取类(包括父类)的Load方法,并且调用这个add_class_to_loadable_list这个函数添加到loadable_class这个全局静态变量中(这个表结构是key-value的字典形式存储),我们贴下add_class_to_loadable_list这个函数的代码:
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
schedule_class_load这个函数会调用**cls->setInfo(RW_LOADED); **这个函数设置一个标记。
prepare_load_methods这个函数类搜集完后,开始搜集分类,以下代码就是:
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[I];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
会调用add_category_to_loadable_list这个函数据,把load方法以key-value的形存储在loadable_categories这张表中。
类的load和分类的load方法搜索完后,接着回到load_images这个函数中,调用了call_load_methods这个函数。
在call_load_methods这个函数中会do while循环调用call_class_loads和call_category_loads。
** call_load_methods在这个函数中循环从loadable_classes取出类的load方法,然后往相应的类发送消息,从以上可以看出load方法是在_objc_init的时候加载的**。
它的代码如下:
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
int I;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
分类的Load方法加载call_category_loads这个函数原理基本一样,这里不再介绍,我们只贴下代码:
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
// Compact detached list (order-preserving)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[I];
} else {
shift++;
}
}
used -= shift;
// Copy any new +load candidates from the new list to the detached list.
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[I];
}
// Destroy the new list.
if (loadable_categories) free(loadable_categories);
// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}
return new_categories_added;
}
从上面可以得出:
1. initialize方法与Load方法哪个先调用?
答案:load方法先调用,initialize是在第一次消息发送的时候调用,也就是在lookUpImpOrForward的时候调用的。
load方法->C++构造函数(这个是在objc源码中实现的)->main函数
load方法->main函数->C++构造函数(这个是在OC中实现的)。
2. 分类的加载顺序?
跟Build Phases中的顺序有关(编译顺序)。
Runtime是什么
runtime 是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能
运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时
平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代码,Runtime
是 Object-C
的幕后⼯作者。
⽅法的本质,SEL是什么?IMP是什么?两者之间的关系⼜是什么?
SEL 是⽅法编号 ~ 在read_images 期间就编译进⼊了内存
IMP 就是我们函数实现指针 ,找imp 就是找函数的过程
SEL 就相当于书本的⽬录 tittle
IMP 就是书本的⻚码
SEL : ⽅法编号
IMP : 函数指针地址
⽅法的本质:发送消息 ,消息会有以下⼏个流程
1:快速查找 (objc_msgSend)~ cache_t 缓存消息
2:慢速查找~ 递归⾃⼰| ⽗类 ~ lookUpImpOrForward
3:查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod
4:消息快速转发~ forwardingTargetForSelector
5:消息慢速转发~ methodSignatureForSelector & forwardInvocation
能否向编译后的得到的类中增加实例变量?能否想运⾏时创建的类中添加实例变量
答案:
1:不能向编译后的得到的类中增加实例变量
2:只要类没有注册到内存还是可以添加
原因:我们编译好的实例变量存储的位置在 ro,⼀旦编译完成,内存结构就完全确定
就⽆法修改,可以添加属性 + ⽅法
[self class]和[super class]的区别以及原理分析
我们在RoTeacher.m文件中加入以下代码:
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
这里的[self class]和[super class]返回的是什么,我们分析下。
从运行结果来看都是RoTeacher,这j时RoTeacher是继承于NSObject。
我们让RoTeacher继承于RoPerson,这时候运行,发现[self class]和[super class]打印的依然是RoTeacher。
我们知道RoTeacher是继承于RoPerson,按道理来讲[super class]应该输出RoPerson,为什么是RoTeacher呢?我们继续分析。
我们看下class的代码:
- (Class)class {
return object_getClass(self);
}
这个方法是来自于NSObject,因为RoTeacher和RoPerson都没有这个方法。
我们先看[self class]这个调用,针对上面的代码,object_getClass(self)这里的self是指RoTeacher,为什么呢,self是什么?
- (Class)class {
return object_getClass(self);
}
这里相当于
- (Class)class (id self, sel _cmd){
return object_getClass(self);
}
是隐藏的参数名字。
我们先看下[self class](这里的self是RoTeacher):
class方法在底层会转化为成objc_msgSend(id receive, sel _cmd),这里的receive就是self,所以这里调用class方法中的self是RoTeacher,这里的self是实例变量,我们再看下object_getClass这个函数的代码:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
这里的obj参数是self,也就是RoTeacher的实例对象,实例对象的isa指针是指向类,所以这里返回的是RoTeacher。
我们再看[super class]调用:
super在这里是关键字。
[super class]在底层会走objc_msgSendSuper这个函数,objc_msgSendSuper(void /* struct objc_super super, SEL op, ... / )这个是它的objc_msgSendSuper。
我们看下objc_super结构体**,如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
id receiver是消息接受者,
super_class代表的是第一个查找的类,并不是它的父类。
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);函数,同样也有objc_super结构体。
[super class]在编译的时候是objc_msgSendSuper,在运行的时候是objc_msgSendSuper2(这里可以通过汇编跟踪一下),通过objc_msgSendSuper的汇编查看,在里面有跳转到objc_msgSendSuper2的代码。
[super class]的调用走到objc_msgSendSuper2时,它的objc_super这个参数中的id receiver是消息接受者,在这里就是RoTeacher,所以在走到以下代码时
- (Class)class (id self, sel _cmd){
return object_getClass(self);
}
这里的self也就是RoTeacher,[super class]打印的也是RoTeacher。
在这里:
self是形参名字。
super是关键字。
内存平移相关面试题
首先我们把RoPerson的代码贴出来,如下:
RoPerson.h代码:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface RoPerson : NSObject
@property (nonatomic, copy) NSString *ro_name;
//@property (nonatomic, copy) NSString *ro_hobby;
- (void)saySomething;
@end
NS_ASSUME_NONNULL_END
RoPerson.m代码:
#import "RoPerson.h"
@implementation RoPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
ViewController.h代码:
#import "ViewController.h"
#import "RoPerson.h"
@interface ViewController ()
@end
@implementation ViewController
// 高地址 -> 低地址
void roFunction(id person, id roSel, id roSel2){
NSLog(@"person = %p",&person);
NSLog(@"person = %p",&roSel);
NSLog(@"person = %p",&roSel2);
}
struct ro_struct{
NSNumber *num1;
NSNumber *num2;
} ro_struct;
- (void)viewDidLoad {
[super viewDidLoad];
RoPerson *person = [RoPerson alloc];
[person saySomething];
Class cls = [RoPerson class];
void *ro = &cls; //
[(__bridge id)ro saySomething];
}
@end
1. [person saySomething];是否可以调起来并输出打印?
2. [(__bridge id)ro saySomething];是否可以调起来并输出打印?
我们分析下。
首先在[person saySomething];打个断点,并运行项目,
如图:
两个都可以正常调起。
[person saySomething];可以正常调起,为什么[(__bridge id)ro saySomething];这里可以调起呢?我们继续分析。
saySomething是在RoPerson类的data里面,person这个对象是如何找到saySomething这个方法呢,是通过首地址isa找到RoPerson类,然后不断的平移,找到methodList,然后二分查找,找到saySomething这个方法,也就是说,person对象是通过isa(即首地址去查找,这里的isa指向了类RoPerson)。
我们再来看下[(__bridge id)ro saySomething];:
Class cls = [RoPerson class];获取RoPerson类,void *ro = &cls;把cls这个对象的地址给到ro,这时的ro就是指向RoPerson的首地址即isa指向了RoPerson类,所以可以调起saySomething。
接着我们改动下代码:
RoPerson的代码:
#import "RoPerson.h"
@implementation RoPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.ro_name);
}
@end
请问这个时候[person saySomething];和[(__bridge id)ro saySomething];各打印什么。
我们运行项目后,得到如下图:
ro_name没有赋值,[person saySomething];调用的时候为null正常,那么[(__bridge id)ro saySomething];打印的是RoPerson,为什么会这样?我们继续分析,我们终端打下person对象,如图:
4
person对象跟这里的[(__bridge id)ro saySomething];地址一样,我们对比下person和ro**。
person是一个对象,开辟了内存空间,也就是具备内存,它的内存结构,如下:
isa
成员变量(这里有ro_name成员变量)
ro是一个指针地址,它没有内存空间,不具备内存。
person是怎么访问ro_name,是通过内存平移,不断的从开辟的平移偏移量获取数据。
接着我们再修改下ViewController的viewDidLoad方法,如下:
RoPerson *person = [RoPerson alloc];
person.ro_name = @"robert";
[person saySomething];
Class cls = [RoPerson class];
void *ro = &cls;
[(__bridge id)ro saySomething];
我们把RoPerson的ro_name改为retain。
我们修改ViewController代码,如下:
#import "ViewController.h"
#import "RoPerson.h"
@interface RoPersonP : NSObject
@property (nonatomic, retain) NSString *ro_name;
//@property (nonatomic, copy) NSString *ro_hobby;
- (void)saySomething;
@end
@implementation RoPersonP
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.ro_name);
}
@end
@interface ViewController ()
@end
@implementation ViewController
// 高地址 -> 低地址
void roFunction(id person, id roSel, id roSel2){
NSLog(@"person = %p",&person);
NSLog(@"person = %p",&roSel);
NSLog(@"person = %p",&roSel2);
}
struct ro_struct{
NSNumber *num1;
NSNumber *num2;
} ro_struct;
- (void)viewDidLoad {
[super viewDidLoad];
RoPersonP *person = [RoPersonP alloc];
person.ro_name = @"robert";
[person saySomething];
Class cls = [RoPersonP class];
void *ro = &cls;
[(__bridge id)ro saySomething];
}
@end
接着在终端执行clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.5.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk ViewController.m这个命令,我们看下ViewController.cpp文件。
我们找到ro_name的getter方法static NSString * _I_RoPersonP_ro_name(RoPersonP * self, SEL _cmd) { return (*(NSString *_strong *)((char *)self + OBJC_IVAR$_RoPersonP$_ro_name)); },
它的查找方式,是(char *)self(self是ViewController)+ OBJC_IVAR_$_RoPersonP$_ro_name)(偏移)。
extern "C" unsigned long int OBJC_IVAR__ro_name attribute ((used, section ("__DATA,__objc_ivar"))) = OFFSETOFIVAR(struct RoPersonP, _ro_name);这就是OBJC_IVAR_$_RoPersonP$_ro_name。也就是首地址平移。
person这个对象找ro_name,也就是person的首地址+平移8字节。
而ro访问ro_name时,也模访person,ro+平移8字节,而ro不具备内存,是Class,是在ViewController中的viewDidLoad方法压栈的地址,也就是cls地址+8字节。
我们再打断点,运行项目,如图:
5
从上图可以看出person与ro刚好差了8字节,ro平移8字节,刚好指向person对象。
我们在RoPerson在加一个属性,RoPerson的代码
.h
@interface RoPerson : NSObject
@property (nonatomic, retain) NSString *ro_name;
@property (nonatomic, copy) NSString *ro_hobby;
- (void)saySomething;
@end
.m
@implementation RoPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__,self.ro_hobby);
}
@end
我们运行项目,得到如下图:
这个时候[(__bridge id)ro saySomething]打印的是ViewController。
[(__bridge id)ro saySomething]这个时候查找的时候,要加上0x10(即16字节),而不是0x8。
在viewDidLoad这个方法的隐藏参数(id self, sel _cmd)是可以压栈,[super viewDidLoad];中的super的最终走objc_msgSendSuper有一个结构体参数objc_super,里面又有id receiver和Class super_class两个要入栈。
我们贴下viewDidLoad的代码,里面有验证方式,如图:
6
从上图可以看出结构体是压入栈了,也可以看出是倒着压栈的。
我们再来看下参数是怎么入栈的,如图:
7
参数入栈是正序压栈的。
修改RoPerson.h文件
@interface RoPerson : NSObject
@property (nonatomic, copy) NSString *ro_hobby1;
@property (nonatomic, copy) NSString *ro_hobby;
@property (nonatomic, copy) NSString *ro_height;
@property (nonatomic, copy) NSString *ro_age;
@property (nonatomic, copy) NSString *ro_name;
- (void)saySomething;
@end
8
在viewDidLoad中[(__bridge id)ro saySomething];调起时,访问的id self参数,也就是ViewController
补充 结构体压栈的原理
[super viewDidLoad]和[RoPerson alloc]的隐藏参数id self,SEL _cmd为什么没有压栈,我们这里解释下。
临时变量才会压栈,参数会压栈,会在自己的栈空间。
所以[super viewDidLoad]和[RoPerson alloc]这里面的参数会在自己的栈空间压栈,跟这里的viewDidLoad无关。
objc_msgSendSuper,内部会自动创建 一个objc_super的临时变量,相当于创建一个person对象一样,所以会压栈。
methodswizzling面试题
methodswizzling方法交换在runtime中称为黑魔法,我们今天来介绍一下。
我们先来看张图:
9
从张图中,可以看出methodswizzling是交换了两个方法的IMP。
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[RoRuntimeTool ro_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(ro_studentInstanceMethod)];
});
}
- (void)ro_studentInstanceMethod{
[self ro_studentInstanceMethod]; //ro_studentInstanceMethod -/-> personInstanceMethod
NSLog(@"roStudent分类添加的ro对象方法:%s",__func__);
}
我们看下这段代码。
1.[self ro_studentInstanceMethod];这里调用的时候为什么没有产生递归?
答案:ro_studentInstanceMethod这个已经被交换了,说白了这个是原始的方法,而不是自己。尽量使用单例方式保证交换一次,为这了安全考虑。
2.交换方法时,父类都实现,子数没有实现,会出现什么情况?
这个时候会出现崩溃,报找不到方法的异常,因为父类的方法已经被交换了,指向了交换方法的IMP,[self ro_studentInstanceMethod];调用的时候(这里的sel是RoPerson)这里调用ro_studentInstanceMethod时,RoPerson是没有这个方法,所以找不到这个方法。当然我们可以通过在RoStudent中class_addMethod添加方法,然后替class_replaceMethod替换。
3.如果要交换的方法,子类和父类都没有,会出现什么情况?
同样会产生崩溃,[self ro_studentInstanceMethod];崩溃了,这里产生了死递归,也就是没有交换成功,也就是说ro_studentInstanceMethod。Method oriMethod = class_getInstanceMethod(cls, oriSEL);这里获取的时候,oriMethod就是一个空nil,不存在,class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));这里替换的时候,根本没有替换成功,所以ro_studentInstanceMethod还是指向自身,会产生递归。
我们可以通过以下代码解决
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
添加一个空的IMP 。
网友评论