iOS开发之高级面试题(一)

小编加了一个不错的面试题和iOS技巧分享群,883872094群资料能够自取html

《招聘一个靠谱的 iOS》,其中共55题,除第一题为纠错题外,其余54道均为简答题。ios

传送门 iOS开发之高级面试题(二)git

iOS开发之高级面试题(三)github

iOS开发之高级面试题(四) 面试

  1. 风格纠错题编程

    1. 优化部分
    2. 硬伤部分
  2. 什么状况使用 weak 关键字,相比 assign 有什么不一样?数组

  3. 怎么用 copy 关键字?安全

  4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;网络

  5. 如何让本身的类用 copy 修饰符?如何重写带 copy 关键字的 setter?数据结构

  6. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

  7. @protocol 和 category 中如何使用 @property

  8. runtime 如何实现 weak 属性

  9. @property中有哪些属性关键字?/ @property 后面能够有哪些修饰符?

  10. weak属性须要在dealloc中置nil么?

  11. @synthesize和@dynamic分别有什么做用?

  12. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?

  13. 用@property声明的NSString(或NSArray,NSDictionary)常用copy关键字,为何?若是改用strong关键字,可能形成什么问题?

  14. 对非集合类对象的copy操做

  15. 集合类对象的copy与mutableCopy

  16. @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自动合成新变量么?

  17. 在有了自动合成属性实例变量以后,@synthesize还有哪些使用场景?

  18. objc中向一个nil对象发送消息将会发生什么?

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

  20. 何时会报unrecognized selector的异常?

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

  22. 一个objc对象的isa的指针指向什么?有什么做用?

1. 风格纠错题

enter image description here
修改完的代码:

修改方法有不少种,现给出一种作示例:

// .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
复制代码

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

优化部分

  1. enum 建议使用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型,参见官方的 Adopting Modern Objective-C 一文:

    //定义一个枚举
    typedef NS_ENUM(NSInteger, CYLSex) {
        CYLSexMan,
        CYLSexWoman
    };
    复制代码

    (仅仅让性别包含男和女可能并不严谨,最严谨的作法能够参考 这里 。)

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

    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 方法命名不规范:添加了多余的动词前缀。 请牢记:

    若是方法表示让对象执行一个动做,使用动词打头来命名,注意不要使用 dodoes 这种多余的关键字,动词自己的暗示就足够了。

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

  6. -(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;
    复制代码
  7. 因为字符串值可能会改变,因此要把相关属性的“内存管理语义”声明为 copy 。(缘由在下文有详细论述:用@property声明的NSString(或NSArray,NSDictionary)常用copy关键字,为何?)

  8. “性别”(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 中文翻译)》。)

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

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

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

```
因为是只读属性,因此编译器不会为其建立对应的“设置方法”,即使如此,咱们仍是要写上这些属性的语义,以此代表初始化方法在设置这些属性值时所用的方式。要是不写明语义的话,该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法以前自行拷贝属性值。这种操做多余并且低效。

```
复制代码
  1. initUserModelWithUserName 若是改成 initWithName 会更加简洁,并且足够清晰。
  2. UserModel 若是改成 User 会更加简洁,并且足够清晰。
  3. UserSex若是改成Sex 会更加简洁,并且足够清晰。
  4. 第二个 @property 中 assign 和 nonatomic 调换位置。 推荐按照下面的格式来定义属性
```source-objc
@property (nonatomic, readwrite, copy) NSString *name;
```

