iOS最新面试题总结

作者: wuyukobe | 来源:发表于2018-11-26 18:41 被阅读93次

    最近由于换工作的原因,需要参加面试,然后就到处收集了一些当下的面试题供自己复习使用。下面就分享出来,希望能够帮到你们~

    1、ViewController 的loadView、viewDidLoad、viewDidUnload分别是什么时候调用的? 作用是什么?

    (1) loadView 当第一次使用控制器的view时,会调用loadView方法创建view,一般在这里自定义view 。
    (2) viewDidLoad 当控制器的view创建完毕时会调用,也就是在loadView后调用,一般在这里添加子控件、初始化数据。
    (3) viewDidUnload 当控制器的view因为内存警告被销毁时调用,一般在这里回收跟界面相关的资源(界面都会销毁了,跟界面相关的资源肯定不要了)。

    2、ViewController的didReceiveMemoryWarning是在什么时候调用的?默认的操作是什么?

    当应用程序接收到系统的内容警告时,就有可能调用控制器的didReceiveMemoryWarning方法。
    它的默认做法是:当控制器的view不在窗口上显示时,就会直接销毁,并且调用viewDidUnload方法。

    3、控制器 View的生命周期及相关函数是什么?你在开发中是如何用的?

    (1) 首先判断控制器是否有视图,如果没有就调用loadView方法创建:通过storyboard或者代码;
    (2) 随后调用viewDidLoad,可以进行下一步的初始化操作;(只会被调用一次)
    (3) 在视图显示之前调用viewWillAppear;(该函数可以多次调用)
    (4) 视图已经出现,调用viewDidAppear;
    (5) 在视图消失之前调用viewWillDisappear;(该函数可以多次调用)
    (6) 在布局变化前后,调用viewWill/DidLayoutSubviews处理相关信息。

    4、AppDelegate代理方法调用顺序:
    //加载完main函数之后,调用此方法,进入程序载入
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        NSLog(@"程序载入后");
        return YES;
    }
    //程序将要失去Active状态时调用,比如有电话进来或者按下Home键,之后程序进入后台状态
    - (void)applicationWillResignActive:(UIApplication *)application {
        NSLog(@"将要进入非活动状态");
    }
    //点击home键进入后台时调用此方法
    - (void)applicationDidEnterBackground:(UIApplication *)application {
        NSLog(@"进入后台");
    }
    //唤醒进入后台的app时调用
    - (void)applicationWillEnterForeground:(UIApplication *)application {
        NSLog(@"从后台进入前台");
    }
    //app加载完主视图之后进入活动状态时或者从后台唤醒之后调用
    - (void)applicationDidBecomeActive:(UIApplication *)application {
        NSLog(@"进入活动状态");
    }
    //程序被杀死时调用
    - (void)applicationWillTerminate:(UIApplication *)application {
        NSLog(@"程序将要退出");
    }
    
    5、定义属性时,什么情况使用copy、assign、retain?

    (1) copy:NSString、Block等类型
    (2) assign:非OC对象类型,基本数据类型
    (3) retain:OC对象类型

    6、什么情况使用 weak 关键字?

    (1) 在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如 delegate 代理属性。
    (2) 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak。
    (3) IBOutlet 控件属性一般也使用 weak。当然,也可以使用 strong,但是建议使用 weak。

    7、weak 和 assign 的不同点:

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

    8、下面两种方式都是弱引用代理对象,为什么第一种在代理对象被释放后不会导致崩溃,而第二种会导致崩溃?

    1.@property (nonatomic, weak) id delegate;
    2.@property (nonatomic, assign) id delegate;
    原因:weak和assign是一种“非拥有关系”的指针,通过这两种修饰符修饰的指针变量,都不会改变被引用对象的引用计数。但是在一个对象被释放后,weak会自动将指针指向nil,而assign则不会。在iOS中,向nil发送消息时不会导致崩溃的,所以assign就会导致野指针的错误unrecognized selector sent to instance。所以我们如果修饰代理属性,还是用weak修饰吧,比较安全。

    9、nonatomic 和 atomic的区别:

    (1) nonatomic,非原子性,多线程访问修改不加锁。
    (2) atomic,原子性,多线程访问加锁。
    iOS 推荐我们使用 nonatomic,移动端的开发没有复杂的多线程场景,不加锁解锁可以提高效率。
    系统的可变对象,NSMutableArray,NSMutabelString 都是线程不安全的,多线程修改,需要加锁。

    10、@property属性关键字详解:

    atomic: 默认关键字,也就是说如果什么都不写,默认就是这个。表示该属性是线程同步的。一般用不到,会影响性能。
    nonatomic: 非线程同步,基本都是用这个。
    readwrite: 默认关键字,表示可读可写。
    readonly: 只能读(get),不能写(set),当你希望属性不能被外界直接修改,但是可以访问时使用。
    writeonly: 只能写(set),不能读(get)。一般用不到。
    assign: 默认关键字。非对象类型一般使用此关键字。
    retain: 对象的引用计数+1。ARC下已经不再使用此关键字,用strong代替。
    copy: 拷贝一个新的对象,新对象的引用计数+1,原对象不变。
    strong: 对象的引用计数+1。作用于OC对象,能够维持对象的生命。

    11、深拷贝和浅拷贝区别:

    (1) 深拷贝就是把内容拷贝一份产生一份新的对象,新对象计数器为1,源对象计数器不变。
    (2) 浅拷贝是指针拷贝,把地址给你,你和我指向同一个对象,源对象计数器加一,源对象和副本的计数器相同。
    (3) 我们知道在OC中的拷贝函数有copy和mutablecopy,只要你调用了copy不管是NSString,NSDictionary还是NSArray还是NSMutableString还是NSMutableDictionary,还是NSMutableArray都是copy出来是不可变的副本。
    (4) 可变对象的copy和mutableCopy方法都是深拷贝;不可变对象的copy方法是浅拷贝,mutableCopy的方法是深拷贝。

    12、如何令自己所写的对象具有拷贝功能?

    (1) 如果想让自己的类具备copy方法,并返回不可边类型,必须遵循NSCopying协议,并且实现 - (id)copyWithZone:(NSZone *)zone
    (2) 如果让自己的类具备mutableCopy方法,并且返回可变类型,必须遵守NSMutableCopying,并实现 - (id)mutableCopyWithZone:(nullable NSZone *)zone
    注意:再此说的copy对应不可边类型和mutableCopy对应不可边类型方法,都是遵从系统规则而已。如果你想实现自己的规则,也是可以的。

    13、Block为什么使用copy修饰?

    (1) Block内部没有调用外部变量时存放在全局区(ARC和MRC下均是)。
    (2) Block使用了外部变量,这种情况也正是我们平时所常用的方式,Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block将存放在堆区。所以在使用Block属性时使用Copy修饰,而在ARC模式下,系统也会默认对Block进行copy操作。

    14、你知道几种iOS中消息传递方式?

    (1) 通知:在iOS中由通知中心进行消息接收和消息广播,是一种一对多的消息传递方式。
    (2) 代理:是一种通用的设计模式,iOS中对代理支持的很好,由代理对象、委托者、协议三部分组成。
    (3) block:iOS4.0中引入的一种回调方法,可以将回调处理代码直接写在block代码块中,看起来逻辑清晰代码整齐。
    (4) target action:通过将对象传递到另一个类中,在另一个类中将该对象当做target的方式,来调用该对象方法,从内存角度来说和代理类似。
    (5) KVO:NSObject的Category-NSKeyValueObserving,通过属性监听的方式来监测某个值的变化,当值发生变化时调用KVO的回调方法。

    15、self. 跟 self-> 什么区别?

    (1) self. 是调用get方法或者set方法
    (2) self是当前本身,是一个指向当前对象的指针
    (3) self->是直接访问成员变量

    16、id 声明的变量有什么特性?

    id声明的变量能指向任何OC对象。

    17、id 和 NSObject*的区别:

    (1) id是一个 objc_object 结构体指针,定义是 typedef struct objc_object *id。
    (2) id可以理解为指向对象的指针。所有oc的对象 id都可以指向,编译器不会做类型检查,id调用任何存在的方法都不会在编译阶段报错,当然如果这个id指向的对象没有这个方法,该崩溃还是会崩溃的。
    (3) NSObject *指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。
    (4) 不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject * 可指向的类型是id的子集。

    18、谈谈 instancetype 和 id 的区别:

    (1) 相同点:都可以作为方法的返回类型。
    (2) 不同点:instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象;instancetype只能作为返回值,不能像id那样作为参数。

    19、isKindOfClass和isMemberOfClass的区别:

    isKindOfClass 用来确定一个对象是否是一个类的成员,或者是派生自该类的成员。
    isMemberOfClass只能确定一个对象是否是当前类的成员。

    20、setObject: forKey: 和 setValue: forKey: 的比较:

    介绍:
    (1) - (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;(字典专属方法):
    正常情况下就是这样的,value 和 key都不为nil;
    当两者有一个为nil的时候就直接崩溃;
    而且你要存空值只能存ObjectType类型的,也就是[NSNull null]空对象;
    打印的时候如果key不存在,取出来的值是(null);
    如果key存在,存的是空对象,打印就是<null>。
    (2) - (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;(NSObject KVC的核心方法):
    其实调用的就是上面介绍的setObject:forKey的方法 如果value是nil。那么就是调用remveObjectForKey了;
    这里value可以为nil,但是key还是不能为nil;
    当value为nil的时候就是remveObject:forKey;
    打印的时候如果key不存在,取出来的值是(null)。

    区别:
    1.首先两者的key都不能为nil,setVlue:forKey:的key只能是字符串类型, setObject:forKey:的key可以是任意对象。
    2.setValue:forKey:中的value可以为nil,为nil的时候调用removeObject:forKey:setObject:forKey:中value不可以为nil。
    3.setObject是字典专属的方法,setVlue是任何对象的方法KVC的核心方法,例如
    People *p1 = [[People alloc] init];
    [p1 setValue:@"mkj" forKey:@"name"];
    当对象有name属性的时候就是通过KVC来赋值。

    21、分类和继承的区别:

    (1) 分类可以在不修改原来类模型的基础上拓充方法
    (2) 分类只能扩充方法、不能扩充成员变量;继承可以扩充方法和成员变量
    (3) 继承会产生新的类

    22、分类和扩展区别:

    (1) 分类是有名称的,类扩展没有名称
    (2) 分类只能扩充方法、不能扩充成员变量;类扩展可以扩充方法和成员变量
    (3) 类扩展一般就写在.m文件中,用来扩充私有的方法和成员变量(属性)

    23、Object-C有多继承吗?没有的话用什么代替?

    (1) OC是单继承,没有多继承
    (2) 有时可以用分类和协议来代替多继承

    24、@dynamic 与 @synthesize 关键词:

    (1) @property有两个对应的词,一个是@synthesize,一个是@dynamic。如果@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;
    (2) @synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
    (3) @dynamic告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成。(当然对于readonly的属性只需提供getter即可)。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var =someVar,由于缺setter方法会导致程序崩溃;或者当运行到 someVar = var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

    25、static的作用?

    (1) static修饰的函数是一个内部函数,只能在本文件中调用,其他文件不能调用
    (2) static修饰的全部变量是一个内部变量,只能在本文件中使用,其他文件不能使用
    (3) static修饰的局部变量只会初始化一次,并且在程序退出时才会回收内存

    26、iOS 并行和并发:

    概念:
    1.并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
    2.并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

    区别:
    并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。

    举例:
    1.并发:一个送外卖的A需要把两份外卖分别送到两个客户B和C手里。 A必须先送完B外卖才能接着送C的。这就是并发。
    2.并行:客户C 分别从饿了么和美团订了一共两份外卖。那么外卖员A和外卖员B需要把外卖一同送到客户C手里。 这就是并行。

    27、OC在向一个对象发送消息时,发生了什么?

    根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有找到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息。

    28、一个OC对象的 isa 的指针指向什么?有什么作用?

    每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

    29、下面的代码输出什么?
    @implementation Son : NSObject
    - (id)init
    {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    

    答案:都输出 Son
    1.class 获取当前方法的调用者的类,superClass 获取当前方法的调用者的父类,super 仅仅是一个编译指示器,就是给编译器看的,不是一个指针。
    2.本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用。

    这个题目主要是考察关于OC中对 self 和 super 的理解:
    1.self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者。
    2.当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找。
    3.而当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法。
    4.调用 [self class] 时,会转化成 objc_msgSend 函数。

    30、init方法中的 if (self = [super init]),在super init的时候,为什么要赋值给self,直接super init不就可以了么?

    NSString alloc init之后,可能返回各种不同的对象,这些对象并不是NSString对象,那么,init的时候,肯定改变了返回的对象,这时候必须赋值给self,不然返回的对象获取不到它的指针,内存就leak了。

    31、什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?

    当发送消息的时候,我们会根据类里面的 methodLists 列表去查询我们要动用的SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报 unrecognized selector 错误,当系统查询不到方法的时候,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我一次机会来添加调用不到的方法。或者我们可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法,可以保证不会崩溃。

    32、能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

    1、不能向编译后得到的类增加实例变量。
    2、能向运行时创建的类中添加实例变量。
    原因:1. 编译后的类已经注册在 Runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,Runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用。所以不能向存在的类中添加实例变量。2. 运行时创建的类是可以通过调用class_addIvar函数添加实例变量的。但是是在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

    33、Runtime 如何实现 weak 变量的自动置nil ?

    Runtime 对注册的类,会进行布局,对于 weak 对象会放入一个 hash 表中,用 weak 指向的对象内存地址作为 key。当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

    34、给类添加一个属性后,在类结构体里哪些元素会发生变化?

    instance_size :实例的内存大小;
    objc_ivar_list *ivars:属性列表。

    35、RunLoop是来做什么的?RunLoop和线程有什么关系?主线程默认开启了RunLoop么?子线程呢?

    RunLoop:从字面意思看:运行循环、跑圈,其实它内部就是 do-while 循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。
    RunLoop 和线程的关系:一个线程对应一个 RunLoop,主线程的 RunLoop 默认创建并启动,子线程的 RunLoop 需手动创建且手动启动(调用run方法)。RunLoop 只能选择一个 Mode 启动,如果当前 Mode 中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出 RunLoop。

    36、RunLoop的Mode是用来做什么的?有几种Mode?

    Mode 是 RunLoop 里面的运行模式,不同的模式下的 RunLoop 处理的事件和消息有一定的差别。
    系统默认注册了5个Mode:
    (1)kCFRunLoopDefaultMode: 相当于NSDefaultRunLoopMode,App的默认 Mode,通常主线程是在这个 Mode 下运行的。
    (2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    (3)UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
    (4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    (5)kCFRunLoopCommonModes: 相当于NSRunLoopCommonModes,是 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 的结合。在主线程中两者都有,在子线程中只有NSDefaultRunLoopMode的功能。

    37、为什么把NSTimer对象以 NSDefaultRunLoopMode 添加到主线程循环以后,滑动scrollview的时候NSTimer却不动了?

    NSTimer对象是在 NSDefaultRunLoopMode下面调用消息的,但是当我们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不可以继续响应NSTimer发送的消息。所以如果想在滑动scrollview的情况下面还调用NSTimer的消息,我们可以把RunLoop的模式更改为NSRunLoopCommonModes。

    38、为什么总是要把RunLoop和线程放在一起来讲?

    总的来讲就是:RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。

    39、RunLoop本质:

    RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run];之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。这也就是我们最常使用RunLoop的场景之一,就如小节标题保持线程的存活,而不是线性的执行完任务就退出了。
    ①.RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。
    ②.RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。我们不能创建RunLoop,只能在当前线程当中获取线程对应的RunLoop(主线程RunLoop除外)。
    ③.子线程默认没有RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。
    ④.RunLoop想要正常启用需要运行在添加了事件源的Mode下。
    ⑤.RunLoop有三种启动方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一种无条件永远运行RunLoop并且无法停止,线程永远存在。第二种会在时间到后退出RunLoop,同样无法主动停止RunLoop。前两种都是在NSDefaultRunLoopMode模式下运行。第三种可以选定运行模式,并且在时间到后或者触发了非Timer的事件后退出。

    40、苹果是如何实现Autorelease Pool的?

    Autorelease Pool作用:缓存池,可以避免我们经常写relase的一种方式。其实就是延迟release,将创建的对象,添加到最近的Autorelease Pool中,等到Autorelease Pool作用域结束的时候,会将里面所有的对象的引用计数器Autorelease。

    41、对象是什么时候被释放的?

    每个OC对象都有一个引用计数器,每个新对象的计数器是1,当对象的计数器减为0时,就会被销毁。

    42、用过NSOperationQueue吗? 为什么要使用 NSOperationQueue?实现了什么? 简述它和GCD的区别和类似的地方(提示:可以从两者的实现机制和适用范围来述)?

    使用NSOperationQueue用来管理子类化的NSOperation对象,控制其线程并发数目。
    GCD和NSOperation都可以实现对线程的管理,区别是NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中使用NSOperation的优点是 NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化 NSOperation的设计思路,是具有面向对象的优点(复用、封装),建议在复杂项目中使用。
    GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会使代码更为易读,建议在简单项目中使用。

    区别:
    (1) GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本封装。
    (2) GCD只支持FIFO的队列,NSOperationQueue可以很方便地调整执行顺序、设置最大并发数量。
    (3) NSOperationQueue可以在轻松在Operation间设置依赖关系,而GCD 需要写很多的代码才能实现。
    (4) NSOperationQueue支持KVO,可以监测operation是否正在执行 (isExecuted)、是否结束(isFinished),是否取消(isCanceld) 。
    (5) GCD的执行速度比NSOperationQueue快,任务之间不太互相依赖。

    43、GCD内部是怎么实现的?

    (1)iOS和OS X的核心是XNU内核,GCD是基于XNU内核实现的
    (2)GCD的API全部在libdispatch库中
    (3)GCD的底层实现主要有Dispatch Queue和Dispatch Source
    (4)Dispatch Queue:管理block(操作)
    (5)Dispatch Source:处理事件

    44、UIKit类主要在哪一个应用线程上使用?

    UIKit的界面类只能在主线程上使用,对界面进行更新。多线程环境中要对界面进行更新必须要切换到主线程上。

    45、不同情况下delegate和block的选择?

    (1) 多个消息传递,应该使用delegate。在有多个消息传递时,用delegate实现更合适,看起来也更清晰。block就不太好了,这个时候block反而不便于维护,而且看起来非常臃肿,很别扭。例如UIKit的UITableView中有很多代理如果都换成block实现,代码块会非常臃肿。
    (2) 一个委托对象的代理属性只能有一个代理对象,如果想要委托对象调用多个代理对象的回调应该用block。

    46、KVC和KVO的本质:

    KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。
    (1).KVC要设值,那么就要对象中对应的key,KVC在内部是按什么样的顺序来寻找key的。当调用setValue:属性值 forKey:@”name“的代码时,如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作;
    (2).KVC要取值,当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。

    KVO的本质:简而言之,苹果使用了一种isa交换的技术,当objectA被观察后,objectA对象的isa指针被指向了一个新建的ASClassA的子类NSKVONotifying_ASClassA,且这个子类重写了被观察值的setter方法和class方法,dealloc和_isKVO方法,然后使objectA对象的isa指针指向这个新建的类,然后事实上objectA变为了NSKVONotifying_ASClassA的实例对象,执行方法要从这个类的方法列表里找。

    47、说说你对通知NSNotification的理解(从同步和异步发送通知来说):

    1.同步通知(常用的通知中心发送的通知):等消息发完之后要等处理了消息才跑发送消息之后的代码,这跟多线程中的同步概念相似。

    [[NSNotificationCenter defaultCenter] postNotificationName:K_NOTIFICATION_NAME object:nil];
    

    2.异步通知(通知队列发送的通知):发送完之后就继续跑下面的代码,不需要去管接受通知的处理。

    NSNotificationQueue * notificationQueue = [NSNotificationQueue defaultQueue];
    NSNotification * notification = [NSNotification notificationWithName:K_NOTIFICATION_NAME object:nil];
    [notificationQueue enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationCoalescingOnName forModes:nil];
    

    3.开启一个其他线程去发送以上通知:(1)同步通知正常同步发送,不影响;(2)异步通知的NSPostingStyle如果为NSPostWhenIdle(空闲时候发送)则不会发送通知;若改为NSPostNow,则会立即同步发送通知。(3)每个线程都有一个通知队列,当线程结束了,通知队列就被释放了,所以当前选择发送时机为NSPostWhenIdle时也就是空闲的时候发送通知,通知队列就已经释放了,所以通知发送不出去了。
    如果线程不结束,就可以发送通知了,用runloop让线程不结束。

    4.如果通知队列发送通知的NSNotificationCoalescing为NSNotificationCoalescingOnName(按名称合并通知)
    如果NSPostingStyle为NSPostWhenIdle(空闲时候发送),则多条信息会合并成一条发送;若为NSPostNow(现在发送),则同步发送多条消息。
    就跟dispatch_sync原理一样,就是得发送因为NSPostNow是同步的,所以发送第一条通知,得等处理完第一条通知,才跑发送第二条通知,这样肯定就没有合并消息一说了,因为这有点类似线程阻塞的意思,只有异步,就是三个发送通知全部跑完,在处理通知的时候看是否需要合并和怎么合并,再去处理。

    5.通知队列也可以实现异步,但是真正的异步还是得通过port,底层所有的消息触发都是通过端口来进行操作的.

    48、NSCache优于NSDictionary的几点?

    (1) NSCache 是一个容器类,类似于NSDIctionary,通过key-value 形式存储和查询值,用于临时存储对象。
    注意一点它和NSDictionary区别就是,NSCache 中的key不必实现copy,NSDictionary中的key必须实现copy。
    NSCache中存储的对象也不必实现NSCoding协议,因为毕竟是临时存储,类似于内存缓存,程序退出后就被释放了。
    (2) NSCache可以提供自动删减缓存功能,而且保证线程安全,与字典不同,不会拷贝键。
    (3) NSCache可以设置缓存上限,限制对象个数和总缓存开销。定义了删除缓存对象的时机。这个机制只对NSCache起到指导作用,不会一定执行。
    (4) NSPurgeableData搭配NSCache使用,可以自动清除数据。

    49、堆和栈的区别?

    (1) 堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存。
    (2) 栈空间的内存由系统自动分配,一般存放局部变量等,不需要手动管理内存。

    50、TCP,UDP和socket,Http之间联系和区别:

    (1)TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
    (2)UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
    (3)socket:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
    (4)HTTP(超文本传输协议)是利用TCP在两台电脑(通常是Web服务器和客户端)之间传输信息的协议。客户端使用Web浏览器发起HTTP请求给Web服务器,Web服务器发送被请求的信息给客户端。

    51、TCP和UDP:

    TCP:面向连接,可靠传输(保证数据正确性,顺序性),用于传输大量数据(流模式)、速度慢,建立连接开销比较大(时间,系统资源);流模式:在连接持续的过程中,基本上都是从同一个主机发出的,因此,需要保证数据是有序的到达;三次握手(建立TCP连接,需要C和S发送三个包),四次挥手(TCP连接的断开需要发送4个包)。
    UDP:非连接,不可靠传输,速度快,用于传输少量数据;只要知道接收端的ip和端口,任何主机都可以向接收端发送数据。

    52、https 通信的优点:

    (1) 客户端产生的密钥只有客户端和服务器端能得到;
    (2) 加密的数据只有客户端和服务器端才能得到明文;
    (3) 客户端到服务端的通信是安全的。

    53、https 建立连接过程:

    (1) 客户端发送请求到服务器端;
    (2) 服务器端返回证书和公开密钥,公开密钥作为证书的一部分而存在;
    (3) 客户端验证证书和公开密钥的有效性,如果有效,则生成共享密钥并使用公开密钥加密发送到服务器端;
    (4) 服务器端使用私有密钥解密数据,并使用收到的共享密钥加密数据,发送到客户端;
    (5) 客户端使用共享密钥解密数据;
    (6) SSL加密建立。

    54、TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接:

    (1) 第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
    (2) 第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
    (3) 第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。完成三次握手,客户端与服务器开始传送数据。

    55、按钮或者其它 UIView控件的事件传递的具体过程

    触摸事件的传递是从父控件传递到子控件也就是UIApplication->window->寻找处理事件最合适的view。注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件。

    应用如何找到最合适的控件来处理事件?
    (1) 首先判断主窗口(keyWindow)自己是否能接受触摸事件。
    (2) 判断触摸点是否在自己身上。
    (3) 子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)。
    (4) view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
    (5) 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。

    56、简单说一下时间响应的流程?

    (1) 一个 UIView 发出一个事件之后,首先上传给其父视图;
    (2) 然后父视图上传给其所在的控制器;
    (3) 如果其控制器对事件进行处理,事件传递将终止,否则继续上传父视图;
    (4) 直到遇到响应者才会停止,否则事件将一直上传,直到 UIWindow。

    57、UIView不能接收触摸事件的三种情况:

    (1) 不允许交互:userInteractionEnabled = NO。
    (2) 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件。
    (3) 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
    注 意:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。

    58、iOS程序在调用main函数之前的启动:

    系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。

    59、简单说一下 APP的启动过程,从 main文件开始说起:

    有storyboard情况下:
    (1) main函数;
    (2) UIApplicationMain,创建UIApplication对象,创建UIApplication的delegate对象;
    (3) 根据Info.plist获得最主要storyboard的文件名,加载最主要的 storyboard(有storyboard);
    (4) 创建UIWindow;
    (5) 创建和设置UIWindow的rootViewController;
    (6) 显示窗口;

    没有storyboard情况下:
    (1) main函数;
    (2) UIApplicationMain,创建UIApplication对象,创建UIApplication的delegate对象;
    (3) delegate对象开始处理(监听)系统事件(没有storyboard);
    (4) 程序启动完毕的时候, 就会调用代理的;application:didFinishLaunchingWithOptions:方法,并在application:didFinishLaunchingWithOptions:中创建UIWindow;
    (5) 创建和设置UIWindow的rootViewController;
    (6) 显示窗口。

    60、链表和数组的区别:

    二者都属于一种数据结构。
    从逻辑结构来看:
    (1) 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;数组可以根据下标直接存取。
    (2) 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据 next 指针找到下一个元素。
    从内存存储来看:
    (1) (静态)数组从栈中分配空间,对于程序员方便快速,但是自由度小 。
    (2) 链表从堆中分配空间,,自由度大但是申请管理比较麻烦。
    从上面的比较可以看出,如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反,如果需要经常插入和删除元素就需要用链表数据结构了。

    61、load 和 initialize 有什么用处?

    load:
    (1) 当类对象被引入项目时,Runtime 会向每一个类对象发送 load 消息。
    (2) load 方法会在每一个类甚至分类被引入时仅调用一次。调用的顺序:父类优先于子类, 子类优先于分类。
    (3) 由于 load 方法会在类被 import 时调用一次,而这时往往是改变类的行为的最佳时机,在这里可以使用例如 method swizlling 来修改原有的方法。
    (4) load 方法不会被类自动继承。
    initialize:
    也是在第一次使用这个类的时候会调用这个方法,也就是说 initialize 也是懒加载。

    62、load和initialize区别:

    (1) load和initialize方法都会在实例化对象之前调用,以main函数为分水岭,前者在main函数之前调用,后者在之后调用。这两个方法会被自动调用,不能手动调用它们。
    (2) load和initialize方法都不用显示的调用父类的方法而是自动调用,即使子类没有initialize方法也会调用父类的方法,而load方法则不会调用父类。
    (3) load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
    (4) load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。
    (5) 父有 子有:load和initialize均先调用父类,然后调用子类;
    父有 子无:load只调用父类,initialize会调用两次父类,其中一次是为子类调用;
    父无 子有:load和initialize均只调用子类。

    63、沙盒目录结构是怎样的?各自用于那些场景?

    Application:存放程序源文件,上架前经过数字签名,上架后不可修改。
    Documents:常用目录,iCloud 备份目录,存放数据。
    Library:(1) Caches:存放体积大又不需要备份的数据; (2) Preference:设置目录,iCloud 会备份设置信息。
    tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。

    64、iOS开发中方法延迟执行的几种方式?

    (1) NSObject 的 performSelector方法
    (2) NSTimer定时器
    (3) NSThread线程的sleep
    (4) GCD 的 dispatch_after

    65、通过结构体 category_t 可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。我们这里简述下 Category 的实现原理:

    (1) 在编译时期,会将分类中实现的方法生成一个结构体 method_list_t 、将声明的属性生成一个结构体 property_list_t ,然后通过这些结构体生成一个结构体 category_t 。
    (2) 然后将结构体 category_t 保存下来
    (3) 在运行时期,Runtime 会拿到编译时期我们保存下来的结构体 category_t
    (4) 然后将结构体 category_t 中的实例方法列表、协议列表、属性列表添加到主类中
    (5) 将结构体 category_t 中的类方法列表、协议列表添加到主类的 metaClass 中
    这里需要注意的是:category_t 中的方法列表是插入到主类的方法列表前面(类似利用链表中的 next 指针来进行插入),所以这里 Category 中实现的方法并不会真正的覆盖掉主类中的方法,只是将 Category 的方法插到方法列表的前面去了。运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会停止查找,这里就会出现覆盖方法的这种假象了。

    66、Category 为什么不能添加实例变量?

    通过结构体 category_t ,我们就可以知道,在 Category 中我们可以增加实例方法、类方法、协议、属性。这里没有 objc_ivar_list 结构体,代表我们不可以在分类中添加实例变量。
    因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这个就是 Category 中不能添加实例变量的根本原因。

    67、lldb(gdb)常用的控制台调试命令?

    (1) p 输出基本类型。是打印命令,需要指定类型。是print的简写
    p (int)[[[self view] subviews] count]。
    (2) po 打印对象,会调用对象description方法。是print-object的简写
    po [self view]。
    (3) expr 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。
    (4) bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈。
    (5) br l:是breakpoint list的简写。

    68、你一般是如何调试Bug的?

    (1) 在运行过程中,如果出现EXC_BAD_ACCESS 异常,往往提示的信息很少或者没有提示,启用NSZombieEnabled后在控制台能打印出更多的提示信息,便于debug,请注意,僵尸模式下的调试工作只能在模拟器中实现,我们无法在物理设备上完成这一诊断流程。
    (2) 异常断点,一般程序crash时Xcode一般会定位到main函数中,得不到详细的crash信息,打上异常断点后就极大可能定位到程序的crash处,利于debug。
    (3) 一般来说,在创建工程的时候,应该在Build Settings启用Analyze During 'Build',这样每次编译时都会自动静态分析。这样的话,写完一小段代码之后,就马上知道是否存在内存泄露或其他bug问题,并且可以修bug。
    (4) 如果你想在运行的时候查看APP是否存在内存泄露,你可以使用Xcode上instruments工具上的Leaks模块进行内存分析。但是有些内存泄露是很难检查出来,有时只有通过手动覆盖dealloc方法,看它最终有没有调用。

    69、如何解决TableView卡的问题:

    (1) 复用单元格;
    (2) 单元格中的视图尽量都使用不透明的,单元格中尽量少使用动画;
    (3) 图片加载使用异步加载;
    (4) 滑动时不加载图片,停止滑动时开始加载;
    (5) 单元格中的内容可以在自定义cell类中的drawRect方法内自己绘制;
    (6) 如非必要,减少reloadData全部cell,只reloadRowsAtIndexPaths;
    (7) 如果cell是动态行高,计算出高度后缓存;
    (8) cell高度固定的话直接使用cell.rowHeight设置高度。

    70、卡顿产生的原因:

    在 VSync(垂直同步) 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

    71、离屏渲染:

    (1) 如何检测我的项目里面离屏渲染了:
    打开的正确方式:模拟器的 debug -> 选取 color Offscreen-Rendered。开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
    (2) 离屏渲染会导致CPU在后台保存一份bitmap,所以会导致CPU多余运算。而避免的方式则是避免去做触发的动作:重写drawRect方法;masksToBounds;其他一些手动触发离屏渲染的动作。
    (3) 图像渲染工作原理:由CPU计算好显示内容,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照HSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

    72、如何减小离屏渲染带来的影响?

    (1) 慎用离屏渲染,因为绝大多数时候离屏渲染会影响性能;
    (2) 重写drawRect方法,设置圆角、阴影、模糊效果,光栅化都会导致离屏渲染;
    (3) 设置阴影效果是加上阴影路径;
    (4) 滑动时若需要圆角效果,开启光栅化;

    以上就是我最近复习到的面试题,希望可以帮到你们,欢迎指正交流~

    相关文章

      网友评论

      • 弦in:面试如何了?
        wuyukobe:@弦in 入职半个多月了,面试看中的大多都是基础和底层

      本文标题:iOS最新面试题总结

      本文链接:https://www.haomeiwen.com/subject/zuxfnftx.html