likes
comments
collection
share

与你分享一份面试题关于iOS底层原理

作者站长头像
站长
· 阅读数 24
写在前面: iOS底层原理探索的阶段总结是对于底层探索的总结内容,
篇章不会太多,主要是对于探索中的细节总结。 希望对大家能有帮助。

总结来自以下专栏内容



load 方法在什么时候调用

load_images 分析

load_images 的时候 , 在 add_classs_to_loadable_list 中进行单个类的收集在loadable_classes 表中,在 add_category_to_loadable_list 中进行分类的收集在 loadable_categories表中 ,然后统一的 ( call_class_loads call_category_loads ) 进行递归将 load 方法进行调用。 load方法的调用顺序为父类、子类、分类。

load_images 方法中 会先 发现 load 方法 通过 prepare_load_methods((const headerType *)mh);, 之后是对load方法进行调用 call_load_methods(); ;

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }

    // 如果这里没有+load方法,则返回而不获取锁。
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // 发现 load 方法
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
        // 递归自己到父类一直到nil到load方法
        // 加到表里 add_class_to_loadable_list(cls); 读取类的方法 匹配是 load 后添加 ( cls 和 imp )key-value的形式
        // 做一个标记 cls->setInfo(RW_LOADED);
        // schedule_class_load(remapClass(classlist[i]));
    }

    // 调用 +load 方法
    call_load_methods();
    // do{} while() 循环 来调用
}

此时,又有一个问题 , load 方法和 initialize 方法谁先调用? load 方法

initialize 方法是在第一次消息的时候调用,也就是 lookUpImpOrForward 的时候调用。

  • 分类的⽅法是在类realize之后attach进去的插在前⾯,所以如果分类中实现了initialize方法,会优先调⽤分类的initialize方法。
  • initialize内部实现原理是消息发送,所以如果子类没有实现initialize会调用父类的initialize方法,并且会调用两次
  • 因为内部同时使用了递归,所以如果子类和父类都实现了initialize方法,那么会优先调用父类的,在调用子类的

此处 load 方法, initialize 方法 都可以自起,还有一个方法可以自起 —— c++ 的构造函数 方法。

那么,这些方法的一个先后顺序是怎样的呢?

  • 在分析dyld之后,可以确定这样的一个调用顺序,load->c++->main函数
  • 写在objc工程中的c++方法 在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_registerdyld注册load_images之后再调用
  • 同时,如果objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况

补充

项目中 类和分类的 加载 顺序 是什么?

与你分享一份面试题关于iOS底层原理 通过这里来调整编译顺序

Runtime是什么

  • Runtime是由CC++汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能

  • 运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时,如类扩展和分类的区别

  • 平时编写的OC代码,在程序运⾏过程中,其实最终会转换成RuntimeC语⾔代码,Runtime 是 Object-C 的幕后⼯作者

⽅法的本质,sel是什么?IMP是什么?两者之间的关系⼜是什么?

  • ⽅法的本质:发送消息,消息会有以下⼏个流程:

    1. 快速查找 (objc_msgSend)~ cache_t 缓存消息
    2. 慢速查找~ 递归⾃⼰或⽗类 ~ lookUpImpOrForward
    3. 查找不到消息: 动态⽅法解析 ~ resolveInstanceMethod
    4. 消息快速转发 ~ forwardingTargetForSelector
    5. 消息慢速转发 ~ methodSignatureForSelectorforwardInvocation
  • sel是⽅法编号,在read_images期间就编译进⼊了内存

  • imp就是我们函数实现指针,找imp就是找函数的过程

  • sel就相当于书本的⽬录tittle

  • imp就是书本的⻚码

  • 查找具体的函数就是想看这本书⾥⾯具体篇章的内容

    1. 我们⾸先知道想看什么 ~ tittle (sel)
    2. 根据⽬录对应的⻚码 (imp
    3. 翻到具体的内容 方法实现

能否向编译后的得到的类中增加实例变量? 能否想运行时创建的类中添加实例变量

1:不能向编译后的得到的类中增加实例变量

  • 我们编译好的实例变量存储的位置在 ro,一旦编译完成,内存结构就完全确定 就无法修改
  • 可以通过分类向类中添加方法和属性(关联对象)

2:只要内没有注册到内存还是可以添加

  • 可以添加属性 + 方法

可以通过objc_allocateClassPair在运行时创建类,并向其中添加成员变量和属性,见下面代码:

// 使用objc_allocateClassPair创建一个类Class
const char * className = "SelClass";
Class SelfClass = objc_getClass(className);
if (!SelfClass){
    Class superClass = [NSObject class];
    SelfClass = objc_allocateClassPair(superClass, className, 0);
}
        
// 使用class_addIvar添加一个成员变量
BOOL isSuccess = class_addIvar(SelfClass, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *));

class_addMethod(SelfClass, @selector(addMethodForMyClass:), (IMP)addMethodForMyClass, "V@:");
        

[self class][super class]的区别以及原理分析

我们新建一个继承自 SMPerson 的 SMTeacher 类

@implementation SMTeacher

- (instancetype)init {
    
    self = [super init];
    if (self) {
        
        //SMTeacher - SMPerson
        NSLog(@"%@ - %@", [self class], [super class] );
    }
    
    return self;
}

@end

