美文网首页
iOS类的经典面试题分析

iOS类的经典面试题分析

作者: 大橘猪猪侠 | 来源:发表于2020-09-15 22:37 被阅读0次

【面试题】元类中为什么会有类对象的类方法?

我先来解释一下题目的意思:
首先创建一个Person类,在类中创建一个成员变量,一个属性,一个实例方法,一个类方法,那么在我们创建这个对象之后,查找对象中的属性和方法,结果在类对象中并没有找到该对象的类方法,请看代码:

@interface Person : NSObject{
    NSString *hobby;
}

// 影响的因素的 对象: 属性 : 8 + 8 + 8 + 8 = 32
// 内存的布局 属性
// isa
@property (nonatomic,strong) NSString *name;     

-(void)sayNB;
-(void)say666;


@end

在main函数中创建Person对象,并打上断点,进行调试:

(lldb) p/x Person.class
(Class) $0 = 0x0000000100002278 Person
(lldb) p (class_data_bits_t *)0x0000000100002298
(class_data_bits_t *) $1 = 0x0000000100002298
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000000010069b0d0
(lldb) p $2->methods()
(const method_array_t) $3 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020d8
      arrayAndFlag = 4294975704
    }
  }
}
(lldb) p $3.list
(method_list_t *const) $4 = 0x00000001000020d8
(lldb) p *$4
(method_list_t) $5 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayNB"
      types = 0x0000000100000f84 "v16@0:8"
      imp = 0x0000000100000e00 (KCObjc`-[Person sayNB])
    }
  }
}
(lldb) p $5.get(0)
(method_t) $6 = {
  name = "sayNB"
  types = 0x0000000100000f84 "v16@0:8"
  imp = 0x0000000100000e00 (KCObjc`-[Person sayNB])
}
(lldb) p $5.get(1)
(method_t) $7 = {
  name = ".cxx_destruct"
  types = 0x0000000100000f84 "v16@0:8"
  imp = 0x0000000100000e60 (KCObjc`-[Person .cxx_destruct])
}
(lldb) p $5.get(2)
(method_t) $8 = {
  name = "name"
  types = 0x0000000100000f98 "@16@0:8"
  imp = 0x0000000100000e10 (KCObjc`-[Person name])
}
(lldb) p $5.get(3)
(method_t) $9 = {
  name = "setName:"
  types = 0x0000000100000fa0 "v24@0:8@16"
  imp = 0x0000000100000e30 (KCObjc`-[Person setName:])
}
(lldb) p $5.get(4)
Assertion failed: (i < count), function get, file /Users/pengwenxi/Desktop/objc4-781源码/runtime/objc-runtime-new.h, line 438.
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

在上面调试过程中,我们可以看到当我们使用p $5.get()函数打印类对象的方法时,并没有类方法;在经过一翻推测后,得知类对象的类方法在元类中,那么我们如何证明呢?

那么我们先通过上面的方法去获取元类的方法:

(lldb) p/x Person.class
(Class) $3 = 0x0000000100002278 Person
(lldb) p/x 0x0000000100002278 & 0x0000000ffffffff8ULL
(unsigned long long) $4 = 0x0000000100002278
(lldb) p 0x0000000100002278
(long) $5 = 4294976120
(lldb) x/4gx 0x0000000100002278
0x100002278: 0x0000000100002250 0x0000000100333140
0x100002288: 0x0000000101870020 0x0001802400000003
(lldb) p/x 0x0000000100002250 & 0x0000000ffffffff8ULL
(unsigned long long) $6 = 0x0000000100002250
(lldb) po 0x0000000100002250
Person

(lldb) p (class_data_bits_t *)0x0000000100002270
(class_data_bits_t *) $8 = 0x0000000100002270
(lldb) p $8->data()
(class_rw_t *) $9 = 0x000000010186ff80
(lldb) p $9->methods()
(const method_array_t) $10 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002070
      arrayAndFlag = 4294975600
    }
  }
}
(lldb) p $10.list
(method_list_t *const) $11 = 0x0000000100002070
(lldb) p *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "say666"
      types = 0x0000000100000f84 "v16@0:8"
      imp = 0x0000000100000df0 (KCObjc`+[Person say666])
    }
  }
}

