您好,欢迎访问代理记账网站
移动应用 微信公众号 联系我们

咨询热线 -

电话 15988168888

联系客服
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

【iOS底层】05:类原理分析(下)

目录

回顾:

一、属性、成员变量、实例变量的区别

二、sel&imp 编码表补充

三、setter方法底层原理

四、类方法存储API方式解析

五、isKindOfClass面试题分析

六、总结


回顾:

.objc_object 和 objc_class是继承关系,class继承自object。

.像person和NSObject,person和objc_object没有继承等关系,只是一个上层,一个底层,然后NSObject是按照objc_class的模板创建的。

.id 是 objc_object *类型的

.class是objc_class *类型的

一、属性、成员变量、实例变量的区别

属性=带下划线的成员变量+setter+getter方法

实例变量=特殊的成员变量(实例化的),比如NSObject *objc,UIButton等。

查看main.cpp文件:终端cd到源码的main.m所在文件夹下,执行“clang -rerite-objc main.m -o main.cpp”即可编译出main.cpp文件。

@interface LGPerson : NSObject
{
    // STRING   int  double  float char bool
    NSString *hobby; // 字符串
    NSObject *objc;  // 结构体
}

@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;

@end

可以看出编译成cpp文件以后,LGPerson_IMP里除了我们的成员变量hobby、objc外,还多了几项和属性名称相同但名字前带下划线的成员变量_nickName和  _name

同时还生成了属性nickName和name的setter、getter方法:

其中getter方法都是通过self指针偏移来获取值的,而setter方法则不同,我们发现nickName属性的setter方法是调用了objc_setProperty(),而name属性则是通过指针平移的方式给相应位置赋值的,这是为什么呢?我们放到 三、setter方法底层原理 中去解答。

{{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
	{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_LGPerson_setNickName_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_},

 比如"@16@0:8"这代表什么意思?我们先来补一下编码表的知识~

二、sel&imp 编码表补充

sel:方法编号(相当于一本书的目录名称)

imp:函数指针地址(相当于一本书的目录页码)

 

ivar_getTypeEncoding()方法  shirt+command+0打开苹果文档搜索该方法,

底部Type Encodings链接点击进入 获取编码表。

这里贴出来看一下:

Table 6-1  Objective-C type encodings

Code

Meaning

c

char

i

An int

s

short

l

long

l is treated as a 32-bit quantity on 64-bit programs.

q

long long

C

An unsigned char

I

An unsigned int

S

An unsigned short

L

An unsigned long

Q

An unsigned long long

f

float

d

double

B

A C++ bool or a C99 _Bool

v

void

*

A character string (char *)

@

An object (whether statically typed or typed id)

#

A class object (Class)

:

A method selector (SEL)

[array type

An array

{name=type...}

A structure

(name=type...)

A union

bnum

A bit field of num bits

^type

A pointer to type

?

An unknown type (among other things, this code is used for function pointers)

 那么我们对应上边的get方法的编码"@16@0:8"来看一下他的意义->

@:代表返回值类型id

16:方法总共占用16字节

@:第一个参数,类型为id,(就是self)

0:从0号位置开始

::SEL方法

8:从8号位置开始

---------------------------

那么我们看另外一个set方法的"v24@0:8@16"

v:表示返回值是void类型

24:方法总共占用24字节

@第一个参数类型为id

0:第一个参数从0号位置开始

::SEL方法

8:从8号位置开始

@:表示需要传入的参数为id类型

16:表示传入参数从16号位置开始

一句话概括就是:id(0-8字节)+SEL(8-16字节)+传入参数id(16-24字节),总共占用24字节返回void。

三、setter方法底层原理

这里我们继续探究上边遗留的问题,为什么有的自动创建objc_setProperty()方法,从.cpp源码看不出来了,我们转向LLVM源码,我们用VSCode打开。 

  • 什么是LLVM?

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)。

  • 我们为什么要去LLVM里找呢?
     因为在运行时类随时可以会调用其中的属性和方法,那么就需要底层的setter方法在编译时就创建好,那么我们去LLVM里找的方向就应该是对的。

 经过一通查找我们发现好像copy修饰的属性和别的会不一样,那么是不是这样呢,我们验证一下。

@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *nickName2;
@property (nonatomic) NSString *nickName3;
@property (atomic) NSString *nickName4;

@property (nonatomic, strong) NSString *name;
@property (atomic, strong) NSString *name2;

为了排除atomic、nonatomic、strong的影响我们加了这么多对比数据,然后重新编译cpp文件看一下。

结果可以看出确实只有copy修饰的属性nickName、nickName2,编译器才会给创建objc_setProperty()方法 。

  • 这里又留下一个疑问 :什么情况下会创建属性的ojbc_getProperty()方法呢?我们以后讨论~

四、类方法存储API方式解析

在上一篇博客:【iOS底层】04:类原理分析(上)中我们探究了类方法是存储在元类里的,是凭借猜测在数据里查找验证的。

  • 那么有没有其他方法探究呢,我们来看一下。
@interface LGPerson : NSObject
{
    NSObject *objc; 
    NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSObject *obj;


- (void)sayHello;
+ (void)sayHappy;

@end

LGPerson中我们定义如上的成员变量,属性和实例方法、类方法。

在main.m中我们写一个方法来输出下LGPerson类里的方法都有哪些。

void lgObjc_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);
}

 然后在main函数里调用该方法:

LGPerson *person = [LGPerson alloc];
Class pClass = object_getClass(person);
lgObjc_copyMethodList(pClass);

输出结果:

Method, name: sayHello
Method, name: name
Method, name: .cxx_destruct
Method, name: setName:
Method, name: obj
Method, name: setObj:

可以看出除了属性的get,set方法,对象方法sayHello之外还有一个.cxx_destruct析构函数。

并没有找到类方法sayHappy

接下来看下元类里有哪些方法呢,同样用上面的lgObjc_copyMethodList函数,我们传入LGPerson的元类。

const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
lgObjc_copyMethodList(metaClass);

输出结果:

Method, name: sayHappy

再次验证了类方法的数据是放在元类里的。

  • 我们再换第三种方法验证一下

class_getInstanceMethod 得到类的实例方法

class_getClassMethod 得到类的类方法

我们用class_getInstanceMethod方法看看类和元类里都有哪些实例方法。

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));
    
    NSLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

