美文网首页2020 iOS 面试题
面了20多家总结出来的部分iOS面试题(四)

面了20多家总结出来的部分iOS面试题(四)

作者: JoeyM | 来源:发表于2020-08-07 16:45 被阅读0次

    23. 有没有使用过performSelector?

    • 这题主要是想问的是有没有动态添加过方法
    • 话不多说上代码
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *p = [[Person alloc] init];
    
        // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
        // 动态添加方法就不会报错
        [p performSelector:@selector(eat)];
    }
    
    @end
    
    
    @implementation Person
    
    // **这里真是奇葩, 实在想不到什么时候才有这种使用场景, 我再外面找不到方法, 我再当前类里面直接在写一个方法就好咯,干嘛要在这里写这个玩意, 还要写一个C语言的东西, 既然面试想问, 那咱就要会!**
    
    // void(*)()
    // 默认方法都有两个隐式参数,
    void eat(id self,SEL sel)
    {
        NSLog(@"%@ %@",self,NSStringFromSelector(sel));
    }
    
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if (sel == @selector(eat)) {
            // 动态添加eat方法
    
            // 第一个参数:给哪个类添加方法
            // 第二个参数:添加方法的方法编号
            // 第三个参数:添加方法的函数实现(函数地址)
            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
            class_addMethod(self, @selector(eat), eat, "v@:");
        }
        return [super resolveInstanceMethod:sel];
    }
    @end
    
    • 当然面试的时候也可能问你这个
    // 延时操作 和GCD的after 一个效果
    [p performSelector:@selector(eat) withObject:nil afterDelay:4];
    
    • 你以为完了? 错了,大概率面试官会问你,*** 上面这段代码放在子线程中 是什么样子的?为什么?**

      —首先 上面这个方法其实就是内部创建了一个NSTimer定时器,然后这个定时器会添加在当前的RunLoop中所以上面代码放到子线程中不会有任何定时器相关方法被执行,如果想要执行,开启当前线程即可 即

    [[NSRunLoop currentRunLoop] run];
    
    // 完整调用
     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(queue, ^{
            //  [[NSRunLoop currentRunLoop] run]; 放在上面执行时不可以的,因为当前只是开启了runloop 里面没有任何事件(source,timer,observer)也是开启失败的
             [self performSelector:@selector(test) withObject:nil afterDelay:2];
             [[NSRunLoop currentRunLoop] run];
    });
    
    // 由此我自行又做了一个测试, 把        
    [self performSelector:@selector(test)];
    在子线程调用,是没有任何问题的。
    
    // 我又测试了一下,
     [self performSelector:@selector(test) withObject:nil afterDelay:2];
     这个方法在主线程执行  打印线程是1
    
    在子线程中调用打印线程 非1
    
    • 然后面试官开始飘了, 开始问你关于NSTimer相关问题?怎么办? 答: 搞他!

    引申 NSTimer在子线程执行?

    • NSTimer直接在在子线程是不会被调用的, 想要执行请开启当前的Runloop 。具体开启方案上面题有说,不赘述。

    引申 为什么说NSTimer不准确?

    • NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期 减少误差的方法 代码如下
    // 在子线程中开启NStimer,或者更改当前Runloop的Mode 为NSRunLoopCommonModes
    [[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    
    // 利用CADisplayLink (iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高)
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    
    // 利用GCD
    NSTimeInterval interval = 1.0;
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        NSLog(@"GCD timer test");
    });
    dispatch_resume(_timer);
    

    24. 为什么AFN3.0中需要设置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0却不需要?

    • 功能不一样, 2.x是基于NSURLConnection的,其内部实现要在异步并发,所以不能设置1。 3.0 是基于NSURLSession其内部是需要串行的鉴于一些多线程数据访问的安全性考虑, 设置这个达到串行回调的效果。

    AFNetworking 2.0 和3.0 的区别?

    • AFN3.0剔除了所有的NSURLConnection请求的API
    • AFN3.0使用NSOperationQueue代替AFN2.0的常驻线程

    2.x版本常驻线程的分析

    • 在请求完成后我们需要对数据进行一些序列化处理,或者错误处理。如果我们在主线中处理这些事情很明显是不合理的。不仅会导致UI的卡顿,甚至受到默认的RunLoopModel的影响,我们在滑动tableview的时候,会导致时间的处理停止。

    • 这里时候我们就需要一个子线程来处理事件和网络请求的回调了。但是,子线程在处理完事件后就会自动结束生命周期,这个时候后面的一些网络请求得回调我们就无法接收了。所以我们就需要开启子线程的RunLoop来保存线程的常驻。

    • 当然我们可以每次发起一个请求就开启一条子线程,但是这个想一下就知道开销有多大了。所以这个时候保活一条线程来对请求得回调处理是比较好的一个方案。

    3.x版本不在常驻线程的分析?

    • 在3.x的AFN版本中使用的是NSURLSession进行封装。对比于NSURLConnection,NSURLSession不需要在当前的线程等待网络回调,而是可以让开发者自己设定需要回调的队列。

    • 所以在3.x版本中AFN使用了NSOperationQueue对网络回调的管理,并且设置maxConcurrentOperationCount为1,保证了最大的并发数为1,也就是说让网络请求串行执行。避免了多线程环境下的资源抢夺问题。

    25. autoreleasePool 在何时被释放?

    • ARC中所有的新生对象都是 自动加autorelese的, @atuorelesepool 大部分时候解决了瞬时内存暴增的问题 。
    • MRC中的情况 关键词变了NSAutoreleasePool。
    //来自Apple文档,见参考
    NSArray *urls = <# An array of file URLs #>;
    for (NSURL *url in urls) { 
      @autoreleasepool { 
            NSError *error;
            NSString *fileContents = [NSString stringWithContentsOfURL:urlencoding:NSUTF8StringEncoding error:&error]; 
    }
    
    // 如果循环次数非常多,而且循环体里面的对象都是临时创建使用的,就可以用@autoreleasepool 包起来,让每次循环结束时,可以及时释放临时对象的内存
    
    // for 和 for in 里面是没有自动包装@autoreleasepool着的,而下面的方法是由@autoreleasepool自动包围的
    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        // 这里被一个局部@autoreleasepool包围着
    }];
    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString* str = [[[NSString alloc] initWithString:@"666"] autorelease];
    [pool drain];
    
    // 其作用于为drain 和 init 之间
    
    • 回归正题@autoReleasePool什么时间释放?
      • 一个被autoreleasepool包裹生成得对象,都会在其创建生成之后自动添加autorelease, 然后被autorelease对象得释放时机 就是在当前runloop循环结束的时候自动释放的
      • 参考链接:http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

    子线程中的autorelease变量什么时候释放?

    • 子线程中会默认包裹一个autoreleasepool的, 释放时机是当前线程退出的时候。

    autoreleasepool是如何实现的?

    • @autoreleasepool{} 本质上是一个结构体:
    • autoreleasepool会被转换成__AtAutoreleasePool
    • __AtAutoreleasePool 里面有两个函数objc_autoreleasePoolPush(),objc_autoreleasePoolPop().,其实一些列下来之后实际上调用得是AutoreleasePoolPage类中得push 和 pop两个类方法
    • push就是压栈操作,
    • pop就是出栈操作于此同时对其对象发送release消息进行释放

    26. iOS界面渲染机制? [这是很大的一个模块,里面牵扯很多东西, 耐心看下去]

    • 先简单解释一下渲染机制

    首先iOS渲染视图的核心是Core Animation,其渲染层次依次为:图层树->呈现树->渲染树

    • 一共三个阶段

    • CPU阶段(进行Frame布局,准备视图和图层之间的层级关系)

    • OpenGL ES阶段(iOS8以后改成Metal), (渲染服务把上面提供的图层上色,生成各种帧)

    • GPU阶段 (把上面操作的东西进行一些列的操作,最终展示到屏幕上面)

    • 稍微详细说明

    • 首先一个视图由CPU进行Frame布局,准备视图和图层的层及关系。

    • CUP会将处理视图和图层的层级关系打包,通过IPC(进程间的通信)通道提交给渲染服务(OpenGL和GPU)

    • 渲染服务首先将图层交给OpenGL进行纹理生成和着色,生成前后帧缓存,再根据硬件的刷新帧率,一般以设备的VSync信号和CADisplayLink(类似一个刷新UI专用的定时器)为标准,进行前后帧缓存的切换

    • 最后,将最终 要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换, 应用纹理混合,最终显示在屏幕上。

    程序卡顿的原因?

    • 正常渲染流程
    • CPU计算完成之后交给GPU,来个同步信号Vsync 将内容渲染到屏幕上
    • 非正常(卡顿/掉帧)的流程
    • CPU计算时间正常或者慢,GPU渲染时间长了, 这时候Vsync信号, 由于没有绘制完全,CUP开始计算下一帧,当下一帧正常绘制成功之后,把当前没有绘制完成的帧丢弃, 显示了下一帧,于是这样就造成了卡顿。
      需要注意的是:Vsync时间间隔是固定的, 比如60帧率大的Vsync 是每16ms就执行一个一次,类似定时器一样

    这里会出现一个面试题!!!
    题目如下:

    • 从第一次打开App到完全开始展现出UI,中间发生了什么? 或者App是怎么渲染某一个View的?
    • 回答就是上面的稍微详细说明,如果要求更详细, 可以继续深究一下。

    在科普一下
    1.Core Animation
    Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

    2.CPU渲染职能

    • 布局计算:如果视图层级过于复杂,当试图呈现或者修改的时候,计算图层帧率就会消耗一部分时间,
    • 视图懒加载: iOS只会当视图控制器的视图显示到屏幕上才会加载它,这对内存使用和程序启动时间很有好处,但是当呈现到屏幕之前,按下按钮导致的许多工作都不会被及时响应。比如,控制器从数据局中获取数据, 或者视图从一个xib加载,或者涉及iO图片显示都会比CPU正常操作慢得多。
    • 解压图片:PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对 图层内容赋值的时候(直接或者间接使用 UIImageView )或者把它绘制到 Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。
    • Core Graphics绘制:如果对视图实现了drawRect:或drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
    • 图层打包:当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示 屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环 转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须 要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层 级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用 程序可控的。

    3.GPU渲染职能
    GPU会根据生成的前后帧缓存数据,根据实际情况进行合成,其中造成GPU渲染负担的一般是:离屏渲染,图层混合,延迟加载。

    这里又会出现一个面试题!!!
    一个UIImageView添加到视图上以后,内部如何渲染到手机上的?

    图片显示分为三个步骤: 加载、解码、渲染
    通常,我们程序员的操作只是加载,至于解码和渲染是由UIKit内部进行的。
    例如:UIImageView显示在屏幕上的时候需要UIImage对象进行数据源的赋值。而UIImage持有的数据是未解码的压缩数据,当赋值的时候,图像数据会被解码变成RGB颜色数据,最终渲染到屏幕上。


    看完上面的又来问题了!
    关于UITableView优化的问题?(真他妈子子孙孙无穷尽也~)
    先说造成UITableView滚动时候卡顿的的原因有哪些?

    • 隐式绘制 CGContext
    • 文本CATextLayer 和 UILabel
    • 光栅化 shouldRasterize
    • 离屏渲染
    • 可伸缩图片
    • shadowPath
    • 混合和过度绘制
    • 减少图层数量
    • 裁切
    • 对象回收
    • Core Graphics绘制
    • -renderInContext: 方法

    在说关于UITableView的优化问题!

    基础的

    • 重用机制(缓存池)
    • 少用有透明度的View
    • 尽量避免使用xib
    • 尽量避免过多的层级结构
    • iOS8以后出的预估高度
    • 减少离屏渲染操作(圆角、阴影啥的)

    • **** 解释一下为什么减少离屏渲染操作?****
    • 需要创建新的缓冲区
    • 整个过程需要多次切换上下文环境, 显示从当前的屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果 显示到屏幕有上, 又要将上下文环境从离屏切换到当前屏幕,
    • ****那些操作会触发离屏渲染?****
    • 光栅化 layer.shouldRasterize = YES
    • 遮罩layer.mask
    • 圆角layer.maskToBounds = Yes,Layer.cornerRadis 大于0
    • 阴影layer.shadowXXX

    进阶的

    • 缓存cell的高度(提前计算好cell的高度,缓存进当前的模型里面)
    • 异步绘制
    • 滑动的时候,按需加载

    高阶的

    • 你想不到 竟然不推荐用UILabel。哈哈哈~ 至于为什么 看下面的链接吧

    至于上面的那些基础的,涉及到渲染级别的自己说的时候悠着点,面试官如果想搞你的话,考一考你最上面的那些,CUP和GUP,以及openGL相关, 在考一下你进程通信IPC,以及VSync信号啥的, 这些东西太鸡儿高深了,没点匠心 这东西还真搞不了,要想研究可以看看YYKit的作者写的一篇关于页面流畅的文章:https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/

    卡顿检测的方法

    • 卡顿就是主线程阻塞的时间问题,可以添加Observer到主线程Runloop中,通过监听Runloop状态切换的耗时,以达到监听卡顿的目的

    继续

    既然都是图形绘制了,那就再研究一下事件响应链&原理

    传统的问法来了:UIView和CALayer的区别?
    通常我们这样回答:UIView可以响应用户事件,而CALayer不能处理事件


    回答这个之前, 先回顾一下另外一个经典面试题:事件响应链和事件传递?

    基本概念:

    • 响应链: 是由链接在一起的响应者(UIResponse子类)组成的,一般为第一响应着到application对象以及中间所有响应者一起组成的。

    • 事件传递: 获取响应链之后, 将事件由第一响应者网application的传递过程

    • enter image description here
    • enter image description here
    • 事件的分发和传递

    • 当程序中发生触摸事件之后,系统会将事件添加到UIApplication管理的一个队列当中

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

    • UIWindow将事件向下分发,即UIView或者UIViewController

    • UIView首先看自己能否处理这个事件,触摸点是否在自己身上,自己的透明度是否大于0,01,userInteractionEnabled 是否是YES, Hidden实际是NO,如果这些都满足,那么继续寻找其子视图

    • 遍历子控件,重复上面步骤

    • 如果没有找到,那么自己就是改事件的处理者

    • 如果自己不能处理,那么就不做任何处理 即视为没有合适的View能接收处理当前事件,则改事件会被废弃。

    • *** 怎么寻找当前触摸的是哪一个View?***
      下面中两个方法
    // 此方法返回的View是本次点击事件需要的最佳View
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    
    // 判断一个点是否落在范围内
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    

    事件传递给控件之后, 就会调用hitTest:withEvent方法去寻找更合适的View,如果当前View存在子控件,则在子控件继续调用hitTest:withEvent方法判断是否是合适的View, 如果还不是就一直遍历寻找, 找不到的话直接废弃掉。

    // 因为所有的视图类都是继承BaseView
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
       // 1.判断当前控件能否接收事件
       if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
       // 2. 判断点在不在当前控件
       if ([self pointInside:point withEvent:event] == NO) return nil;
       // 3.从后往前遍历自己的子控件
       NSInteger count = self.subviews.count;
       for (NSInteger i = count - 1; i >= 0; i--) {
          UIView *childView = self.subviews[I];
           // 把当前控件上的坐标系转换成子控件上的坐标系
          CGPoint childP = [self convertPoint:point toView:childView];
          UIView *fitView = [childView hitTest:childP withEvent:event];
           if (fitView) { // 寻找到最合适的view
               return fitView;
           }
       }
       // 循环结束,表示没有比自己更合适的view
       return self;
       
    }
    
    • 判断触摸点是否在视图内?
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    
    • tableView 加一个tap的手势, 点击当前cell的位置 哪个事件被响应 为什么?
    • tap事件被响应, 因为tap事件添加之后,默认是取消当前tap以外的所有事件的, 也就是说, tap事件处于当前响应者链的最顶端, 解决的办法执行tap的delagete, 实现
    -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
    {  
        if([touch.view isKindOfClass:[XXXXcell class]])  
        {  
            return NO;  
        }  
        return YES;  
    }
    
    

    相关文章

      网友评论

        本文标题:面了20多家总结出来的部分iOS面试题(四)

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