您的位置:首页 > 产品设计 > UI/UE

iOSInterviewQuestions/iOS面试问题

2015-09-09 19:49 671 查看
转自: https://github.com/junxianhu/iOSInterviewQuestions
1. 风格纠错题

typedef enum {
    UserSex_Man,
    UserSex_Woman
}UserSex;
@interface UserModel : NSObject

@property (nonatomic, strong) NSString *name;
@property (assign,nonatomic) int age;
@property (nonatomic,assign) UserSex sex;

-(id)initUserModelWithUserName:(NSString *)name WithAge:(int)age;
-(void)doLogIn;
@end

修改完的代码:

修改方法有很多种,现给出一种做示例:
// .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


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


优化部分

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

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


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

int -> NSInteger
  unsigned -> NSUInteger
  float -> CGFloat
  动画时间 -> NSTimeInterval


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

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

doLogIn方法不应写在该类中:虽然
LogIn
的命名不太清晰,但笔者猜测是login的意思,而登录操作属于业务逻辑,观察类名 UserModel ,以及属性的命名方式,该类应该是一个 Model 而不是一个“ MVVM 模式下的
ViewModel ”:

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

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

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

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

-(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;


由于字符串值可能会改变,所以要把相关属性的“内存管理语义”声明为copy。(原因在下文有详细论述:用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?)

“性别”(sex)属性的:该类中只给出了一种“初始化方法” (initializer)用于设置“姓名”(Name)和“年龄”(Age)的初始值,那如何对“性别”(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化方法的初始化方法。举例说明:

// .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属性,最终给出的修改代码应该是:

// .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 编程艺术 (Zen and the Art of the Objective-C
Craftsmanship 中文翻译)》
。)

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

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

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

initUserModelWithUserName
如果改为
initWithName
会更加简洁,而且足够清晰。
UserModel
如果改为
User
会更加简洁,而且足够清晰。
UserSex
如果改为
Sex
会更加简洁,而且足够清晰。

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

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


属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。 这样做你的属性更容易修改正确,并且更好阅读。这在《禅与Objective-C编程艺术
>》里有介绍。而且习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作。

硬伤部分

在-和(void)之间应该有一个空格
enum中驼峰命名法和下划线命名法混用错误:枚举类型的命名规则和函数的命名规则相同:命名时使用驼峰命名法,勿使用下划线命名法。
enum左括号前加一个空格,或者将左括号换到下一行
enum右括号后加一个空格
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的意思,应该是粗心手误造成的。

2. 什么情况使用 weak 关键字,相比 assign 有什么不同?

什么情况使用 weak 关键字?

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

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

不同点:

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

assigin 可以用非OC对象,而weak必须用于OC对象

3. 怎么用 copy 关键字?

用途:

NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;

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此特质所表达的所属关系与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;

两个问题:
1、添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃.因为copy就是复制一个不可变NSArray的对象;
2、使用了atomic属性会严重影响性能 ;

第1条的相关原因在下文中有论述《用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?》 以及上文《怎么用 copy 关键字?》也有论述。
比如下面的代码就会发生崩溃

// .h文件
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
// 下面的代码就会发生崩溃

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协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyiog与NSMutableCopying协议。

具体步骤:

需声明该类遵从NSCopying协议

实现NSCopying协议。该协议只有一个方法:

- (id)copyWithZone: (NSZone*) zone


注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是“copyWithZone”方法。

以第一题的代码为例:

// .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


然后实现协议中规定的方法:

- (id)copyWithZone:(NSZone *)zone {
    CYLUser *copy = [[[self class] allocWithZone:zone] 
                     initWithName:_name
                                  age:_age
                                  sex:_sex];
    return copy;
}


但在实际的项目中,不可能这么简单,遇到更复杂一点,比如类对象中的数据结构可能并未在初始化方法中设置好,需要另行设置。举个例子,假如CYLUser中含有一个数组,与其他CYLUser对象建立或解除朋友关系的那些方法都需要操作这个数组。那么在这种情况下,你得把这个包含朋友对象的数组也一并拷贝过来。下面列出了实现此功能所需的全部代码:

[code]
// .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;
- (void)addFriend:(CYLUser *)user;
- (void)removeFriend:(CYLUser *)user;

@end


// .m文件

// .m文件

@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];
}


