《招聘一个靠谱的iOS》— Part ⅠⅠ


本Part整理 ChenYiLong 原文章中余下的部分相关的题目。

编码风格纠错

原代码:
image


修改方法有很多种,现给出一种做示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// .h文件
// 这是第一种修改方法,后面会给出第二种修改方法

typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};

@interface CYLUser : NSObject<NSCopying>

@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;

@end

下面对具体修改的地方,分两部分做下介绍:硬伤部分优化部分。因为硬伤部分没什么技术含量,为了节省大家时间,放在后面讲,大神请直接看 优化部分


优化部分
1.enum 建议使用 NS_ENUMNS_OPTIONS 宏来定义枚举类型,参见官方的 Adopting Modern Objective-C 一文:

1
2
3
4
5
//定义一个枚举
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};

关于性别定义,最严谨的做法可以这样:

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, CYLUserGender) {
CYLUserGenderUnknown,
CYLUserGenderMale,
CYLUserGenderFemale,
CYLUserGenderNeuter
};


2.age 属性的类型:应避免使用基本类型,建议使用 Foundation 数据类型,对应关系如下:

1
2
3
4
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
动画时间 -> NSTimeInterval

同时考虑到 age 的特点,应使用 NSUInteger ,而非 int 。 这样做的是基于64-bit 适配考虑,详情可参考出题者的博文《64-bit Tips》


3.如果工程项目非常庞大,需要拆分成不同的模块,可以在类、typedef宏命名的时候使用前缀。


4.doLogIn方法不应写在该类中:


虽然LogIn的命名不太清晰,但笔者猜测是login的意思, (勘误:Login是名词,LogIn 是动词,都表示登陆的意思。见:Log in vs. login


登录操作属于业务逻辑,观察类名 UserModel ,以及属性的命名方式,该类应该是一个 Model 而不是一个“ MVVM 模式下的 ViewModel ”:

无论是 MVC 模式还是 MVVM 模式,业务逻辑都不应当写在 Model 里:MVC 应在 C,MVVM 应在 VM。


如果抛开命名规范,假设该类真的是 MVVM 模式里的 ViewModel ,那么 UserModel 这个类可能对应的是用户注册页面,如果有特殊的业务需求,比如: -logIn 对应的应当是注册并登录的一个 Button ,出现 -logIn 方法也可能是合理的。


5.doLogIn 方法命名不规范:添加了多余的动词前缀。 请牢记:

如果方法表示让对象执行一个动作,使用动词打头来命名,注意不要使用 do,does 这种多余的关键字,动词本身的暗示就足够了。


应为 -logIn (注意: Login 是名词, LogIn 是动词,都表示登陆。 见 Log in vs. login


6.-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中不要用 with 来连接两个参数: withAge: 应当换为 age:age: 已经足以清晰说明参数的作用,也不建议用 andAge::通常情况下,即使有类似 withA:withB: 的命名需求,也通常是使用 withA:andB: 这种命名,用来表示方法执行了两个相对独立的操作(从设计上来说,这时候也可以拆分成两个独立的方法),它不应该用作阐明有多个参数,比如下面的:

1
2
3
4
5
6
//错误,不要使用"and"来连接参数
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//错误,不要使用"and"来阐明有多个参数
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正确,使用"and"来表示两个相对独立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;

7.由于字符串值可能会改变,所以要把相关属性的“内存管理语义”声明为 copy 。


8.“性别”(sex)属性的:该类中只给出了一种“初始化方法” (initializer)用于设置“姓名”(Name)和“年龄”(Age)的初始值,那如何对“性别”(Sex)初始化?


Objective-C 有 designated 和 secondary 初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化方法的初始化方法。举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// .m文件

@implementation CYLUser

- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
}
return self;
}

- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age {
return [self initWithName:name age:age sex:nil];
}

@end

上面的代码中 initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。因为仅仅是调用类实现的 designated 初始化方法。


因为出题者没有给出 .m 文件,所以有两种猜测:1:本来打算只设计一个 designated 初始化方法,但漏掉了“性别”(sex)属性。那么最终的修改代码就是上文给出的第一种修改方法。2:不打算初始时初始化“性别”(sex)属性,打算后期再修改,如果是这种情况,那么应该把“性别”(sex)属性设为 readwrite 属性,最终给出的修改代码应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// .h文件
// 第二种修改方法(基于第一种修改方法的基础上)

typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};

@interface CYLUser : NSObject<NSCopying>

@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readwrite, assign) CYLSex sex;

- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;

@end

.h 中暴露 designated 初始化方法,是为了方便子类化 (参考 《禅与 Objective-C 编程艺术》)


9.按照接口设计的惯例,如果设计了“初始化方法” (initializer),也应当搭配一个快捷构造方法。而快捷构造方法的返回值,建议为 instancetype,为保持一致性,init 方法和快捷构造方法的返回类型最好都用 instancetype。


10.如果基于第一种修改方法:既然该类中已经有一个“初始化方法” (initializer),用于设置“姓名”(Name)、“年龄”(Age)和“性别”(Sex)的初始值: 那么在设计对应 @property 时就应该尽量使用不可变的对象:其三个属性都应该设为“只读”。用初始化方法设置好属性值之后,就不能再改变了。在本例中,仍需声明属性的“内存管理语义”。于是可以把属性的定义改成这样

1
2
3
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;

由于是只读属性,所以编译器不会为其创建对应的“设置方法”,即便如此,我们还是要写上这些属性的语义,以此表明初始化方法在设置这些属性值时所用的方式。要是不写明语义的话,该类的调用者就不知道初始化方法里会拷贝这些属性(name),他们有可能会在调用初始化方法之前自行拷贝属性值。这种操作多余而且低效。


11.initUserModelWithUserName 如果改为 initWithName 会更加简洁,而且足够清晰。


12.UserModel 如果改为 User 会更加简洁,而且足够清晰。个人不认同原文。


13.UserSex如果改为Sex 会更加简洁,而且足够清晰。个人不认同原文。


14.第二个 @property 中 assign 和 nonatomic 调换位置。 推荐按照下面的格式来定义属性

1
@property (nonatomic, readwrite, copy) NSString *name;

习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作。


个人不认同原文。我的实际开发习惯中,nonatomic 可以说在 object property 中 99.9% 的情况都会使用,所以对我来说它不怎么重要。我的定义习惯是:

1
@property (copyreadwrite, nonatomic) NSString *name;

习惯问题罢了。


硬伤部分

  1. 在 - 和 (void) 之间应该有一个空格
  2. enum 中驼峰命名法和下划线命名法混用错误:枚举类型的命名规则和函数的命名规则相同:命名时使用驼峰命名法,勿使用下划线命名法
  3. enum 左括号前加一个空格,或者将左括号换到下一行
  4. enum 右括号后加一个空格
  5. UserModel :NSObject 应为 UserModel : NSObject,也就是:右侧少了一个空格
  6. @interface@property 属性声明中间应当间隔一行
  7. 两个方法定义之间不需要换行,有时为了区分方法的功能也可间隔一行,但示例代码中间隔了两行
  8. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中方法名与参数之间多了空格。而且 - 与 (id) 之间少了空格
  9. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中 (NSString*)name,应为 (NSString *)name,少了空格。
objc中向一个nil对象发送消息将会发生什么?

在 Objective-C 中向 nil 发送消息是完全有效的——只是在运行时不会有任何作用:


1.如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:

1
Person *motherInlaw = [[aPerson spouse] mother];

如果 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。


2.如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。


3.如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。


4.如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。


具体原因如下:
objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。还是举个栗子,贴一个 objc 的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// runtime.h(类在runtime中的定义)

struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。(?)

objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

方法编译之后就是objc_msgSend()函数调用。


我们用 clang 分析下,clang 提供一个命令,可以将Objective-C的源码改写成C++语言,借此可以研究下 [obj foo]objc_msgSend() 函数之间有什么关系。


以下面的代码为例,由于 clang 后的代码达到了10万多行,为了便于区分,添加了一个叫 iOSinit 方法,

1
2
3
4
5
6
7
8
9
10
11
//  main.m

#import "CYLTest.h"

int main(int argc, char * argv[]) {
@autoreleasepool {
CYLTest *test = [[CYLTest alloc] init];
[test performSelector:(@selector(iOSinit))];
return 0;
}
}

在终端中输入

1
clang -rewrite-objc main.m

就可以生成一个main.cpp的文件,在最底端(10万4千行左右),可以看到:


image


我们可以看到大概是这样的:

1
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));

也就是说,[obj foo] 在objc动态编译时,会被转意为:objc_msgSend(obj, @selector(foo))

什么时候会报unrecognized selector的异常?

简单来说,当调用该对象上某个方法,而该对象上没有实现这个方法的时候,可以通过“消息转发”进行解决。


简单流程如下,在上一题中也提到过,objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。


objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:


1.Method resolution
objc运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。


2.Fast forwarding
如果目标对象实现了 -forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。


3.Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送 -methodSignatureForSelector: 消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回 nil,Runtime 则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。


关于这部分可以查看在这篇 blog 中查看更多。

一个objc对象如何进行内存布局?(考虑有父类的情况)

1.所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中.


2.一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的

  • 对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)
  • 成员变量的列表
  • 属性列表

它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个 superclass 的指针,指向他的父类对象。


每个 Objective-C 对象都有相同的结构,如下图所示:


image


翻译过来就是:


image


根对象就是NSobject,它的 superclass 指针指向 nil


类对象既然称为对象,那它也是一个实例。类对象中也有一个 isa 指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的 isa 指针指向自己,superclass 指针指向 NSObject 类。看图:


image

一个objc对象的isa的指针指向什么?有什么作用?

指向他的类对象,从而可以找到对象上的方法

下面的代码输出什么?
1
2
3
4
5
6
7
8
9
10
11
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

答案:都输出 Son

NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son


这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。


我们都知道:self 是类的隐藏参数,指向当前调用方法的这个类的实例。那 super 呢?


很多人会想当然的认为“super 和 self 类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者!他们两个的不同点在于:super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。


上面的例子不管调用 [self class] 还是 [super class],接受消息的对象都是当前 Son *xxx 这个对象。


当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。至于上面为什么没有输出父类名字,下面会给出答案。

如果在 Father 类中实现了 class,那么将会调用 Father 的 class,而不再试输出“Son”。


这也就是为什么说“不推荐在 init 方法中使用点语法”,如果想访问实例变量 iVar 应该使用下划线 _iVar ,而非点语法 self.iVar 。


点语法 self.iVar 的坏处就是子类有可能覆写 setter 。假设 Person 有一个子类叫 ChenPerson,这个子类专门表示那些姓“陈”的人。该子类可能会覆写 lastName 属性所对应的设置方法:

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
33
34
35
//
// ChenPerson.m

#import "ChenPerson.h"

@implementation ChenPerson

@synthesize lastName = _lastName;

- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([super class]));
}
return self;
}

- (void)setLastName:(NSString*)lastName
{
//设置方法一:如果setter采用是这种方式,就可能引起崩溃
// if (![lastName isEqualToString:@"陈"])
// {
// [NSException raise:NSInvalidArgumentException format:@"姓不是陈"];
// }
// _lastName = lastName;

//设置方法二:
_lastName = @"陈";
NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"会调用这个方法,想一下为什么?");

}

@end

在基类 Person 的默认初始化方法中,可能会将姓氏设为空字符串。此时若使用点语法 self.lastName 也即 setter 设置方法,那么调用将会是子类的设置方法,如果在刚刚的 setter 代码中采用设置方法一,那么就会抛出异常,


为了方便采用打印的方式展示,究竟发生了什么,我们使用设置方法二


如果基类的代码是这样的:

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
//
// Person.m
// nil对象调用点语法

#import "Person.h"

@implementation Person

- (instancetype)init
{
self = [super init];
if (self) {
self.lastName = @"";
//NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
//NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, self.lastName);
}
return self;
}

- (void)setLastName:(NSString*)lastName
{
NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"根本不会调用这个方法");
_lastName = @"炎黄";
}

@end

那么打印结果将会是这样:

1
2
3
🔴类名与方法名:-[ChenPerson setLastName:](在第36行),描述:会调用这个方法,想一下为什么?
🔴类名与方法名:-[ChenPerson init](在第19行),描述:ChenPerson
🔴类名与方法名:-[ChenPerson init](在第20行),描述:ChenPerson

接下来让我们利用 runtime 的相关知识来验证一下 super 关键字的本质,使用clang重写命令:

1
$ clang -rewrite-objc test.m

将这道题目中给出的代码被转化为:

1
2
3
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));

NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

从上面的代码中,我们可以发现在调用 [self class] 时,会转化成 objc_msgSend函数。看下函数定义:

1
id objc_msgSend(id self, SEL op, ...)

我们把 self 做为第一个参数传递进去。


而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:

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

第一个参数是 objc_super 这样一个结构体,其定义如下:

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

结构体有两个成员,第一个成员是 receiver, 类似于上面的 objc_msgSend 函数第一个参数 self 。第二个成员是记录当前类的父类是什么。


所以,当调用 [self class] 时,实际先调用的是 objc_msgSend 函数,第一个参数是 Son 当前的这个实例,然后在 Son 这个类里面去找 - (Class)class 这个方法,没有,去父类 Father里找,也没有,最后在 NSObject 类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。


objc Runtime开源代码对- (Class)class方法的实现:

1
2
3
- (Class)class {
return object_getClass(self);
}

而当调用 [super class] 时,会转换成 objc_msgSendSuper 函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。 第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。


第二步是去 Father这个类里去找 - (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用。


此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。


参看链接:刨根问底Objective-C Runtime(1)- Self & Super

runtime 如何通过 selector 找到对应的 IMP 地址?(分别考虑类方法和实例方法)

每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现。

使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

无论在MRC下还是ARC下均不需要。


2011年版本的Apple API 官方文档 - Associative References一节中有一个MRC环境下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在MRC下,使用runtime Associate方法关联的对象,不需要在主对象dealloc的时候释放
// 摘自2011年版本的Apple API 官方文档 - Associative References

static char overviewKey;

NSArray *array = [[NSArray alloc] initWithObjects:@"One", @"Two", @"Three", nil];
// For the purposes of illustration, use initWithFormat: to ensure
// the string can be deallocated
NSString *overview = [[NSString alloc] initWithFormat:@"%@", @"First three numbers"];

objc_setAssociatedObject (
array,
&overviewKey,
overview,
OBJC_ASSOCIATION_RETAIN
);

[overview release];
// (1) overview valid
[array release];
// (2) overview invalid

文档中指出:

At point 1, the string overview is still valid because the OBJC_ASSOCIATION_RETAIN policy specifies that the array retains the associated object. When the array is deallocated, however (at point 2), overview is released and so in this case also deallocated.
在(1)处,字符串 overview 仍然可用,因为策略 OBJC_ASSOCIATION_RETAIN 指定了该 array retain 那个被关联的对象。然而当数组被销毁,在(2)处,overview 就会被 release,并且同时被释放掉了。因为 retainCount 为0。


我们可以看到,在 [array release]; 之后,overview 就会被 release 释放掉了。


既然会被销毁,那么具体在什么时间点?


根据 WWDC 2011, Session 322 (第36分22秒) 中发布的内存销毁时间表,被关联的对象在生命周期内要比对象本身释放的晚很多。它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。


对象的内存销毁时间表,分四个步骤:


1.调用 -release :引用计数变为零

  • 对象即将被销毁,生命周期即将结束.
  • 不能再有新的 __weak 弱引用, 否则将指向 nil.
  • 调用 [self dealloc]


2.父类 调用 -dealloc

  • 继承关系中最底层的父类 调用 -dealloc
  • 如果是 MRC 代码 则会手动释放实例变量们(iVars)
  • 继承关系中每一层的父类 都调用 -dealloc


3.NSObject 调 -dealloc

  • 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法


4.调用 object_dispose()

  • 为 C++ 的实例变量们(iVars)调用 destructors
  • 为 ARC 状态下的 实例变量们(iVars) 调用 -release
  • 解除所有使用 runtime Associate方法关联的对象
  • 解除所有 __weak 引用
  • 调用 free()


对象的内存销毁时间表,来自 Stack Overflow 的回答

objc 中的类方法和实例方法有什么本质区别和联系?

类方法:

  1. 类方法是属于类对象的
  2. 类方法只能通过类对象调用
  3. 类方法中的self是类对象
  4. 类方法可以调用其他的类方法
  5. 类方法中不能访问成员变量
  6. 类方法中不能直接调用对象方法


实例方法

  1. 实例方法是属于实例对象的
  2. 实例方法只能通过实例对象调用
  3. 实例方法中的self是实例对象
  4. 实例方法中可以访问成员变量
  5. 实例方法中直接调用实例方法
  6. 实例方法中也可以调用类方法(通过类名)



以上