2018年iOS面试题总结(三)

一、nil和Nil及NULL和NSNull的区别

一、nil
我们给对象赋值时一般会使用object = nil,表示我想把这个对象释放掉;
或者对象由于某种原因,经过多次release,于是对象引用计数器为0了,系统将这块内存释放掉,这个时候这个对象为nil,我称它为“空对象”。(注意:我这里强调的是“空对象”,下面我会拿它和“值为空的对象”作对比!!!)
所以对于这种空对象,所有关于retain的操作都会引起程序崩溃,例如字典添加键值或数组添加新原素等,具体可参考如下代码:

2018年iOS面试题总结(三)_第1张图片

二、NSNull
NSNull和nil的区别在于,nil是一个空对象,已经完全从内存中消失了,而如果我们想表达“我们需要有这样一个容器,但这个容器里什么也没有”的观念时,我们就用到NSNull,我称它为“值为空的对象”。如果你查阅开发文档你会发现NSNull这个类是继承NSObject,并且只有一个“+ (NSNull *) null;”类方法。这就说明NSNull对象拥有一个有效的内存地址,所以在程序中对它的任何引用都是不会导致程序崩溃的。参考代码如下:

2018年iOS面试题总结(三)_第2张图片

三、Nil
nil和Nil在使用上是没有严格限定的,也就是说凡是使用nil的地方都可以用Nil来代替,反之亦然。只不过从编程人员的规约中我们约定俗成地将nil表示一个空对象,Nil表示一个空类。参考代码如下:

2018年iOS面试题总结(三)_第3张图片

四、NULL
我们知道Object-C来源于C、支持于C,当然也有别于C。而NULL就是典型C语言的语法,它表示一个空指针,参考代码如下:
int *ponit = NULL;


二、NSString属性什么时候用copy,什么时候用strong?

我们在声明一个NSString属性时,对于其内存相关特性,通常有两种选择(基于ARC环境):strong与copy。那这两者有什么区别呢?什么时候该用strong,什么时候该用copy呢?让我们先来看个例子。

示例

我们定义一个类,并为其声明两个字符串属性,如下所示:

@interface TestStringClass ()
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, copy) NSString *copyedString;
@end

上面的代码声明了两个字符串属性,其中一个内存特性是strong,一个是copy。下面我们来看看它们的区别。

首先,我们用一个不可变字符串来为这两个属性赋值,

- (void)test {
    NSString *string = [NSString stringWithFormat:@"abc"];
    self.strongString = string;
    self.copyedString = string;
    NSLog(@"origin string: %p, %p", string, &string);
    NSLog(@"strong string: %p, %p", _strongString, &_strongString);
    NSLog(@"copy string: %p, %p", _copyedString, &_copyedString);
}

其输出结果是:

origin string: 0x7fe441592e20, 0x7fff57519a48
strong string: 0x7fe441592e20, 0x7fe44159e1f8
copy string: 0x7fe441592e20, 0x7fe44159e200

我们要以看到,这种情况下,不管是strong还是copy属性的对象,其指向的地址都是同一个,即为string指向的地址。如果我们换作MRC环境,打印string的引用计数的话,会看到其引用计数值是3,即strong操作和copy操作都使原字符串对象的引用计数值加了1。

接下来,我们把string由不可变改为可变对象,看看会是什么结果。即将下面这一句

NSString *string = [NSString stringWithFormat:@"abc"];

改成:

NSMutableString *string = [NSMutableString stringWithFormat:@"abc"];

其输出结果是:

origin string: 0x7ff5f2e33c90, 0x7fff59937a48
strong string: 0x7ff5f2e33c90, 0x7ff5f2e2aec8
copy string: 0x7ff5f2e2aee0, 0x7ff5f2e2aed0

可以发现,此时copy属性字符串已不再指向string字符串对象,而是深拷贝了string字符串,并让_copyedString对象指向这个字符串。在MRC环境下,打印两者的引用计数,可以看到string对象的引用计数是2,而_copyedString对象的引用计数是1。

此时,我们如果去修改string字符串的话,可以看到:因为_strongString与string是指向同一对象,所以_strongString的值也会跟随着改变(需要注意的是,此时_strongString的类型实际上是NSMutableString,而不是NSString);而_copyedString是指向另一个对象的,所以并不会改变。

结论

由于NSMutableString是NSString的子类,所以一个NSString指针可以指向NSMutableString对象,让我们的strongString指针指向一个可变字符串是OK的。

而上面的例子可以看出,当源字符串是NSString时,由于字符串是不可变的,所以,不管是strong还是copy属性的对象,都是指向源对象,copy操作只是做了次浅拷贝。

当源字符串是NSMutableString时,strong属性只是增加了源字符串的引用计数,而copy属性则是对源字符串做了次深拷贝,产生一个新的对象,且copy属性对象指向这个新的对象。另外需要注意的是,这个copy属性对象的类型始终是NSString,而不是NSMutableString,因此其是不可变的。