不过也有争议,有人说“苹果如果像下面这样干,是不是效率会高一些?”
[code]
- (void)setName:(NSString *)name {
    if (_name != name) {
    //[_name release];//MRC
    _name = [name 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中判断,为什么不把代码写好?


或者

[code]
[a setX:[a x]];   //队友咆哮道:你在干嘛?!!


不要在setter里进行像[code]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)中做:
[code]
- (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 这门语言才能根据名称自动创建出存取方法。其实也可以把属性当做一种关键字,其表示:

编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。 所以你也可以这么说:

@property = getter + setter;

例如下面这个类:

@interface Person : NSObject 
@property NSString *firstName; 
@property NSString *lastName; 
@end

上述代码写出来的类与下面这种写法等效:

[code]
@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),表示该变量距离存放对象的内存区域的起始地址有多远。
setter与getter方法对应的实现函数
ivar_list
:成员变量列表
method_list
:方法列表
prop_list
:属性列表

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

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

在protocol中使用property只会生成setter和getter方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性

category 使用 @property 也是只会生成setter和getter方法的声明,如果我们真的需要给category增加属性的实现,需要借助于运行时的两个函数:

objc_setAssociatedObject

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属性
id obj1;

objc_initWeak(&obj1, obj);

/*obj引用计数变为0,变量作用域结束*/

objc_destroyWeak(&obj1);

下面对用到的两个方法
objc_initWeak
objc_destroyWeak
做下解释:

总体说来,作用是: 通过
objc_initWeak
函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过
objc_destoryWeak
函数释放该变量(obj1)。

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

属性可以拥有的特质分为四类:

原子性---
nonatomic
特质

在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不使用同步锁。请注 意,尽管没有名为“atomic”的特质(如果某属性不具备nonatomic特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。

读/写权限---
readwrite(读写)
readooly (只读)


内存管理语义---
assign
strong
weak
unsafe_unretained
copy


方法名---
getter=<name>
setter=<name>


getter=<name>
的样式:

@property (nonatomic, getter=isOn) BOOL on;

[/code]
setter=<name>
这种不常用,也不推荐使用。故不在这里给出写法。)

不常用的:
nonnull
,
null_resettable
,
nullable


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

不需要。

在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理

即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil:

在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

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

@property有两个对应的词,一个是@synthesize,一个是@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是
@syntheszie var = _var;

@synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成。(当然对于readonly的属性只需提供 getter即可)。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到
instance.var = someVar
,由于缺setter方法会导致程序崩溃;或者当运行到
someVar
 = var
时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

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

对应基本数据类型默认关键字是

atomic,readwrite,assign

对于普通的OC对象

atomic,readwrite,strong

13.
用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

因为父类指针可以指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

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

为了理解这种做法,首先要知道,两种情况:

对非集合类对象的copy与mutableCopy操作;
对集合类对象的copy与mutableCopy操作。

1.
对非集合类对象的copy操作:

在非集合类对象中:对immutable对象进行copy操作,是指针复制,mutableCopy操作时内容复制;对mutable对象进行copy和mutableCopy都是内容复制。用代码简单表示如下:

[immutableObject copy] // 浅复制
    [immutableObject mutableCopy] //深复制
    [mutableObject copy] //深复制
    [mutableObject 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,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

2、集合类对象的copy与mutableCopy

集合类对象是指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] //单层深复制

这个代码结论和非集合类的非常相似。

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

在回答之前先说明下一个概念:

实例变量 = 成员变量 = ivar

指的是一个东西。

如果使用了属性的话,那么编译器就会自动编写访问属性所需的方法,此过程叫做“自动合成”( 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


上述语法会将生成的实例变量命名为[code]_myFirstName
_myLastName
,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

总结下@synthesize合成实例变量的规则,有以下几点:

如果指定了成员变量的名称,会生成一个指定的名称的成员变量,

如果这个成员已经存在了就不再生成了.

如果是
@synthesize foo;
还会生成一个名称为foo的成员变量,也就是说:

如果没有指定成员变量的名称会自动生成一个属性同名的成员变量,

如果是
@synthesize foo = _foo;
就不会生成成员变量了.

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

回答这个问题前,我们要搞清楚一个问题,什么情况下不会autosynthesis(自动合成)?

同时重写了setter和getter时
重写了只读属性的getter时
使用了@dynamic时
在 @protocol 中定义的所有属性
在 category 中定义的所有属性

重载的属性

当你在子类中重载了父类中的属性,你必须 使用
@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


上述语法会将生成的实例变量命名为[code]_myFirstName
_myLastName
,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

举例说明:应用场景:
结果编译器报错:
// .m文件
// 打开第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(实例变量/成员变量)。这时候有两种选择:

要么如第14行:手动创建ivar
要么如第17行:使用
@synthesize foo = _foo;
,关联@property与ivar。

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

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

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

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


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

如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0。
如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0。
如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

具体原因如下:
objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

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

17.
objc中向一个对象发送消息[obj foo]和[code]objc_msgSend()

函数之间有什么关系?具体原因同上题:该方法编译之后就是
objc_msgSend()
函数调用.

也就是说:

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


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

简单来说:

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

简单的流程如下,在上一题中也提到过:

objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。

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

Method resolution

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

Fast forwarding

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

Normal forwarding

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

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

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

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

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

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

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

[thead]
[/thead]

Objective-C 对象的结构图
ISA指针
根类的实例变量
倒数第二层父类的实例变量
...
父类的实例变量
类的实例变量
根对象就是NSobject,它的superclass指针指向nil

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

如图:



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

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

21. 下面的代码输出什么?

@implementation Son : Father
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end
<strong>答案:</strong>

都输出 Son

NSStringFromClass([self class]) = Son

NSStringFromClass([super class]) = Son


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

self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。

上面的例子不管调用
[self class]
还是
[super class]
,接受消息的对象都是当前
Son *xxx
这个对象。而不同的是,super是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

真的是这样吗?继续看:

使用clang重写命令:

$ clang -rewrite-objc test.m

[/code]
发现上述代码被转化为:

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

<pre name="code" class="objc">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, ...)


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

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

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

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

struct objc_super {
   __unsafe_unretained id receiver;
   __unsafe_unretained Class super_class;
};
结构体有两个成员,第一个成员是 receiver, 类似于上面的
objc_msgSend
函数第一个参数self 。第二个成员是记录当前类的父类是什么。[/code]
所以,当调用 [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。

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

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

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

在ARC下不需要。

在MRC中,对于使用retain或copy策略的需要 。

在MRC下也不需要

无论在MRC下还是ARC下均不需要
// 对象的内存销毁时间表


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()

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

类方法:

类方法是属于类对象的
类方法只能通过类对象调用
类方法中的self是类对象
类方法可以调用其他的类方法
类方法中不能访问成员变量
类方法中不定直接调用对象方法

实例方法:

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

25.
_objc_msgForward
函数是做什么的,直接调用它将会发生什么?

_objc_msgForward
是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,
_objc_msgForward
会尝试做消息转发。

我们可以这样创建一个
_objc_msgForward
对象:

IMP msgForwardIMP = _objc_msgForward;


《objc中向一个对象发送消息
[obj foo]
objc_msgSend()
函数之间有什么关系?》曾提到
objc_msgSend
在“消息传递”中的作用。在“消息传递”过程中,
objc_msgSend
的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用
_objc_msgForward
函数指针代替
IMP 。最后,执行这个 IMP 。
Objective-C运行时是开源的,所以我们可以看到它的实现。打开
Apple Open Source 里Mac代码里的obj包 下载一个最新版本,找到
objc-runtime-new.mm
,进入之后搜索
_objc_msgForward


虽然Apple没有公开
_objc_msgForward
的实现源码,但是我们还是能得出结论:

_objc_msgForward
是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,
_objc_msgForward
会尝试做消息转发。

结合《NSObject官方文档》,排除掉 NSObject 做的事,剩下的就是
_objc_msgForward
消息转发做的几件事:

调用
resolveInstanceMethod:
方法 (或
resolveClassMethod:
)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始
objc_msgSend
流程。这一次对象会响应这个选择器,一般是因为它已经调用过
class_addMethod
。如果仍没实现,继续下面的动作。

调用
forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。

调用
methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用
doesNotRecognizeSelector
抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给
forwardInvocation:


调用
forwardInvocation:
方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。

调用
doesNotRecognizeSelector:
,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

上面前4个方法均是模板方法,开发者可以override,由 runtime 来调用。最常见的实现消息转发:就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的

也就是说
_objc_msgForward
在进行消息转发的过程中会涉及以下这几个方法:

resolveInstanceMethod:
方法 (或
resolveClassMethod:
)。

forwardingTargetForSelector:
方法

methodSignatureForSelector:
方法

forwardInvocation:
方法

doesNotRecognizeSelector:
方法

下面回答下第二个问题“直接
_objc_msgForward
调用它将会发生什么?”

直接调用
_objc_msgForward
是非常危险的事,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。

就好像跑酷,干得好,叫“耍酷”,干不好就叫“作死”。

正如前文所说:

_objc_msgForward
是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,
_objc_msgForward
会尝试做消息转发。

26.
runtime如何实现weak变量的自动置nil?

runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

上篇中的《runtime 如何实现 weak 属性》有论述。(注:在上篇的《使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到
__weak
引用的解除时间。)

27. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;

解释下:

因为编译后的类已经注册在 runtime 中,类结构体中的
objc_ivar_list
实例变量的链表 和
instance_size
实例变量的内存大小已经确定,同时runtime 会调用
class_setIvarLayout
class_setWeakIvarLayout
来处理 strong weak 引用。所以不能向存在的类中添加实例变量;

运行时创建的类是可以添加实例变量,调用
class_addIvar
函数。但是得在调用
objc_allocateClassPair
之后,
objc_registerClassPair
之前,原因同上。

28. runloop和线程有什么关系?

总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。
Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。

runloop 和线程的关系:

主线程的run loop默认是启动的。

iOS的应用程序里面,程序启动后会有一个如下的main()函数

int main(int argc, char * argv[]) {
@autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}


重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。

[code]NSRunLoop *runloop = [NSRunLoop currentRunLoop];


29.
runloop的mode作用是什么?

model 主要是用来指定事件在运行循环中的优先级的,分为:

NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
UITrackingRunLoopMode:ScrollView滑动时
UIInitializationRunLoopMode:启动时
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

苹果公开提供的 Mode 有两个:

NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
NSRunLoopCommonModes(kCFRunLoopCommonModes)

30. 以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中 NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到 UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会 影响scrllView的滑动。

如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

同时因为mode还是可定制的,所以:

Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:

//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];


31. 猜想runloop内部是如何实现的?

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

或使用伪代码来展示下:

int main(int argc, char * argv[]) {
 //程序一直运行状态
 while (AppIsRunning) {
      //睡眠状态,等待唤醒事件
      id whoWakesMe = SleepForWakingUp();
      //得到唤醒事件
      id event = GetEvent(whoWakesMe);
      //开始处理事件
      HandleEvent(event);
 }
 return 0;
}


32. objc使用什么机制管理对象内存?

通过 retainCount 的机制来决定对象是否需要释放。 每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了。

33.
ARC通过什么方式帮助开发者管理内存?

编译时根据代码上下文,插入 retain/release

34.
不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

分两种情况:手动干预释放时机、系统自动去释放。

手动干预释放时机--指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。

系统自动去释放--不手动指定autoreleasepool

Autorelease对象会在当前的 runloop 迭代结束时释放。

如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

35.
BAD_ACCESS在什么情况下出现?

访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环

36.
苹果是如何实现autoreleasepool的?

autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.

[code]objc_autoreleasepoolPush

objc_autoreleasepoolPop

objc_aurorelease


看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作。

37.
使用block时什么情况会发生引用循环,如何解决?

一个对象中强引用了block,在block中又使用了该对象,就会发射循环引用。 解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用。

id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏
id __block weakSelf = self;

38.
在block内如何修改block外部变量?

默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上
__block
来让其写操作生效,示例代码如下:

__block int a = 0;
void  (^foo)(void) = ^{ 
    a = 1; 
}
f00(); 
//这里,a的值被修改为1


39. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?

系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑:

所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,比如这些:

[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }]; 

[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }]; 

[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" 
                                                  object:nil 
                           queue:[NSOperationQueue mainQueue]
                                              usingBlock:^(NSNotification * notification) {
                                                    self.someProperty = xyz; }];

这些情况不需要考虑“引用循环”。

但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:

__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^
{
__typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
} );


