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

710 阅读11分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

在上一篇文章中我们通过lldb分析来获取了类的属性和对象方法,却没有找到成员变量和类方法的存储位置;今天我们先来说一下成员变量的问题,既然属性和成员变量的存储没有在一起,那么说明属性和成员变量是不一样的,那么他们有什么区别呢?

成员变量和属性

我们将上一篇文章中的Person类修改如下:

@interface Person : NSObject{
    NSString *nickName;
    NSObject *objc;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *realName;
@property (nonatomic, assign) NSInteger age;
- (void)run;
+ (void)eat;
@end

@implementation Person
- (void)run {

}

+ (void)eat {

}
@end

Person中存在:

  • 属性:namerealNameage
  • 成员变量:nickName
  • 实例变量:objc实例变量是一种特殊的成员变量(以对象为类型)
  • 方法:- (void)run+ (void)eat

我们使用xcrun命令来生成Personcpp文件类分析一下:

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc Person.m -o Person.cpp

我们在cpp文件中找到类Person实现的地方:

我们可以发现,在cpp文件中所有的属性都被注释掉了,生成对应的带_的成员变量,并且生成了对应的settergetter方法

属性 = 成员变量 + setter + getter

除此之外,我们还在cpp文件中发现类如下代码:

那么v16@0:8@16@0:8这样的字符串我们称为编码(Apple自带的),那么这些编码在什么地方呢?我们在工程中使用快捷键command + shift +0打开如下窗口:

搜索ivar_getTypeEncoding:

打开如图所示链接:

前往网页,查看相关编码信息

以下代码,大家可自行打印输出,查看结果:

NSLog(@"char --> %s",@encode(char));
NSLog(@"int --> %s",@encode(int));
NSLog(@"short --> %s",@encode(short));
NSLog(@"long --> %s",@encode(long));
NSLog(@"long long --> %s",@encode(long long));
NSLog(@"unsigned char --> %s",@encode(unsigned char));
NSLog(@"unsigned int --> %s",@encode(unsigned int));
NSLog(@"unsigned short --> %s",@encode(unsigned short));
NSLog(@"unsigned long --> %s",@encode(unsigned long long));
NSLog(@"float --> %s",@encode(float));
NSLog(@"bool --> %s",@encode(bool));
NSLog(@"void --> %s",@encode(void));
NSLog(@"char * --> %s",@encode(char *));
NSLog(@"id --> %s",@encode(id));
NSLog(@"Class --> %s",@encode(Class));
NSLog(@"SEL --> %s",@encode(SEL));
int array[] = {1,2,3};
NSLog(@"int[] --> %s",@encode(typeof(array)));
typedef struct person{
    char *name;
    int age;
}Person;
NSLog(@"struct --> %s",@encode(Person));

typedef union union_type{
    char *name;
    int a;
}Union;
NSLog(@"union --> %s",@encode(Union));

int a = 2;
int *b = {&a};
NSLog(@"int[] --> %s",@encode(typeof(b)));

接下来我们以@16@0:8为例解释一下编码的含义:

  • @: 是一个id类型,因为getter方法是有返回值的
  • 16: 整串编码所占用的内存
  • @: 参数id, id self 默认第一个参数
  • 0: 从0号位置开始
  • :: 表示SEL,SEL _cmd 默认第二个参数
  • 8: 表示从8号位置开始

还记得我们生成的cpp文件中,gettersetter方法:

static NSString * _Nonnull _I_Person_name(Person * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_name)); }
static void _I_Person_setName_(Person * self, SEL _cmd, NSString * _Nonnull name) { (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_name)) = name; }

static NSString * _Nonnull _I_Person_realName(Person * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_realName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setRealName_(Person * self, SEL _cmd, NSString * _Nonnull realName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _realName), (id)realName, 0, 1); }

static NSInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }

这里面分为了两种赋值方式:

  • objc_setProperty
  • ((char *)self + OBJC_IVAR_$_Person$_name)) = name 内存平移赋值

接下来我们结合llvm来分析一下这两种赋值方式的区别:

objc_setProperty分析

LLVM源码分析

我们知道,setter方法的本质是对内存进行赋值,我们可以给一个类设置任何属性,但是最终底层的代码实现是不变的,那么就需要有一个封装层来动态处理这些属性赋值的过程,objc_setProperty方法我们可以看做是对这个赋值过程进行的一个封装;

一个正常的setter方法我们可以这样理解:对于ivarsetter方法,有一个selimp是对应的,那么现在通过一个入口使得sel绑定的imp重定向到了objc_setProperty方法上,这个过程是在编译阶段LLVM完成的;

接下来我们通过LLVM来分析一下objc_setProperty:

LLVM源码下载

我们通过逆向推导的方式来分析objc_setProperty的实现:

