面试题学习(1)

2018-04-12  本文已影响7人  叔叔不吃棒棒糖

前言

过几天准备面试了,搜了搜晚上的面试题,好多好多。说实话我大部分都答不上来,也没法全看,找了《招聘一个靠谱的iOS》这套面试题。里面有原作者的链接,答案当然已经有了,但是每道题还是需要认真想想,毕竟最终目的不是为了应付面试,是为了自己的技术能更近一步,万丈高楼平地起,一步一步来吧。
准备一天弄8-10道题,也就是五天内全都弄完,最近也没什么项目,白天可以用来搞这个,时间充足,所有题目能用代码验证的都会用代码验证并附上结果保证结论的正确性。

一、风格纠错题

codeStyle1.png

下面是一种答案

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

从枚举开始分析

typedef NS_ENUM(NSInteger,YFQSex) {
    YFQSexMan,
    YFQSexWoman
};

这是作者给出的,图片中的枚举方式是苹果并不推荐的。苹果推荐使用NS_OPTIONS与NS_ENUM。
那个NS_OPTIONS与NS_ENUM又有什么区别呢。
我们先来看两者例子

typedef NS_ENUM(NSInteger,YFQSex) {
    YFQSexMan = 0,
    YFQSexWoman = 1
};
typedef NS_OPTIONS(NSUInteger, YFQCharacter) {
    YFQCharacterIrritability = 1<<0,
    YFQCharacterBrave = 1<<1,
    YFQCharacterLazy = 1<<2
};
  1. 先说使用场景,NS_ENUM用在只能存在一个枚举值,例如Switch case。NS_OPTIONS用在允许同时存在多个枚举值。NS_ENUM定义通用枚举,NS_OPTIONS定义位移枚举。
  2. 那么问题来了,位移枚举又是什么。那什么又是位移呢,在这里(<<)代表左移。先来看<<的时候发生了什么。
以1<<2为例
移位发生在二进制下,所以翻译成我们的语言就是将0000 0001左移两位,变成了0000 0100。
转化为十进制就是2的2次方4,结果会在下方验证。

那么我们知道了移位,返回来再看NS_OPTIONS的使用场景,为什么通过移位就能实现多个枚举值同时存在呢。

    self.character = YFQCharacterLazy | YFQCharacterBrave;
    if(self.character & YFQCharacterLazy){
        NSLog(@"lazy");
    }
    if(self.character & YFQCharacterBrave){
        NSLog(@"brave");
    }
    if(self.character & YFQCharacterIrritability){
        NSLog(@"irritability");
    }
    NSLog(@"%lu",YFQCharacterLazy);

这里模拟了同时具备lazy和brave性格,打印如下。

2018-04-10 13:52:37.652530+0800 YFQResponseChain[24326:659897] lazy
2018-04-10 13:52:37.652799+0800 YFQResponseChain[24326:659897] brave
2018-04-10 13:52:37.652959+0800 YFQResponseChain[24326:659897] 4

lazy二进制0000 0100,brave二进制0000 0010,或运算后0000 0110,所以与运算就会打印出lazy和brave。lazy打印出来也是4,证明了上述的说法。
那么扯个淡

YFQCharacterHappy = 3<<2

打印出来是多少呢,经过运算应该是12。

  1. 那么最后一个问题,为什么会出现两种枚举呢。
    在使用或运算操作两个枚举值时,C++默认为运算结果的数据类型是枚举的底层数据类型即NSUInteger,且C++不允许它隐式转换为枚举类型本身,所以C++模式下定义了NS_OPTIONS宏以保证不出现类型转换。(摘自以下文章,说实话个人不是太理解)。
    该部分参考以下两文章
    详解枚举NS_OPTIONS与NS_ENUM的区别与格式
    位运算和枚举

类的命名

@interface CYLUser : NSObject<NSCopying>

如果工程项目非常庞大,需要拆分成不同的模块,可以在类、typedef宏命名的时候使用前缀。
前缀为三个字母,双字母前缀为 Apple 的类预留。
还要注意使用驼峰命名法。
同时也解释了枚举用YFQSex。
此处的NSCopying会在接下来的问题中详细解答。

