Mac开发-再谈成员变量与初始化

初始化中的self与super

这里主要介绍一下在Objective-C中最常用的两个关键字selfsuper,它们常用在对象初始化方法里。不知道大家有没有研究过这个初始化方法:

1
2
3
4
5
6
7
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
}
return self;
}

相信很多人对这段代码非常熟悉,我们来看几个问题。

selfsuper是什么?

self是一个隐藏参数变量,指向当前调用方法的对象,还有一个隐藏参数是_cmd,代表当前方法selector。在runtime时会调用objc_msgSend()方法。

super并不是隐藏参数,只是编译器的指令符号,在runtime时调用objc_msgSendSuper()方法。

官方文档针对selfsuper已有解释。

self
Whenever you’re writing a method implementation, you have access to an important hidden value, self. Conceptually, self is a way to refer to “the object that’s received this message.” It’s a pointer, just like the greeting value above, and can be used to call a method on the current receiving object.

super
There’s another important keyword available to you in Objective-C, called super. Sending a message to super is a way to call through to a method implementation defined by a superclass further up the inheritance chain. The most common use of super is when overriding a method.

可以简单理解为:self调用自己方法,super调用父类方法。self是当前方法的调用对象,super是预编译指令。其中[self class][super class]的输出是一样的。

[super init]做了什么?

想要明白这个问题需要理解self和super的底层实现原理。

当使用self调用方法时,会首先从当前类的方法列表中开始寻找,如果没有再从父类中寻找。Runtime会调用objc_msgSend函数:

1
id objc_msgSend(id theReceiver, SEL theSelector, ...)

第一个参数是消息接收者,第二个参数是调用的具体类方法的selector,后面是 selector 方法的可变参数。
[self setSize:]为例,编译器会转换成objc_msgSend的函数调用,其中 theReceiverselftheSelector@selector(setSize:),这个selector是从当前self的class方法列表开始找的setSize,一旦找到后把对应的selector传递过去。

而使用super调用方法时,则从父类的方法列表中开始找,然后调用父类的这个方法。Runtime会使用objc_msgSendSuper函数:

1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一个参数是个objc_super的结构体,第二个参数还是类似objc_msgSend类方法的selector。而objc_super的结构体如下:

1
2
3
4
struct objc_super {
id receiver;
Class superClass;
};

当编译器遇到[super setSize:]时,开始做下面几个事:

  1. 构建objc_super的结构体,此时这个结构体的第一个成员变量receiver就是子类,和self中相同。而第二个成员变量superClass就是指父类。然后调用objc_msgSendSuper的方法,将这个结构体和setSize的selector传递过去。

  2. 函数里面做的事情类似这样:从objc_super结构体指向的superClass的方法列表开始找setSize的selector,找到后再用objc_super->receiver去调用这个selector。

也即super关键字只是Objective-C编译器的一个预编译符号,等价于[self class]

为什么要把[super init]赋值给self

Objective-C具有类继承特性,子类继承父类从而获得相关的属性和方法,所以在子类的初始化方法中,必须首先调用父类的初始化方法,完成父类相关资源的初始化。

[super init]selfsuper中调用init,然后super会调用其父类的init方法,以此类推直到找到根类NSObject中的init。然后根类中的init负责初始化内存区域,添加一些必要的属性,返回内存指针,延着继承链,指针从上到下进行传递,同时在不同的子类中可以向内存添加必要的属性。最后直到我们当前类中把内存地址赋值给self参数。当然,如果调用[super init]失败的话,可以通过判断self是否为空来决定是否执行子类的初始化操作。

为了更清楚理解self和super,看下面两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
// Son
// Son