通过获取元类的方法,我们看到了类对象中的类方法。

下面我们通过另一种方式去验证他的存在:

void Objc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        NSLog(@"Method, name: %@", key);
    }
    free(methods);
}

Objc_copyMethodList 函数:用于获取类的方法列表
上面的函数打印结果为:Method, name: sayHello

void lgInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    LGLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

InstanceMethod_classToMetaclass 函数:用于获取类的实例方法
方法打印结果:InstanceMethod_classToMetaclass - 0x1000031b0-0x0-0x0-0x100003148

void ClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    // 元类 为什么有 sayHappy 类方法 0 1
    //
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

ClassMethod_classToMetaclass 函数:用于获取类的类方法
方法打印结果:ClassMethod_classToMetaclass-0x0-0x0-0x100003148-0x100003148

void IMP_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);

    // - (void)sayHello;
    // + (void)sayHappy;
    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));

    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    NSLog(@"%s",__func__);
}

IMP_classToMetaclass 函数:用于获取方法的实现
方法打印结果为:
0x100001d00-0x7fff66ea8580-0x7fff66ea8580-0x100001d30 lgIMP_classToMetaclass

在上述的方法中,pClass为类,metaClass为元类;
第一个方法获取类的方法列表,主要获取Person类的方法列表,但是结果就只获得了一个sayHello方法;
第二个方法中,获取类的实例方法,分别对Person的类和元类分别获取他们的实例方法,从结果可以得到:在类中,只获得了一个sayHello方法,而在元类中也只获得了一个sayHappy方法。
第三个方法中,我们首先需要了解class_getClassMethod这个方法的作用,该方法主要是获取类的类方法,通过下面的源码中可以看出,当cls是元类时,就不会继续递归查询下去,反之则继续递归查询。
通过打印的结果可以得知:在类对象中,会递归查询到该对象的元类中去查找类方法,在元类中则直接查询类方法。

//获取类方法
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls || !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

//获取元类
 // NOT identical to this->ISA when this is a metaclass
Class getMeta() {
    if (isMetaClass()) return (Class)this;
    else return this->ISA();
}

第四个方法中,我们首先需要了解class_getMethodImplementation方法的作用:

`如果向一个类的实例发送一条消息,该函数会返回该条消息的IMP。class_getMethodImplementation可能比method_getImplementation更高效。返回的指针可能会是一个方法的IMP,也可能是runtime内部的一个函数。比如说,如果一个类的对象不能响应一个selector,这个函数指针返回的就会是runtime里面消息转发机制的一部分。`

下面是这个方法的源代码:

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    //没有找到imp,就进行消息转发
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

从方法的打印结果可以明白,imp2和imp3结果是一样的,由于我们证明过在类对象中没有类方法,在元类中,存在实例方法;因此,可以证明这两个imp是进行了消息转发,而imp1和imp4则返回了函数指针地址。

【面试题】iskindOfClass 和 isMemberOfClass 的理解:

下面附上一张经典的图,来区分isa和类对象:

isa流程图.png

下面请看代码:

//类方法调用
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
        BOOL re3 = [(id)[Person class] isKindOfClass:[Person class]];       //
        BOOL re4 = [(id)[Person class] isMemberOfClass:[Person class]];     //
        NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

//实例方法调用
        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
        BOOL re7 = [(id)[Person alloc] isKindOfClass:[Person class]];       //
        BOOL re8 = [(id)[Person alloc] isMemberOfClass:[Person class]];     //
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

我们来先来看一下isKindOfClassisMemberOfClass的源码,他们都有两个方法,一个是实例方法,一个是类方法:

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