输出结果为:

lgInstanceMethod_classToMetaclass - 0x1000081c0-0x0-0x0-0x100008158

可以看出method2、method3都是空,再次证明对象方法存在在类里边,类方法存在在元类里。

  • 用第四种方法验证:使用class_getClassMethod函数
void lgClassMethod_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));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

输出结果:

lgClassMethod_classToMetaclass-0x0-0x0-0x100008158-0x100008158

因为-sayHello()是个实例方法所以method1和2都是空,method3有值,但是类方法在元类里不是以实例方法存在的吗?为什么也有值呢?

我们看一下class_getClassMethod的源码:

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
        if (isMetaClassMaybeUnrealized()) return (Class)this;
        else return this->ISA();
    }
// Like isMetaClass, but also valid on un-realized classes
    bool isMetaClassMaybeUnrealized() {
        static_assert(offsetof(class_rw_t, flags) == offsetof(class_ro_t, flags), "flags alias");
        static_assert(RO_META == RW_META, "flags alias");
        if (isStubClass())
            return false;
        return data()->flags & RW_META;
    }

 isMetaClassMaybeUnrealized()注释里写了该方法类似isMetaClass即判断是否是元类。

那么getMeta()就很明白了,如果是元类就返回原值,不是则返回原值的元类。

那么实际上对于元类来说class_getClassMethod()方法调用的还是class_getInstanceMethod()方法来获取类的实例方法。

那么我们就清楚了,method3是看元类里有没有实例方法sayHappy(),method4是看元类自己有没有实例方法sayHappy()。

  • 至此我们归纳出来了一点:

对于底层来说,没有类方法一说,通通都是对象方法。

sayHelloLGPerson类的对象方法,sayHappyLGPerson元类的对象方法。

  •  看完了实例方法我们来看下方法的实现--IMP

class_getMethodImplementation()  获取类的方法的实现

这个方法是SEL对应找IMP的过程。 