类似的:

[code]
__weak __typeof__(self) weakSelf = self;
  _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
                                                                object:nil
                                                                 queue:nil
                                                            usingBlock:^(NSNotification *note) {
      __typeof__(self) strongSelf = weakSelf;
      [strongSelf dismissModalViewControllerAnimated:YES];
  }];


self --> _observer --> block --> self 显然这也是一个循环引用。

40. GCD的队列([code]dispatch_queue_t

)分哪两种类型?串行队列Serial Dispatch Queue
并行队列Concurrent Dispatch Queue

41.
如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合并图片
});


42.[code]dispatch_barrier_async

的作用是什么?在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。
dispatch_barrier_async
函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行
dispatch_barrier_async
函数追加的处理,等
dispatch_barrier_async

追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。

打个比方:比如你们公司周末跟团旅游,高速休息站上,司机说:大家都去上厕所,速战速决,上完厕所就上高速。超大的公共厕所,大家同时去,程序猿很快就结束了,但程序媛就可能会慢一些,即使你第一个回来,司机也不会出发,司机要等待所有人都回来后,才能出发。
dispatch_barrier_async
函数追加的内容就如同 “上完厕所就上高速”这个动作。

43.
苹果为什么要废弃
dispatch_get_current_queue

dispatch_get_current_queue
容易造成死锁

