美文网首页interview
OC底层原理探索—经典面试题原理

OC底层原理探索—经典面试题原理

作者: 十年开发初学者 | 来源:发表于2021-07-26 17:01 被阅读0次

1.loadinitialize方法的调用原则和调用顺序?

load
  • load方法在应用程序加载过程中(dyld)完成调用,在main之前
  • 在底层进行load_images处理时,维护了两个load的加载表,一个是本类的表,另一个是分类的表,所以说有先对本类的load发起调用
  • 在对类 load方法进行处理时,进行递归处理,以确保父类优先被处理
  • load方法的调用顺序是父类、子类、分类
  • 在分类中load调用顺序,是根据编译的顺序为准
initialize
  • initialize是在第一次消息发送的时候进行调用,load先于initialize
  • 分类中实现initialize方法会被优先调用,并且本类中的initialize不会被调用,
  • initialize原理是消息发送,所有当子类没有实现时,会调用父类还会被调用两次
  • 如果子类,父类同时实现,先调用父类,在调用子类
c++构造函数
  • 在分析dyld后,可以确定这样个调用流程load->c++->main
  • 但是如果c++写在objc工程中,在objc_init()调用时,会通过static_init()方法优先调用c++函数,而不需要等到_dyld_objc_notify_register向dyld注册load_images之后再调用
  • 同时,如果objc_init()自启的话也不需要dyld进行启动,也可能会发生c++函数在load方法之前调用的情况

方法的本质

  • 方法的本质:发送消息

消息发送的流程

  • 首先进入快速查找也就是通过objc_msgSend去缓存(cache_t)中查找
  • 慢速查找:通过递归自己或者父类查找,也就是lookupImporForward方法
  • 动态方法解析:resolveInstanceMethod
  • 消息快速转发:forwardingTargetForSelector
  • 消息慢速转发:methodSignatureForSelectorforwardInvocation

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

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

  • 首先编译好的实例变量存储在ro中,一旦完成编译,内存结构确定
  • 可以通过分类以关联对象形式向类中添加分类和属性
  1. 可以向运⾏时创建的类中添加实例变量
  • 可以通过objc_allocateClassPair运行时创建类,并添加属性、实例变量、方法等