这里还有一个性能问题,即在源字符串是NSMutableString,strong是单纯的增加对象的引用计数,而copy操作是执行了一次深拷贝,所以性能上会有所差异。而如果源字符串是NSString时,则没有这个问题。

所以,在声明NSString属性时,到底是选择strong还是copy,可以根据实际情况来定。不过,一般我们将对象声明为NSString时,都不希望它改变,所以大多数情况下,我们建议用copy,以免因可变字符串的修改导致的一些非预期问题。


三、 UIViewController的生命周期

UIViewController是IOS程序中的一个重要组成部分,扮演者一个大管家的身份,管理着程序中的众多视图:

何时加载view,加载的原则是什么,视图何时消失等问题,文档中讲的都很详细。

  Controller的view最好在需要显示时再去加载,并且在系统发出内存警告时释放不必要的view及相关的可再生的数据对象。

一、UIViewController的初始化

  初始化时会根据需要调用init,initWithCoder等相关函数,这个时候我们可以做一下简单的初始化操作,建立ViewController中需要使用的数据模型等,不建议在初始化阶段就直接创建view及其他与显示有关的对象(应该放到loadView的时候去创建,或者采用懒加载的方法创建)。

  我们都知道ViewController可以通过代码和xib两种方式创建,这两种方式的初始化流程也不尽相同。

  1)使用xib创建的VC

  xib其实最终是会把我们的设置保存成一个数据集(xml文件),当需要初始化构建VC的时候,回去读取记录的数据集,然后帮我们动态的创建VC,因此可以想象它在初始化时会先去找看是否实现initWithCoder方法,如果该类实现了该方法,就直接调用initWithCoder方法创建对象,如果没有实现的话就调用init方法。调用完初始化方法以后紧接着会调用awakeFromNib方法,在这个方法里面我们可以做进一步的初始化操作。

  2)使用代码创建VC

  使用代码创建时,我们根据需要手动的创建VC中的数据,如果自己定制VC时,还需要在init中调用[super init]

二、UIViewController中View的load和unload

  前面讲了不建议在VC初始化的时候就创建view及其他与显示相关的代码,官方文档建议将View的初始化操作放到loadView的时候再做,当VC接到内存告警时会调用didRecieveMemoryWarning这个时候我们就要做出响应,释放暂时不需要的对象。如果无视这个警告,系统内存不够用时会会继续发送,如果还得不到处理就会强制退出程序。下面看具体的loadView和unloadView时候都会做什么操作。

  1)Load周期

2018年iOS面试题总结(三)_第4张图片

  当需要显示或者访问view属性时,view没有创建的话,VC就会调用loadView方法,在这个时候会创建一个view并将其赋给VC.view属性。紧接着就会调用VC的viewDidLoad方法,这个时候VC.view保证是有值的,可以做进一步的初始化操作,例如添加一些subview。注意:定制VC时,如果覆盖loadView方法,不需要调用[super loadView]方法。

  2)Unload周期

2018年iOS面试题总结(三)_第5张图片

  当app收到内存警告的时候,会调用每一个VC的didRecieveMemoryWarning方法,我们需要做出响应,释放程序中暂时不需要的资源。通常都会重写该方法,重写时候需要调用super的该方法。如果检测到当前VC的view可以被安全释放的话,就会调用viewWillUnload方法,这个我们必须要重视,因为当VC的view消失时候它的subviews可能会被一起释放,我们需要根据具体情况做一些记录,以保证下次能够正确创建,同时不出现内存泄漏。调用viewWillUnload以后,会将VC.view属性设置成nil,然后在调用viewDidUnload方法,这个时候我们可以释放那些强引用的对象。


四、代理delegate与通知Notification、block的使用区别delegate与block一般用于两个对象1对1之间的通信交互、delegate需要定义协议方法,代理对象需要实现协议方法

并且需要建立代理关系才可以实现通信。

block更加简洁,不需要定义繁琐的协议方法,但是如果通信时间比较多的话,建议使用delgate。

Notfication主要用于1对多的通信,而且通信对象之间不需要建立关系,但是使用通知,代码的可读性差。


五、OC-- 判断字符串是否是纯数字

#pragma  mark -- 判断字符串是否是纯数字

+ (BOOL)isPureInt:(NSString *)string{

    NSScanner* scan = [NSScanner scannerWithString:string];

    int val;

    return [scan scanInt:&val] && [scan isAtEnd];

}


六、iOS关于属性关键字

一、@property

  • @property 其实就是在编译阶段由编译器自动帮我们生成ivar成员变量getter方法,setter方法。