44.
以下代码运行结果如何?

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}

只输出:1 。发生主线程锁死。

45.
addObserver: forKeyPath: options: context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?

// 添加键值观察
/*
1 观察者,负责处理监听事件的对象
2 观察的属性
3 观察的选项
4 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];


observer中需要实现一下方法:

[code]
// 所有的 kvo 监听到事件,都会调用此方法
/*
 1. 观察的属性
 2. 观察的对象
 3. change 属性变化字典(新/旧)
 4. 上下文,与监听的时候传递的一致
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;


46.
如何手动触发一个value的KVO

所谓的“手动触发”是区别于“自动触发”:

自动触发是指类似这种场景:在注册 KVO 之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。

想知道如何手动触发,必须知道自动触发 KVO 的原理:

键值观察通知依赖于 NSObject 的两个方法: [code]willChangeValueForKey:
didChangevlueForKey:
。在一个被观察属性发生改变之前,
willChangeValueForKey:
一定会被调用,这就 会记录旧的值。而当改变发生后,
didChangeValueForKey:
会被调用,继而
observeValueForKey:ofObject:change:context:

也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。

那么“手动触发”的使用场景是什么?一般我们只在希望能控制“回调的调用时机”时才会这么做。

具体做法如下:

如果这个
value
是 表示时间的
self.now
,那么代码如下:最后两行代码缺一不可。

//  .m文件
//  手动触发 value 的KVO,最后两行代码缺一不可。

//@property (nonatomic, strong) NSDate *now;
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self willChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
    [self didChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
}


但是平时我们一般不会这么干,我们都是等系统去“自动触发”。“自动触发”的实现原理:

比如调用
setNow:
时,系统还会以某种方式在中间插入
wilChangeValueForKey:
didChangeValueForKey:
observeValueForKeyPath:ofObject:change:context:
的调用。

大家可能以为这是因为
setNow:
是合成方法,有时候我们也能看到人们这么写代码:

- (void)setNow:(NSDate *)aDate {
    [self willChangeValueForKey:@"now"]; // 没有必要
    _now = aDate;
    [self didChangeValueForKey:@"now"];// 没有必要
}

这是完全没有必要的代码,不要这么做,这样的话,KVO代码会被调用两次。KVO在调用存取方法之前总是调用
willChangeValueForKey:
,之后总是调用
didChangeValueForkey:
。怎么做到的呢?答案是通过 isa 混写(isa-swizzling)。下文《apple用什么方式实现对一个对象的KVO?》会有详述。

47.
若一个类有实例变量
NSString *_foo
,调用setValue:forKey:时,可以以foo还是
_foo
作为key?

都可以。

48.
KVC的keyPath中的集合运算符如何使用?

必须用在集合对象上或普通对象的集合属性上
简单集合运算符有@avg, @count , @max , @min ,@sum,
格式 @"@sum.age"或 @"集合属性.@max.age"

49.
KVC和KVO的keyPath一定是属性么?

KVO支持实例变量

50.
如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?

请参考:《如何自己动手实现 KVO》

51.
apple用什么方式实现对一个对象的KVO?

Apple 的文档可以看出:Apple 并不希望过多暴露 KVO 的实现细节。不过,要是借助 runtime 提供的方法去深入挖掘,所有被掩盖的细节都会原形毕露:

当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。
重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。
最后通过
isa 混写(isa-swizzling)
把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,
对象就神奇的变成了新创建的子类的实例。我画了一张示意图,如下所示:



KVO 确实有点黑魔法:

Apple 使用了
isa 混写(isa-swizzling)
来实现 KVO 。

键值观察通知依赖于 NSObject 的两个方法:
willChangeValueForKey:
didChangevlueForKey:
。在一个被观察属性发生改变之前,
willChangeValueForKey:
一定会被调用,这就 会记录旧的值。而当改变发生后,
didChangeValueForKey:
会被调用,继而
observeValueForKey:ofObject:change:context:

也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们只在希望能控制回调的调用时机时才会这么做。大部分情况下,改变通知会自动调用。

比如调用
setNow:
时,系统还会以某种方式在中间插入
wilChangeValueForKey:
didChangeValueForKey:
observeValueForKeyPath:ofObject:change:context:
的调用。大家可能以为这是因为
setNow:
是合成方法,有时候我们也能看到人们这么写代码:

- (void)setNow:(NSDate *)aDate {
    [self willChangeValueForKey:@"now"]; // 没有必要
    _now = aDate;
    [self didChangeValueForKey:@"now"];// 没有必要
}


这是完全没有必要的代码,不要这么做,这样的话,KVO代码会被调用两次。KVO在调用存取方法之前总是调用 [code]willChangeValueForKey:
,之后总是调用
didChangeValueForkey:
。怎么做到的呢?答案是通过 isa 混写(isa-swizzling)。第一次对一个对象调用
addObserver:forKeyPath:options:context:
时,框架会创建这个类的新的 KVO 子类,并将被观察对象转换为新子类的对象。在这个
KVO 特殊子类中, Cocoa 创建观察属性的 setter ,大致工作原理如下:

- (void)setNow:(NSDate *)aDate {
    [self willChangeValueForKey:@"now"];
    [super setValue:aDate forKey:@"now"];
    [self didChangeValueForKey:@"now"];
}


这种继承和方法注入是在运行时而不是编译时实现的。这就是正确命名如此重要的原因。只有在使用KVC命名约定时,KVO才能做到这一点。

KVO 在实现中通过 [code]isa 混写(isa-swizzling)
把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。这在Apple
的文档可以得到印证:

Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true
class ...

然而 KVO 在实现中使用了
isa 混写( isa-swizzling)
,这个的确不是很容易发现:Apple 还重写、覆盖了
-class
方法并返回原来的类。 企图欺骗我们:这个类没有变,就是原本那个类。。。

但是,假设“被监听的对象”的类对象是
MYClass
,有时候我们能看到对
NSKVONotifying_MYClass
的引用而不是对
MYClass
的引用。借此我们得以知道 Apple 使用了
isa 混写(isa-swizzling)
。具体探究过程可参考这篇博文


52. IBOutlet连出来的视图属性为什么可以被设置成weak?

参考链接:
Should IBOutlets be strong or weak under ARC?

文章告诉我们:

因为既然有外链那么视图在xib或者storyboard中肯定存在,视图已经对它有一个强引用了。

不过这个回答漏了个重要知识,使用storyboard(xib不行)创建的vc,会有一个叫 _topLevelObjectsToKeepAliveFromStoryboard的私有数组强引用所有top level的对象,所以这时即便outlet声明成weak也没关系

53.
IB中User Defined Runtime Attributes如何使用?

它能够通过KVC的方式配置一些你在interface builder 中不能配置的属性。当你希望在IB中作尽可能多得事情,这个特性能够帮助你编写更加轻量级的viewcontroller

54. 如何调试BAD_ACCESS错误

重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object
通过 Zombie



3 设置全局断点快速定位问题代码所在行
4 Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer



55. lldb(gdb)常用的调试命令?

breakpoint 设置断点定位到某一个函数
n 断点指针下一步
po打印对象

更多 lldb(gdb) 调试命令可查看

The LLDB Debugger
苹果官方文档:
iOS Debugging Magic
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: