我的iOS面试题
从今天开始每天总结几道iOS面试题!!!
Day--1
1.#include与#import的区别、#import与@class的区别
a.#include与#import的区别:#include用于对系统文件的引用,编译器会在系统文件目录下去查找该文件。#include与#import 都会包含引用的类的所有信息,包括实体变量和方法,但是#import处理了重复引用的问题,而include需要自己处理重复引用。
b.#import与@class的区别:1.import会包含这个类的所有信息,包括实体变量和方法,而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑,后面会再告诉你。2.在头文件中, 一般只需要知道被引用的类的名称就可以了。 不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。 而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
2.深、浅拷贝的基本概念以及他们的区别
深拷贝 : 拷贝出来的对象与源对象地址不一致! 这意味着我修改拷贝对象的值对源对象的值没有任何影响.
浅拷贝 : 拷贝出来的对象与源对象地址一致! 这意味着我修改拷贝对象的值会直接影响到源对象.
这里有更详细的介绍,以及copy的详细的用法详解。让我们谢谢原作者,我只是搬运工
3. 什么情况使用 weak 关键字,相比 assign 有什么不同?
一.什么情况使用 weak 关键字
1)在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如:delegate代理属性
2)自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak,自定义IBOutlet控件属性一般也使用weak;当然,也可以使用strong。
二.不同点:
1)weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。
2)使用weak不会让引用计数器+1,weak当对象销毁的时候,指针会被自动设置为nil,而assign不会* assigin 可以用非OC对象,而weak必须用于OC对象。
4. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的
@property 的本质 :@property = ivar + getter + setter;
“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。
ivar、getter、setter 是如何生成并添加到这个类中的?
完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”( autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。
也就是说我们每次在增加一个属性,系统都会在ivar_list中添加一个成员变量的描述,在method_list中增加setter与getter方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出setter与getter方法对应的实现,在setter方法中从偏移量的位置开始赋值,在getter方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.
Day--2
5. @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 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
6. GCD底层的三种队列和简单理解
1.the main queue: 与主线程功能相同。是一个串行队列。
主队列为串行队列,可以又叫 UI队列,关于处理UI界面的请求都在主队列中异步并发执行。
2.global queues: 全局队列是并发队列,并由整个进程共享。global_queue是一个全局并发队列,即加入global_queue的代码块会立即被执行
3.用户队列: 用户队列是串行的
7. 请简述AFNetWorking的实现原理
-
NSURLConnection + NSOperation
-
NSURLConnection 是 Foundation URL 加载系统的基石。一个 NSURLConnection 异步地加载一个NSURLRequest 对象,调用 delegate 的 NSURLResponse / NSHTTPURLResponse 方法,其 NSData 被发送到服务器或从服务器读取;delegate 还可用来处理 NSURLAuthenticationChallenge、重定向响应、或是决定 NSCachedURLResponse 如何存储在共享的 NSURLCache 上。
-
NSOperation 是抽象类,模拟单个计算单元,有状态、优先级、依赖等功能,可以取消。
-
AFNetworking 的第一个重大突破就是将两者结合。AFURLConnectionOperation 作为 NSOperation 的子类,遵循 NSURLConnectionDelegate 的方法,可以从头到尾监视请求的状态,并储存请求、响应、响应数据等中间状态。
8. CALayer和UIView的关系
1.UIView本身不具备显示的功能,是它内部的CALayer在起作用
2.CALayer是图层,和界面展示相关。UIView很多属性和方法和CALayer里的属性和方法是一致的。UIView可以看做是CALayer的管理者,除了负责视图展示之外,还可以处理一些事件,比如手势交互等。但我们对UIView做的一些有关视图显示和动画的操作,本质上还是对CALayer进行的操作。
3.CALayer 和UIView的选择
•通过CALayer,就能做出跟UIImageView一样的界面效果
•既然CALayer和UIView都能实现相同的显示效果,那究竟该选择谁好呢?
其实,对比CALayer,UIView多了一个事件处理的功能。也就是说,CALayer不能处理用户的触摸事件,而UIView可以;所以,
如果显示出来的东西需要跟用户进行交互的话,用UIView;
如果不需要跟用户进行交互,用UIView或者CALayer都可以
Day--3
9. iOS中有哪些数据持久化方式,请简要介绍它们的应用场景
iOS下数据持久化常用的几种方式:
- NSUserDefaults
- plist(属性列表)
- NSKeyedArchiver(对象归档)
- iOS的嵌入式关系数据库SQLite3
- 苹果公司提供的持久化工具 Core Data
上面几种方式,有一个共同的要素,就是应用的/Documents文件夹。每个应用都有自己的/Documents文件夹,且仅能读写各自/Documents文件中的内容。
10. 事件是如何传递的,简述事件响应链机制
- 响应者链条:是由多个响应者对象连接起来的链条
- 作用:清楚的看见每个响应者之间的联系,并且可以让一个事件由多个对象处理。
- 响应者对象:有响应和处理事件能力的对象。
事件的产生和传递过程:
1.发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的队列事件中
2.UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)
3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
4.找到合适的视图控件后,就会调用视图控件的touches方法来作事件的具体处理:touchesBegin… touchesMoved…touchesEnded等
5.这些touches方法默认的做法是将事件顺着响应者链条向上传递,将事件叫个上一个相应者进行处理
响应者链条其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条称之为响应者链条
一般默认做法是控件将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理。那么如何判断当前响应者的上一个响应者是谁呢?有以下两个规则:
1.判断当前是否是控制器的View,如果是控制器的View,上一个响应者就是控制器
2.如果不是控制器的View,上一个响应者就是父控件
UIApplication–>UIWindow–>递归找到最合适处理的控件–>控件调用touches方法–>判断是 否实现touches方法–>没有实现默认会将事件传递给上一个响应者–>找到上一个响应者–>找不到方法作废
11. 理解单例设计模式
一、单例模式的作用
它可以保证某个类在运行过程中,只有一个实例,也就是对象实例只占用一份系统内存资源。
二、单例的要点
该类有且只有一个实例
该类必须能自行创建这个实例
该类必须能够向整个系统提供这个实例
三、单例的优缺点
-
优点:
-
提供了唯一实例的受访对象
-
因为在系统中只存在一个实例,在频繁访问和调用时,节省了系统创建和销毁资源的开销,提高系统性能。
-
因为单例化的类,控制了实例化的过程,所以能更灵活修改实例化的过程。
-
缺点:
-
单例模式没有抽象层,不容易扩展。
-
单例模式往往职责过重,一定程度上违背了“单一职责原则”。
#import <Foundation/Foundation.h>
@interface Class : NSObject
+ (instancetype)defaultManager;
@end
//重写init
- (instancetype)init {
@throw [NSException exceptionWithName:@"不允许调用Class" reason:@"因为Class是一个单例,只能通过default方法获获取对象" userInfo:nil];
}
//私有方法创建
- (instancetype)initPrivate {
if (self = [super init]) {
//干一些事情
}
return self;
}
+ (instancetype)defaultManager{
static DataBaseManager * instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
if (!instance) {
instance = [[Class alloc]initPrivate];
}
});
return instance;
}
Day--4
12. 下面的代码输出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
2016-05-13 17:52:51.694 Test[12816:262134] Son
2016-05-13 17:52:51.696 Test[12816:262134] Son
原因:调用父类初始化的时候,是不看指针看对象的,因此谁调用super谁就是super的拥有者
13. objc中的类方法和实例方法有什么本质区别和联系?
类方法
- 类方法属于类对象
- 类方法只能通过类对象调用
- 类方法中的self是类对象
- 类方法可以调用其他类方法
- 类方法中不能访问成员变量
- 类方法不能直接调用对象方法
实例方法
- 实例方法是属于实例对象的
- 实例方法只能呢通过实例对象调用
- 实例方法中的self是实例对象
- 实例方法中可以访问成员变量
- 实例方法中直接调用实例方法
- 实例方法中也可以调用类方法(通过类名)
14. SDWebImage的原理
1.入口
setImageWithURL:placeholderImage:options: 会先把placeholderImage展示,然后 SDWebImageManager根据URL开始处理图片.
2.进入
SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给SDImageCache从缓存查找图片是否已经下载 :
通过: queryDiskCacheForKey:delegate:userInfo:.
3.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存SDWebImageDelegate回调imageCache:didFindImage:ForKey:userInfo: 到SDWebImageManager
4.SDWebImageManagerDelegate回调
webImageManager:didFinishWithImage:
到UIImageView+WebCache等前端展示图片
5.如果内存缓存中没有,生成NSInvocationOperation添加到队列开始从硬盘查找图片是否已经缓存。
6.根据 URLKey 在硬盘缓存目录下尝试读取图片文件.这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调.
7.如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中,如果内存空间过小会先清空内存缓存.
SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo: 进而回调展示图片.
8.如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调imageCache:didNotFindImageForKey:userInfo:。
9.共享或重新生成一个下载器 SDWebImageDownloder开始下载图片
10.图片下载由 NSURLConnection来做,实现相关的Delegate来判断图片下载中,下载完成和下载失败.
11.connection:didReceiveData:
中利用 ImageIO 做了按图片下载进度加载效果。
12.connectionDidFinishLoading:
数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
13.图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
14.在主线程notifyDelegateOnMainThreadWithInfo:
宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:
回调给 SDWebImageDownloader。
15.imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
16.通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
17.将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
18.SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
19.SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache
,方便使用。
20.SDWebImagePrefetcher 可以预先下载图片,方便后续使用
15. BAD_ACCESS在什么情况下出现?如何调试BAD_ACCESS错误?
什么是 EXC_BAD_ACCESS?
不管什么时候当你遇到EXC_BAD_ACCESS这个错误,那就意味着你向一个已经释放的对象发送消息。访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。
EXC_BAD_ACCESS的本质
技术层面的解释有些复杂。在C和Objective-C中,你一直在处理指针。指针无非是存储另一个变量的内存地址的变量。当您向一个对象发送消息时,指向该对象的指针将会被引用。这意味着,你获取了指针所指的内存地址,并访问该存储区域的值。
当该存储器区域不再映射到您的应用时,或者换句话说,该内存区域在你认为使用的时候却没有使用,该内存区域是无法访问的。 这时内核会抛出一个异常( EXC ),表明你的应用程序不能访问该存储器区域(BAD ACCESS) 。
总之,当你碰到EXC_BAD_ACCESS ,这意味着你试图发送消息到的内存块,但内存块无法执行该消息。但是,在某些情况下, EXC_BAD_ACCESS是由被损坏的指针引起的。每当你的应用程序尝试引用损坏的指针,一个异常就会被内核抛出。
调试EXC_BAD_ACCESS
1.重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object
2.通过 Zombie
3.设置全局断点快速定位问题代码所在行
4.Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer
16.什么是block,简述block实现原理
-
BLOCK是什么?
-
苹果推荐的类型,效率高,在运行中保存代码。用来封装和保存代码,有点像函数,BLOCK可以在任何时候执行。
-
BOLCK和函数的相似性:
-
可以保存代码
-
有返回值
-
有形参
-
调用方式一样。
-
block类型:void(^)()
-
Block存储
-
Block默认存储在栈中,访问了外界的对象,不会对对象retain;如果对block进行一次copy,block就会存储在堆中,访问了外界的对象,会对对象进行retain操作。
-
Block的定义
注意: -
如果block中没有使用外部变量,默认就是全局
-
如果block中使用了外部变量,就是堆
-
Block可以访问局部变量,但是不能修改。
-
ARC中,默认局部变量是强引用
block循环引用问题
block尽量少使用self
block尽量少使用下划线(_)直接访问成员属性
要避免强引用到self的话,用__weak把self重新引用一下就行
Block的实现
我们所需要知道的是 block 就是一个对象,在它所在的内存中,保存着block自身的实现函数,可在调用block时用block自身的代码替代,同时保持着一个Block描述,标志着block的内存size与持有对象的指针。当声明与实现一个Block时,创建的闭包会捕获在它的域中的任何涉及的变量,通过在内存中持有他们,能够在block的实现中对其进行访问。在默认情况下,任何在block的域中被捕获的变量都不能被修改,除非这个变量已被给予了__block的标志。当block捕获了一个对象时,它会对其进行retain操作,并在block代码执行完毕完release对象,这样才能保证在block执行过程中,对象不会因引用计数为0而被释放掉。我们需要理解的是,block本身就是一个对象,它对其他对象的引用与一般的对象引用类似,都是需要对引用对象进行retain与release
17. 写一个宏MIN,这个宏输入两个参数并返回较小的一个
#define MIN(a,b) ((a)>(b)?(b):(a))
18.frame和bounds有什么不同?
frame是在父视图坐标系下的位置和大小。bounds是自身坐标系下的位置和大小。
frame以父控件的左上角为坐标原点,bounds以控件本身的左上角为坐标原点
frame:以父控件左上角为原点。bounds:以自己的左上角为原点,bounds x,y永远为0(这是错误的认识)
frame和bounds都是用来描述一块区域frame:描述可视范围,也就是说从左上角的0,0点开始延伸,它延伸的区域就是我们的可视范围
bounds:描述可视范围在内容的区域,所有的子控件都有内容,它就类似于空气,是看不到的,正常情况下,内容是无限大的,所有的子控件其实都是放在内容上的,在可视范围内的内容我们才能 看见,所以正常情况下,内容的左上角(bounds)与可视范围(frame)的左上角是重合的,当修 改bounds的x与y都会导致子控件跟着移动.需要注意的是,可视范围(frame)是永远不会变的,它是相对父控件的.
所有的子控件都是相对于内容bounds:修改内容原点
相对性:可视范围相对于父控件位置永远不变 可视范围相对于内容,位置改变
Day-5
19.程序输出的结果
int main(){
int a[5] = {1, 2, 3, 4, 5};
int * ptr = (int *)(&a + 1);
printf("%d\n", *(a + 1));
printf("%d", *(ptr - 1));
return 0;
}
2
5Program ended with exit code: 0
1.*(a+1)=a[0+1]=a[1]=2
2.&a表示对数组取地址,&a+1表示a[5]后面一个地址,(ptr-1)表示对当前数组元素地址向前移动一位,并取值,故等于a[4]=5
20.谈谈RunLoop的简单理解
一、RunLoop从字面意思上看:
- 运行循环
- 跑圈
RunLoop的基本作用:
- 保持程序的持续运行
- 处理APP中各种事件(比如:触摸事件,定时器事件,Selector事件等)
- 能节省CPU资源,提高程序的性能:该做事的时候就唤醒,没有事情就睡眠
假如没有了RunLoop:
- 大家都知道程序的入口是main函数:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
-
如果没有RunLoop,程序就会在main函数执行完毕的时候退出,也正是因为有了RunLoop,导致main函数没有马上退出,保证了程序持续运行;
-
其实是在UIApplicationMain函数内部启动了一个RunLoop;
-
这个默认启动的RunLoop是跟主线程相关联的
-
RunLoop内部其实是有一个do-while循环(可以从RunLoop源码中找到),暂且可以理解为下面的代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL running = YES;
do {
/**
* 在这里执行各种任务,处理事件
* 这个过程是持续运行
*/
} while (running);
}
return 0;
}
二、RunLoop对象
- iOS中有2套API来访问和使用
- RunLoopFoundation框架中的NSRunLoop;
- Core Foundation中的CFRunLoop;
- NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
三、RunLoop与线程
-
每条线程都有唯一的一个与之对应的RunLoop对象
-
主线程中的RunLoop由系统自动创建,子线程中RunLoop可以通过手动创建
-
RunLoop在线程结束的时候会被销毁
-
获取RunLoop对象
-
Foundation框架中
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
- Core Foundation框架中
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
21.找错误
试题1:
char * GetMemory(void){
char p[] = "Hello World";
return p;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
char * str = NULL;
str = GetMemory();
printf("%s\n",str);
}
return 0;
}
错误为:
char p[] = "Hello World";
return p;//不能返回栈地址,因为栈空间自动释放,应该返回栈空间的数据
正确应该为:
char *p = "Hello World";
试题2:
void GetMemory(char **p, int num){
*p = (char *)malloc(num);
}
void Test(void){
char * str = NULL;
GetMemory(&str, 100);
strcpy(str, "Hello");
printf(str);
}
// 堆空间需要手动释放,申请了堆内存时也要判断是否申请成功
char * str = NULL;
GetMemory(&str, 100);
if(str) {
strcpy(str, "Hello");
printf("%s",str);
free(str);
str = NULL;
}
Day-6
22. 写出下列两个属性的Setter方法
@property (nonatomic, retain) NSString * name;
@property (nonatomic, copy) NSString * name;
- (void) setName:(NSString *) name {
if( _name != name) { [
_name release];
_name = [name retain];
}
}
- (void) setName:(NSString *) name {
_name = [name copy];
}
retain修饰的属性setter方法的实现步骤:
1:判断新值与旧值是否相等,如果不等执行以下操作
2:将旧值执行一次release操作(旧值release)
3:再将新值执行一次retain操作再赋给旧值(新值retain再赋值)
copy修饰的属性:如果要保证返回的是一个不可变的版本就要将新值执行一次copy操作
23. 类别和继承什么区别
区别:
- 1.类别是对方法的扩展,不能添加成员变量。继承可以在原来父类的成员变量的基础上,添加新的成员变量
- 2.类别只能添加新的方法,不能修改和删除原来的方法。继承可以增加、修改和删除方法。
- 3.类别不提倡对原有的方法进行重载。继承可以通过使用super对原来方法进行重载。
- 4.类别可以被继承,如果一个父类中定义了类别,那么其子类中也会继承此类别。
24. 线程与进程的区别和联系?
-
进程: 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。
-
线程: 通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。
-
线程与进程的区别:
-
a.地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
-
b.通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
-
c.调度和切换:线程上下文切换比进程上下文切换要快得多。
-
d.在多线程OS中,进程不是一个可执行的实体