使用“自动合成”( auto synthesis)这个过程由编译器在编译阶段执行自动合成,所以编译器里看不到这些“合成方法”(synthesized method)的源代码。除了生成getter、setter方法之外,编译器还要自动向类中添加成员变量(在属性名前面加下划线,以此作为实例变量的名字)反编译相关的代码大致生成:

OBJC_IVAR_$类名$属性名称        // 该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表 示该变量距离存放对象的内存区域的起始地址有多远

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

二 、 readwrite,readonly,assign,retain,copy,nonatomic,atomic,strong,weak属性的作用分别是什么。

关键字 注释
readwrite 此标记说明属性会被当成读写的,这也是默认属性。
readonly 此标记说明属性只可以读,也就是不能设置,可以获取。
assign 不会使引用计数加1,也就是直接赋值。
retain 会使引用计数加1。
copy 建立一个索引计数为1的对象,在赋值时使用传入值的一份拷贝。
nonatomic 非原子性访问,多线程并发访问会提高性能。
atomic 原子性访问。
strong 打开ARC时才会使用,相当于retain。
weak 打开ARC时才会使用,相当于assign,可以把对应的指针变量置为nil。

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

首先明白什么情况使用 weak 关键字?

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

delegate 代理属性,代理属性也可使用
assign自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak
自定义IBOutlet 控件属性一般也使用weak;当然,也可以使用 strong,但是建议使用 weak

weak 和 assign 的不同点:

  • weak 策略在属性所指的对象遭到摧毁时,系统会将 weak 修饰的属性对象的指针指向 nil,在 OC 给 nil 发消息是不会有什么问题的;如果使用 assign 策略在属性所指的对象遭到摧毁时,属性对象指针还指向原来的对象,由于对象已经被销毁,这时候就产生了野指针,如果这时候在给此对象发送消息,很容造成程序奔溃assigin 可以用于修饰非 OC 对象,而 weak 必须用于 OC 对象。

四、使用 atomic 一定是线程安全的吗?

  • 答案很明显。不是,atomic 的本意是指属性的存取方法是线程安全的,并不保证整个对象是线程安全的。

例如:

声明一个 NSMutableArray 的原子属性 stuff,此时 self.stuff 和 self.stuff =othersulf 都是线程安全的。但是,使用[self.stuff objectAtIndex:index]就不是线程安全的,需要用互斥锁来保证线程安全性。

五、@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 = instance.var 时,由于缺 getter 方法同样会导致崩溃。

编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定

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

基本数据: atomic,readwrite,assign
普通的 OC 对象: atomic,readwrite,strong

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

先回答第二个问题:不会!!!不会!!!不会!!!
@synthesize 合成成员变量的规则,有以下几点:

  • 如果指定了成员变量的名称,会生成一个指定的名称的成员变量如果这个成员已经存在了就不再生成了。
  • 如果指定@synthesize foo;就会生成一个名称为 foo 的成员变量,也就是说:会自动生成一个属性同名的成员变量。
@interface XMGPerson:NSObject
@property (nonatomic, assign) int age;
@end
@implementation XMGPerson
// 不加这语句默认生成的成员变量名为_age
// 如果加上这一句就会生成一个跟属性名同名的成员变量

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

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

首先的搞清楚什么情况下不会 autosynthesis(自动合成):

  • 同时重写了 setter 和 getter 时
  • 重写了只读属性的 getter 时
  • 使用了@dynamic 时

在 @protocol 中定义的所有属性在 category 中定义的所有属性重载的属性,当你在子类中重载了父类中的属性,必须使用@synthesize 来手动合成ivar。
应用场景:
当你同时重写了 setter 和 getter 时,系统就不会生成 ivar。这时候有两种选择手动创建ivar

  • 使用@synthesize foo = _foo
  • 关联@property 与 ivar可以用来修改成员变量名,一般不建议这么做,建议使用系统自动生成的成员变量

九、怎么用 copy 关键字?

  • NSString、NSArray、NSDictionary 等等经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary.
    为确保对象中的属性值不会无意间变动,应该在设置新属性值时拷贝一份,保护其封装性block,也经常使用 copy,关键字block。

  • 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.
    在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但是建议写上 copy,因为这样显示告知调用者“编译器会自动对 block 进行了 copy 操作。

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

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

关键字 注释
浅复制(shallow copy) 在浅复制操作时,对于被复制对象的每一层都是指针复制。
深复制(one-level-deep copy) 在深复制操作时,对于被复制对象,至少有一层是深复制。
完全复制(real-deep copy) 在完全复制操作时,对于被复制对象的每一层都是对象复制。

非集合类对象的 copy 与 mutableCopy

[不可变对象 copy] // 浅复制 
[不可变对象 mutableCopy] //深复制
[可变对象 copy] //深复制 
[可变对象 mutableCopy] //深复制

类对象的 copy 与 mutableCopy