写一个方法来输出下结果

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

    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);
}

结果为:

0x100003ad0-0x7fff203fb5c0-0x7fff203fb5c0-0x100003b10

我们又有疑惑了,imp1和imp4有值没问题,但是为什么imp2和imp3里也有呢,并且打印的地址相同,我们查看一下源码:

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

    // Translate forwarding function to struct-returning version
    if (imp == (IMP)&_objc_msgForward /* not _internal! */) {
        return (IMP)&_objc_msgForward_stret;
    }
    return imp;
}
IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

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

    lockdebug_assert_no_locks_locked_except({ &loadMethodLock });

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

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

 发现如果没有imp,默认会返回一个_objc_msgForward.

那么是怎么回事呢?

答:_objc_msgForward是IMP类型的,用于消息转发的,当向一个对象发送消息,但他没有实现的时候,_objc_msgForward会尝试做消息转发。

过程就是:首先在Class的缓存里查找IMP(没有缓存则初始化缓存),若没找到,则向父类的Class中查找IMP,如果到根类一直没有查找到对应的IMP,则会用_objc_msgForward的函数指针代替IMP。所以上面输出的imp2和imp3的地址都是_objc_msgForward的函数指针地址。

详细的另一篇大佬博客里有写,我就不做详细展开了,这里贴个传送门。

五、isKindOfClass面试题分析

void lgKindofDemo(void){
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
    BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
    BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson 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)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
    BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
    NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}

这里我们分析下这个面试题。

+ (BOOL)isKindOfClass:(Class)cls {
    //元类 vs 传入类
    //根元类 vs 传入类
    //根类 vs 传入类
    //一步步是 调用者及其父类 vs 传入类
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

 相当于下图这么一个流程:

 re1:如上图第一次是NSObject元类NSObject对比不同,第二次是NSObject根类NSObject对比相同,答案为1

+ (BOOL)isMemberOfClass:(Class)cls {
    //元类 vs 传入类
    return self->ISA() == cls;
}

re2:NSObject的元类和NSObject 类对比不同。答案是0

re3:如上图从LGPerson的元类开始1、2、3分别去和传入者LGPerson类对比,结果都是不等,答案为0

re4:LGPerson的元类LGPerson类不等,答案是0

- (BOOL)isKindOfClass:(Class)cls {
    //类 vs 传入类
    //父类 vs 传入类
    //根类 vs 传入类
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

re5:NSObject对象的类和NSObject相同,第一次for循环就return了,答案是1。

- (BOOL)isMemberOfClass:(Class)cls {
    //类 vs 传入类
    return [self class] == cls;
}

re6:NSObject对象的类和NSObject相同,答案是1。

re7:LGPerson对象的类和LGPerson类相同,答案是1。

re8:LGPerson对象的类和LGPerson类相同,答案是1。

总结是:1,0,0,0,1,1,1,1。

我们输出打印一下:

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

与我们的结果相同。

  • 小结:

其实这个面试题考察的就是isKindOfClassisMemberOfClass这两个的实例方法和类方法的源码,以及isa经典图的理解。

+isKindOfClass():是调用者A的元类metaA、元类父类superMetaA、根元类rootMetaA、以及最终的根类rootA,和传入者classB的对比。

-isKindOfClass():是调用者A的类classA、父类superA、根类rootA,和传入者classB对比。

二者的区别是:首次对比是从调用者A的元类(C)开始比还是直接拿A类(C)开始比。之后都是循环C的父类和B进行比较。

+isMemberOfClass():调用者A元类metaA和传入者B比较。

-isMemberOfClass():调用者类classA和传入者B比较。

相比较isKindOfClass方法,isMemberOfClass方法没有了循环,只是调用者元类或调用者类,直接和传入者B比较,逻辑就简单了很多,没有isKindOfClass循环父类那么麻烦。

上面两段总结可能比较绕,小伙伴结合源码多看两遍~ 

六、总结

getter,setter方法实际是一个中间层,供上层统一调用,通过传入cmd标识区分,然后由getter、setter方法统一调用下层代码,这样效率就高很多了~

另外一定要深刻理解isa的经典流程图,对平时开发很有帮助!

 


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进