属性

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

由于能力原因,这里我也不会写太深,尽量挨个都说到。
首先,属性定义的规则是 原子性,读写 和 内存管理。

  1. @property声明可以直接生成一个set和get方法,写和读。
    如果括号内的关键字一个不写,
    readwrite,atomic(即不写nonatomic)
    非ARC环境下, 默认为assign
    在ARC环境下,默认为strong
  2. nonatomic与atomic
    atomic保证多线程安全,性能低于nonatomic。
    比如对于atomic,当线程a的get方法还没结束,线程b调用set方法,那么线程a还是能得到一个完整的对象。但是这样消耗资源。
    对于nonatomic就不保证线程a能得到完整的对象,因此速度快。
    但是atomic也不能保证线程的绝对安全,只是增加了安全的几率。
  3. readonly
    这个比较简单,如果我们声明了这个关键字,那么对应的属性就无法修改,我们在使用set时会报错,这个关键字的声明根据使用场景来定。
    既然该类中已经有一个“初始化方法” (initializer),用于设置“姓名”(Name)、“年龄”(Age)和“性别”(Sex)的初始值: 那么在设计对应 @property 时就应该尽量使用不可变的对象:其三个属性都应该设为“只读”。用初始化方法设置好属性值之后,就不能再改变了。在本例中,仍需声明属性的“内存管理语义”。于是可以把属性的定义改成这样
  4. copy
    这个在下面有个问题中专门解释
  5. assign
    先说使用场景,assign使用在基本的数据类型(NSUInteger等)、枚举上。
    这里说一下为什么不用在其他数据类型上。assign做的事是让这个属性只复制你给它的值得指针,所以他们指向共同的区域。当使用malloc分配一块内存区域,并将地址赋值给指针a。指针b也想指向这块区域,就将a赋值给了b,此处用assign。当a不需要这块区域时,可以释放掉。此时b就变成了野指针,程序会crash掉。
    代码如下图
@property (nonatomic,weak) id      weakPoint;

@property (nonatomic,assign) id    assignPoint;

@property (nonatomic,strong) id    strongPoint;
 self.strongPoint = [NSDate date];
 NSLog(@"strong属性:%@",self.strongPoint);
 self.weakPoint = self.strongPoint;
 self.assignPoint = self.strongPoint;
 self.strongPoint = nil;
 NSLog(@"weak属性:%@",self.weakPoint);
 NSLog(@"assign属性:%@",self.assignPoint);
2018-04-10 15:34:15.972369+0800 YFQResponseChain[26616:721600] strong属性:2018-04-10 07:34:15 +0000
2018-04-10 15:34:15.972608+0800 YFQResponseChain[26616:721600] weak属性:(null)

会在最后crash。
代码中涉及了weak,可以看到weak并没有crash而是变成了null。这里也是weak和assign的区别。weak相较于assign多了一个功能就是当它指向的对象不存在时,会自动赋值nil,再向weak修饰的属性发消息时也不会crash。
摘自文章
再战 OC @property属性
## IOS开发中ARC下的assign和weak区别

  1. 属性类型
    这里讲int改为NSUInteger。
    oc的建议使用类型
    int -> NSInteger
    unsigned -> NSUInteger
    float -> CGFloat
    动画时间 -> NSTimeInterval
    使用NSUInteger是保证在64位系统和32位系统下的兼容性,int只能运行在32位系统下。

方法

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

这里涉及到了方法的命名规则,首先参数可以不加with或者使用with...and。直接写参数名简洁明了,不建议使用and。
至于快捷构造方法作者是这么解释的,我有点不太懂。
如果设计了“初始化方法” (initializer),也应当搭配一个快捷构造方法。而快捷构造方法的返回值,建议为 instancetype,为保持一致性,init 方法和快捷构造方法的返回类型最好都用 instancetype。
至于为什么把dologin删除,和MVC设计模式有关。

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

第一个问题

1、在arc模式下,针对可能出现的循环引用的情况,在其中一端使用weak,例如delegate,block
2、自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。
那么循环引用是什么呢,简单来说就是a强引用b,b强引用a。当我们想释放掉a的时候,发现a被b强引用了,于是我们就得先释放掉b,但是b又被a强引用,这就造成了ab都无法释放。真实的情况可能会被这个复杂,比如block。a有一个属性teacher,a就是强引用teachter,teacher又强引用了一个block,block中又调用了a中的一个属性于是就造成了a->teacher->block->a;解决办法就是在teacher引用block之前使用weak修饰self,使block中对self的应用是弱引用。delegate的使用场景也同样,是父子关系。
摘自以下文章:
理解 ARC 下的循环引用
ARC下用块(block)的循环引用问题样例探究
控件使用weak的会在下面的会问题中提到。

三、怎么用 copy 关键字?

这个问题问的是怎么使用,关于为什么使用以及和其他关键字的区别会在下面的问题中讨论。

  1. 任何可以用一个可变的对象设置的((比如 NSString,NSArray,NSURLRequest))属性的内存管理类型必须是 copy 的。
    这是为了确保防止在不明确的情况下修改被封装好的对象的值。比如执行 array(定义为 copy 的 NSArray 实例) = mutableArray,copy 属性会让 array 的 setter 方法为 array = [mutableArray copy], [mutableArray copy] 返回的是不可变的 NSArray 实例,就保证了正确性。用其他属性修饰符修饰,容易在直接赋值的时候,array 指向的是 NSMuatbleArray 的实例,在之后可以随意改变它的值,就容易出错。
  2. 属性可以存储一个代码块。为了让它存活到定义的块的结束,必须使用 copy (block 最早在栈里面创建,使用 copy让 block 拷贝到堆里面去)
    (关于block中为什么要使用copy在原答案中会有更详细的解释,我复制过来也没什么意思,自己去体会寻找的乐趣吧)
    扩展文章
    iOS开发怎么使用copy关键字

四、这个写法会出什么问题: @property (copy) NSMutableArray *array;

首先使用copy错误,如上个问题所说,copy修饰的属性在调用set方法时,会生成一个不可变对象,所以实际上如果调用这个属性会生成一个NSArray。如果我们按照NSMutableArray来使用属性就会报错,代码如下

NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
    self.array = array;
    [self.array removeObjectAtIndex:0];
'-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x60000023c900'

还有就是此处没有声明原子性,默认是atomic,在ios中这会很影响性能,除非特殊需要,否则都使用nonatomic。
在Mac OS X中应该是会使用到atomic中的,原子性应该用在多线程中,保证运算的完整性。

五、如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

这个重点讲讲copy吧
关于第一个问题更通俗的说法是自定义对象支持拷贝操作。我们先来看一个具体的使用场景。

UserModel *model = [[UserModel alloc]initWithName:@"tony" Age:23];
UserModel *person1 = [model copy];
    person1.name = @"rose";
    NSLog(@"person.name = %@,person.age = %lu",model.name,model.age);
    NSLog(@"person1.name = %@,person1.age = %lu",person1.name,model.age);
2018-04-11 11:25:12.426325+0800 YFQResponseChain[35246:1390059] person.name = tony,person.age = 23
2018-04-11 11:25:12.426470+0800 YFQResponseChain[35246:1390059] person1.name = rose,person1.age = 23

这里我们将name的关键词改为readWrite,所以我们看到新copy的person1对象的name是rose,age则是copy过来的23。
那么怎么实现的copy呢,首先在第一个问题中我们针对usermodel类声明了协议NSCopying。然后我们在.m文件里加了这样一个方法。

- (id)copyWithZone:(NSZone *)zone{
    UserModel *copy = [[[self class] allocWithZone:zone] initWithName:_name Age:_age];
    return copy;
}

接下来的讨论将会围绕这个方法展开。
一下内容摘自《Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》第22条。
为什么会出现NSZone呢?因为以前开发程序时,会据此把内存凤城不同的区(zone),而对象会创建在某个区里。现在不用了,每个程序只有一个区:“默认区”(dafault zone)。所以说,尽管必须实现这个方法,但你不必担心其中的zone参数。
copy方法由NSObejct实现,该方法只是以默认区为参数来调用“copyWithZone”。我们总是想覆写copy方法,其实真正需要实现的却是“copyWithZone”方法,这个问题大家注意。

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject<NSCopying>