isKindOfClass源码可以分析得出:当传递过来的class是否等于当前的类,不是的话就找父类,进行递归查询。如果存在则返回YES,不存在返回NO。

在类方法中,拿当前类的元类与传入的类进行对比,
在实例方法中,拿当前的类与传入的类进行对比;

坑点:当我们通过在几个方法处打断点进行调试时,他却不会执行我们打断点的地方,那这是什么原因呢?

答案:因为在llvm中编译时对其进行了优化处理,他执行的是下面的这个方法:

BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->superclass) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

通过Class cls = obj->getIsa();这一行代码分别获取类的isa(元类,类方法)和对象的isa(类,实例方法)。

下面我们来解释一下运行程序中的re1-re8:

在re1中,使用NSObject对象与NSObject进行对比:

拿到NSObject(传入的类)的根元类与NSObject对比,结果不相等,
而找NSObject的根元类的父类为NSObject,因此相等:

在re3中,拿Person对象与Person对比:

Person(传入的类) VS Person的元类,不相等;
继续拿Person与Person的根元类(NSObject)对比,不相等;

在re5中,NSObject对象与NSObject对比,在实例方法中执行程序:

拿NSObject(传入的类)(根类)VS 对象的类NSObject跟类对比,相等;

在re7中,Person对象与Person对比:

Person(传入的类) VS 对象的类即Person,相等;

在re2中,将NSObject与NSObject对比,使用类方法:

NSObject根类 VS NSObject的元类即根元类,不相等(注意,不会再进行递归查找父类)

在re4中,拿Person与Person对比:

Person类与Person的元类对比,不相等。

在re6中,拿NSObject对象与NSObject对比,使用实例方法:

拿Person类与Person对象的类对比,相等。

re8中同样如此。

来看一下打印结果:

 re1 :1
 re2 :0
 re3 :0
 re4 :0
 re5 :1
 re6 :1
 re7 :1
 re8 :1

第二道面试题我们要理解类对象、元类,根元类之间的关系,在上面的图中具体的表现出来了。

相关文章

  • iOS经典面试题总结--内存管理

    iOS经典面试题总结--内存管理 iOS经典面试题总结--内存管理

  • iOS类的经典面试题分析

    【面试题】元类中为什么会有类对象的类方法? 我先来解释一下题目的意思:首先创建一个Person类,在类中创建一个成...

  • iOS 底层探索:类中方法的举例分析

    文集:iOS 底层探索之路 前言 这篇主要内容是通过面试题举例分析类的结构。 准备工作 复习:iOS 底层探索 :...

  • IOS面试(2018)

    经典面试题 非技术面试题 C面试题1 C面试题2 C面试题3 iOS笔试题01 iOS笔试题02 iOS笔试题03...

  • ios-面试题链接(四)

    经典面试题 非技术面试题 C面试题1 C面试题2 C面试题3 iOS笔试题01 iOS笔试题02 iOS笔试题03...

  • 2018 iOS面试题系列

    经典面试题 非技术面试题 C面试题1 C面试题2 C面试题3 iOS笔试题01 iOS笔试题02 iOS笔试题03...

  • 关于isa走位的2道经典面试题

    第一道经典面试题:类方法的归属分析 关于类方法和实例方法的归属分析,我们首先得知道:实例方法是在类中,而类方法是在...

  • iOS经典面试题分析

    面试题一 在iOS类的结构分析中的探索中,我们知道了实例方法 存储在类中,类方法存储在元类中,接下来我们来分析一下...

  • iOS底层面试题--RunLoop

    什么是RunLoop? iOS底层面试题--RunLoop RunLoop面试题分析

  • iOS面试

    iOS面试题阿里、字节:一套高效的iOS面试题iOS源码分析 1、谈下iOS开发中知道的哪些锁? 哪个性能最差?S...

网友评论

      本文标题:iOS类的经典面试题分析

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