美文网首页
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类的经典面试题分析

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