我的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 
@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中,进程不是一个可执行的实体

你可能感兴趣的:(我的iOS面试题)