属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。 这样作你的属性更容易修改正确,而且更好阅读。这在[《禅与Objective-C编程艺术 >》](https://github.com/oa414/objc-zen-book-cn#%E5%B1%9E%E6%80%A7%E5%AE%9A%E4%B9%89)里有介绍。并且习惯上修改某个属性的修饰符时,通常从属性名从右向左搜索须要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操做。
复制代码

硬伤部分

  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 前多了空格。
  10. -(id)initUserModelWithUserName: (NSString*)name withAge:(int)age; 方法中 (NSString*)name,应为 (NSString *)name,少了空格。
  11. doLogIn方法中的 LogIn 命名不清晰:笔者猜想是login的意思,应该是粗心手误形成的。 (勘误: Login 是名词, LogIn 是动词,都表示登录的意思。见: Log in vs. login 

2. 什么状况使用 weak 关键字,相比 assign 有什么不一样?

什么状况使用 weak 关键字?

  1. 在 ARC 中,在有可能出现循环引用的时候,每每要经过让其中一端使用 weak 来解决,好比: delegate 代理属性

  2. 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性通常也使用 weak;固然,也可使用strong。在下文也有论述:《IBOutlet连出来的视图属性为何能够被设置成weak?》

不一样点:

  1. weak 此特质代表该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign相似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操做。

  2. assigin 能够用非 OC 对象,而 weak 必须用于 OC 对象

3. 怎么用 copy 关键字?

用途:

  1. NSString、NSArray、NSDictionary 等等常用copy关键字,是由于他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;

  2. 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 操做”,他们有可能会在调用以前自行拷贝属性值。这种操做多余而低效。

    enter image description here

下面作下解释: copy 此特质所表达的所属关系与 strong 相似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,常常用此特质来保护其封装性,由于传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时如果不拷贝字符串,那么设置完属性以后,字符串的值就可能会在对象不知情的状况下遭人更改。因此,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无心间变更。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

用 @property 声明 NSString、NSArray、NSDictionary 常用 copy 关键字,是由于他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操做,为确保对象中的字符串值不会无心间变更,应该在设置新属性值时拷贝一份。

该问题在下文中也有论述:用@property声明的NSString(或NSArray,NSDictionary)常用copy关键字,为何?若是改用strong关键字,可能形成什么问题?

4. 这个写法会出什么问题: @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 属性一般都不会有性能瓶颈。

5. 如何让本身的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

若想令本身所写的对象具备拷贝功能,则需实现 NSCopying 协议。若是自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。

具体步骤:

  1. 需声明该类听从 NSCopying 协议

  2. 实现 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:person];
}

- (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];
    }
}
复制代码

这样真得高效吗?不见得!这种写法“看上去很美、很合理”,但在实际开发中,它更像下图里的作法:

enter image description here

克强总理这样评价你的代码风格:

enter image description here

我和总理的意见基本一致:

老百姓 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;
    }

复制代码

6. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

@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
复制代码

我为了搞清属性是怎么实现的,曾经反编译过相关的代码,他大体生成了五个东西

  1. OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
  2. setter 与 getter 方法对应的实现函数
  3. ivar_list :成员变量列表
  4. method_list :方法列表
  5. prop_list :属性列表

也就是说咱们每次在增长一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增长 setter 与 getter 方法的描述,在属性列表中增长一个属性的描述,而后计算该属性在对象中的偏移量,而后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了可以读取正确字节数,系统对象偏移量的指针类型进行了类型强转.

7. @protocol 和 category 中如何使用 @property

  1. 在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,咱们使用属性的目的,是但愿遵照我协议的对象能实现该属性

  2. category 使用 @property 也是只会生成 setter 和 getter 方法的声明,若是咱们真的须要给 category 增长属性的实现,须要借助于运行时的两个函数:

    1. objc_setAssociatedObject
    2. objc_getAssociatedObject

8. runtime 如何实现 weak 属性

要实现 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_initWeakobjc_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;
    }];
}
复制代码

也就是有两个步骤:

  1. 在setter方法中作以下设置:

    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    复制代码
  2. 在属性所指的对象遭到摧毁时,属性值也会清空(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

iOS开发之高级面试题(二)

iOS开发之高级面试题(三)

iOS开发之高级面试题(四)

图文来源于网络,若有侵权请联系小编删除

相关文章
相关标签/搜索