在上一节中 iOS底层探究--------类的原理分析(上),我们探究分析了类的isa的走位图、类的继承链、类的结构等等相关的内容,那么今天接着上一节的内容往下走。
1、类的内存的ro数据
1.1、ro是怎么来的
在objc源码中,再创建一个继承于TestPerson类的TestSon类。在这个TestSon类里面添加一个成员变量subString和- (void)personSay;方法。如图:
然后运行起来,通过lldb调试,去查找类里面的信息。
通过调试,发现,
firstSubclass是为nil,但是我们明明已经创建了一个TestSon类,继承于TestPerson类的,那么TestPerson类的子类,就应该是TestSon类才是的,为什么是nil了?(在上篇文章,就出现了这个问题)那么接下来,我们就来探索解答。
我们是不是可以去查看TestSon类里面的信息了,看看能不能找到一些线索?那么,再通过lldb调试,在查看TestSon类的信息:
这是为什么?先给个小小的提示:
懒加载。在后面的内容里面,有详细的阐述哦。😄
在上一节的文章里面,还留下了几个问题,比如:
在通过
lldb调试,查看TestPerson类的信息时,我们只找到了name、hobby的getter和setter方法,还有- (void)sayGood;方法等这些信息,但是属性subject和类方法+ (void)say666却没有找到。那是为什么了?
由于Objecttive-C运行时,会使用数据结构来追踪类。因为在运行时,类数据是变化的,我们根据类的结构图可以知道,类里面包含ISA、superclass、 cache、bits。bits里面就有个class_rw_t,那么,在class_rw_t里面,有个class_ro_t结构体,它里面包含了更多的只能读的信息,如:类名称、方法、协议和实例变量的信息等等相关的内容。当类第一次从磁盘加载到内存中时,就是如此的,但是一经使用后,它们就会发生变化。那么这里面就有两个词:clean memory和dirty memory。clean memory是指加载后不会发生更改的内存,而class_ro_t就属于此,因为是只读的;而dirty memory是指在进程运行时,会发生改变的内存。那么类结构一经使用,就会变成dirty memory,因为运行时会向他写入新的数据,所以只要进程一运行,那么dirty memory就存在。(详细的内容,可以看 2020年的WWDC官网)。
根据上述关系,就能得到下图:
1.2、查看ro里的内存内容
到此,就比较清晰了,那么我们想要找的信息,就在class_ro_t结构体在class_rw_t中调用所声明的指针地址里面,而class_ro_t结构体的声明是*or,如此的话,我们找先关的信息,知道到or()所查找的地址里面,再通过get()就能找到了。
那么接下来重新运行工程,就通过lldb调试来验证下:
①、先拿到data()里面的内容的地址 $4(也就是class_rw_t里面的内容)
②、再在class_rw_t里面,查找ro()(也就是class_ro_t)的内容:
③、到了这一步,就能通过get()方法,找到对应的信息了
到此,就把ro()里面的内容查找出来了。
是不是感觉对类的了解有增加了一层,
不要骄傲啊,继续往下走😁
2、成员变量和属性以及编码
2.1、成员变量
我们建立一个project工程,如下图:
在
show in finder,找到工程文件,然后,在终端打开,再执行clang 命令,将main.m文件转化成c++文件(在 iOS的底层探究--------对象的本质一文中,有具体描述)。得到main.cpp文件后,打开此文件。
在main.cpp文件中,全文索引TestPerson,就能找到对应部分:
还会主动生成相应的
setter和getter方法:
但是,根据
setter和getter方法图,可以看到,成员变量nickName的setter方法,和成员变量aname、name的setter方法是不一样,成员变量aname、name的setter方法是通过指针的偏移来存取值,但是成员变量nickName的setter方法却不是这样子的。这是为什么了?好,我们接着往下找答案。
2.2、编码以及编码的解读
在main.cpp文件中,还生成了对应的编码,如:"hobby", "@\"NSString\"、"nickName", "@16@0:8"等这些。那么这些编码是什么了?
如@16@0:8,从左往右看,第一个@表示id类型;16表示所占用的总内存;第二个@符号表示参数(默认的id(self),_cmd);0表示从0号位置开始;“:”符号表示SEL(查Type Encodings得到);8代表从8号位置开始(倒推下,SEL占8个字节,又从8号位开始,所以占总内存16字节)。
再举个例子,如:v24@0:8@16,从左往右看,v代表void方法;24表示占用的总内存;第一个@表示参数id self;0表示0号位置开始;“:”表示SEL;8表示从8号位置开始;第二个@表示传入的参数;16我表示从16号位置开始(倒推下,从16号位开始,传入的string参数8字节,刚好总内存24字节)
这些是苹果系统自带的。苹果有个方法是: ivar_getTypeEncoding();。可以通过command + shift + 0快捷键,进入到当前的documentation文件。然后在这个文件中,搜索ivar_getTypeEncoding。
进入网页后,就是对应的编码,如图(部分):
当然,也可以通过下面的方式打印出来
#pragma mark - 各种类型编码
void lgTypes(void){
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)));
}
//可以直接打印出来。
还可以通过下面的方法查询打印的结果:
void lgObjc_copyIvar_copyProperies(Class pClass){
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
//获取实例变量名
const char*cName = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:cName];
LGLog(@"class_copyIvarList:%@",ivarName);
}
free(ivars);
unsigned int pCount = 0;
objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
for (unsigned int i=0; i < pCount; i++) {
objc_property_t const property = properties[i];
//获取属性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
//获取属性值
LGLog(@"class_copyProperiesList:%@",propertyName);
}
free(properties);
}
调用:
LGPerson *person = [LGPerson alloc];
Class pClass = object_getClass(person);
lgObjc_copyIvar_copyProperies(pClass);
总之,方式多多啊😄。
在刚刚的2.1中,就有个setter方法的问题,那么接下来,就进行全面的底层分析
3、setter方法的底层原理 (上)
在TestPerson类中,hobby称为属性,是因为是基本类型string,除此之外还有:int、double、float、char、bool、number、integer等等。
而实例变量是一个对象,在实例变量里面,是可以添加基本数据类型的。
刚刚上文中,成员变量
aname、name的setter方法是通过地址偏移实现的,而nickName的setter方法却不是这样子的,为什么了?
在OC中,给一个成员变量赋值,通常是找这个成员变量的setter方法和getter方法。但是在刚刚看的nickName的setter方法里面:
调用的是
objc_setProperty。
在日常开发过程中,我们会定义很多的成员变量,这些成员变量要赋值的话,在OC层,就需要找setter方法,在底层(c、c++),苹果系统已经绑定好了,有固定的方法来处理这些逻辑。然而在OC层,由于定义的成员变量都是多种多样的,那么setter方法也是各式各样。如果是每个setter方法,在底层都有一个契合的工具,那显然是不可能的。
但是,setter方法的本质一样的,就是对一个相对应的内存里面赋值,所以就可以对所有的setter方法进行封装,封装成一个基类的方法。那么这样子,就能在OC层和底层之间,产生一个中间层:objc_setProperty。那么底层只需要对中间层的objc_setProperty进行对接实现就行,底层的代码就可以固定不变了。然后,只需要对OC层的各类的setter方法进行动态处理,和中间层objc_setPropert对接上就行。而各个成员变量的setter方法,只是他们_cmd的名称不一样。那么在编译的时候,就能获取每个类的ivars,ivars里面包含了成员变量的信息,只要让成员变量的setter方法的sel定向去找对一个与之相匹配的imp,此时还是编译阶段,这个imp还没有实现。把imp重定向到objc_setPropert里面。这样,就形成一个OC层通向中间层objc_setPropert的入口。
4、setter方法的底层原理(下)
现在,我们知道了nickName的setter方法是通过objc_setProperty这个中间层来对接底层的,那么底层是怎么对接到的objc_setProperty了?也就是说,怎么去告诉底层,有property属性调用?那么接下来,就通过逆推,来分析这些流程。这就需要用到底层llvm-projct的源码了。
这里提供了LLVM的官网
还有llvm-project源码的下载地址
之所以查看LLVM的源码,而不是objc的源码,是因为,我们在探究的时候,objc_setProperty并不是直接和成员变量的方法(如:setName)进行关联,而是在编译的时候,直接绑定(也不是在运行时再通过setName去找对应的imp做处理)。本身在底层就有处理这些逻辑的对应的固定方法,所以,才需要在调用该成员变量的时候,重定向imp来对接上底层的那些固定的方法。
在工程中打开源码(可以不用xcode打开,也可以用其他的工具打开,如:Visual Studio Code),然后,就从objc_setProperty开始推导。首先在源码文件中搜索objc_setProperty,就回出现很多关于objc_setProperty的索引结果。
相信看到这么多的索引结果,都会头疼下,不知从哪里开始入手。不要慌😄,首先了,要理清一个思路,那就是底层在对接objc_setProperty时,一定会创建一个内容作为对接objc_setProperty的入口,找到这个内容之后,就可一个接着往下跟:为什么要创建、需要哪些条件。。。。等等。那么就跟着这个思路,开始找这个创建的内容。就能找到:
选中搜索结果,查看源码:
通过源码,就能知道,之所以要创建CreateRuntimeFunction()方法,是因为要去拿到getSetPropertyFn()(获取当前set属性的函数),接下来,就以getSetPropertyFn()为索引条件搜索。通过索引发现,是由GetPropertySetFunction()调用了getSetPropertyFn()。
那么再接着通过GetPropertySetFunction()往下找,寻找调用GetPropertySetFunction()方法的地方。
找到调用的地方,就能看到,调用GetPropertySetFunction()方法,会生成一个条件setPropertyFn,当条件不满足,就会报出一个错误码,当条件满足的时候,就继续往下执行。那么接下来,寻找的线索,就是这个条件setPropertyFn了。因为是一个局部变量,那么跟着原文往上找:
case PropertyImplStrategy::GetSetProperty:
case PropertyImplStrategy::SetPropertyAndExpressionGet: {
llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
llvm::FunctionCallee setPropertyFn = nullptr;
//中间这处代码省略掉了。。。
else {
setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction();
if (!setPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C setter requiring atomic copy");
return;
}
}
//此处一下代码省略掉了。。。。
发现,是通过llvm::FunctionCallee setPropertyFn = nullptr;定义的一个局部变量。好,到了这里,是不是感觉线索就断了?no、no、no,我们再放大下视野,这个判断是包含在 PropertyImplStrategy::SetPropertyAndExpressionGet:这个里面的,而 PropertyImplStrategy::SetPropertyAndExpressionGet:又包含在switch (strategy.getKind()) 里面,所以,最终来源还是在switch (strategy.getKind()) 里面 :
switch (strategy.getKind()) {
case PropertyImplStrategy::Native: {//省略了代码}
case PropertyImplStrategy::GetSetProperty:
case PropertyImplStrategy::SetPropertyAndExpressionGet: {//省略了代码}
}
通过简化的源码,就能看出,当满足不同的条件时,就执行一个相对应的过程,而这个条件就是以PropertyImplStrategy进行主导的,而PropertyImplStrategy也是在某个地方被赋值了,才能在这里,根据赋的值做判断,来执行所对应的过程。
而PropertyImplStrategy的里面是怎么定义的了?
namespace {
class PropertyImplStrategy {
public:
enum StrategyKind {
/// The 'native' strategy is to use the architecture's provided
/// reads and writes.
Native,
/// Use objc_setProperty and objc_getProperty. 这个类型使用了setter方法和getter方法
GetSetProperty,
/// Use objc_setProperty for the setter, but use expression
/// evaluation for the getter.
SetPropertyAndExpressionGet,
/// Use objc_copyStruct.
CopyStruct,
/// The 'expression' strategy is to emit normal assignment or
/// lvalue-to-rvalue expressions.
Expression
};
StrategyKind getKind() const { return StrategyKind(Kind); }
bool hasStrongMember() const { return HasStrong; }
bool isAtomic() const { return IsAtomic; }
bool isCopy() const { return IsCopy; }
CharUnits getIvarSize() const { return IvarSize; }
CharUnits getIvarAlignment() const { return IvarAlignment; }
PropertyImplStrategy(CodeGenModule &CGM,
const ObjCPropertyImplDecl *propImpl);
private:
unsigned Kind : 8;
unsigned IsAtomic : 1;
unsigned IsCopy : 1;
unsigned HasStrong : 1;
CharUnits IvarSize;
CharUnits IvarAlignment;
};
}
那么接下来,我们是不是就可以把PropertyImplStrategy赋值作为线索接着往下探寻,所以,就是索引PropertyImplStrategy,查看其赋值的地方,赋值也就伴随这初始化。找到的初始化,并赋值的地方
/// Pick an implementation strategy for the given property synthesis.
PropertyImplStrategy::PropertyImplStrategy(CodeGenModule &CGM,
const ObjCPropertyImplDecl *propImpl) {
const ObjCPropertyDecl *prop = propImpl->getPropertyDecl();
ObjCPropertyDecl::SetterKind setterKind = prop->getSetterKind();
IsCopy = (setterKind == ObjCPropertyDecl::Copy);
IsAtomic = prop->isAtomic();
HasStrong = false; // doesn't matter here.
// Evaluate the ivar's size and alignment.
ObjCIvarDecl *ivar = propImpl->getPropertyIvarDecl();
QualType ivarType = ivar->getType();
auto TInfo = CGM.getContext().getTypeInfoInChars(ivarType);
IvarSize = TInfo.Width;
IvarAlignment = TInfo.Align;
// If we have a copy property, we always have to use getProperty/setProperty.
// TODO: we could actually use setProperty and an expression for non-atomics.
if (IsCopy) {
Kind = GetSetProperty;
return;
}
//下面的代码省略掉了。。。。
我们发现,当条件满足是IsCopy时,Kind = GetSetProperty,从PropertyImplStrategy的定义中,类型GetSetProperty是使用了setter方法和getter方法的。而达到IsCopy的条件是(setterKind == ObjCPropertyDecl::Copy)。到了这里,是不是很明了了,nickName之所以setter方法调用的objc_setProperty,是因为在声明的时候,使用的copy关键字修饰的。
那么综合来看,为什么声明成员变量,使用strong和copy修饰会存在差别?原因就是,虽然在OC层的效果是一样的,但是在底层的调用上,就存在很大的区别。strong修饰的,在底层,是通过地址平移来获取对应的值。而copy修饰的,是动态处理setter方法,通过中间层objc_setProperty和底层进行对接,对sel所对应的imp进行了重定向,对接上处理这些逻辑的对应方法,还有copy会对当前的内存进行复制操作。如,在查看objc源码中objc_setProperty的实现里面就有(reallySetProperty方法里的内容):
以及原子和非原子的区别:
好,回到LLVM中,根据源码,除了copy修饰的关键字以外,还有Retain、Only、Atomic、Strong。。。等等。
//省略代码。。。
// Handle retain.
if (setterKind == ObjCPropertyDecl::Retain) {
// In GC-only, there's nothing special that needs to be done.
if (CGM.getLangOpts().getGC() == LangOptions::GCOnly) {
// fallthrough
// In ARC, if the property is non-atomic, use expression emission,
// which translates to objc_storeStrong. This isn't required, but
// it's slightly nicer.
} else if (CGM.getLangOpts().ObjCAutoRefCount && !IsAtomic) {
// Using standard expression emission for the setter is only
// acceptable if the ivar is __strong, which won't be true if
// the property is annotated with __attribute__((NSObject)).
// TODO: falling all the way back to objc_setProperty here is
// just laziness, though; we could still use objc_storeStrong
// if we hacked it right.
if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)
Kind = Expression;
else
Kind = SetPropertyAndExpressionGet;
return;
// Otherwise, we need to at least use setProperty. However, if
// the property isn't atomic, we can use normal expression
// emission for the getter.
} else if (!IsAtomic) {
Kind = SetPropertyAndExpressionGet;
return;
// Otherwise, we have to use both setProperty and getProperty.
} else {
Kind = GetSetProperty;
return;
}
}
//省略代码。。。
那么我们也可以直接在工程中进行验证,定义多个不同修饰词修饰的成员变量:
在终端中,执行clang -rewrite-objc main.m -o main.cpp,查看main.cpp文件里面,他们对应的getter和setter方法:
①、@property (nonatomic, copy) NSString *nickName_zero所对应的setter方法
②、@property (atomic, copy) NSString *nickName_one;所对应的setter方法
③、@property (nonatomic) NSString *nickName_two;找不到它的objc_setProperty方法,也就是没有setter方法
④、@property (atomic) NSString *nickName_three;也没有setter方法
那么,通过这样一个对比,就比较清楚,这个
setter方法的产生,是跟谁有关系了。
到了这里,是不是对成员变量的setter和getter方法的底层实现和原理,有了比较清晰的了解了。😄
5、类方法的存储
前面,我们在探究类的内存的时候,有在TestPerson类里面定义了属性、成员变量、实例方法、类方法等,在通过lldb调试的时候,在类的内存里面,只找到了属性、成员变量、实例方法,但是类方法,却没有找到。
运行工程,把工程的machO文件用MachOView打开,发现能够找到+ (void)say666类方法的:
- (void)sayGood方法是对象方法(实例方法),根据前面lldb调试的结果,这个方法是存储在类里面的。方法的存储和变量存储的方式是不一样的,对象方法存储在类里面,是为了避免多个方法名的浪费。
而类方法
+ (void)say666,则不一样。
我们在OC层面上说的对象方法、类方法,都是我们人为的加上去的,而在c和c++底层,都统称为函数。如果对象方法存储在类里面,类方法也存储在类里面,就如同刚刚那张图,两种方法都用同一个名字,那么该如何去区分了?所以就存在很大的漏洞。
- 那么
OC为了避免这个现象的存在,就有了元类的产生。而类方法也就放在元类的内存当中。
接下来进行验证:
最后,就找到了
+ (void)say666类方法。就完美的验证了刚刚的结论。
当然除了lldb调试之外,我们还能通过方法打印,来验证
6、类方法存储的API方式解析
我们新建一个project工程,在工程里面,创建一个TestPerson类,在TestPerson类里面添加一些变量和方法,如下图:
可以把当前类所有的class方法,通过api打印出来,还可以通过当前的类,打印他对应的实例方法,还有类方法。
①、打印当前类所有方法
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);
}
②、查找当前类的实例方法
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);
}
③、查找当前类的类方法
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));
// - (void)sayHello;
// + (void)sayHappy;
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);
}
④、通过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));// 0
// sel -> imp 方法的查找流程 imp_farw
IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy)); // 0
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
NSLog(@"%s",__func__);
}
运行工程,通过断点调试,设置了5个断点:
当走完断点①,查找类里面的方法,打印的结果:
当走完断点②,查找元类里面的方法,打印的结果:
当走完断点③,查找类和元类里面的方法,打印的结果:
当走完断点④,查找类和元类里面的方法,打印的结果:
可以看出通过底层,在类和元类中,都能找到类方法,为什么在元类中,能找到类方法了?在类里面的类方法是以实例方法的形式存储在元类中的。就需要到objc的源码中,查看class_getClassMethod方法的底层了。如下图,从底层的实现中,可以看出传进去是元类,不满足if条件,那么,就以实例方法返回了。
而
cls->getMeta()就判断是不是元类,如图:
得出个结论:在底层没有类方法之称,都是对象方法。
再走完断点⑤,通过IMP查找类里面的方法,打印的结果:
看这个结果,就很奇怪了,类里面能找到实例方法是正常的,但是元类里面也能找到,那就不正常了,还有就是类里面能找到类方法,也不正常。按理说,这两个不正常的地方,返回
0x0才对的,但是现在是有地址的。为什么了?
通过objc源码,找到class_getMethodImplementation方法的实现,如图:
寻找
imp的过程,其实就是sel去寻找imp,也就是方法的查找过程,当查找不到imp的时候,就返回_objc_msgForward。再返回去看断点⑤打印的结果,发现,类里面查找的类方法的地址和元类查找到的实例方法的地址是一样的,都是0x7fff201baac0。
到了此处,欧耶,大功告成,类的原理的底层探究,就完成了,感谢各位的光临