运行项目后打印

SMTeacher - SMTeacher

和我们的猜想在super这一部分的打印是不一样的, 接下来我们就需要分析下原因了:

首先 class 实现如下:

- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

方法中有两个隐藏参数 : - (Class)class( id self, SEL _cmd ) { } ,此方法在OC底层都是通过 objc_msgSend(id self, SEL _cmd) 方法来发送消息。

[self class]

所以第一个参数是方法的调用者 SMTeacher 实例class方法 通过isa找到实例变量的类也就是 SMTeacher 类

[super class]

super 和 self 不一样的点首先是 super 是一个编译期的关键字,并不是参数名。

通过断点调试信息可以看到,实际上是调用了 objc_msgSendSuper2

与你分享一份面试题关于iOS底层原理

objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

super 底层是一个结构体

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};

可以看到 receiver 依然是 SMTeacher 实例, 只不过 会先去调用SMTeacher的父类中的方法。

所以,其本质是 objc_msgSendSuper,消息的接受者还是 self ,方法编号: class。 只是 objc_msgSenderSuper 会更快,直接跳过self的查找。

补充

通过clang SMTeacher.h 文件 可以看到 super 是调用的 objc_msgSendSuper

与你分享一份面试题关于iOS底层原理

查看objc_msgSendSuper内部实现,最终调用到了objc_msgSendSuper2

与你分享一份面试题关于iOS底层原理

内存平移

准备

案例代码:

    Person1 *p = [Person1 alloc];
    [p sleep]; 
    
    Class cls = [Person1 class];
    void *sm = &cls;
    [(__bridge id)sm sleep];
- (void)sleep {
    
    NSLog(@"person - %s", __func__);
}

打印结果:

person - -[Person sleep]
person - -[Person sleep]

显然 对于 实例p 执行 sleep 方法 打印内容,我们都能理解,那么,指针sm 为什么也可以 执行 sleep 方法 打印内容呢?

这样就要看一下,实例对象执行方法的一个过程:

  1. 实例对象的 isa
  2. 类的首地址
  3. 通过内存平移找到类的 bits
  4. rw 指向 ro
  5. 找到 methods 这样就可以在方法列表中找到方法的地址,然后去执行。

这样的一个流程下来,对于 指针 *sm 来说,它也是指向 Person类 的 , 所以按照上面的流程 同样可以找到方法,然后去执行。

  • 现在对 sleep 稍作修改:
- (void)sleep {
    
    NSLog(@"person - %s -%@", __func__ , self.name);
}

运行后打印内容:

与你分享一份面试题关于iOS底层原理

我们对比一下 实例p 和 指针sm,他们很明显是不一样的, 我们先 xcrun 一下 看看底层源码:

_I_Person1_name

与你分享一份面试题关于iOS底层原理

OBJC_IVAR_$_Person1$_name

与你分享一份面试题关于iOS底层原理

  • 所以在方法中打印name, 我们能获取到是通过 内存的偏移 来获取到的。

与你分享一份面试题关于iOS底层原理

内存地址偏移 0x8 在viewDidLoad 的栈帧中打印内容,接下来我们探索下 viewDidLoad这个方法的栈帧( 到底哪些内容压栈了 ):

viewDidLoad 中哪些内容压栈了

  • 栈内存地址: 高 -> 低
  • 堆内存地址: 低 -> 高

首先在 Person1 中 再添加一个属性:

@interface Person1 : NSObject

@property (nonatomic, retain) NSString *like;
@property (nonatomic, retain) NSString *name;

- (void)sleep;

@end

在此运行项目,查看打印的内容:

person - -[Person1 sleep] -SuperMan
person - -[Person1 sleep] -<ViewController: 0x7ff0b1504a60>

这次很奇怪 是打印了 ViewController

这次因为添加了个属性,所以这次的 Person1 打印name的时候需要偏移0x10;

  • 参数压栈
  • 结构体压栈

验证:

  • 自定义需要参数的方法

与你分享一份面试题关于iOS底层原理

参数从前往后压栈
  • 自定义一个结构体
struct SMPer {
    NSNumber *num1;
    NSNumber *num2;
};

打印内容

person - -[Person1 sleep] -SuperMan
person - -[Person1 sleep] -50

明显看到了我们定义的结构体的地址是在p和cls之间,(结构体是倒着压栈的)

与你分享一份面试题关于iOS底层原理

结构体是从后往前压栈的

最后,我们看下整个 viewDidLoad 方法的压栈情况: 还记得最开始我们 xcrun 了 ViewController.m 我们就在这个文件中找到 viewDidLoad 的方法,来分析:

struct __rw_objc_super { 
	struct objc_object *object; 
	struct objc_object *superClass; 
	__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};

...

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){
        
        (id)self,
        (id)class_getSuperclass(objc_getClass("ViewController"))
        
    }
      , sel_registerName("viewDidLoad"));



    Person1 *p = ((Person1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person1"), sel_registerName("alloc"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sleep"));

    Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person1"), sel_registerName("class"));
    void *sm = &cls;
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)sm, sel_registerName("sleep"));
}

xcrun还原底层代码后,看起来就舒服多了,分总结如下:

与你分享一份面试题关于iOS底层原理

转载自:https://juejin.cn/post/6992745514743627806
评论
请登录