
接着上一章的类的原理分析(上)我们继续看看类里面剩下的东西。
成员变量、实例变量、属性的区别
我们创建一个工程在main.m中直接创建一个ZYPerson的类,并且给它创建属性、实例变量、成员变量。上代码:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface ZYPerson : NSObject{
NSString *subject;
NSObject *myObjc;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, strong) NSString *myNickName;
@end
@implementation ZYPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
我们利用clang 把 main.m文件转成c++文件看看底层它们分别是以什么形式存在。命令:clang -rewrite-objc main.m -o mian.cpp。
我们看到下面的代码中有一些"c"、"i"、"s"、"q"等字符以及下面代码里的一些”@16@0:8"、"v24@0:8@16"等字符。
上代码:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[12];
} _OBJC_$_INSTANCE_METHODS_ZYPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
12,
{{(struct objc_selector *)"name", "@16@0:8", (void *)_I_ZYPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_ZYPerson_setName_},
{(struct objc_selector *)"hobby", "@16@0:8", (void *)_I_ZYPerson_hobby},
{(struct objc_selector *)"setHobby:", "v24@0:8@16", (void *)_I_ZYPerson_setHobby_},
{(struct objc_selector *)"myNickName", "@16@0:8", (void *)_I_ZYPerson_myNickName},
{(struct objc_selector *)"setMyNickName:", "v24@0:8@16", (void *)_I_ZYPerson_setMyNickName_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_ZYPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_ZYPerson_setName_},
{(struct objc_selector *)"hobby", "@16@0:8", (void *)_I_ZYPerson_hobby},
{(struct objc_selector *)"setHobby:", "v24@0:8@16", (void *)_I_ZYPerson_setHobby_},
{(struct objc_selector *)"myNickName", "@16@0:8", (void *)_I_ZYPerson_myNickName},
{(struct objc_selector *)"setMyNickName:", "v24@0:8@16", (void *)_I_ZYPerson_setMyNickName_}}
};
这些其实是我们系统设定的一些编码。我们也有ivar_getTypeEncoding(<#Ivar _Nonnull v#>)这样的API,同时可以通过快捷键CommandShift+0,然后搜索ivar_getTypeEncoding,找到最下方的Type Encodings点击进入网页就能找到以下图表:

每一个编码都有对应的意思。我们可以根据这个表知道”@16@0:8"、"v24@0:8@16"这种东西是什么意思。如下图:
示例一: @16@0:8
第一位 @ : id类型
第二位 16 : 总共占用的内存 16
第三位 @ :参数 id-self
第四位 0 : 从0号位置开始
第五位 : : 参数SEL
第六位 8 : 从8号位置开始
倒推:self 从0号位置开始,因为self 指针占 8字节,所以会排到7号位;SEL从8号位置开始,因为SEL 刚好是8字节 所以到SEL 存完就刚好到16号位 对应开始的 16。
示例一:’v24@0:8@16‘
第一位 v : 无返回值 void
第二位 ’24’ : 总共占用的内存 24
第三位 ’@‘ : 参数 id-self
第四位 ‘0’ : 从0号位置开始 第五位 ’:’ : 参数SEL第六位8: 从8号位置开始 第七位@: 参数string第八位16: 从16`号位置开始
倒推:self 从0号位置开始,因为self指针占 8字节,所以会排到7号位;SEL从8号位置开始,因为SEL 刚好是8字节所以到SEL存完就刚好16;因为后面我们自己传了一个参数 string 类型的, 所以这个参数从 16号位置开始,因为string 类型占用 8字节,所以当最后一个string类型参数存完就到了 16+8 = 24号位置了。所以对一个前面的 24。
对于上面的字符这里也提供一个方法供大家去打印查看:
上代码:
#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];
NSLog(@"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)];
//获取属性值
NSLog(@"class_copyProperiesList:%@",propertyName);
}
free(properties);
}
objc_setProperty 和 指针平移 的区别
我们打开main.cpp文件然后全局搜索我们的类ZYPerson。如图:

从上图可以看到我们的属性在底层其实都帮我们转成了_xx的形式并且都放进了NSObject_IVARS里面,同时我们也看到生成了下面的setter/getter方法。在这些方法里,我们细心点可以看到,myNickName的setter方法和其他两个name、hobby的setter方法不一样。如上图展示的红色框部分1、2、3。1、2是用objc_setProperty,而3却是直接用内存平移的方式设置值。接下来我们来探究下到底是什么原因造成这两种不同以及他们的区别。
我们在.cpp文件已经没有办法定位这个问题的关键点了。所以我们我们思考下,我们在将代码编译的过程中,我们是将这些成员变量、属性都进行了编译并且存进了类的ivars里面(这个在前面的文章我们有探究并且找到)。那我们能不能跟踪这个ivars里面的变量的走向呢?我们知道每个成员变量赋值的过程肯定是经过sel->imp将值写入内存中。那我们能不能拦截到这一步将这个imp重定向到objc_setProperty呢?,跟着这样一个思路我们尝试找找看,另外我们知道这些步骤肯定是在编译的时候就已经完成,所以我们眼光需要定位到LLVM阶段。下面我们就用VSCode打开LLVM 源码看看:
附:LLVM下载地址
我们将LLVM拉到VSCode里打开(这个打开比较快),然后全局搜索我们要找的objc_setProperty.在展示出来的这些文件中我们看到一个CGObjCMac.cpp文件我们随便查看,发现有一个方法里有一个方法 是创建 这个 objc_setProperty的runtime方法。如下图:

我们就思考,这里都已经开始创建了,那是什么条件导致他创建这个呢?我们采用倒推法,跟踪这个方法的上层方法,我们看到创建的方法是在一个叫 getSetPropertyFn()的方法里调用。那我们搜索下这个getSetPropertyFn() 方法看他是在什么情况下调用的,如下图:

我们又找到一个方法:GetPropertySetFunction()。我们继续往上层找,搜索这个方法,显示出了很多个,但是我们要找的是调用的地方不是这个方法实现的地方,以为我们是要往上层继续找方法直到 我们找到 什么条件调用这个方法为止。我们看下图:

在这个图片中我们看到了调用。那我们就不禁想为什么在这个地方调用呢?我们看看这个调用的条件,也就是这个调用前的代码。往上翻一下我们看到 这是一个很大的switch,有很多的case比如
case PropertyImplStrategy::Native:
case PropertyImplStrategy::GetSetProperty:
case PropertyImplStrategy::SetPropertyAndExpressionGet:等。
同时我们在Native这个case里看到,他的内容就是获取本地ivars的地址argAddr,然后又获取到ivars的值、地址等信息,然后进行地址处理、最后进行了存储
llvm::StoreInst *store = Builder.CreateStore(load, ivarAddr);
store->setAtomic(llvm::AtomicOrdering::Unordered);
如下图:

这个过程就跟我们之前文章讲到的内存平移一样。只不过我们当时是取值,这里是存储值。所以我们就知道存储形式的分别就是这个策略导致的。同时这个策略来自于switch 中的一个实例变量:switch (strategy.getKind()),所以我们跟着 strategy.getKind这个实例变量往上找。这个实例变量是通过这句代码带过来的 PropertyImplStrategy strategy(CGM, propImpl),所以我们继续跟踪PropertyImplStrategy,同时我们要思考什么时候给这个策略赋值的呢?如果能找到那我们是不是又可以更进一步?所以我们接下来找给这个PropertyImplStrategy赋值的地方。全局搜索:

我们发现这个类里分的情况刚好有注释当GetSetProperty和vSetPropertyAndExpressionGet时都有objc_setProperty。
我们接着看看这个类到底在什么时候进行的初始化呢?我们接着找:如图

从上图我们就看到了一个特别赋值的地方,这就看到了曙光。
所以我们看到copy 是影响它的一个重要因素。我们接下来验证下,如下代码:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface ZYPerson : NSObject{
NSString *subject;
NSObject *myObjc;
}
@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *myName;
@property (nonatomic) NSString *nNickName;
@property (atomic) NSString *aNickName;
@end
@implementation ZYPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZYPerson *person = [ZYPerson alloc];
// person.name = @"ZY";
// person.myNickName = @"ZY_Nick";
}
return 0;
}
我们设置了四个属性,修饰的关键字各不相同只是为了验证是否真的是copy 来影响setter方法是否走objc_setProperty方法。我们利用clang再次编译一下.m 成.cpp。如图:

从代码上我们看到属性name、myName有copy方法,其他的没有,我们从上图也可以看到红色框标注的地方。确实是name、myName这两个属性有objc_setProperty。其他的nNickName、aNickName都没有。同样我们也可以根据探索objc_setProperty的方法去探索objc_getProperty的条件和实现机制。因为我们可以看到上面的图片中有出现objc_getProperty。
补充:
1,我们通过前面几章应该对底层的objc_object、objc_class;person 、NSObject;id 、class;有些清楚的了解了。这里再次总结下:
遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!