`        const char *className = "SHObject";
        Class objc_class = objc_getClass(className);
        if (!objc_class) {
            Class superClass = [NSObject class];
            objc_class = objc_allocateClassPair(superClass, className, 0);
        }
        class_addIvar(objc_class, "name", sizeof(NSString *), log2(_Alignof(NSString *)),  @encode(NSString *));
        class_addMethod(objc_class, @selector(addName:), (IMP)addName, "V@:");

[self class]和[super class]区别

来看下下面案例,LGTeacher类继承自LGPerson,在LGTeacher的init初始化方法中,调用了[self class]和[super class],结果会是什么

    // LGPerson
    @interface LGPerson : NSObject
    @end

    @implementation LGPerson
    @end
    
    // LGTeacher
    @interface LGTeacher : LGPerson
    @end

    @implementation LGTeacher
    - (instancetype)init{
        self = [super init];
        if (self) {
           NSLog(@"%@ - %@", [self class], [super class]);
        }
        return self;
    }
    @end

首先这两个类中都没有实现class方法,那么根据继承关系,他们最终会调用到NSObject中的class方法

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

由上图可知这两个方法返回的self对应的类。关于这个self是谁,这里涉及到消息发送objc_msgSend,有两个隐形参数,分别是id self 和 SEL sel,这里SEL sel没啥好说的,主要来说下id self

  • [self class]输出LGTeacher,这里消息的发送者是LGTeacher对象,通过调用NSObject 的 class,但是消息的接受者没有发生变化,所以是LGTeacher
  • [super class]这里的输出仍然是LGteacher,至于为什么,我们来clang一下,查看下cpp文件
    image.png
    通过上图我们看到[super class]的低层实现时objc_msgSendSuper方法,同时存在存在id self 和 SEL sel两个隐形参数
/// Specifies the superclass of an instance. 
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 */
};

我们来查看下objc_super结构体

由上图知:id receiverClass super_class两个参数,其中super_class表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher中调用[super class],其内部会调用objc_msgSendSuper方法,并且会传入参数objc_super,其中receiver是LGTeacher对象,super_class是LGTeacher的父类,也就是要第一个查找的类。

内存偏移

案例1

创建一个person类

@interface Person : NSObject
@property (nonatomic,copy)NSString *name;


- (void)say1;
@end

@implementation Person

- (void)say1{
    NSLog(@"%s",__func__);
}

@end

viewDidload中添加以下代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    Person *person = [[Person alloc] init];
    [person say1];
    
    Class cls = [Person class];
    void *sh  =&cls;
    [(__bridge  id)sh say1];
    
}

查看下打印


image.png

分析:

  • 这两处调用的本质objc_msgSend的调用,在汇编源码中进行方法的快速查找
  • [person say1];通过person对象的isa指针找到对应的类,在类中进行地址平移,首先在cache_t中快速查找,如果找不到,则在类或者父类的方法列表遍历查找
  • [(__bridge id)sh say1],这里能够调用成功的原因是,Class cls = [Person class];cls是一个指针,指向一个objc_class指针,这里是指向Person类,将cls地址赋给sh,shcls的地址,也是指向类
image.png
由上图知,sh是指向Person类的 image.png

总结:

  • person对象里面的isa指向Person类,通过内存平移的方式找到say1方法
  • sh指向clscls指向Person类,同样通过首地址平移找到say1
案例2

接着上面的案例,我们新增一个属性打印

@implementation Person

- (void)say1{
    NSLog(@"%s,%@",__func__,self.name);
}

@end

查看打印


image.png

首先我们先要了解,取出属性的值,其实是要先计算出偏移大小,在通过内存平移获取值。其实是Person类内部存储着成员变量,每次偏移8字节进行存取

至于sh打印的self.name的值是Person;0X600...,是因为cls只有Person类的内存首地址,但是没有person对象的内存结构,所以sh只能在栈里面进行内存平移。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Class cls = [Person class] ;
    
    void *sh  =&cls;
    [(__bridge  id)sh say1];
    Person *person = [[Person alloc] init];
    [person say1];
    
    
    // 下面代码为打印栈结构
    void *sp = (void *)&self;
    void *end = (void *)&person;
    long count = (sp - end) / 0x8;
    
    for (long i = 0; i < count; i++) {
        void *address = sp - 0x8 * I;
        if (i == 1) {
            NSLog(@"%p : %s",address, *(char **)address);
        } else {
            NSLog(@"%p : %@",address, *(void **)address);
        }
    }
}

看下栈结构打印


image.png
  • 0x16f35ffb8 : <ViewController: 0x136906d30>viewDidload第一个隐形参数id self
  • 0x16f35ffb0 : viewDidLoad``是viewDidload第二个隐形参数SEL _cmd`
  • 0x16f35ffa8 : ViewController,这个是结构体class压栈
  • 0x16f35ffa0 : <ViewController: 0x136906d30> 为结构体receiver压栈
  • Personsh压栈
  • 0x16f35ff90 : <Person: 0x16f35ff98>person压栈

什么可以压栈

  • 方法的参数viewDidLoad的(id self, SEL _cmd)。
  • 结构体参数objc_super,相当于下边这块代码创建了一个sh_objc_super的临时变量,所以也可以压栈。
struct objc_super sh_objc_super;
sh_objc_super.super_class = class;
sh_objc_super.receiver = receiver;
  • 临时变量即:sh、person

相关文章

网友评论

    本文标题:OC底层原理探索—经典面试题原理

    本文链接:https://www.haomeiwen.com/subject/gefimltx.html