美文网首页iOS技术专题
iOS面试中经常遇到的问题(原理篇一)

iOS面试中经常遇到的问题(原理篇一)

作者: 马威明 | 来源:发表于2018-04-27 18:39 被阅读0次

          近期在准备找工作和面试的事,闲暇之余,总结我近期面试的一些常见问题与心得。自己做一下笔记,也给近期需要面试的同学一些参考,文章如有不严谨、错误或侵权之处,欢迎各路大牛提出并指正。

          大致问题应该是这样的:首先会问几个深度稍微高点的问题(如响应者链如何执行的、KVO的底层实现原理、core Frameworks等),然后再转到基础 问一些老生常谈的问题(如tableView的优化、关于三种多线程的用法等)。

          接下来进入正题:

    1、UIViewController的超类(父类)是谁?响应者链是什么?事件发生后的处理过程?

           答:UIViewController的父类是UIResponder(所有能接受并处理事件的对象都直接或者间接继承了UIResponder,其中UIApplication、UIViewController、UIView都是直接继承UIResponder)。

           响应者链简单的说,就是一系列响应者对象构成的一条具有先后关系的链条。

           事件发生之后,处理事件的过程分为事件的传递事件的响应

           事件发生后首先执行传递操作,事件的传递过程为:事件(如触摸、移动等)发生后,系统会将该事件加入到一个由UIApplication管理的事件队列中,UIApplication会从队列中取出最前面的事件,并将事件分发出去进行处理, 第一步通常先传递给主窗口UIWindow,然后由主窗口在视图层次结构中寻找最合适的视图来处理事件。

           找到合适的控件的方法为:

           1、首先判断主窗口能否能够接受事件(判断方式为:一、是否直接或者间接继承了UIResponder;二、是否开启了用户交互self.userInteractionEnabled = YES;三、是否设置了隐藏self.hidden = YES;四、透明度是否小于0.01self.alpha < 0.01)。 

           2、如果能接受事件、则判断触摸点是否在自己身上。

           3、如果触摸点在自己身上,则主窗口从后往前遍历自己的子控件(为了找到适合处理事件 的子view)。

           4、找到合适的子控件以后重复上面的3个步骤(1、判断子控件是否能够接受事件,2、判断触摸点是否在子控件上,3、如果满足条件1和2,则继续遍历子控件的子控件)。

           5、如果能找到合适的子控件,就继续遍历,如果没有合适的子控件,那么自己就成为处理事件最合适的view。

           判断一个view是否为更合适的view 的方法底层实现是,当事件传递给一个view时,该view会调用hitTest:withEvent:方法,返回当前最合适的view,该方法内部判断的默认顺序为:

           1、判断view能否接受事件(if(self.userInteractionEnabled ==NO||self.hidden ==YES||self.alpha <=0.01)return nil。

           2、调用pointInside:withEvent:判断事件是否在自己的坐标内,如果该方法返回NO,则return nil。if([selfpointInside:point withEvent:event] ==NO) return nil。

           3、从后往前遍历子控件,找到合适的就返回子控件(判断子控件是否合适,首先要把自己坐标上的点转换成子控件坐标上的点CGPoint childPoint =[self convertPoint:point toView:self.subviews[i]],然后子控件调用hitTest:withEvent:方法,如果能处理事件且点在内部,则返回当前子控件if([self.subviews[i] hitTest:childPoint withEvent:event]) ){return [self.subviews[i]},若找不到,就返回自己。

           至此为止,一个更合适的view已经找到,说白了事件的传递过程就是通过hitTest:withEvent:方法寻找到最合适的响应链

           因为最合适的view是调用hitTest:withEvent:方法返回的view所以在需要时,我们可以重写父控件或者当前控件的hitTest:withEvent:方法,返回指定的view作为最适合的view。    


           接下来就是事件的响应过程。

           当找到最适合处理事件的view(first responder)时,如果当前view实现了touces方法,则该事件由当前view来接受,如果方法内部实现了super touches方法,则该事件会沿着响应链往上传递,接着上一个响应者(next responder)就会调用touches方法处理事件。

           如果当前响应者不实现touches方法,则系统默认把事件向上级响应者传递。

           以此特性可以重写自己的touches方法和父控件的touches方法达到一个事件多个对象处理的目的。

    下一个响应者(next responder)的指向规则为:

    UIView

    如果 view 是一个 view controller 的 root view,nextResponder 是这个 view controller.

    如果 view 不是 view controller 的 root view,nextResponder 则是这个 view 的 superview

    UIViewController

    如果 view controller 的 view 是 window 的 root view, view controller 的 nextResponder 是这个 window

    如果 view controller 是被其他 view controller presented调起来的,那么 view controller 的 nextResponder 就是发起调起的那个 view controller

    UIWindow

    window 的 nextResponder 是 UIApplication 对象。

    UIApplication

    UIApplication 对象的 nextResponder 是 app delegate, 但是 app delegate 必须是 UIResponder 对象,并且不能使是view ,view controller 或  UIApplication 对象他本身。

    以上是事件响应的过程。

    总结起来,事件触发以后处理过程分为事件的传递事件的响应两部分。

    事件的传递由父控件向子控件传递(由底层向离用户近的view传递):

    UIApplication -> UIWindow -> UIView -> responsive view。

    事件的响应由子控件向父控件传递(由离用户最近的view向底层传递):

    responsive view –> super view –> …..–> view controller –> window –> Application。

    2、什么是内存泄漏?你见过那些内存泄漏?用什么工具检测内存泄漏?

           答:内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果引用自《百度百科-内存泄漏》。


           iOS在ARC(自动管理内存)机制下导致内存泄漏的根本原因是循环强引用,循环强引用的根本原因是对象的引用计数器(retainCount )无法归零

    常见的循环强引用有:

    1、代理模式中,声明代理属性时,如果用strong修饰delegate,则会发生循环强引用的问题,self在main函数结束之前,引用计数器为1,遵循代理以后,delegate又引用了一次self,此时self的retainCount == 2,当main 函数结束以后,self引用计数器减1,此时self的retainCount == 1,声明delegate的viewControllerA仍然被self引用。viewControllerA的retainCount == 1。此时viewControllerA和viewControllerB互相引用,不能释放。所以要用weak修饰delegate避免循环强引用问题。

    //viewControllerA

    声明代理属性

    //viewControllerB

    遵循代理方

    2、ViewController中使用Block时,如果Block内部引用了self的某些对象,因为self本身已经持有Block,所以会发生循环强引用。

    一般情况下,Block都用于两个类之间的相互传值,只有一个类的话,使用Block意义就不大了。

    3、ViewController中的NSTimer方法,如果调用之后不进行invalidate的话,会使定时器种方法循环执行,不能释放。

    4、ViewController的子视图,如自定义cell,如果引用了ViewController的某个对象,可能会造成循环强引用,所以一般在子视图用到父视图的对象时,用weak或者assign修饰。

           检测内存泄漏的工具有:

    1、Analyze静态分析(xcode-->product-->Analyze)

           静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,如果需要,我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments中的Leaks方法进行排查。

    2、Instrument的Leaks(检查Leaked memory内存泄漏)或者Allocations(检查Abandoned memory的内存泄漏)(xcode-->product-->profile)。

    从苹果的开发者文档里可以看到,一个 app 的内存分三类:

    Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

    Abandoned memory: Memory still referenced by your application that has no useful purpose.

    Cached memory: Memory still referenced by your application that might be used again for better performance.

    Leaked memory和Abandoned memory都属于内存泄漏(应该释放而没有释放的内存),在MRC下Leaked memory内存泄露很容易出现,因为很容易忘记release,这种内存泄漏是已经不被引用了(retainCount == 0了),但是没有被释放。而在ARC下,大多数内存泄漏都是循环强引用造成的,这种内存泄漏称为Abandoned memory。

    3、tableView的内存/性能优化和iOS性能优化?

           答:tableView内存优化核心思想是:

           1、cell的复用机制,在使用cell时,只会创建一屏幕多一个的cell,例如:如果一屏幕能装下10个cell,系统只需要创建11个cell,当显示第11个cell时,第一个cell正在失去焦点,当滑动到第12个cell时,第一个cell已经完全失去焦点,放在缓存池等待复用。此时第12个cell就会使用cellIdentifier标识去缓存池中寻找相对应的cell,如果存在,就不用创建新的cell,直接从缓存池中取出来重新赋值。这样做的目的是为了防止cell创建过多浪费内存。

           2、cell滑动时数据( 图片)按需加载,在cell数据过于庞大时,快速滑动很容易造成页面卡桢,对此问题,我首先想到的是数据的异步加载,但异步加载在滑动时,操作依然会被执行,开启线程过多时,对整个app的性能也会造成很大的影响。

           此时可以利用父类UIScrollView的两个代理来解决这个问题。

    方法一:- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate(此方法在停止拖拽时执行)

    方法二:- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView(此方法在减速停止时执行)

           可利用此方法在tableView停止滑动时,进行图片加载。加载前,调用 indexPathsForVisibleRows或者visibleCells遍历出可见部分的indexPath数组或者cell数组,然后按需加载。

           也可以在-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法中通过截取tableView的当前状态来进行图片异步加载操作。

           具体如下图:

    cell按需加载示例

    最后也别忘记在内存紧张的情况下释放调所有的异步线程,以保证的你的app不会被系统强制关闭

    - (void)didReceiveMemoryWarning{

    //  释放调异步加载图片的线程以及所有图片资源对象

    }

    还有千万别忘记销毁的时候手动把所有的使用到的代理设置nil

           3、提前计算并缓存好cell高度。在此给大家推荐一个由国人团队开发的优化计算 UITableViewCell 高度的轻量级框架UITableView+FDTemplateLayoutCell。

           4、加载多个Views的时候,尽量设置为不透明,opaque = YES,不然系统在显示view时,要计算多个view的叠加颜色,很浪费性能。

           5、在添加view时,尽量在初始化的时候都添加上,在使用时用hidden来控制是否显示,避免view的动态添加浪费性能。


    休息一下

     

     4、全局变量、全局静态变量、局部静态变量?

    答:先引用一下百度百科官方定义《静态变量-引自百度百科》,

    在OC中,三者相同之处是三者都在堆上分配内存,变量的生命周期和整个进程相同。区别主要在于作用域的不同。

    1、全局变量:也称为外部全局变量,该变量不仅可以在所定义的文件内被访问,也可以在其他文件中被访问。定义在方法外部的变量,除静态变量之外,都是外部全局变量。

    2、全局(局部)静态变量:在声明变量时,加上static关键字即为静态变量。全局静态变量和局部静态变量的区别在于声明的位置。定义在方法外的静态变量称为全局静态变量。定义在方法内部的静态变量称为局部静态变量。全局静态变量的作用域为当前文件(类),局部静态变量的作用域为方法内部。

    推荐一篇我感觉还不错的博文《objective-c--静态变量,外部全局变量,常量总结》。

     5、有使用过svn/git吗?git常用的命令有哪些

           答:都用过,目前正在使用的是gitlab,工作中git常用命令有:

            git clone(克隆代码到本地)   git add.(代码添加到本地临时仓)   git commit -m(提交代码到本地仓库)  git push origin(同步代码到远程仓) git branch(查看当前分支) git checkout -b(创建并切换到新分支) 等。

     6、什么是设计模式?常用设计模式有哪些?分别描述一下。

           答:按照惯例,仍然先引用一下设计模式的百度百科官方定义《设计模式-百度百科》。

           简单的理解,设计模式就是一种编程思想,用一套成熟解决方案去处理某一类型的具体问题。

           iOS 中,常用的设计模式有如下几种:

    一、代理模式:当一个类的某些功能需要由别的类来实现,但是又不确定具体会是哪个类实现的时候,会运用到代理模式,委托方制定协议方法,只要满足条件的类都可以作为代理。

    二、单例模式:单例就相当于一个全局变量,不论在哪里需要用到这个类的实例变量,都可以通过单例方法来取得一旦创建了一个单例类,不论你在多少个界面中初始化调用了这个单例方法取得对象,它们所有的对象都是指向的同一块内存存储空间、即单例类保证了该类的实例对象是唯一存在的。

    三、工厂模式:当很多不同的方法,不同的场景需要用到同一类型的对象时,如果每次都初始化,会有很多重复或者类似的代码,此时用到工厂方法,可以在不同场景直接调用该类方法,减少代码量与代码臃肿度。

    四、观察者模式:观察者模式是一种一对多的依赖关系,可以让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。观察者模式一般分为KVO(Key Value Observing)、Notification两种。

    五、MVC/MVVM模式:通过数据模型(Model),控制器逻辑(Controller),视图展示(View)将应用程序进行逻辑划分。斯坦福大学iOS开发公开课中对MVC的解释非常的形象:

    Model= What your applicatioin is(not how it isdisplayed);

    Controller= How your Model is presented to the users(UIlogic);

    View= Your Controller’s minions.

    中文理解就是:

    Model就是APP的构成成分,但并不代表它是如何展示给用户的;

    Controller就是Model展示给用户的方式,代表了UI的逻辑;

    View犹如Controller的小黄人,指呈现给用户的界面。

    六、策略者模式:官方定义:The Strategy Pattern definesa family of algorithms,encapsulates each one,and makes them interchangeable.Strategy lets the algorithm vary independently from clients that use it.

    个人理解:策略模式就是封装起来的一系列算法,这些算法独立于用户独立变化,不同的算法以不同的方式实现接口。用户使用时不需要再使用if-else判断执行的算法而是直接调用不同的接口。iOS开发中常见于验证码或者密码的数字或者字母的限定。

    7、KVO/KVC的底层实现?

           KVO(Key-Value Observing),是一种观察者设计模式(另一种观察者模式是Notification),当指定的对象的属性被修改后,则其观察者就会接受到通知。简单的说就是每次指定的被观察对象的属性被修改后,KVO就会自动通知相应的观察者了。

           KVO使用简单,底层实现复杂,故称之为黑魔法(isa-swizzling)。具体的实现过程如下:

    被监听者Person类 监听者类

           当观察对象Person时,KVO机制动态创建一个继承自Person的名为 NSKVONotifying_Person的新类,且为NSKVONotifying_Person重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

           在这个过程,被观察对象的 isa 指针从指向原来的Person类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_Person,来实现当前类属性值改变的监听;  从应用层看,我们完全没有意识到有新的类出现,这是因为系统“隐瞒”了对KVO的底层实现过程。但是此时如果我们创建一个新的名为“NSKVONotifying_Person”的类,就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_Person的中间类,并让isa指针指向这个中间类了。(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就变成新子类的对象了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

                  KVO为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:上述例子中,当 person.name 的值改变时,Person对象的 isa 指针会指向 NSKVONotifying_Person,意味着,在程序运行时,会动态生成一个 NSKVONotifying_Person 类,该类继承于 Person,而且该类中也有个 -setName: 方法,方法中在设置 name 的同时实现了:

    动态生成的NSKVONotifying_Person类

           KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法,这种继承方式是在运行时而不是编译时实现的。此时已修改后的属性值,已准备就绪,等待被使用。

    8、iOS有几种多线程?各有什么特点?

    答:开启多线程有四种方式:

    1、pthread  2、NSThread  3、GCD  4、NSOperationQueue

          pthread 是一套通用的多线程的 API,可以在Unix / Linux / Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,我们在 iOS 开发中几乎不使用 pthread,仅做了解。以下引自《百度百科 - pthread》:

           POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

    其中iOS有三种多线程方式:

           1、NSThread

           NSThread创建线程的方式有两种:

           NSThread*thread = [[NSThread alloc]  initWithTarget:self selector:@selector(test:) object:nil];

           [NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:nil];

           2、GCD

                  GCD示例链接

           3、NSOperationQueue

                  NSOperation示例链接

    9、iOS常用的加锁方式?各有什么特点?

    加锁的目的通常是为了线程同步,避免多个线程同时访问某一块内存造成数据的不安全,某个线程加了锁之后,操作完再把锁释放,期间要是有其他线程想去访问被加锁的变量就需要先拿到锁,而锁没有释放的话,就只有等待,直到拥有锁的线程把锁释放,才可以继续操作。

    市面上常见的枷锁方式有以下几种:

    1、@synchronized关键字锁

    2、NSLock 对象锁(NSOperation&NSOperationQueue)

    3、NSRecursiveLock 递归锁

    4、NSCondition断言

    5、NSConditionLock 条件锁

    6、pthread_mutex 互斥锁(C语言)

    7、dispatch_semaphore 信号量(GCD)

    8、OSSpinLock(效率最快 有安全问题)

    具体使用看这里或者这里

    相关文章

      网友评论

        本文标题:iOS面试中经常遇到的问题(原理篇一)

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