[不可变对象 copy] // 浅复制 
[不可变对象 mutableCopy] //单层深复制
[可变对象 copy] //单层深复制
[可变对象 mutableCopy] //单层深复
  • 这里需要注意的是集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。

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

因为 copy 策略拷贝出来的是一个不可变对象,然而却把它当成可变对象使用,很容易造成程序奔溃这里还有一个问题,该属性使用了同步锁,会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明 nonatomic 可以节省这些虽然
很小但是不必要额外开销,在 iOS 开发中应该使用 nonatomic 替代 atomic.

十二、如何让自定义类可以用 copy 修饰符?如何重写带 copy 关键字的 setter?

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopyiog 与NSMutableCopying 协议,不过一般没什么必要,实现 NSCopying 协议就够了

// 实现不可变版本拷贝
- (id)copyWithZone:(NSZone *)zone; // 实现可变版本拷贝
- (id)mutableCopyWithZone:(NSZone *)zone;
// 重写带 copy 关键字的 setter
- (void)setName:(NSString *)name {
    _name = [name copy];
}

七、 RunLoop运行模式相关

Core Foundation框架中关于RunLoop的5个类

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef 基于时间的触发器,基本上说的就是NSTimer
CFRunLoopObserverRef`

在RunLoop中有多个运行模式,但RunLoop只能选择一种模式运行
Mode里面至少要有一个Timer或Source

CFRunLoopModeRef代表RunLoop的运行模式

一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer

每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作CurrentMode

如果需要切换Mode,只能退出Loop ,在重新指定一个Mode进入

这样做主要是为了分割开不同组的Source/Timer/Observer,让其互不影响

系统默认注册了5个Mode常用的有3个

kCFRynLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后不再使用
GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到

代码示例

主线程已经自动创建了Runloop
下述指定Runloop的运行模式为 NSDefaultRunLoopMode
缺点:在UIScrollView拖拽的时候定时器会等到拖拽结束才继续执行
原因:UIScrollView滚动时Timer失效

UIScrollView滚动时,RunLoop模型模式切换到了UITrackingRunLoopMode模式,所以导致Timer失效
UITrackingRunLoopMode:界面追踪运行模式,定时器只有在scrollview拖拽的时候才会运行

@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self timer];
}
- (void)timer {
    // 1. 创建定时器 : 使用该方法创建的定时器是不会添加到RunLoop中的
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    // 2. 添加定时器到runloop中,指定runloop的运行模式为默认
    // 2.1 定时器
    // 2.2 runloop的运行模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
 
}
- (void)run {
    NSLog(@"run -------%@---%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}

@end

下述指定Runloop的运行模式为 UITrackingRunLoopMode
缺点:正常情况下不会执行定时器,拖拽UIScrollview的时候才会执行定时器任务

@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self timer];
}
- (void)timer{
    // 1. 创建定时器 : 使用该方法创建的定时器是不会添加到RunLoop中的
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    // 指定Timer即可在scrollView滑动时出发定时器,但是这样在scrollview不滑动的情况下Timer就不会触发
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
}
- (void)run {
    NSLog(@"run -------%@---%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}

@end

下述指定Runloop的运行模式为 NSRunLoopCommonModes
缺点:完美,解决上述两种运行模式的缺陷

NSRunLoopCommonModes = NSDefaultRunLoopMode && UITrackingRunLoopMode
占用,标签,凡是添加到NSRunLoopCommonModes中的事件都会被同时添加到打上common标签的运行模式下

输出运行模式结果

entries =>
   0 : <CFString 0x104a50210 [0x103b19a40]>{contents = "UITrackingRunLoopMode"}
   2 : <CFString 0x103b3a5e0 [0x103b19a40]>{contents = "kCFRunLoopDefaultMode"}
@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self timer];
}

- (void)timer{
    // 1. 创建定时器 : 使用该方法创建的定时器是不会添加到RunLoop中的
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    //解决方法一:添加两种模式:比较笨的方法
    //解决方法二:
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//    NSLog(@"%@",[NSRunLoop currentRunLoop]);输出可查看被打上标签的模式
}
- (void)run {
    NSLog(@"run -------%@---%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}

@end

子线程中需要手动创建之后开启runloop才能保证线程不死,定时器才能正常工作

 
#import "ViewController.h"

@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 如果这个方法在子线程中调用
    [NSThread detachNewThreadSelector:@selector(timer1) toTarget:self withObject:nil];
}

- (void)timer{
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    // 1. 创建定时器 : 该方法内部会自动添加到runloop中,并且设置运行模式为默认,但在子线程中默认不会创建RunLoop,需手动创建
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    // 子线程创建runloop后默认不开启
    // 开启runloop
    [currentRunLoop run];
}

- (void)run {
    NSLog(@"run -------%@---%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}

@end




你可能感兴趣的:(IOS入门之面试)