iOS-YZWL公司面试题

作者: 路飞_Luck | 来源:发表于2019-06-11 19:48 被阅读121次
    目录
    • UIWindow,UIView,CALayer的区别
    • 事件传递和响应机制
    • UIView block动画实现原理
    一 UIWindow,UIView,CALayer的区别
    1.1 UIWindow
    @interface UIWindow : UIView
    
    @property(nonatomic) UIWindowLevel windowLevel;                   // default = 0.0
    @property(nonatomic,readonly,getter=isKeyWindow) BOOL keyWindow;
    - (void)becomeKeyWindow;                               // override point for subclass. Do not call directly
    - (void)resignKeyWindow;                               // override point for subclass. Do not call directly
    
    - (void)makeKeyWindow;
    - (void)makeKeyAndVisible;                             // convenience. most apps call this to show the main window and also make it key. otherwise use view hidden property
    
    @property(nullable, nonatomic,strong) UIViewController *rootViewController NS_AVAILABLE_IOS(4_0);  // default is nil
    @end
    

    继承自UIView,是一种特殊的 UIView通常在一个app中只会有一个keyUIWindow。

    iOS程序启动完毕后,创建的第一个视图控件就是UIWindow,接着创建控制器的view,最后将控制器的view添加到UIWindow上,于是控制器的view就显示在屏幕上了

    主要作用是提供一个区域用来显示UIView;将事件分发给UIView;与UIViewController一起处理屏幕的旋转事件。

    相关参考 iOS开发UI篇—UIWindow简单介绍

    1.2 UIView
    @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>
    
    @property(nonatomic,readonly,strong) CALayer  *layer;
    @end
    
    @interface UIResponder : NSObject <UIResponderStandardEditActions>
    

    继承自UIResponder,间接继承自NSObject,主要是用来构建用户界面的,并且可以响应事件。

    对于UIView,侧重于对内容的显示管理;其实是相对于CALayer的高层封装。

    相关参考 iOS学习——UIView的研究

    1.3 CALayer
    @interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
    

    直接继承自NSObject,所以不能响应事件

    其实就是一个图层,UIView之所以能显示在屏幕上,主要是它内部有一个CALayer对象。在创建UIView时,它内部会自动创建一个图层,当UIView需要显示在屏幕上的时候,会调用drawRect:方法进行绘图,并且会将所有内容绘制到自己的图层上,绘图完毕后,系统会将图层拷贝到屏幕上,这样完成UIView的显示。

    image
    • layer给view提供了基础设施,使得绘制内容和呈现更高效动画更容易、更低耗
    • layer不参与view的事件处理、不参与响应链

    相关参考 【重读iOS】认识CALayer

    二 事件传递和响应机制
    2.1 事件的产生
    • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

    • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)

    • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。

    找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

    2.2 事件的传递
    • 触摸事件的传递是从父控件传递到子控件

    • 也就是UIApplication->window->寻找处理事件最合适的view

    注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

    2.3 应用如何找到最合适的控件来处理事件?
    • 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
    • 2.判断触摸点是否在自己身上
    • 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
    • 4.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
    2.3.1 寻找最合适的view底层剖析

    两个重要的方法:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
    
    2.3.2 hitTest:withEvent 方法介绍

    什么时候调用?

    • 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

    作用

    • 寻找并返回最合适的view(能够响应事件的那个最合适的view)

    注 意
    1.不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
    2.如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

    2.3.3 pointInside:withEvent 方法介绍

    判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

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

    • 不允许交互:userInteractionEnabled = NO
    • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
    • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
    2.4 事件的响应
    2.4.1 触摸事件处理的整体过程
    • 1 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
    • 2 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
    • 3 这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
    2.4.2 响应者链条

    在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫响应者链。也可以说,响应者链是由多个响应者对象连接起来的链条

    在iOS中响应者链的关系可以用下图表示:

    image

    响应者对象 能处理事件的对象,也就是继承自UIResponder的对象

    作用 能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

    如何判断上一个响应者

    • 1 如果当前这个view是控制器的view,那么控制器就是上一个响应者
    • 2 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

    响应者链的事件传递过程

    • 1 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
    • 2 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
    • 3 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
    • 4 如果UIApplication也不能处理该事件或消息,则将其丢弃
    2.5 总结

    事件处理的整个流程总结:

    • 1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
    • 2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
    • 3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
    • 4.最合适的view会调用自己的touches方法处理事件
    • 5.touches默认做法是把事件顺着响应者链条向上抛。

    如何做到一个事件多个对象处理:
    因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
        // 1.自己先处理事件...
        NSLog(@"do somthing...");
        // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
        [super touchesBegan:touches withEvent:event]; 
    }
    

    事件的传递和响应的区别:
    事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。

    推荐参考 史上最详细的iOS之事件的传递和响应机制-原理篇

    备注:敢号称史上最强,并且阅读量超 8 万,评论达 180,赞赏 6,应该有其参考价值吧。
    
    三 UIView block动画实现原理

    在了解UIView block动画实现原理之前,需要先了解CALayer的可动画属性。

    3.1 CALayer的可动画属性

    CALayer拥有大量的属性,看CALayer的头文件内容,会发现很多的属性的注释中,最后会有一个词叫做Animatable,直译过来是可动画的。下面的截图只是CALayer众多可动画属性中的一部分(注意frame并不是可动画的属性)

    /* The bounds of the layer. Defaults to CGRectZero. Animatable. */
    
    @property CGRect bounds;
    
    /* The position in the superlayer that the anchor point of the layer's
     * bounds rect is aligned to. Defaults to the zero point. Animatable. */
    
    @property CGPoint position;
    
    /* The Z component of the layer's position in its superlayer. Defaults
     * to zero. Animatable. */
    
    @property CGFloat zPosition;
    
    /* Defines the anchor point of the layer's bounds rect, as a point in
     * normalized layer coordinates - '(0, 0)' is the bottom left corner of
     * the bounds rect, '(1, 1)' is the top right corner. Defaults to
     * '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */
    
    @property CGPoint anchorPoint;
    
    /* The Z component of the layer's anchor point (i.e. reference point for
     * position and transform). Defaults to zero. Animatable. */
    
    @property CGFloat anchorPointZ;
    
    /* A transform applied to the layer relative to the anchor point of its
     * bounds rect. Defaults to the identity transform. Animatable. */
    
    @property CATransform3D transform;
    

    如果一个属性被标记为Animatable,那么它具有以下两个特点:

    1、直接对它赋值可能产生隐式动画;
    2、我们的CAAnimation的keyPath可以设置为这个属性的名字。

    当我们直接对可动画属性赋值的时候,由于有隐式动画存在的可能,CALayer首先会判断此时有没有隐式动画被触发。它会让它的delegate(没错CALayer拥有一个属性叫做delegate)调用actionForLayer:forKey:来获取一个返回值,这个返回值在声明的时候是一个id对象,当然在运行时它可能是任何对象。这时CALayer拿到返回值,将进行判断:

    • 如果返回的对象是一个nil,则进行默认的隐式动画;
    • 如果返回的对象是一个[NSNull null] ,则CALayer不会做任何动画;
    • 如果是一个正确的实现了CAAction协议的对象,则CALayer用这个对象来生成一个CAAnimation,并加到自己身上进行动画。

    理解完这些,我们再来分析UIView的block动画就容易理解了。

    3.2 UIView的block动画
    Amazing things happen when they are in a block.
    

    有趣的是,如果这个CALayer被一个UIView所持有,那么这个CALayer的delegate就是持有它的那个UIView。

    大家应该可以思考出这样的问题:为什么同样的一行代码在block里面就有动画在block外面就没动画,就像下面这样:

    /** 产生动画 */
    - (void)createAnimation {
        UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
        redView.backgroundColor = [UIColor redColor];
        [self.view addSubview:redView];
        
        // 这样写没有动画
        redView.center = CGPointMake(200, 300);
        
        
        [UIView animateWithDuration:1.25 animations:^{
            // 写在block里面就有动画
            redView.center = CGPointMake(200, 300);
        }];
    }
    
    3.gif

    既然UIView就是CALayer的delegate,那么actionForLayer:forKey:方法就是由UIView来实现的。所以UIView可以相当灵活的控制动画的产生。

    当我们对UIView的一个属性赋值的时候,它只是简单的调用了它持有的那个CALayer的对应的属性的setter方法而已,根据上面的可动画属性的特点,CALayer会让它的delegate(也就是这个UIView)调用actionForLayer:forKey:方法。实际上结果大家都应该能想得到:在UIView的动画block外面,UIView的这个方法将返回NSNull,而在block里面,UIView将返回一个正确的CAAction对象(这里将不深究UIView是如何判断此时setter的调用是在动画block外面还是里面的)。

    为了证明这个结论,我们将继续进行实验:

    /** UIView 动画产生原理 */
    - (void)uiviewAnimation {
        NSLog(@"%@",[self.view.layer.delegate actionForLayer:self.view.layer forKey:@"position"]);
        
        [UIView animateWithDuration:1.25 animations:^{
            NSLog(@"%@",[self.view.layer.delegate actionForLayer:self.view.layer forKey:@"position"]);
        }];
    }
    

    我们分别在block外面和block里面打印actionForLayer:forKey:方法的返回值,看看它究竟是什么玩意。

    2019-06-11 19:45:23.973002+0800 YaZhaiInterviewQuestionDemo[93569:1953578] <null>
    2019-06-11 19:45:23.973622+0800 YaZhaiInterviewQuestionDemo[93569:1953578] <_UIViewAdditiveAnimationAction: 0x600001ff0d40>
    

    打印发现,我们的结论是正确的:在block外面,这个方法将返回一个NSNull(是尖括号的null,nil打印出来是圆括号的null),而在block里面返回了一个叫做UIViewAdditiveAnimationAction类的对象,这个类是一个私有类,遵循了苹果一罐的命名规范: xxAction,一定就是一个实现了CAAction协议的对象了。

    这也就说明了为什么我们对一个view的center赋值,如果这行代码在动画block里面,就会有动画,在block外面则没有动画。

    相关参考 iOS CoreAnimation专题——原理篇(二) UIView block动画实现原理

    相关文章

      网友评论

        本文标题:iOS-YZWL公司面试题

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