美文网首页
2019-11-14 记iOS高级工程师面试题

2019-11-14 记iOS高级工程师面试题

作者: 飞哥漂流记 | 来源:发表于2019-11-18 00:04 被阅读0次

    1. 简述一下iOS的内存管理?

    内存条中主要分为几大类:栈区(stack)、堆区(heap)、常量区、代码区(.text)、保留区。常量区分为未初始化区域(.bss)和已初始化区域(.data),栈区stack存储顺序是由高地址存向低地址,而堆区是由低地址向高地址存储。内存条中地址由低到高的区域分别为:保留区,代码区,已初始化区(.data),未初始化区(.bss),堆区(heap),栈区(stack),内核区。而程序员操作的主要是栈区与堆区还有常量区

    当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。

    MRC(手动管理引用计数):在MRC中增加的引用计数都是需要自己手动释放的,遵循4个法则:

    自己生成的对象,自己持有。

    非自己生成的对象,自己也能持有。

    不在需要自己持有对象的时候,释放。

    非自己持有的对象无需释放。

    ARC(自动管理引用计数):  现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮你做了。为什么说是大部分呢,因为底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

    2. 属性关键字有哪些? block和delegate作为属性用什么修饰 为什么?

    关键字                                                              注释

    readwrite                   此标记说明属性会被当成读写的,这也是默认属性

    readonly                    此标记说明属性只可以读,也就是不能设置,可以获取。

    assign                       不会使引用计数加1,也就是直接赋值。 基本数据类型和C数据类型默认属性

    retain                         会使引用计数加1。

    copy                           建立一个索引计数为1的对象,在赋值时使用传入值的一份拷贝。

    nonatomic                 非原子性访问,多线程并发访问会提高性能。

     atomic                       原子性访问。默认属性

    strong                        打开ARC时才会使用,相当于retain。默认属性

    weak                          打开ARC时才会使用,相当于assign,可以把对应的指针变量置为nil

    基本数据: atomic,readwrite,assign

    普通的 OC 对象: atomic,readwrite,strong

    block在创建的时候,它的内存是分配在栈(stack)上,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃(栈区上的过了作用域就清除了).

    因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block拷贝(copy)到堆,所以说为了block属性声明和实际的操作一致,最好声明为copy

    使用copy修饰就会对Block的内部对象进行强引用,导致循环引用。内存无法释放。就需要__weak对block内部对象进行修饰

    如果代理是strong修饰,那么当Viewcontroller需要释放的时候—->就需要先释放这个A—->A就需要释放这个代理—–>代理又需要释放这个Viewcontroller,就会引起循环引用。故而需要使用weak

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

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

    assign自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak

    自定义IBOutlet 控件属性一般也使用weak;当然,也可以使用 strong,但是建议使用 weak

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

    4. 怎么用 copy 关键字?

    NSString、NSArray、NSDictionary 等等经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary.

    因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.

    为确保对象中的属性值不会无意间变动,应该在设置新属性值时拷贝一份,保护其封装性block,也经常使用 copy,关键字block

    使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.

    在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但是建议写上 copy,因为这样显示告知调用者“编译器会自动对 block 进行了 copy 操作。

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

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

    [可变对象 copy]//深复制 

    [可变对象 mutableCopy]//深复制

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

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

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

    6.. 简述一下KVO和KVC的原理?具体到方法

    KVC/KVO实现的根本是Objective-C的动态性和runtime

    KVC:Key-Value Coding,即键值编码 是一种不通过存取方法 而是通过属性字符串间接访问属性的机制 。

    -(id)valueForKey:(NSString*)key;

    -(void)setValue:(id)value forKey:(NSString*)key;

    -(id)valueForKeyPath:(NSString*)keyPath;

    -(void)setValue:(id)value forKeyPath:(NSString*)keyPath;

    前两个方法无论获取值还是赋值,只需要传入属性名称的字符串就行了。但KVC也提供了传入path的方法。所谓path,就是用点号连接的多层级的属性,比如student.name,student属性里的name属性。

    KVC的方法查找顺序:

    ①检查是否存在-<key>、-is<key>(只针对布尔值有效)或者-get<key>的访问器方法,如果有可能,就是用这些方法返回值;

    检查是否存在名为-set<key>:的方法,并使用它做设置值。对于-get<key>和-set<key>:方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致;

    ②如果上述方法不可用,则检查名为-_<key>、-_is<key>(只针对布尔值有效)、-_get<key>和-_set<key>:方法;

    ③如果没有找到访问器方法,可以尝试直接访问实例变量。实例变量可以是名为:<key>或_<key>;

    ④如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

    KVO:key-value observing 即键值观察 提供了一种当前对象属性被修改的时候通过当前对象的机制,KVO很适合实现model和controller之间的通讯。

    当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

    派生类在被重写的 setter 方法实现真正的通知机制

    新类会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:

    - (void)willChangeValueForKey:(NSString *)key

    - (void)didChangeValueForKey:(NSString *)key

    其中,didChangeValueForKey:方法负责调用:

    - (void)observeValueForKeyPath:(NSString *)keyPath   ofObject:(id)object   change:(NSDictionary *)change context:(void *)context

    KVO的缺点:

    KVO的回调机制不能传一个Selector或block作为回调 必须回调-addObserver:forKeyPath:options:context:方法

    具体方法:

    首先给目标对象的属性添加观察:-(void)addObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options context:(nullablevoid*)context;

    实现下面方法来接收通知,需要注意各个参数的含义:

    -(void)observeValueForKeyPath:(nullable NSString*)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*,id>*)change context:(nullablevoid*)context;

    最后要移除观察者:

    -(void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath;

    7. setvalue forkey  和 setobject forkey的区别?

     setValue:forKey: 的value是可以为nil的(但是当value为nil的时候,会自动调用removeObject:forKey方法);

    setObject:forKey: 的value则不可以为nil。

    setValue:forKey: 的key必须是不为nil的字符串类型;

    setObject:forKey: 的key可以是不为nil的所有类型。

    8. iOS的数据本地存储有方式哪些?

    iOS本地持久化存储的路径:

    Documents: 最常用的目录,存放重要的数据,iTunes同步时会备份该目录

    Library/Caches: 一般存放体积大,不重要的数据,iTunes同步时不会备份该目录

    Library/Preferences: 存放用户的偏好设置,iTunes同步时会备份该目录

    tmp: 用于存放临时文件,在程序未运行时可能会删除该文件夹中的数据,iTunes同步时不会备份该目录

    存储方式:NSUserDefaults、Plist、NSKeyedArchiver、SQLite3、Core Data、Keychain、FMDB

    NSUserDafaults存储:

    写入:NSUserDefaults*login=[NSUserDefaults standardUserDefaults];

    login setObject:self.passwordField.text forKey:@"token"]

    ;[login synchronize];

    取出:NSUserDefaults*login=[NSUserDefaults standardUserDefaults];

    NSString*str=[login objectForKey:@"token"];

    特点:只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义数据。

    键值对存储,直接指定存储类型。

    plist方式存储:

    重要方法: NSString*path=[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)objectAtIndex:0]stringByAppendingPathComponent:@"password.plist"];

    特点:

    只能存储OC常用数据类型(NSString、NSDictionary、NSArray、NSData、NSNumber等类型)而不能直接存储自定义模型对象。

    plist文件存储的位置,但一般存在Documents中

    如果存储图片路径的话,一定要存储相对位置,因为每次启动APP,plist文件的路径就会变化,自然图片的位置也就变化了。

    NSKeyedArchiver归档(NSCoding)方式存储:

    -(nullable instancetype)initWithCoder:(nonnull NSCoder*)aDecoder{

    -(void)encodeWithCoder:(nonnull NSCoder*)aCoder{

    FMDB方式存储:

    NSString*docuPath=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)[0];

    NSString*dbPath=[docuPath stringByAppendingPathComponent:@"test.db"];

    NSLog(@"!!!dbPath = %@",dbPath);//2.创建对应路径下数据库

    db=[FMDatabase databaseWithPath:dbPath];

    FMDatabase:一个FMDatabase对象代表一个单独的SQLite数据库,通过SQLite语句执行数据库的增删改查操作

    FMResultSet:使用FMDatabase对象查询数据库后的结果集

    FMDatabaseQueue:用于多线程操作数据库,它保证线程安全

    开启事务 :beginTransaction

    回滚事务:rollback

    提交事务:commit

    // 插入数据-(void)insertDataWithSQL:(NSString*)sql{[self.queue inDatabase:^(FMDatabase*db){BOOL result=[db executeUpdate:sql withArgumentsInArray:nil];if(result){NSLog(@"插入数据成功");}else{NSLog(@"插入数据失败");}}];}

    10. iOS多线程有哪些?GCD项目中有什么地方应用到?线程死锁有哪些原因?怎么避免?

    NSThread:

    NSThread是封装程度最小最轻量级的,使用更灵活,但要手动管理线程的生命周期、线程同步和线程加锁等,开销较大;

    两种创建方式:

    NSThread*newThread=[[NSThread alloc]initWithTarget:selfselector:@selector(run)object:nil];

    /* 开启线程 */[newThread start];

    [NSThread detachNewThreadSelector:@selector(run)toTarget:selfwithObject:nil];[NSThread detachNewThreadWithBlock:^{NSLog(@"block run...");}];

    NSOperation是基于GCD的一个抽象基类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,但比GCD可控性

    更强,例如可以加入操作依赖(addDependency)、设置操作队列最大可并发执行的操作个数

    (setMaxConcurrentOperationCount)、取消操作(cancel)等。

    NSOperation作为抽象基类不具备封装我们的操作的功能,需要使用两个它的实体子类:NSBlockOperation和

    NSInvocationOperation,或者继承NSOperation自定义子类。

    NSBlockOperation和NSInvocationOperation用法的主要区别是:前者执行指定的方法,后者执行代码块,相对来说后者更加灵活易用。

    NSInvocationOperation*invoOpertion=[[NSInvocationOperation alloc]initWithTarget:selfselector:@selector(run)object:nil];[invoOpertion start];/* NSBlockOperation初始化 */NSBlockOperation*blkOperation=[NSBlockOperation blockOperationWithBlock:^{NSLog(@"NSBlockOperation");}];[blkOperation start];

     GCD 中两个核心概念:『任务』 和 『队列』

    任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方

    式:『同步执行』和『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

    可以使用 dispatch_queue_create 方法来创建队列。该方法需要传入两个参数:

    第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。

    第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,

    DISPATCH_QUEUE_CONCURRENT 表示并发队列

    GCD 默认提供了:『主队列(Main Dispatch Queue)』。所有放在主队列中的任务,都会放到主线程中执行。可使

    用 dispatch_get_main_queue() 方法获得主队列。

    区别                         并发队                                          列串行队列                                                 主队列

    同步(sync)    没有开启新线程,串行执行任务       没有开启新线程,串行执行任务             死锁卡住不执行

    异步(async)   有开启新线程,并发执行任务           有开启新线程(1条),串行执行任务  没有开启新线程,串行执行任务

    『主线程』中调用『主队列』+『同步执行』会导致死锁问题。

    这是因为主队列中追加的同步任务和主线程本身的任务两者之间相互等待,阻塞了『主队列』,最终造成了主队列所在的线程

    (主线程)死锁问题。

    GCD 线程间的通信:

    // 获取主队列 dispatch_queue_t mainQueue = dispatch_get_main_queue();

    // 回到主线程

            dispatch_async(mainQueue, ^{

                // 追加在主线程中执行的任务

                [NSThread sleepForTimeInterval:2];              // 模拟耗时操作

                NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程

            });

    GCD 栅栏方法:dispatch_barrier_async

    我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一

    样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到

    dispatch_barrier_async方法在两个操作组间形成栅栏。

    dispatch_barrier_async(queue, ^{

            // 追加任务 barrier

            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作

            NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程

        });

    GCD 延时执行方法:dispatch_after

    在指定时间(例如 3 秒)之后执行某个任务。可以用 GCD 的dispatch_after 方法来实现。

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

            // 2.0 秒后异步追加任务代码到主队列,并开始执行

            NSLog(@"after---%@",[NSThread currentThread]);  // 打印当前线程

        });

    GCD 一次性代码(只执行一次):dispatch_once

    我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的dispatch_once方法。使用

    dispatch_once方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once也可以保证

    线程安全。

    /**

    * 一次性代码(只执行一次)dispatch_once

    */

    - (void)once {

        static dispatch_once_t onceToken;

        dispatch_once(&onceToken, ^{

            // 只执行 1 次的代码(这里面默认是线程安全的)

        });

    }

    GCD 快速迭代方法:dispatch_apply

    通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的方法dispatch_apply。dispatch_apply按照指定的次数将指定

    的任务追加到指定的队列中,并等待全部队列执行结束。

    我们可以利用并发队列进行异步执行。比如说遍历

     0~5 这 6 个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个

    数字。

    GCD 队列组:dispatch_group

    有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我

    们可以用到 GCD 的队列组。

    调用队列组的dispatch_group_async先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的

    dispatch_group_enter、dispatch_group_leave组合来实现dispatch_group_async。

    调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会

    阻塞当前线程)。

    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1

    dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。

    当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中

    的任务。

    GCD 信号量:dispatch_semaphore

    dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量

    dispatch_semaphore_signal:发送一个信号,让信号总量加 1

    dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

    Dispatch Semaphore 在实际开发中主要用于:保持线程同步,将异步执行任务转换为同步执行任务

    保证线程安全,为线程加锁

    我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将

    将异步执行任务转换为同步执行任务。比如说:AFNetworking 中 AFURLSessionManager.m 里面的tasksForKeyPath:方法。

    通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。

    11. Masony布局中使用了block?为什么没有造成循环引用?

    查看masonry源码可以看到究竟:masonry中设置布局的方法中的block对象并没有被View所引用,而是直接在方法内部同步执

    行,执行完以后block将释放,其中捕捉的外部变量的引用计数也将还原到之前。

    12. 在cell多列布局中,每列有多个lable,假如后台数据返回的数量不一 ,怎么保证cell的高度是按最大lable布局?

    13. 说一下iOS中主流的设计模式 ,

    目前常用的几种设计模式:代理模式、观察者模式、MVC模式、单例模式、策略模式、工厂模式、MVVM

    14. JSON解析的原理 ?

    Runtime运行时机制 利用class_copyPropertyList  property_getName等方法

    15. 简述一下响应者链?假如一个viewA 上有一个viewB,且viewB的面积大于viewA 点击viewB且不和viewA重叠的区域 是否会响应 为什么?

    响应者对象UIResponder,只有继承UIResponder的的类,才能处理事件。

    1.当iOS程序中发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列中

    2.UIApplication将处于任务队列最前端的事件向下分发。即UIWindow。

    3.UIWindow将事件向下分发,即UIView。

    4.UIView首先看自己是否能处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图。

    5.遍历子控件,重复以上两步。

    6.如果没有找到,那么自己就是事件处理者。如果

    7.如果自己不能处理,那么不做任何处理。

    其中 UIView不接受事件处理的情况主要有以下三种

    1)alpha <0.01

    2)userInteractionEnabled = NO

    3.hidden = YES.

    怎么寻找最合适的view:

    此方法返回的View是本次点击事件需要的最佳View

    -(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event

    // 判断一个点是否落在范围内

    -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event

    相关文章

      网友评论

          本文标题:2019-11-14 记iOS高级工程师面试题

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