首先,我们可以在LLVM的源码中搜索objc_setProperty,定位到如下代码:

系统在getSetProperty方法中通过CreateRuntimeFunction来创建运行时方法objc_setProperty,那么我们可以继续探索什么地方调用了getSetPropertyFn方法:

可以看到有两处调用了getSetPropertyFn方法,最终都指向GetPropertySetFunction,我们继续搜索:

这里是创建了一个PropertyImplStrategy类型的strategy,然后根据strategy.getKind()返回的枚举值做不通的处理,那么我们只需要去找到在PropertyImplStrategyKind的赋值情况即可:

通过搜索PropertyImplStrategy,我们定位到如下代码:

根据上图中红框部分的解释:当有propertycopy描述的情况时,会在setter方法中出现objc_setProperty进行赋值的情况出现

验证

我们将Person类进行如下修改:

@interface Person : NSObject{
    NSString *nickName;
    NSObject *objc;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *realName;
@property (atomic, copy) NSString *acRealName;
@property (atomic) NSString *aRealName;
@property (nonatomic, retain) NSString *nrRealName;
@property (atomic, retain) NSString *arRealName;
@property (retain) NSString *rRealName;


@property (nonatomic, assign) NSInteger age;
- (void)run;
+ (void)eat;
@end

@implementation Person
- (void)run {

}

+ (void)eat {

}
@end

然后使用xcrun命令生成cpp文件如下:

结论

  • objc_setProperty
    • nonatomiccopy修饰的属性,生成的setter方法会调用objc_setProperty
    • atomiccopy修饰的属性,生成的setter方法会调用objc_setProperty
    • nonatomicretain修饰的属性,生成的setter方法会调用objc_setProperty
    • atomicretain修饰的属性,生成的setter方法会调用objc_setProperty
    • 仅用retain修饰的属性,生成的setter方法会调用objc_setProperty
  • objc_getProperty
    • atomiccopy修饰的属性,生成的getter方法会调用objc_getProperty
    • atomicretain修饰的属性,生成的getter方法会调用objc_getProperty
    • 仅用retain修饰的属性,生成的getter方法会调用objc_getProperty

使用copy或者retain修饰的属性,生成的setter方法会调用objc_setProperty

使用copy或者retain且使用非nonatomic修饰的属性,生成的getter方法会调用objc_getProperty

其他均采用内存平移赋值/取值

注意: 此处真机添加objc_getProperty符号断点时,nonatomic, copy修饰的属性也会触发objc_getProperty,猜测Clang真机运行有差异;

类的成员变量的存储

WWDC 2020中关于runtime的优化分析

WWDC 2020中关于runtime的优化

成员变量的存储分析

在上篇文章中,我最终留下了一个疑问:类的成员变量存储到哪里去了?WWDC 2020针对运行时进行了优化。在优化过程中,将ivar存在了ro中,那么我们分析一下源码能否找到 ro的相关操作:

那么我们通过lldb来分析一下ro

继续分析:

最终我们找到了成员变量nickName,以及属性生成的带_的成员变量

那么我们来看一下baseProperties里边存放的什么东西?

我们最后得出结论:basePropertiesproperties()存放的都是属性

类方法的存储

所谓的对象方法或者是类方法都是我们在OC层面的概念,在C/C++底层统称为函数。那么如果我们在OC中同时定义了-(void)run+(void)run,那么我们在底层代码中放在一起存储的话,我们如何区分呢?最大可能就是我们没有找到的类方法,被存储在了别处,那么在什么地方呢,那就是我们之前引入的元类,类方法就存储在元类中,接下来我们用lldb来验证一下:

协议方法的获取

Person类中添加协议

@protocol PersonDelegate <NSObject>
- (void)delegatMethod;
@end

@interface Person : NSObject<PersonDelegate>{
    NSString *nickName;
    NSObject *objc;
}

按照获取方法和属性的方式,lldb调试至此:

返回了protocol_list_t类型的数据,通过查看源码:

按照之前的逻辑,我们应该找到protocol_ref_t类型的数据,查看protocol_list_t的源码发现:

全局搜索protocol_ref_t找到了其类型转换的代码:

经过查看protocol_t类型的源码发现其很有可能就是我们最后要找的数据存储的地方:

lldb打印:

类的懒加载

我们创建一个继承自Person的类Teacher

@interface Teacher : Person

@end

@implementation Teacher

@end

此时我们使用lldb进行打印调试:

我们在lldb中加载了一下Teacher类,这里就能打印出firstSubclass侧面验证了我们类的加载是一个懒加载的过程

类方法存储的API方法解析

除了以上使用lldb的方法分析,我们还可以使用API分析方法的存储

第一种分析方案

void testCopyMethodList(Class cls) {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (unsigned int i = 0; i < count; i++) {
        Method const method = methods[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"-->%@", methodName);
    }
}

Person *person = [Person alloc];
Class cls = object_getClass(person); 
testCopyMethodList(cls); // 打印Person的方法列表
NSLog(@"------分割线-------");
const char *className = class_getName(cls);
Class metaClass = objc_getMetaClass(className);
testCopyMethodList(metaClass); // 打印Person的元类的方法列表

打印结果:

-->run
-->name
-->.cxx_destruct
-->setName:
-->age
-->setAge:
-->realName
-->setRealName:
------分割线-------
-->eat

在类Person中打印出了属性的settergetter方法,对象方法以及一个C++的析构方法,在Person的元类中打印出了类方法eat

第二种分析方案

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

