风格纠错题html
下面的代码输出什么?ios
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
修改方法有不少种,现给出一种作示例:github
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 修改完的代码,这是第一种修改方法,后面会给出第二种修改方法 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
下面对具体修改的地方,分两部分作下介绍:硬伤部分和优化部分 。由于硬伤部分没什么技术含量,为了节省你们时间,放在后面讲,大神请直接看优化部分。web
enum 建议使用 NS_ENUM
和 NS_OPTIONS
宏来定义枚举类型,参见官方的 Adopting Modern Objective-C 一文:编程
//定义一个枚举 typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman };
(仅仅让性别包含男和女可能并不严谨,最严谨的作法能够参考 这里 。)api
age 属性的类型:应避免使用基本类型,建议使用 Foundation 数据类型,对应关系以下:数组
int -> NSInteger unsigned -> NSUInteger float -> CGFloat 动画时间 -> NSTimeInterval
同时考虑到 age 的特色,应使用 NSUInteger ,而非 int 。 这样作的是基于64-bit 适配考虑,详情可参考出题者的博文《64-bit Tips》。缓存
doLogIn方法不该写在该类中:安全
虽然LogIn
的命名不太清晰,但笔者猜想是login的意思, (勘误:Login是名词,LogIn 是动词,都表示登录的意思。见: Log in vs. login )
不管是 MVC 模式仍是 MVVM 模式,业务逻辑都不该当写在 Model 里:MVC 应在 C,MVVM 应在 VM。
(若是抛开命名规范,假设该类真的是 MVVM 模式里的 ViewModel ,那么 UserModel 这个类可能对应的是用户注册页面,若是有特殊的业务需求,好比: -logIn
对应的应当是注册并登陆的一个 Button ,出现 -logIn
方法也多是合理的。)
doLogIn 方法命名不规范:添加了多余的动词前缀。 请牢记:
若是方法表示让对象执行一个动做,使用动词打头来命名,注意不要使用
do
,does
这种多余的关键字,动词自己的暗示就足够了。
应为 -logIn
(注意: Login
是名词, LogIn
是动词,都表示登录。 见 Log in vs. login )
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中不要用 with
来链接两个参数: withAge:
应当换为age:
,age:
已经足以清晰说明参数的做用,也不建议用 andAge:
:一般状况下,即便有相似 withA:withB:
的命名需求,也一般是使用withA:andB:
这种命名,用来表示方法执行了两个相对独立的操做(从设计上来讲,这时候也能够拆分红两个独立的方法),它不该该用做阐明有多个参数,好比下面的:
//错误,不要使用"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;
“性别”(sex)属性的:该类中只给出了一种“初始化方法” (initializer)用于设置“姓名”(Name)和“年龄”(Age)的初始值,那如何对“性别”(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的观念。 designated 初始化方法是提供全部的参数,secondary 初始化方法是一个或多个,而且提供一个或者更多的默认参数来调用 designated 初始化方法的初始化方法。举例说明:
// .m文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // @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 属性,最终给出的修改代码应该是:
// .h文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 第二种修改方法(基于第一种修改方法的基础上) 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 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译)》。)
若是基于第一种修改方法:既然该类中已经有一个“初始化方法” (initializer),用于设置“姓名”(Name)、“年龄”(Age)和“性别”(Sex)的初始值: 那么在设计对应 @property
时就应该尽可能使用不可变的对象:其三个属性都应该设为“只读”。用初始化方法设置好属性值以后,就不能再改变了。在本例中,仍需声明属性的“内存管理语义”。因而能够把属性的定义改为这样
@property (nonatomic, readonly, copy) NSString *name; @property (nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) CYLSex sex;
因为是只读属性,因此编译器不会为其建立对应的“设置方法”,即使如此,咱们仍是要写上这些属性的语义,以此代表初始化方法在设置这些属性值时所用的方式。要是不写明语义的话,该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法以前自行拷贝属性值。这种操做多余并且低效。
initUserModelWithUserName
若是改成 initWithName
会更加简洁,并且足够清晰。UserModel
若是改成 User
会更加简洁,并且足够清晰。UserSex
若是改成Sex
会更加简洁,并且足够清晰。第二个 @property
中 assign 和 nonatomic 调换位置。 推荐按照下面的格式来定义属性
@property (nonatomic, readwrite, copy) NSString *name;
属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。 这样作你的属性更容易修改正确,而且更好阅读。这在《禅与Objective-C编程艺术 >》里有介绍。并且习惯上修改某个属性的修饰符时,通常从属性名从右向左搜索须要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操做。
UserModel :NSObject
应为UserModel : NSObject
,也就是:
右侧少了一个空格。@interface
与 @property
属性声明中间应当间隔一行。-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中方法名与参数之间多了空格。并且 -
与(id)
之间少了空格。-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中方法名与参数之间多了空格:(NSString*)name
前多了空格。-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中 (NSString*)name
,应为 (NSString *)name
,少了空格。doLogIn方法中的 LogIn
命名不清晰:笔者猜想是login的意思,应该是粗心手误形成的。 (勘误: Login
是名词,LogIn
是动词,都表示登录的意思。见: Log in vs. login )
什么状况使用 weak 关键字?
不一样点:
weak
此特质代表该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign相似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign
的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操做。用途:
block 也常用 copy 关键字,具体缘由见官方文档:Objects Use Properties to Keep Track of Blocks:
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 能够把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 仍是 strong 效果是同样的,但写上 copy 也无伤大雅,还能时刻提醒咱们:编译器自动对 block 进行了 copy 操做。若是不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操做”,他们有可能会在调用以前自行拷贝属性值。这种操做多余而低效。你也许会感受我这种作法有些怪异,不须要写依然写。若是你这样想,实际上是你“日用而不知”,你平时开发中是常常在用我说的这种作法的,好比下面的属性不写copy也行,可是你会选择写仍是不写呢?
@property (nonatomic, copy) NSString *userId; - (instancetype)initWithUserId:(NSString *)userId { self = [super init]; if (!self) { return nil; } _userId = [userId copy]; return self; }
下面作下解释: copy 此特质所表达的所属关系与 strong 相似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,常常用此特质来保护其封装性,由于传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时如果不拷贝字符串,那么设置完属性以后,字符串的值就可能会在对象不知情的状况下遭人更改。因此,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无心间变更。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。
用
@property
声明 NSString、NSArray、NSDictionary 常用 copy 关键字,是由于他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操做,为确保对象中的字符串值不会无心间变更,应该在设置新属性值时拷贝一份。
该问题在下文中也有论述:用@property声明的NSString(或NSArray,NSDictionary)常用copy关键字,为何?若是改用strong关键字,可能形成什么问题?
@property (copy) NSMutableArray *array;
两个问题:一、添加,删除,修改数组内的元素的时候,程序会由于找不到对应的方法而崩溃.由于 copy 就是复制一个不可变 NSArray 的对象;二、使用了 atomic 属性会严重影响性能 ;
第1条的相关缘由在下文中有论述《用@property声明的NSString(或NSArray,NSDictionary)常用 copy 关键字,为何?若是改用strong关键字,可能形成什么问题?》 以及上文《怎么用 copy 关键字?》也有论述。
好比下面的代码就会发生崩溃
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 下面的代码就会发生崩溃 @property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 下面的代码就会发生崩溃 NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil]; self.mutableArray = array; [self.mutableArray removeObjectAtIndex:0];
接下来就会奔溃:
-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
第2条缘由,以下:
该属性使用了同步锁,会在建立时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,经过声明 nonatomic 能够节省这些虽然很小可是没必要要额外开销。
在默认状况下,由编译器所合成的方法会经过锁定机制确保其原子性(atomicity)。若是属性具有 nonatomic 特质,则不使用同步锁。请注意,尽管没有名为“atomic”的特质(若是某属性不具有 nonatomic 特质,那它就是“原子的”(atomic))。
在iOS开发中,你会发现,几乎全部属性都声明为 nonatomic。
通常状况下并不要求属性必须是“原子的”,由于这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操做,还需采用更为深层的锁定机制才行。例如,一个线程在连续屡次读取某属性值的过程当中有别的线程在同时改写该值,那么即使将属性声明为 atomic,也仍是会读到不一样的属性值。
所以,开发iOS程序时通常都会使用 nonatomic 属性。可是在开发 Mac OS X 程序时,使用 atomic 属性一般都不会有性能瓶颈。
若想令本身所写的对象具备拷贝功能,则需实现 NSCopying 协议。若是自定义的对象分为可变版本与不可变版本,那么就要同时实现
NSCopying
与NSMutableCopying
协议。
具体步骤:
实现 NSCopying 协议。该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到让本身的类用 copy 修饰符,咱们老是想覆写copy方法,其实真正须要实现的倒是 “copyWithZone” 方法。
以第一题的代码为例:
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 修改完的代码 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
而后实现协议中规定的方法:
- (id)copyWithZone:(NSZone *)zone { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; return copy; }
但在实际的项目中,不可能这么简单,遇到更复杂一点,好比类对象中的数据结构可能并未在初始化方法中设置好,须要另行设置。举个例子,假如 CYLUser 中含有一个数组,与其余 CYLUser 对象创建或解除朋友关系的那些方法都须要操做这个数组。那么在这种状况下,你得把这个包含朋友对象的数组也一并拷贝过来。下面列出了实现此功能所需的所有代码:
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 以第一题《风格纠错题》里的代码为例 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; - (void)addFriend:(CYLUser *)user; - (void)removeFriend:(CYLUser *)user; @end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // @implementation CYLUser { NSMutableSet *_friends; } - (void)setName:(NSString *)name { _name = [name copy]; } - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex { if(self = [super init]) { _name = [name copy]; _age = age; _sex = sex; _friends = [[NSMutableSet alloc] init]; } return self; } - (void)addFriend:(CYLUser *)user { [_friends addObject:user]; } - (void)removeFriend:(CYLUser *)user { [_friends removeObject:user]; } - (id)copyWithZone:(NSZone *)zone { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [_friends mutableCopy]; return copy; } - (id)deepCopy { CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; } @end
以上作法能知足基本的需求,可是也有缺陷:
若是你所写的对象须要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
【注:深浅拷贝的概念,在下文中有介绍,详见下文的:用@property声明的 NSString(或NSArray,NSDictionary)常用 copy 关键字,为何?若是改用 strong 关键字,可能形成什么问题?】
在例子中,存放朋友对象的 set 是用 “copyWithZone:” 方法来拷贝的,这种浅拷贝方式不会逐个复制 set 中的元素。若须要深拷贝的话,则可像下面这样,编写一个专供深拷贝所用的方法:
- (id)deepCopy {
CYLUser *copy = [[[self class] allocWithZone:zone] initWithName:_name age:_age sex:_sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; }
至于如何重写带 copy 关键字的 setter这个问题,
若是抛开本例来回答的话,以下:
- (void)setName:(NSString *)name { //[_name release]; _name = [name copy]; }
不过也有争议,有人说“苹果若是像下面这样干,是否是效率会高一些?”
- (void)setName:(NSString *)name { if (_name != name) { //[_name release];//MRC _name = [name copy]; } }
这样真得高效吗?不见得!这种写法“看上去很美、很合理”,但在实际开发中,它更像下图里的作法:
克强总理这样评价你的代码风格:
我和总理的意见基本一致:
老百姓 copy 一下,咋就这么难?
你可能会说:
之因此在这里作if判断
这个操做:是由于一个 if 可能避免一个耗时的copy,仍是很划算的。 (在刚刚讲的:《如何让本身的类用 copy 修饰符?》里的那种复杂的copy,咱们能够称之为 “耗时的copy”,可是对 NSString 的 copy 还称不上。)
可是你有没有考虑过代价:
你每次调用
setX:
都会作 if 判断,这会让setX:
变慢,若是你在setX:
写了一串复杂的if+elseif+elseif+...
判断,将会更慢。
要回答“哪一个效率会高一些?”这个问题,不能脱离实际开发,就算 copy 操做十分耗时,if 判断也不见得必定会更快,除非你把一个“ @property他当前的值 ”赋给了他本身,代码看起来就像:
[a setX:x1];
[a setX:x1]; //你肯定你要这么干?与其在setter中判断,为何不把代码写好?
或者
[a setX:[a x]]; //队友咆哮道:你在干吗?!!
不要在 setter 里进行像
if(_obj != newObj)
这样的判断。(该观点参考连接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure )
什么状况会在 copy setter 里作 if 判断? 例如,车速可能就有最高速的限制,车速也不可能出现负值,若是车子的最高速为300,则 setter 的方法就要改写成这样:
-(void)setSpeed:(int)_speed{ if(_speed < 0) speed = 0; if(_speed > 300) speed = 300; _speed = speed; }
回到这个题目,若是单单就上文的代码而言,咱们不须要也不能重写 name 的 setter :因为是 name 是只读属性,因此编译器不会为其建立对应的“设置方法”,用初始化方法设置好属性值以后,就不能再改变了。( 在本例中,之因此还要声明属性的“内存管理语义”--copy,是由于:若是不写 copy,该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法以前自行拷贝属性值。这种操做多余而低效)。
那如何确保 name 被 copy?在初始化方法(initializer)中作:
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex { if(self = [super init]) { _name = [name copy]; _age = age; _sex = sex; _friends = [[NSMutableSet alloc] init]; } return self; }
@property 的本质是什么?
@property = ivar + getter + setter;
下面解释下:
“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。
“属性” (property)做为 Objective-C 的一项特性,主要的做用就在于封装对象中的数据。 Objective-C 对象一般会把其所须要的数据保存为各类实例变量。实例变量通常经过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。这个概念已经定型,而且经由“属性”这一特性而成为 Objective-C 2.0
的一部分。 而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。 正由于有了这种严格的命名规范,因此 Objective-C 这门语言才能根据名称自动建立出存取方法。其实也能够把属性当作一种关键字,其表示:
编译器会自动写出一套存取方法,用以访问给定类型中具备给定名称的变量。 因此你也能够这么说:
@property = getter + setter;
例以下面这个类:
@interface Person : NSObject @property NSString *firstName; @property NSString *lastName; @end
上述代码写出来的类与下面这种写法等效:
@interface Person : NSObject - (NSString *)firstName; - (void)setFirstName:(NSString *)firstName; - (NSString *)lastName; - (void)setLastName:(NSString *)lastName; @end
ivar、getter、setter 是如何生成并添加到这个类中的?
“自动合成”( autosynthesis)
完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫作“自动合成”(autosynthesis)。须要强调的是,这个过程由编译 器在编译期执行,因此编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 以外,编译器还要自动向类中添加适当类型的实例变量,而且在属性名前面加下划线,以此做为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName
与 _lastName
。也能够在类的实现代码里经过 @synthesize
语法来指定实例变量的名字.
@implementation Person @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end
我为了搞清属性是怎么实现的,曾经反编译过相关的代码,他大体生成了五个东西
OBJC_IVAR_$类名$属性名称
:该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。ivar_list
:成员变量列表method_list
:方法列表prop_list
:属性列表也就是说咱们每次在增长一个属性,系统都会在 ivar_list
中添加一个成员变量的描述,在 method_list
中增长 setter 与 getter 方法的描述,在属性列表中增长一个属性的描述,而后计算该属性在对象中的偏移量,而后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了可以读取正确字节数,系统对象偏移量的指针类型进行了类型强转.
category 使用 @property 也是只会生成 setter 和 getter 方法的声明,若是咱们真的须要给 category 增长属性的实现,须要借助于运行时的两个函数:
objc_setAssociatedObject
objc_getAssociatedObject
要实现 weak 属性,首先要搞清楚 weak 属性的特色:
weak 此特质代表该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 相似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
那么 runtime 如何实现 weak 变量的自动置nil?
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址做为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到全部以a为键的 weak 对象,从而设置为 nil。
(注:在下文的《使用runtime Associate方法关联的对象,须要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到__weak
引用的解除时间。)
咱们能够设计一个函数(伪代码)来表示上述机制:
objc_storeWeak(&a, b)
函数:
objc_storeWeak
函数把第二个参数--赋值对象(b)的内存地址做为键值key,将第一个参数--weak修饰的属性变量(a)的内存地址(&a)做为value,注册到 weak 表中。若是第二个参数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除,
你能够把objc_storeWeak(&a, b)
理解为:objc_storeWeak(value, key)
,而且当key变nil,将value置nil。
在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
而若是a是由 assign 修饰的,则: 在 b 非 nil 时,a 和 b 指向同一个内存地址,在 b 变 nil 时,a 仍是指向该内存地址,变野指针。此时向 a 发送消息极易崩溃。
下面咱们将基于objc_storeWeak(&a, b)
函数,使用伪代码模拟“runtime如何实现weak属性”:
// 使用伪代码模拟:runtime如何实现weak属性
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong id obj1; objc_initWeak(&obj1, obj); /*obj引用计数变为0,变量做用域结束*/ objc_destroyWeak(&obj1);
下面对用到的两个方法objc_initWeak
和objc_destroyWeak
作下解释:
整体说来,做用是: 经过objc_initWeak
函数初始化“附有weak修饰符的变量(obj1)”,在变量做用域结束时经过objc_destoryWeak
函数释放该变量(obj1)。
下面分别介绍下方法的内部实现:
objc_initWeak
函数的实现是这样的:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)做为参数,调用objc_storeWeak
函数。
obj1 = 0;
obj_storeWeak(&obj1, obj);
也就是说:
weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
而后obj_destroyWeak
函数将0(nil)做为参数,调用objc_storeWeak
函数。
objc_storeWeak(&obj1, 0);
前面的源代码与下列源代码相同。
// 使用伪代码模拟:runtime如何实现weak属性
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); /* ... obj的引用计数变为0,被置nil ... */ objc_storeWeak(&obj1, 0);
objc_storeWeak
函数把第二个参数--赋值对象(obj)的内存地址做为键值,将第一个参数--weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。若是第二个参数(obj)为0(nil),那么把变量(obj1)的地址从 weak 表中删除,在后面的相关一题会详解。
使用伪代码是为了方便理解,下面咱们“真枪实弹”地实现下:
如何让不使用weak修饰的@property,拥有weak的效果。
咱们从setter方法入手:
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; }
也就是有两个步骤:
在setter方法中作以下设置:
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。作到这点,一样要借助 runtime:
//要销毁的目标对象 id objectToBeDeallocated; //能够理解为一个“事件”:当上面的目标对象销毁时,同时要发生的“事件”。 id objectWeWantToBeReleasedWhenThatHappens; objc_setAssociatedObject(objectToBeDeallocted, someUniqueKey, objectWeWantToBeReleasedWhenThatHappens, OBJC_ASSOCIATION_RETAIN);
知道了思路,咱们就开始实现 cyl_runAtDealloc
方法,实现过程分两部分:
第一部分:建立一个类,能够理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助 block 执行“事件”。
// .h文件
// .h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 这个类,能够理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。 typedef void (^voidBlock)(void); @interface CYLBlockExecutor : NSObject - (id)initWithBlock:(voidBlock)block; @end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 这个类,能够理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。 #import "CYLBlockExecutor.h" @interface CYLBlockExecutor() { voidBlock _block; } @implementation CYLBlockExecutor - (id)initWithBlock:(voidBlock)aBlock { self = [super init]; if (self) { _block = [aBlock copy]; } return self; } - (void)dealloc { _block ? _block() : nil; } @end
第二部分:核心代码:利用runtime实现cyl_runAtDealloc
方法
// CYLNSObject+RunAtDealloc.h文件
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 利用runtime实现cyl_runAtDealloc方法 #import "CYLBlockExecutor.h" const void *runAtDeallocBlockKey = &runAtDeallocBlockKey; @interface NSObject (CYLRunAtDealloc) - (void)cyl_runAtDealloc:(voidBlock)block; @end // CYLNSObject+RunAtDealloc.m文件 // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // 利用runtime实现cyl_runAtDealloc方法 #import "CYLNSObject+RunAtDealloc.h" #import "CYLBlockExecutor.h" @implementation NSObject (CYLRunAtDealloc) - (void)cyl_runAtDealloc:(voidBlock)block { if (block) { CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block]; objc_setAssociatedObject(self, runAtDeallocBlockKey, executor, OBJC_ASSOCIATION_RETAIN); } } @end
使用方法: 导入
#import "CYLNSObject+RunAtDealloc.h"
而后就可使用了:
NSObject *foo = [[NSObject alloc] init]; [foo cyl_runAtDealloc:^{ NSLog(@"正在释放foo!"); }];
若是对 cyl_runAtDealloc
的实现原理有兴趣,能够看下这篇博文 Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object
属性能够拥有的特质分为四类:
原子性--- nonatomic
特质
在默认状况下,由编译器合成的方法会经过锁定机制确保其原子性(atomicity)。若是属性具有 nonatomic 特质,则不使用同步锁。请注意,尽管没有名为“atomic”的特质(若是某属性不具有 nonatomic 特质,那它就是“原子的” ( atomic) ),可是仍然能够在属性特质中写明这一点,编译器不会报错。如果本身定义存取方法,那么就应该听从与属性特质相符的原子性。
readwrite(读写)
、readonly (只读)
assign
、strong
、 weak
、unsafe_unretained
、copy
方法名---getter=<name>
、setter=<name>
getter=<name>
的样式:
@property (nonatomic, getter=isOn) BOOL on;
( `setter=`这种不经常使用,也不推荐使用。故不在这里给出写法。)
setter=<name>
通常用在特殊的情境下,好比:
在数据反序列化、转模型的过程当中,服务器返回的字段若是以 init
开头,因此你须要定义一个 init
开头的属性,但默认生成的 setter
与 getter
方法也会以 init
开头,而编译器会把全部以 init
开头的方法当成初始化方法,而初始化方法只能返回 self 类型,所以编译器会报错。
这时你就可使用下面的方式来避免编译器报错:
@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;
另外也能够用关键字进行特殊说明,来避免编译器报错:
@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy __attribute__((objc_method_family(none)));
nonnull
,null_resettable
,nullable
不须要。
在ARC环境不管是强指针仍是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮咱们处理
即使是编译器不帮咱们作这些,weak也不须要在 dealloc 中置nil:
正如上文的:runtime 如何实现 weak 属性 中提到的:
咱们模拟下 weak 的 setter 方法,应该以下:
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; }
也即:
在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
@syntheszie var = _var;
instance.var = someVar
,因为缺 setter 方法会致使程序崩溃;或者当运行到 someVar = var
时,因为缺 getter 方法一样会致使崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。对应基本数据类型默认关键字是
atomic,readwrite,assign
对于普通的 Objective-C 对象
atomic,readwrite,strong
参考连接:
若是咱们使用是 strong ,那么这个属性就有可能指向一个可变对象,若是这个可变对象在外部被修改了,那么会影响该属性.
copy 此特质所表达的所属关系与 strong 相似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,常常用此特质来保护其封装性,由于传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时如果不拷贝字符串,那么设置完属性以后,字符串的值就可能会在对象不知情的状况下遭人更改。因此,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无心间变更。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。
举例说明:
定义一个以 strong 修饰的 array:
@property (nonatomic ,readwrite, strong) NSArray *array;
而后进行下面的操做:
NSMutableArray *mutableArray = [[NSMutableArray alloc] init]; NSArray *array = @[ @1, @2, @3, @4 ]; self.array = mutableArray; [mutableArray removeAllObjects];; NSLog(@"%@",self.array); [mutableArray addObjectsFromArray:array]; self.array = [mutableArray copy]; [mutableArray removeAllObjects];; NSLog(@"%@",self.array);
打印结果以下所示:
2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] ( ) 2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] ( 1, 2, 3, 4 )
(详见仓库内附录的 Demo。)
为了理解这种作法,首先要知道,两种状况:
在非集合类对象中:对 immutable 对象进行 copy 操做,是指针复制,mutableCopy 操做时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。用代码简单表示以下:
好比如下代码:
NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy NSString *stringCopy = [string copy];
查看内存,会发现 string、stringCopy 内存地址都不同,说明此时都是作内容拷贝、深拷贝。即便你进行以下操做:
[string appendString:@"origion!"]
stringCopy 的值也不会所以改变,可是若是不使用 copy,stringCopy 的值就会被改变。 集合类对象以此类推。 因此,
用 @property 声明 NSString、NSArray、NSDictionary 常用 copy 关键字,是由于他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操做,为确保对象中的字符串值不会无心间变更,应该在设置新属性值时拷贝一份。
集合类对象是指 NSArray、NSDictionary、NSSet ... 之类的对象。下面先看集合类immutable对象使用 copy 和 mutableCopy 的一个例子:
NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]]; NSArray *copyArray = [array copy]; NSMutableArray *mCopyArray = [array mutableCopy];
查看内容,能够看到 copyArray 和 array 的地址是同样的,而 mCopyArray 和 array 的地址是不一样的。说明 copy 操做进行了指针拷贝,mutableCopy 进行了内容拷贝。但须要强调的是:此处的内容拷贝,仅仅是拷贝 array 这个对象,array 集合内部的元素仍然是指针拷贝。这和上面的非集合 immutable 对象的拷贝仍是挺类似的,那么mutable对象的拷贝会不会相似呢?咱们继续往下,看 mutable 对象拷贝的例子:
NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil]; NSArray *copyArray = [array copy]; NSMutableArray *mCopyArray = [array mutableCopy];
查看内存,如咱们所料,copyArray、mCopyArray和 array 的内存地址都不同,说明 copyArray、mCopyArray 都对 array 进行了内容拷贝。一样,咱们能够得出结论:
在集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。可是:集合对象的内容复制仅限于对象自己,对象元素仍然是指针复制。用代码简单表示以下:
[immutableObject copy] // 浅复制 [immutableObject mutableCopy] //单层深复制 [mutableObject copy] //单层深复制 [mutableObject mutableCopy] //单层深复制
这个代码结论和非集合类的很是类似。
参考连接:iOS 集合的深复制与浅复制
_foo
的实例变量,那么还会自动合成新变量么?在回答以前先说明下一个概念:
实例变量 = 成员变量 = ivar
这些说法,笔者下文中,可能都会用到,指的是一个东西。
正如 Apple官方文档 You Can Customize Synthesized Instance Variable Names 所说:
若是使用了属性的话,那么编译器就会自动编写访问属性所需的方法,此过程叫作“自动合成”( auto synthesis)。须要强调的是,这个过程由编译器在编译期执行,因此编辑器里看不到这些“合成方法” (synthesized method)的源代码。除了生成方法代码以外,编译器还要自动向类中添加适当类型的实例变量,而且在属性名前面加下划线,以此做为实例变量的名字。
@interface CYLPerson : NSObject @property NSString *firstName; @property NSString *lastName; @end
在上例中,会生成两个实例变量,其名称分别为 _firstName
与 _lastName
。也能够在类的实现代码里经过 @synthesize
语法来指定实例变量的名字:
@implementation CYLPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end
上述语法会将生成的实例变量命名为 _myFirstName
与 _myLastName
,而再也不使用默认的名字。通常状况下无须修改默认的实例变量名,可是若是你不喜欢如下划线来命名实例变量,那么能够用这个办法将其改成本身想要的名字。笔者仍是推荐使用默认的命名方案,由于若是全部人都坚持这套方案,那么写出来的代码你们都能看得懂。
总结下 @synthesize 合成实例变量的规则,有如下几点:
若是是 @synthesize foo;
还会生成一个名称为foo的成员变量,也就是说:
若是没有指定成员变量的名称会自动生成一个属性同名的成员变量,
@synthesize foo = _foo;
就不会生成成员变量了.假如 property 名为 foo,存在一个名为 _foo
的实例变量,那么还会自动合成新变量么? 不会。以下图:
回答这个问题前,咱们要搞清楚一个问题,什么状况下不会autosynthesis(自动合成)?
重载的属性
当你在子类中重载了父类中的属性,你必须 使用 @synthesize
来手动合成ivar。
除了后三条,对其余几个咱们能够总结出一个规律:当你想手动管理 @property 的全部内容时,你就会尝试经过实现 @property 的全部“存取方法”(the accessor methods)或者使用 @dynamic
来达到这个目的,这时编译器就会认为你打算手动管理 @property,因而编译器就禁用了 autosynthesis(自动合成)。
由于有了 autosynthesis(自动合成),大部分开发者已经习惯不去手动定义ivar,而是依赖于 autosynthesis(自动合成),可是一旦你须要使用ivar,而 autosynthesis(自动合成)又失效了,若是不去手动定义ivar,那么你就得借助 @synthesize
来手动合成 ivar。
其实,@synthesize
语法还有一个应用场景,可是不太建议你们使用:
能够在类的实现代码里经过 @synthesize
语法来指定实例变量的名字:
@implementation CYLPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end
上述语法会将生成的实例变量命名为 _myFirstName
与 _myLastName
,而再也不使用默认的名字。通常状况下无须修改默认的实例变量名,可是若是你不喜欢如下划线来命名实例变量,那么能够用这个办法将其改成本身想要的名字。笔者仍是推荐使用默认的命名方案,由于若是全部人都坚持这套方案,那么写出来的代码你们都能看得懂。
举例说明:应用场景:
//
// .m文件 // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong // 打开第14行和第17行中任意一行,就可编译成功 @import Foundation; @interface CYLObject : NSObject @property (nonatomic, copy) NSString *title; @end @implementation CYLObject { // NSString *_title; } //@synthesize title = _title; - (instancetype)init { self = [super init]; if (self) { _title = @"微博@iOS程序犭袁"; } return self; } - (NSString *)title { return _title; } - (void)setTitle:(NSString *)title { _title = [title copy]; } @end
当你同时重写了 setter 和 getter 时,系统就不会生成 ivar(实例变量/成员变量)。这时候有两种选择:
@synthesize foo = _foo;
,关联 @property 与 ivar。更多信息,请戳- 》 When should I use @synthesize explicitly?
在 Objective-C 中向 nil 发送消息是彻底有效的——只是在运行时不会有任何做用:
若是一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:
Person * motherInlaw = [[aPerson spouse] mother];
若是 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。
具体缘由以下:
objc是动态语言,每一个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
那么,为了方便理解这个内容,仍是贴一个objc的源代码:
// runtime.h(类在runtime中的定义)
// http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong 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_msgSend()
函数之间有什么关系?具体缘由同上题:该方法编译以后就是objc_msgSend()
函数调用.
咱们用 clang 分析下,clang 提供一个命令,能够将Objective-C的源码改写成C++语言,借此能够研究下[obj foo]和objc_msgSend()
函数之间有什么关系。
如下面的代码为例,因为 clang 后的代码达到了10万多行,为了便于区分,添加了一个叫 iOSinit 方法,
//
// main.m // http://weibo.com/luohanchenyilong/ // https://github.com/ChenYilong // Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved. // #import "CYLTest.h" int main(int argc, char * argv[]) { @autoreleasepool { CYLTest *test = [[CYLTest alloc] init]; [test performSelector:(@selector(iOSinit))]; return 0; } }
在终端中输入
clang -rewrite-objc main.m
就能够生成一个main.cpp
的文件,在最低端(10万4千行左右)
咱们能够看到大概是这样的:
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));
也就是说:
[obj foo];在objc动态编译时,会被转意为:
objc_msgSend(obj, @selector(foo));
。
简单来讲:
当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 能够经过“消息转发”进行解决。
简单的流程以下,在上一题中也提到过:
objc是动态语言,每一个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,而后在该类中的方法列表以及其父类方法列表中寻找方法运行,若是,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。可是在这以前,objc的运行时会给出三次拯救程序崩溃的机会:
Method resolution
objc运行时会调用+resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。若是你添加了函数,那运行时系统就会从新启动一次消息发送的过程,不然 ,运行时就会移到下一步,消息转发(Message Forwarding)。
Fast forwarding
若是目标对象实现了-forwardingTargetForSelector:
,Runtime 这时就会调用这个方法,给你把这个消息转发给其余对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,固然发送的对象会变成你返回的那个对象。不然,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。由于这一步不会建立任何新的对象,但下一步转发会建立一个NSInvocation对象,因此相对更快点。
Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:
消息得到函数的参数和返回值类型。若是-methodSignatureForSelector:
返回nil,Runtime则会发出-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。若是返回了一个函数签名,Runtime就会建立一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
为了能更清晰地理解这些方法的做用,git仓库里也给出了一个Demo,名称叫“ _objc_msgForward_demo
”,可运行起来看看。
每个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的
它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass的指针,指向他的父类对象。
每一个 Objective-C 对象都有相同的结构,以下图所示:
翻译过来就是
Objective-C 对象的结构图 |
---|
ISA指针 |
根类的实例变量 |
倒数第二层父类的实例变量 |
... |
父类的实例变量 |
类的实例变量 |
指向他的类对象,从而能够找到对象上的方法
@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 时,则从父类的方法列表中开始找。而后调用父类的这个方法。
这也就是为何说“不推荐在 init 方法中使用点语法”,若是想访问实例变量 iVar 应该使用下划线( _iVar
),而非点语法(self.iVar
)。
点语法( self.iVar
)的坏处就是子类有可能覆写 setter 。假设 Person 有一个子类叫 ChenPerson,这个子类专门表示那些姓“陈”的人。该子类可能会覆写 lastName 属性所对应的设置方法:
//
// ChenPerson.m // // // Created by https://github.com/ChenYilong on 15/8/30. // Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved. // #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; //设置方法二:若是setter采用是这种方式,就可能引发崩溃 _lastName = @"陈"; NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"会调用这个方法,想一下为何?"); } @end
在基类 Person 的默认初始化方法中,可能会将姓氏设为空字符串。此时若使用点语法( self.lastName
)也即 setter 设置方法,那么调用将会是子类的设置方法,若是在刚刚的 setter 代码中采用设置方法一,那么就会抛出异常,
为了方便采用打印的方式展现,究竟发生了什么,咱们使用设置方法二。
若是基类的代码是这样的:
//
// Person.m // nil对象调用点语法 // // Created by https://github.com/ChenYilong on 15/8/29. // Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved. // #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
那么打印结果将会是这样的:
🔴类名与方法名:-[ChenPerson setLastName:](在第36行),描述:会调用这个方法,想一下为何? 🔴类名与方法名:-[ChenPerson init](在第19行),描述:ChenPerson 🔴类名与方法名:-[ChenPerson init](在第20行),描述:ChenPerson
我在仓库里也给出了一个相应的 Demo(名字叫:Demo_21题_下面的代码输出什么)。有兴趣能够跑起来看一下,主要看下他是怎么打印的,思考下为何这么打印。
接下来让咱们利用 runtime 的相关知识来验证一下 super 关键字的本质,使用clang重写命令:
$ clang -rewrite-objc test.m
将这道题目中给出的代码被转化为:
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
函数。看下函数定义:
id objc_msgSend(id self, SEL op, ...)
咱们把 self 作为第一个参数传递进去。
而在调用 [super class]时,会转化成 objc_msgSendSuper
函数。看下函数定义:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super
这样一个结构体,其定义以下:
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方法的实现:
- (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。
参考连接:微博@Chun_iOS的博文刨根问底Objective-C Runtime(1)- Self & Super
每个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,经过这个方法名称就能够在方法列表中找到对应的方法实现.
在MRC中,对于使用retain或copy策略的须要 。
不管在MRC下仍是ARC下均不须要。
2011年版本的Apple API 官方文档 - Associative References 一节中有一个MRC环境下的例子:
// 在MRC下,使用runtime Associate方法关联的对象,不须要在主对象dealloc的时候释放
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong // 摘自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 theOBJC_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.
咱们能够看到,在[array release];
以后,overview就会被release释放掉了。
既然会被销毁,那么具体在什么时间点?
根据 WWDC 2011, Session 322 (第36分22秒) 中发布的内存销毁时间表,被关联的对象在生命周期内要比对象自己释放的晚不少。它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。
对象的内存销毁时间表,分四个步骤:
// 对象的内存销毁时间表 // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) // https://github.com/ChenYilong // 根据 WWDC 2011, Session 322 (36分22秒)中发布的内存销毁时间表 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()
对象的内存销毁时间表:参考连接。
类方法:
实例方法:
@property部分主要参考 Apple官方文档:Properties Encapsulate an Object’s Values runtime部分主要参考Apple官方文档:Declared Properties
_objc_msgForward
函数是作什么的,直接调用它将会发生什么?BAD_ACCESS
在什么状况下出现?dispatch_queue_t
)分哪两种类型?dispatch_barrier_async
的做用是什么?dispatch_get_current_queue
?- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); }); NSLog(@"3"); }
NSString *_foo
,调用setValue:forKey:时,能够以foo仍是_foo
做为key?BAD_ACCESS
错误