续写iOS 面试题及答案20道(一)
21.OC中什么是ARC?Objective-C的内存管理机制是什么?
ARC即Automatic Refrence Counting,它是Objective-C的内存管理机制。就是在代码中自动加入了retain/release,原先需要手动添加用于管理内存的引用计数的代码可以由编译器自动完成。
ARC的使用是为了解决对象retain和release匹配的问题。以前因手动管理而造成内存泄漏或者重复释放的问题将不复存在。
以前需要手动去释放内存,retain了以后必须release释放内存。alloc new retain都需要release,这种操作为MRC(Manual Reference Counting).
ARC与Garbage Collection的区别在于, Garbage Collection在运行时管理内存,可以解决retain cycle,而ARC在编译时管理内存。
22.OC中什么情况下会出现循环引用?
循环引用就是两个或以上对象互相强引用,当这些对象释放的时候,导致互相释放不掉,这种情况就是循环引用,这也是内存泄漏的一种情况。 在OC中解决方法就是在一个对象的强引用Strong(强引用) 改成weak(弱引用)。
循环引用其实在Xcode工具中是可以检测到的。比如:
Xcode->Debug Memory Graph检查,就是在一个对象中打一个断点,点击输出控制器上面的三个点用线串起来的按钮来查看类中的引用图,根据该不该释放的问题来查找是否循环引用了。
在Xcode左上角,感叹号按钮由一个runtime的选项,一般如果有内存泄漏会直接在里面暴漏出来
Xcode->Product->analyze 来发现内存泄漏问题。
23.说明比较OC中关键字:strong、weak、assign和Copy
参数 | 详解 |
---|---|
strong | 表示指向并拥有该对象,其修饰的对象引用计数+1,该对象只要引用计数不为0,就不会被销毁。当然,强行将其设为nil也可以销毁它。不需要手动释放内存 |
weak | 类似assign,一般情况下修饰对象,而assign一般修饰值类型,比assign多了一点就是对象被销毁的时候,weak引用会自动设置为nil,防止野指针的发生,修饰的对象引用计数不会改变。在一个强引用引用该对象时,如果这个强饮用被释放掉了,那么同样,这个对象也会被释放。 |
assign | 直接赋值,默认值,主要是修饰一些基本数据类型,值类型,比如说NSInteger、CGFloat,这些数值主要存在于栈中。 |
copy | copy与strong类似,不同之处是,strong赋值的话,会是指针的复制,并且内容是同一块儿地址,而copy不同,每次赋值或复制的时候都会复制一份新的对象,指针指向不同的地址,copy一般用在修饰有对应可变类型的不可变对象上,如NSString,NSArray和NSDictionary。 |
在OC中基本的数据类型的属性变量的关键字是(atomic、readwrite、assign);普通属性的默认关键字是(atomic、readwrite、Strong)
24.说明比较关键字:atomic和nonatomic
参数 | 详情 |
---|---|
atomic | 作用是系统生成的setter/getter方法会保证get、set操作的完整性,不受其他线程影响,如果A线程的getter方法运行到一半的时候,线程B调用了setter方法,那么线程A还是能够得到一个完好无损的对象,所以速度比较慢,atomic比nonatomic安全,但是并不是绝对的安全,比方说多个线程同时调用set和get方法,会导致获得的对象不一样,如果想要线程绝对的安全,就要用@synchronized 代码块。 |
nonatomic | 这个修饰的对象并不会保证setter/getter方法的完整性,但是速度比atomic快,当多线程访问的时候有可能得到的是未初始化的对象,当然也不是线程安全的。 |
25.atomic是百分之百线程安全的吗?
atomic不是百分之百线程安全的,只是保证多线程的情况下,getter、setter方法调用的完整性,在多线程访问的情况下,能够有效完整的调用完setter、getter方法。
但是在多线程并发的情况下,得到的结果不能够保证是统一的,比如说线程A在调用属性M的setter方法并且进行到了一半的时候,线程B调用了getter方法想要获取到M的内容,那么线程B拿到的是线程A赋值之前的内容,如果需求是要获取到线程A赋完值以后的内容,那么这就是线程不安全的实例。
26.Runloop和线程的关系?
runloop是每一个线程一直运行的一个对象,它主要用来负责响应需要处理的各种事件和消息。每一个线程都有且仅有一个runloop与其对应。没有线程,就没有runloop。
在所有的线程中,只有主线程的runloop是默认启动的。main函数会设置一个NSRunLoop对象。而其他线程的runloop默认是没有启动的。可以通过[NSRunLoop currentRunLoop]获取当前的runloop,调用run启动。
27.说明并比较关键字:__weak和__block。
-
__weak与weak基本上是一样的,前者修饰变量(variable),后者修饰属性(perproty)。
__weak主要用于防止在block中的循环引用,
__block
也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的。就是可以被重新赋值。__block
用于修饰某些block内部将要修改的外部的变量。
__weak
和__block
的使用场景几乎与block息息相关。而所谓block,就是Objective-C对于闭包的实现。闭包就是没有名字的函数,或者可以理解为指向函数的指针。
使用注意,如果在block中去修改外部的变量(除静态、全局),需要在block外部声明
__weak
重新修饰,然后在block中在用__block
在去修饰__weak
修饰的变量。__weak
修饰是为了告诉编译器,重新修饰的变量是一个不使用引用计数的弱引用,
而在block中使用__block
是为了告诉编译器,我需要在block使用这个变量不管是外面的变量是否被释放掉,我的block中的这个变量要在整个block函数中应该全程都是被保留的。如:
__weak typeof(self)weakself = self;
self.Copyblock = ^{
__block typeof(weakself)blockself = weakself;
[blockself.array addObject:[UserModel new]];
};
self.Copyblock();
28.什么是block?它和代理的区别是什么?
block是一个回调的方式,本质上也是一个OC对象,一个函数方法对象。
block分为三种: 静态block、栈block、堆block。
block和代理的区别:
block
集中代码块,而delegate
则是分散代码块。block
可读性更好、使用简单,而delegate
则需要声明协议、声明代理属性、遵守协议、实现协议的方法。而block
只需要声明属性、实现就可以。block
是一个轻量级的回调,可以直接上下文,因为代码内联运行效率更高,block就是一个对象,实现了匿名函数的功能。所以把block
当作一个成员变量、属性、参数使用,使用起来非常的灵活。就想AFN的请求数据,GCD的多线程,而delegate
就没有这么灵活。block
适用于轻便、简单的回掉,比如一些简单回调网络数据,相对来说借口比较少的回调等, 如果是一些复杂的函数回掉,并且接口过多,那么使用block就会显得不利于维护、耦合性太大,而代理则完全相反,接口回调少的时候使用起来比较麻烦,但是一旦接口回调多的时候,会显得更加清晰明朗。block
的运行成本高,block
出栈的时候需要将数据从栈内存拷贝到堆内存,如是对象是加计数,那么使用完或者block
置为nil后才能被消除;delegate
只是保存了一个对象指针,直接回调,并没有额外消耗。block
容易产生循环引用,当然(delegate
需要weak
修饰,不然也是有循环引用),因为为了block
不被系统回收,所以我们都用copy关键字修饰,实行强引用,block
对捕获的变量也都是强引用,所以就会造成循环引用。
如果使用? 1)简单的来讲优先使用block 2) 如果回调的状态多,超过2个以上就是用代理 3)如果回调过于频繁,还是使用代理。 按需使用
29.属性中copy和Strong的面试问题?
@property(nonatomic,strong)NSString * title;
@property(nonatomic,assign)int num;
第一个属性:title:
- NSString这个类型有可变不可变的类型,NSMutableString这个类型,并且是NSString的子类。那么使用strong的话容易被外界所修改。比方说:
类A中定义了title属性(nonatomic,strong),类A中有个变量(NSMutableString)的变量Mutabletitle
//mutableString这个参数是类A的一个变量类型是NSMutableString
NSMutableString* mutableString = [[NSMutableString alloc]initWithString:@"mutablestring"];
UserModel *user = [[UserModel alloc]init];
//首先将字符串赋值
user.title = @"title";
user.title = mutableString; //这时候title==mutableString==@"mutablestring"
//修改mutableString的值
[mutableString appendString:@"111"];
//得到的结果是user.title == mutableString == @"mutablestring111"
NSLog(@"title=%@--%p mutableString=%@--%p",user.title,user.title,mutableString,mutableString);
如果在开发中,我们只是修改mutableString的值而并不想修改user.title的值,那么这样做就是错误的。
分析:由于strong修饰的属性是强引用,user.title = mutableString 这个意思就是直接指针复制,这两个变量所拥有的内存地址是一样的,当一个变量修改另外的变量也会跟着修改。
第二个属性num,这个属性不应该用int来修饰,int是代表32位的整型数据,而在iOS中有个整型NSinteger这个属性,表示在32位系统中是32位的整型,在64位系统中会是64位整型数据, NSinteger更加准确。当然还有 CGFloat代替float NSUInteger代替unsigned。
改正代码:
@property(nonatomic,copy)NSString * title;
@property(nonatomic,assign)NSInteger num;
30.阅读下述代码,看这个代码是否能够更加优化,在架构解耦中应该怎么做。
typedef enum{
normal = 0;
VIP = 1;
}UserPowerType;
@interface UserModel:NSObject
@property(nonatomic,copy)NSString *name;
@property(nonatomic,strong)UIImage *UserIcon;
@property(nonatomic,assign)UserPowerType Type;
@end
1.首先enum的定义,apple官方推荐使用NS_ENUM来定义枚举类型,同时在每个枚举的前面需加上枚举的名称,也方便swift和OC混编。
2.UIImage这个参数不应该出现在UserModel中,因为UIImage这个明显是UIKit中的组建,是放到View 中的内容,无论是MVC还是MVVM或者是VIPER,Model都应该跟View划清界限。
优化后的代码:
typedef NS_ENUM(NSUInteger, UserPowerType) {
UserPowerTypeNormal,
UserPowerTypeVIP,
};
@interface UserModel:NSObject
@property(nonatomic,copy)NSString *name;
@property(nonatomic,strong)NSData *UserIconData;
@property(nonatomic,assign)UserPowerType Type;
@end
31.阅读下述代码从内存管理中分析其真正的意义。
NSString *str1 =@"helloworld";
NSString *str2 =@"helloworld";
if(str1 == str2){
NSLog(@"equal");
}else{
NSLog(@"not equal");
}
NSLog(@"str1=%p,str2=%p",str1,str2);
- ==符号并不是判断这两个值是否相等,而是判断这两个指针是否指向同一个对象,如果要判断两个对象的值是否相同应该用方法
isEqualToString
这个方法。 - 那么上述代码为什么打印出来的结果是equal呐?因为在iOS的编译器环境中,会优化内存分配,当两个指针指向两个值一样的NSString时,两者会同时指向一个地址。所以代码打印的结果是equal。
32.阅读下述有关多线程的代码,并说明有什么问题?
UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
label.text = @"123456";
label.textColor = [UIColor redColor];
[self.view addSubview:label];
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc]init];
[backgroundQueue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:4];
label.text = @"amaze";
}];
这段代码最大的问题,只要跟刷新界面有关系的内容必须在主线程中调用。
正确的代码修正:
UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
label.text = @"123456";
label.textColor = [UIColor redColor];
[self.view addSubview:label];
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc]init];
[backgroundQueue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:4];
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
label.text = @"amaze";
}];
}];
33.以ScheduledTimerWithTimeInterval的方式触发的timer,在滑动页面上的列表时,timer会暂停,为什么?该如何解决?
造成此问题的原因在于滑动页面上的列表时,当前线程的runloop切换了mode的模式,导致timer暂停。
runloop中的mode主要用来指定事件在runloop中的优先级,具体有以下几种:
参数 | 详情 |
---|---|
default(NSDefaultRunLoopMode) | 默认设置,一般情况下使用 |
Connection(NSConnectionReplyMode) | 用于处理NSConnection相关事件,开发者一般用不到 |
Modal(NSModalPanelRunLoopMode) | 用于处理model panels |
Event Tracking(NSEventTrackingRunLoopMode) | 用于处理拖拽和用户交互的模式 |
Common (NSRunLoopConmmonModes) | 模式合集,默认包括Default、modal和EventTracking三大模式,可以处理几乎所有的事件 |
根据上面的参数介绍在看这道问题,在滑动列表时,runloop的mode由原来的Default模式切换到EventTracking模式,timer原来运作在Default模式中,模式切换了以后自然而然就停止工作了。
解决方法:方法一是将timer加入NSRunloopCommonModes中。方法二是将timer放到另一个线程中,然后开启另一个线程的runloop,这样可以保证与主线程互不干扰,而现在主线程正在处理页面滑动。
代码示例:
方法一:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
方法二:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(Repeat) userInfo:nil repeats: YES];
[[NSRunLoop currentRunLoop] run];
})
34.Swift VS Objective-C
这么多年的Swift完善,Apple的两种语言已经从一开始的分道扬镳,到现在已经有了天壤之别。
细数两种语言大的方面不一样的地方:
- Swift更注重安全性,比如说Swift的可选、不可选类型,? ! 而OC当中并没有这些规定,如果非要说共同点就是iOS9.0之后出现的关键字 _NonNull(不可以为nil) 、_nullable(可以为nil)就算是不遵守这个也就是给个警告,主要是为了混编开发。
- Swfit有面向对象编程、面向协议编程、函数式编程,而OC几乎只有面向对象的编程。
- Swift更加注重值类型的数据结构,而OC遵循了C语言的一套,注重指针和索引。
- Swfit是静态语言,而OC是动态语言。
35.为什么将String、Array和Dictionary设计成值类型?
在Swift中这些设置成值类型,而OC中则是引用类型。具体的分析如下:
- 值类型相比较引用类型来说最大的优势是可以高效的使用内存,值类型在栈上进行操作,而引用类型在堆上面进行操作,栈的操作知识指针的上下移动,而堆上的操作就需要合并、移位、重新链接。Swift大幅度减少了堆上的内存分配和回收次数,同时copy-on-write又将值传递和复制的开销降到了最低。
- 同时这样设计也是为了线程更加安全,通过Swift的let设置,使得这些数据达到了真正意义上的不变,也从根本上解决了多线程中内访问和操作顺序问题。
36.如何用Swift协议中的部分方法设计成可选(optional)
方法一: 用OC的方式实现,在方法前面添加 @objc optional
@objc protocol Myprotocol{
func requiredFunc()
@objc optional func optionalFunc()
}
方法二:用扩展来实现可选方法,扩展协议
protocol Myprotocol{
func requiredFunc()
func optionalFunc()
}
extebsion Myprotocol {
func optionalFunc() {
print("to do")
}
}
Class Myclass: Myprotocol {
func requiredFunc() {
print("only need to implement the required")
}
}
37.判断下面所述代码有什么问题?
protocol ProtocolDelegate {
func doSomething()
}
class MyClass {
weak var delegate:ProtocolDelegate?
}
weak var delegate:ProtocolDelegate? //报错弱引用只能修饰引用类型,而在Swift中协议是可以被值类型实现,enum、struct等,所以需要区别出来。
weak关键词用于ARC环境下,为引用类型提供引用计数这样的内存管理,它是不能够修饰值类型的。
方法一:
直接在声明的协议前面加上 @objc这样就是告诉编译器这个协议还是OC中的协议,修饰对像一个样就没毛病
@objc protocol ProtocolDelegate { //方法一
func doSomething()
}
方法二:
在声明的协议后面添加关键字 class 告诉编译器只能由class来实现,那么就忽略掉了值类型。
protocol ProtocolDelegate: class { //方法二
func doSomething()
}
38.在Swift和Objective-C的混编中如何在Swift文件中调用OC方法,在OC文件中调用Swift方法
Swift调用OC
Swift调用OC需要在ProjectName-Bridging-Header.h中添加OC的头文件,这样就可以调用了,一般Swift项目中添加OC文件就有提示自动添加侨接文件。
在OC中调用Swift代码,则需要导入Swift生成的头文件ProjectName-Swift.h
当然还有就是在OC中调用Swift代码需要在Swift方法属性前面添加@objc来声明。
39.比较Swift和OC中初始化方法(init)的不同
在OC中初始化方法无法完全保证属性变量完成初始化,也没有对起最警告处理,所以在初始化类的时候,有的属性是空值(nil),初始化方法和普通方法也并没有实际差别,可以多次调用。
在Swift中初始化方法必须保证非optional的成员变量得到初始化,同时新增了
convenience
和required
两个修饰初始化方法的关键词,convenience
是对初始化方法的补充,但是在内部必须调用同一个类中的designated初始化方法来完成,required
是强制子类重写父类的初始化方法。
40.比较swift跟OC的protocol的异同
相同点:
都是作为代理,类似接口
不同点:
Swift可以对接口进行抽象,配合扩展、泛型、关联类型等可以实现面向协议编程。同时protocol还可以用于值类型,比如enum、struct等