    Method method1 = class_getInstanceMethod(pClass, @selector(run));
    Method method2 = class_getInstanceMethod(metaClass, @selector(run));

    Method method3 = class_getInstanceMethod(pClass, @selector(eat));
    Method method4 = class_getInstanceMethod(metaClass, @selector(eat));

    NSLog(@"-->%p", method1);
    NSLog(@"-->%p", method2);
    NSLog(@"-->%p", method3);
    NSLog(@"-->%p", method4);
}

Person *person = [Person alloc];
Class cls = object_getClass(person);
testInstanceMethodPlace(cls);

打印结果:

-->0x1000080e8
-->0x0
-->0x0
-->0x100008080

结论分析:

  • 第一行打印结果: Person类有对象方法run
  • 第二行打印结果: Person的元类没有对象方法run -- run存储在Person
  • 第三行打印结果: Person类没有对象方法eat -- eatPerson类的类方法,存储在Person的元类
  • 第四行打印结果: Person的元类有对象方法eat -- eatPerson的元类中以对象方法的形式存在

第三中分析方案

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

    Method method1 = class_getClassMethod(pClass, @selector(run));
    Method method2 = class_getClassMethod(metaClass, @selector(run));

    Method method3 = class_getClassMethod(pClass, @selector(eat));
    Method method4 = class_getClassMethod(metaClass, @selector(eat));

    NSLog(@"-->%p", method1);
    NSLog(@"-->%p", method2);
    NSLog(@"-->%p", method3);
    NSLog(@"-->%p", method4);
}

打印结果:

-->0x0
-->0x0
-->0x1000081e8
-->0x1000081e8

结论分析:

第一行打印结果: Person类没有类方法run -- runPerson类的对象方法 第二行打印结果: Person的元类没有类方法run -- runPerson类的对象方法 第三行打印结果: Person类有类方法eat -- eatPerson类中以类方法存在 第四行打印结果: Person的元类有类方法eat -- ???

疑惑:

为什么第四行打印结果显示:Person的元类会有类方法eat呢,eat在元类中不是应该以对象方法的形式存在么,此时我们结合objc源码分析:

原来在class_getClassMethod的底层实现是调用了class_getInstanceMethod,接下来我们看一下getMeta()方法的实现:

结合判断条件的注释是否是元类,我们可以知道getMeta()就是判断传进来的类是否是元类来返回不同结果的,而此处明显传进来的是Person的元类,所以getMeta()就返回了this元类自己,那么最终class_getClassMethod还是返回了元类的对象方法,即返回了Person元类的对象方法eat,这就是为什么第三行和第四行打印地址一样的原因;

在底层没有类方法,都是对象方法

第四种分析方案

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

    IMP imp1 = class_getMethodImplementation(pClass, @selector(run));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(run));

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

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

打印结果:

-->0x1000038a0
-->0x1002eb100
-->0x1002eb100
-->0x100003890

结论分析:

  • 第一行打印结果: Person类有run方法的实现
  • 第二行打印结果: Person的元类有run方法的实现 -- ???
  • 第三行打印结果: Person类有eat方法的实现
  • 第四行打印结果: Person的元类有eat方法的实现

疑惑:

我们知道寻找imp的过程就是sel映射imp的过程(方法的查找过程)

为什么Person的元类会有类的对象方法run的实现呢?我们结合objc的源码进行分析

当需要映射的imp不存在时,底层方法反悔了一个_objc_msgForward,这就是最终出现Person的元类存在run方法实现结果的原因,那么_objc_msgForward是什么呢

(补充)消息转发机制: _objc_msgForward

我们调用对象的方法在底层都是通过_objc_msgSend方法来给当前对象发送消息,但是它并不是直接发送消息的,而是由一个_objc_msgForwardimp触发了消息转发机制,那么_objc_msgForward做了什么呢?

_objc_msgForward其实是一个imp类型,是一个具体的方法实现(Apple并未公布),其主要作用就是用来进行消息转发,如果一个消息通过_objc_msgSend发送之后没有找到,那么_objc_msgForward就会尝试做消息转发

具体_objc_msgForward是如何进行消息转发的,我们后续会在runtime中一窥究竟......