@property (nonatomic, readonly, copy) NSString *firstName;
@property (nonatomic, readonly, copy) NSString *lastName;

- (instancetype)initWithFirstName:(NSString *)firstName LastName:(NSString *)lastName;


@end
#import "EOCPerson.h"

@implementation EOCPerson

- (id)copyWithZone:(NSZone *)zone
{
    EOCPerson *copy = [[[self class] allocWithZone:zone]
                       initWithFirstName:_firstName LastName:_lastName];
    return copy;
}
- (instancetype) initWithFirstName:(NSString *)firstName LastName:(NSString *)lastName{
    if(self = [super init]){
        _firstName = [firstName copy];
        _lastName = [lastName copy];
    }
    return self;
}

@end

若想使某个类支持拷贝功能,只需声明该类遵从NSCopying协议,并实现其中法即可,也就是上文提到的方法和之前的协议声明。在上述例子中实现的“copyWithZone:”中,我们直接把待拷贝的对象交给“全能初始化方法”,令其执行所有初始化工作。然而有的时候,除了要拷贝对象,还要完成其他一些操作,比如类对象中的数据结构并未在初始化方法中设置好,需要另行设置。举个例子,

#import "EOCPerson.h"

@implementation EOCPerson{
    NSMutableSet *_friends;
}

- (instancetype) initWithFirstName:(NSString *)firstName LastName:(NSString *)lastName{
    if(self = [super init]){
        _firstName = [firstName copy];
        _lastName = [lastName copy];
        _friends = [NSMutableSet new];
    }
    return self;
}

- (void)addFriend:(EOCPerson *)person{
    [_friends addObject:person];
}

- (void)removeFriend:(EOCPerson *)person{
    [_friends removeObject:person];
}

- (id)copyWithZone:(NSZone *)zone
{
    EOCPerson *copy = [[[self class] allocWithZone:zone]
                       initWithFirstName:_firstName LastName:_lastName];
    copy->_friends = [_friends mutableCopy];
    return copy;
}

@end

这次所实现的方法比原来多了一些代码,它把对象的_friends实例变量复制了一份,令copy对象的_friends实例变量指向这个复制过的set。注意,这里使用了->语法,因为_friends并非属性,只是个在内部使用的实例变量。其实也可以声明一个属性来表示它,不过由于该变量不会在本类之外使用,所以那么做没必要。
这个例子提出了一个有趣的问题:为什么要拷贝_friends实例变量呢?不拷贝这个变量,直接令两个对象共享同一个可变的set是否更简单些呢?如果真的这样做了,那么在给原来的对象添加一个新朋友后,靠背过的那个对象居然也“神奇地”与之为友了。在本例中,这显然不是我们想要的效果。然而,那个set若是不可变的,则无需复制,因为其中的内容毕竟不会改变,所以不用担心此类问题。如果复制了,那么内存中将会有两个一摸一样的set,反而造成浪费。
通常情况下,应该像本例这样,采用全能初始化方法来初始化待拷贝的对象。不过有些时候不能这么做,因为全能初始化方法会产生一些“副作用”,这些附加操作对目前的要拷贝的对象无益。比如,初始化方法可能要设置一个复杂的内部数据结构,可是在拷贝后的对象中,这个数据结构立刻就要用其他苏剧来覆写,所以没必要再设置一遍。
仔细看看刚才那个“copyWithZone:”方法,你就会发现,存放朋友对象的那个set通过mutablecopy方法来复制的。此方法来自另个叫做NSMutableCopying的协议。该协议与NSCopying类似,也只定义了一个方法,然而方法名不同:

- (id)mutableCopyWithZone:(NSZone *)zone