当发送class消息时不管是用self还是用super,接受消息主体依然是self ,也就是说self和super指向同一个对象,所以都会打印出Son。(子类没有重写父类的class方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@interface Father : NSObject
- (void)printCurrentClass;
@end
@implementation Father
- (void)printCurrentClass {
NSLog(@"printCurrentClass:%@", [self class]);
}
@end
@interface Son : Father
- (void)printSuperClass;
@end
@implementation Son
- (void)printSuperClass {
[super printCurrentClass];
}
@end
// 调用方法
Son *son = [Son new];
[son printCurrentClass]; // 直接调用父类方法,子类没有重载
[son printSuperClass]; // 间接调用父类方法
// printCurrentClass:Son
// printCurrentClass:Son

printCurrentClass方法体中self始终指向方法的接收者对象son,倘若换成[super class],结果也是一样的。

另外,应当明确selfClass类型,很容易将self作为当前类类型的参数进行传递,这点需要特别注意。

关于赋值

进入正题,我们经常会在官方文档里看到这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface MyClass : NSObject {
MyObject *myObject;
}
@property (nonatomic, retain) MyObject *myObject;
@end
@implementation MyClass
@synthesize myObject;
- (id)init {
if(self = [super init]) {
MyObject * aMyObject = [[MyObject alloc] init];
self.myObject = aMyObject;
[aMyObject release];
}
return self;
}
@end

有人就问, 为什么要这么复杂的赋值?为什么要使用self.? 直接写成self.myObject = [[MyObject alloc] init];不是也没有错么?甚至不加self在有些场景下好像也是正确的。

现在我们来看看内存管理的内容。先看间接赋值方式。

使用self.

1
2
3
MyObject * aMyObject = [[MyObject alloc] init]; //aMyObject retainCount = 1;
self.myObject = aMyObject; //myObject retainCount = 2;
[aMyObject release]; //myObject retainCount = 1;

不使用self.

1
2
3
MyObject * aMyObject = [[MyObject alloc] init]; //aMyObject retainCount = 1;
myObject = aMyObject; //myObject retainCount = 1;
[aMyObject release]; //对象己经被释放

再来看直接赋值方式。

使用self.

1
self.myObject = [[MyObject alloc] init]; //myObject retainCount = 2;

不使用self.

1
myObject = [[MyObject alloc] init]; //myObject retainCount = 1;

现在是不是有点混乱,我们先来把代码改一下,官方的一种常见写法:

1
2
3
4
5
6
7
8
9
@interface MyClass : NSObject {
MyObject * _myObject;
}
@property (nonatomic, retain) MyObject *myObject;
@end
@implementation MyClass
@synthesize myObject = _myObject;
@end

这里如果使用self._myObject = aMyObject或者myObject = aMyObject,就会得到一个错误。

这和Objective-C的存取方法有关:@property (nonatomic, retain) MyObject *myObject是为一个属性设置存取方法,只是平时我们用的方法名和属性名是一样的。如果把它写成不同的名字,关系就很清晰了:_myObject是属性本身,而myObject是对应成员变量的存取方法名。

从Runtime看成员变量

我们每天都在用成员变量,那么你知道成员变量的本质是什么吗?你可能会说,成员变量就是成员变量啊,还有什么本质?平时只管使用,也没有研究过更深层的含义。这里我们从Runtime的角度重新认识一下类中的成员变量和属性的定义和使用。

成员变量

成员变量的定义如下:

1
typedef struct objc_ivar *Ivar;

其中Ivar是实例变量的类型,是一个指向objc_ivar结构体的指针。

成员变量的常见操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取所有成员变量
class_copyIvarList
// 获取成员变量名
ivar_getName
// 获取成员变量类型编码
ivar_getTypeEncoding
// 获取指定名称的成员变量
class_getInstanceVariable
// 获取某个对象成员变量的值
object_getIvar
// 设置某个对象成员变量的值
object_setIvar

下面是一个成员变量的使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Model的头文件声明
@interface Model : NSObject {
NSString * _str1;
}
@property NSString * str2;
@property (nonatomic, copy) NSDictionary * dict1;
@end
// 获取其成员变量
unsigned int outCount = 0;
Ivar * ivars = class_copyIvarList([Model class], &outCount);
for(unsigned int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
const char * type = ivar_getTypeEncoding(ivar);
NSLog(@"类型为 %s 的 %s ",type, name);
}
free(ivars);

输出结果:

1
2
3
runtimeIvar[602:16885] 类型为 @"NSString" 的 _str1
runtimeIvar[602:16885] 类型为 @"NSString" 的 _str2
runtimeIvar[602:16885] 类型为 @"NSDictionary" 的 _dict1

属性

属性的定义如下:

1
typedef struct objc_property *objc_property_t;

其中objc_property_t声明的属性的类型,是一个指向objc_property结构体的指针。

属性的常见操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取所有属性
// 说明:使用`class_copyPropertyList`并不会获取无`@property`声明的成员变量
class_copyPropertyList
// 获取属性名
property_getName
// 获取属性特性描述字符串,返回`objc_property_attribute_t`结构体列表
property_getAttributes
// 获取所有属性特性
property_copyAttributeList

objc_property_attribute_t结构体包含namevalue,常用的属性如下:

结构类型 name value
属性类型 T 变化
编码类型 C(copy) &(strong) W(weak) 空(assign) 等
非/原子性 空(atomic) N(Nonatomic)
变量名称 V 变化

使用property_getAttributes获得的描述是property_copyAttributeList能获取到的所有的namevalue的总体描述。

下面是一个属性的使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int outCount = 0;
objc_property_t * properties = class_copyPropertyList([Model class], &outCount);
for(unsigned int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//属性名
const char * name = property_getName(property);
//属性描述
const char * propertyAttr = property_getAttributes(property);
NSLog(@"属性描述为 %s 的 %s ", propertyAttr, name);
//属性的特性
unsigned int attrCount = 0;
objc_property_attribute_t * attrs = property_copyAttributeList(property, attrCount);
for (unsigned int j = 0; j < attrCount; j ++) {
objc_property_attribute_t attr = attrs[j];
const char * name = attr.name;
const char * value = attr.value;
NSLog(@"属性的描述:%s 值:%s", name, value);
}
free(attrs);
NSLog(@"\n");
}
free(properties);

输出结果:

1
2
3
4
5
6
7
8
9
10
11
runtimeIvar[661:27041] 属性描述为 [email protected]"NSString",&,V_str2 的 str2
runtimeIvar[661:27041] 属性的描述:T 值:@"NSString"
runtimeIvar[661:27041] 属性的描述:& 值:
runtimeIvar[661:27041] 属性的描述:V 值:_str2
runtimeIvar[661:27041]
runtimeIvar[661:27041] 属性描述为 [email protected]"NSDictionary",C,N,V_dict1 的 dict1
runtimeIvar[661:27041] 属性的描述:T 值:@"NSDictionary"
runtimeIvar[661:27041] 属性的描述:C 值:
runtimeIvar[661:27041] 属性的描述:N 值:
runtimeIvar[661:27041] 属性的描述:V 值:_dict1
runtimeIvar[661:27041]

成员变量与属性的应用

Json到Model的转化

相信在开发中,最常用的就是接口数据需要转化成Model了。很多开发者也都使用著名的第三方库如JsonModel、Mantle或MJExtension等。如果只用而不知其所以然,那真和搬砖没啥区别了。

下面我们使用runtime去解析json来给Model赋值。

原理描述:用Runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。

核心方法即在NSObject的分类中添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
// 1.获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = [email protected]"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
// 通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
// 通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
// 立即释放properties指向的内存
free(properties);
// 2.根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}

这里可以进一步思考:

  1. 如何识别基本数据类型的属性并处理
  2. 空(nil,null)值的处理
  3. json中嵌套json(Dict或Array)的处理

快速归档

有时候我们要对一些信息进行归档,如用户信息类UserInfo,这将需要重写initWithCoder和encodeWithCoder方法,并对每个属性进行encode和decode操作。

那么问题来了:当属性只有几个的时候可以轻松写完,如果有几十个属性呢?这时我们就可以使用Runtime中的属性操作来解决这一问题。

原理描述:用Runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。

核心方法即在Model的基类中重写方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (id)initWithCoder:(NSCoder *)aDecoder {
if(self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}

访问私有变量

我们知道,Objective-C中没有真正意义上的私有变量和方法。要想让成员变量私有,就要在.m文件中声明,从而使变量不对外暴露。但如果我们知道这个成员变量的名称,可以通过Runtime获取成员变量,再通过getIvar来获取它的值。(类似于Java反射)

1
2
Ivar ivar = class_getInstanceVariable([Model class], "_str1");
NSString * str1 = object_getIvar(model, ivar);