2、面试整理-视图相关面试题整理

108 阅读5分钟

1、简单的说说UIApplication的作用及继承关系?

(1)UIApplication是继承UIResponder的。UIApplication是整个应用程序的核心。每一个程序在运行期必须有UIApplication(或子类)的一个实例(有且只有一 个),通过[UIApplication shareApplication]可以得到这个单例实例。

(2)UIApplication帮助管理应用程序的生命周期,通过delegate来履行任务。

(3)UIApplication可以接收事件,把所有用户事件都放入队列,逐个处理。它会发送 当前事件给一个合适的目标控件进行处理。他还将部分事件转给delegate对象来处理。 delegate可以处理的事件包括:应用程序的生命周期事件(如程序启动与关闭)、系统事 件(如来电)。

2、UIView和CALayer是什么关系?

(1)UIView是iOS系统中界面元素的继承。所有的界面元素都继承自它,他本身完全是由CoreAnimation来实现的。它真正的绘图部分,是由一个叫CALayer的类来管理。UIView本身更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,如frame,bounds等,实际上内部都是在访问它所包含的CALayer的相关属性。

(2)UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示。

(3)UIView的CALayer类似UIView的子View树形结构,也可以向它的layer上添加子layer。

(4)CALayer坐标系统与UIView有点不一样,它多了一个anchorPoint的属性。

(5)UIView的layer树形在系统内部,被系统维护着三份copy。逻辑树,动画树,显示树。

(6)CALayer默认修改属性支持隐式动画

(7)View可以接受并处理事件,Layer不可以。

3、如何优化UITableView?

(1)复用单元格

(2)使用不透明的视图,单元格中少使用动画。

(3)图片使用异步加载,设置图片加载的并发数。

(4)滑动时不加载图片,停止滑动时再加载。

(5)图片和文字可以直接drawRect

(6)如果cell是动态行高,计算缓存单元格高度

(7)尽量少reloadData,只reloadRowsAtIndexPaths

(8)cell高度固定直接用rowHeight属性设置高度。

4、loadView的作用?

  • loadView用来自定义view,只要实现了这个方法,其他通过xib或storyboard创建的view都不会被加载
  • 看懂控制器view创建的这个图就行

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMzYyMDY3Ni00YTk2NTExMmNlNDEzYzcwLnBuZw.png

5、如何高性能的给 UIImageView 加个圆角?

不好的解决方案

使用下面的方式会强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响,会有卡顿的现象出现

self.view.layer.cornerRadius = 5;
self.view.layer.masksToBounds = YES;

正确的解决方案:使用绘图技术

    - (UIImage *)circleImage
    {
        // NO代表透明
        UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);

        // 获得上下文
        CGContextRef ctx = UIGraphicsGetCurrentContext();

        // 添加一个圆
        CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
        CGContextAddEllipseInRect(ctx, rect);

        // 裁剪
        CGContextClip(ctx);

        // 将图片画上去
        [self drawInRect:rect];

        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

        // 关闭上下文
        UIGraphicsEndImageContext();

        return image;
    }

还有一种方案:使用了贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
                       cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];

6、使用drawRect有什么影响?

  • drawRect方法依赖Core Graphics框架来进行自定义的绘制
  • 缺点:它处理touch事件时每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了
  • 这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制

7、描述下SDWebImage里面给UIImageView加载图片的逻辑?

  • SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后在替换占位图片 - 加载图片的过程大致如下:
  • 首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
  • 如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
  • 如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
  • 下载后的图片会加入缓存中,并写入磁盘中
  • 整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来

8、触摸事件的传递?

  • 触摸事件的传递是从父控件传递到子控件
  • 如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件
  • 不能接受触摸事件的四种情况
    • 不接收用户交互,即:userInteractionEnabled = NO
    • 隐藏,即:hidden = YES
    • 透明,即:alpha <= 0.01
    • 未启用,即:enabled = NO
  • 提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的
  • 如何找到最合适处理事件的控件:
    • 首先,判断自己能否接收触摸事件
    • 可以通过重写hitTest:withEvent:方法验证
    • 其次,判断触摸点是否在自己身上
    • 对应方法pointInside:withEvent:
    • 从后往前(先遍历最后添加的子控件)遍历子控件,重复前面的两个步骤
    • 如果没有符合条件的子控件,那么就自己处理