mutableCopy这个“辅助方法”与copy类似,也是用默认的zone参数来调用“mutableCopyWithZone”。如果你的类氛围可变版本与不可变版本,那么就应该实现NSMutableCopying。若采用此模式,则在可变类中覆写“copyWithZone:”方法时,不要反悔可变版本,而应返回一份不可变版本。无论当前实例是否可变,若需获取其可变版本的拷贝,均应使用mutableCopy方法。同理,若需要不可变的拷贝,则总应通过copy方法来获取。
对于不可变的NSArry与可变的NSMutableArray来说,下列关系总是成立的:
-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray
有个微妙的情况要注意:在可变对象上调用copy方法会返回令恶意不可变的类的实例。这样做是为了能在可变版本与不可变版本之间自由切换。要实现此目标,还有个办法,就是提供三个方法:copy、immutableCopy、mutableCopy,其中,copy所返回的拷贝对象与当前对象的类型一致,而另外两个方法则返回不可变版本和可变版本的拷贝。但是,如果调用者并不知道其所用的实例是否真的可变,那么这种做法就不太好。某种方法可能会把NSMutableArray对象当作NSArray返回给你,而你在上边调用copy方法来复制他。此时你以为拷贝后的对象应该是不可变数组,但实际上它确实可变的。
可以查询类型信息译判断待拷贝的实例是否可变,不过那样做比较麻烦,因为每次复制对象的时候都得查询。为了安全起见,最后还是会使用immutableCopy和mutableCopy这两个方法来复制对象,而这么做就和只有copy与mutableCopy方法的设计方案毫无二致。把拷贝方法称为copy而非immutableCopy的原因在于,NSCopying不仅设计给那些具有可变版本和不可变版本的类来用,而且还要供其他一些类使用,而那些类没有可变和不可变之分,所以说,把拷贝方法叫做immutableCopy不合适。
在编写拷贝方法时,还要决定一个问题,就是应该执行“深拷贝”还是“浅拷贝”。深拷贝的意思就是:在拷贝对象自身时,将其底层数据也一并复制过去。Foundation框架中所有Collection类在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的主要原因在,容器内的对象未必都能拷贝,而且调用者也未必想要在拷贝容器时一并拷贝其中的每个对象。一般情况下,我们会遵照系统框架使用的模式,在自定义的类中以浅拷贝的方式实行“copyWihtZone:”方法。但如果有必要的话,也可以增加一个执行深拷贝的方法。以NSSSet威力,该类提供了下面这个初始化方法,用以执行深拷贝:

- (id)initWithSet:(NSArray *)array copyItemsL:(BOOL)copyItems

若copyItem参数舍为YES,则该方法会向数组中的每个元素发送copy消息,用拷贝好的元素创建新的set,并将其返回给调用者。
以下面为例:

- (id)deepCopy{
    EOCPerson *copy = [[[self class]alloc]initWithFirstName:_firstName LastName:_lastName];
    copy->_friends = [[NSMutableSet alloc]initWithSet:_friends copyItems:YES];
    return copy
}

以下文章中有一些总结可以:
总结
下面是我自己简单的总结:

WechatIMG38.jpeg
关于第二个问题,原作者的意思是没必要重写copy的setter方法。
这个我先也不写了,等有时间再找找资料。

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

  1. @proerty的本质在之前写的一篇文章里有提到
    这篇文章
  2. 自动合成
    完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字.
@synthesize name = _myname;

以下引用作者原话,我看不太懂
我为了搞清属性是怎么实现的,曾经反编译过相关的代码,他大致生成了五个东西

OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
setter 与 getter 方法对应的实现函数
ivar_list :成员变量列表
method_list :方法列表
prop_list :属性列表
也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.

七、@protocol 和 category 中如何使用 @property

  1. protocol
    EOCPerson.h
@protocol personDelegate <NSObject>

@property (nonatomic, copy) NSString *job;

@end


@interface EOCPerson : NSObject<NSCopying>

UserModel.h

@interface UserModel : NSObject<NSCopying,personDelegate>{
    NSString *_job;
}

UserModel.m
自动

@synthesize job;

手动

- (void)setJob:(NSString *)job{
    _job = job;
}
- (NSString *)job{
    return _job;
}

运行

    person1.job = @"伐木";
    NSLog(@"job = %@",person1.job);

结果

2018-04-11 16:11:07.771528+0800 YFQResponseChain[38677:1565889] job = 伐木

2.category的使用在以下文章
这篇文章

参考文章
iOS Category 和 Protocol 中的 Property 你们真的会了么?

八、runtime 如何实现 weak 属性

weak不论是用作property修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak修饰的属性或变量均会被设置为nil,这样可以防止野指针错误,本文要讲解的也正是这个特性,runtime如何将weak修饰的变量的对象在销毁时自动置为nil。

那么runtime是如何实现在weak修饰的变量的对象在被销毁时自动置为nil的呢?一个普遍的解释是:runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中。用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。

参考自以下文章
文章

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

这个其实之前已经都提到过了

  1. nonatomic ,atomic
  2. readonly,readwrite
    3 strong,weak,copy,assgin
    4 getter = name setter = name(setter不常用)
    上述4中可以理解为修改默认getter或setter方法名。

十、weak属性需要在dealloc中置nil么?

不需要。

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

十一、@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 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定

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

这个在上述问题中已经有提到了这里再复制下吧

  1. 对应基本数据类型默认关键字是
    atomic,readwrite,assign
  2. 对于普通的 Objective-C 对象
    atomic,readwrite,strong

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

这个算是对上面关于copy问题的补充。
因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.
copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

举例说明:

定义一个以 strong 修饰的 array:

@property (nonatomic, readwrite, strong) NSArray *array;

    NSArray *array = @[@1,@2,@3];
    NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];
    self.array = mutableArray;
    [mutableArray removeAllObjects];
    NSLog(@"strong = %@",self.array);
    
    [mutableArray addObjectsFromArray:array];
    self.array = [mutableArray copy];
    [mutableArray removeAllObjects];
    NSLog(@"copy = %@",self.array);

结果

2018-04-12 09:51:53.490577+0800 YFQResponseChain[45486:2129258] strong = (
)
2018-04-12 09:51:53.491013+0800 YFQResponseChain[45486:2129258] copy = (
    1,
    2,
    3
)

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

对非集合类对象的 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 appendString:@"origion!"];
    NSLog(@"string = %@",stringCopy);
    NSLog(@"mutableString = %@",string);

结果

2018-04-12 09:52:04.788322+0800 YFQResponseChain[45486:2129258] string = origin
2018-04-12 09:52:04.788616+0800 YFQResponseChain[45486:2129258] mutableString = originorigion!
  1. 集合类对象的copy与mutableCopy
    集合类对象是指 NSArray、NSDictionary、NSSet ... 之类的对象。下面先看集合类immutable对象使用 copy 和 mutableCopy 的一个例子:
  NSMutableString *string = [NSMutableString stringWithString:@"origin"]
    NSArray *array1 = @[string, @[@"c", @"d"]];
    NSArray *copyArray = [array1 copy];
    NSMutableArray *mCopyArray = [array1 mutableCopy];
    [string appendString:@"origion!"];

结果

2018-04-12 09:52:04.789204+0800 YFQResponseChain[45486:2129258] mcopyArray = (
    "originorigion!",
        (
        c,
        d
    )
)

查看内容,可以看到 copyArray 和 array 的地址是一样的,而 mCopyArray 和 array 的地址是不同的。说明 copy 操作进行了指针拷贝,mutableCopy 进行了内容拷贝。但需要强调的是:此处的内容拷贝,仅仅是拷贝 array 这个对象,array 集合内部的元素仍然是指针拷贝。这和上面的非集合 immutable 对象的拷贝还是挺相似的,那么mutable对象的拷贝会不会类似呢?我们继续往下,看 mutable 对象拷贝的例子:

NSMutableArray *array2 = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
    NSArray *copyArray2 = [array2 copy];
    NSMutableArray *mCopyArray2 = [array2 mutableCopy];

查看内存,如我们所料,copyArray、mCopyArray和 array 的内存地址都不一样,说明 copyArray、mCopyArray 都对 array 进行了内容拷贝。同样,我们可以得出结论:

在集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。用代码简单表示如下:

[immutableObject copy] // 浅复制
[immutableObject mutableCopy] //单层深复制
[mutableObject copy] //单层深复制
[mutableObject mutableCopy] //单层深复制
上一篇下一篇

猜你喜欢

热点阅读