9、事件响应者链

  • 如果当前view是控制器的view,那么就传递给控制器
  • 如果控制器不存在,则将其传递给它的父控件
  • 在视图层次结构的最顶层视图也不能处理接收到的事件或消息,则将事件或消息传递给UIWindow对象进行处理
  • 如果UIWindow对象也不处理,则将事件或消息传递给UIApplication对象
  • 如果UIApplication也不能处理该事件或消息,则将其丢弃
  • 补充:如何判断上一个响应者
    • 如果当前这个view是控制器的view,那么控制器就是上一个响应者
    • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMzYyMDY3Ni05Y2U2ZjU4OTNkODkyNGE0LnBuZw.png

10、UIView的绘制原理

20200310111109366.png

当我们调用UIView的setNeedsDisplay方法以后,实际上并没有立刻发生当前视图的绘制工作,而是在之后的某一时机才会进行当前视图的绘制 。为什么没有立刻发生当前视图的绘制工作?(因为到当前runloop将要结束的时候,才会开始介入到UI视图的绘制流程中)

  • 当调用UIView的setNeedsDisplay后
  • 系统会立刻调用view的layer的同名方法[view.layer setNeedsDisplay],之后相当于在layer上面打上了一个脏标记
  • 然后再当前runloop将要结束的时候,才会调用CALayer的display函数方法,然后才进入到当前视图的真正绘制工作的流程当中
  • CALayer的display方法,在内部会首先判断layer的delegate是否响应displayLayer这个方法
  • 若不响应,则系统开始绘制流程
  • 若响应,则开始异步绘制

系统绘制流程

20200310112413819.png

  • 首先CALayer内部会创建一个CGContextRef,在drawRect方法中,可以通过上下文堆栈当中的取出这个context,拿到的就是当前控件或者说视图的上下文或者说是backing store
  • 然后layer会判断它是否有代理,若没有,则调用CALayer的drawInContext
  • 若有则调用代理方法,然后做当前视图的绘制工作(这一步发生在系统内部当中),再在合适的时机,基于drawRect回调方法,
  • drawRect默认操作是什么都不做,而之所以有这个接口,就是为了让我们在系统绘制之上,可以做些自定义的绘制工作。
  • 最后再由CALayer上传对应的backing store给GPU,这里的backing store我们可以理解为位图。

异步绘制流程

-[layer.delegate displayLayer:]

基于layer的delegate,如果实现了displayLayer方法,就可以进入到异步绘制流程当中

  1. 在异步绘制过程中, 需要代理去负责生成对应的bitmap
  2. 设置该bitmap作为layer.contents属性的值 异步绘制的机制和流程

20200310113303236.png

  • 左侧是主队列,右侧是全局并发队列
  • 假如在某一时机调用了setNeedsDiaplay方法后
  • 在当前runloop将要结束的时候,会有系统调用视图所对应layer的display方法
  • 如果代理实现了displayLayer方法,会调用这个代理的displayLayer这个方法
  • 然后通过子线程的切换,我们会在子线程中去做位图的绘制,此时主线程可以去做些其他的工作
  • 然后再回到主队列中,提交这个位图,设置给CALayer的contents属性 子线程的绘制
  1. 通过CGBitmapContextCreat方法,来创建一个位图的上下文
  2. 通过CoreGraphic的相关API,可以做当前UI控件的一些绘制工作
  3. 之后通过CGBitmapContextCreatImage方法,根据所绘制的上下文,生成一张CGImage图片

11、UIView的frame、bounds跟center属性的区别?

  • frame:描述当前视图在其父视图中的位置和大小;
  • bounds:描述当前视图在其自身坐标系统中的位置和大小;
  • center:描述当前视图的中心点在其父视图中的位置;

5371035-a496bfe22e5f019c.webp

// frame 和 bounds 属性都是CGRect类型的,难免认为功能相似
- (CGRect)frame{
     return CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width,self.frame.origin.height);
 }
 
- (CGRect)bounds{
   return CGRectMake(0,0,self.bounds.size.width,self.bounds.size.height);
 }
// bounds属性高于frame属性,任何视图都一定存在bounds属性,但不是所有视图都存在frame属性的,某种程度上frame属性的存在依存于bounds属性。 bounds --> 指定坐标系 --> frame来参照指定的坐标系

5371035-5f165563490136ae.webp

8b5e571a270b4a62983aad29bbff3751.png

优秀参考:

blog.ibireme.com/2015/11/12/…