011-Application,VIewController,View的总结

应用程序 = 代码 + 系统框架

iOS 应用入口 —— Main 函数

main 函数由 Xcode 在创建工程时提供,一般不改变其中内容,除非真的知道自己在做什么。

#import 
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

main 函数做的事情是调用 UIApplicationMain 函数,这个函数的作用是创建用户程序对象、从 可用的 storyBoard 或 nib 文件(在 info.plist 文件定义)加载用户界面、创建应用程序委托和初始化主事件循环(main event loop),这个函数虽然定义了返回值为 int,但 app 在运行时这个函数就不会运行结束,所以不会返回值。

iOS MVC 架构

MVC 架构

  • Model——负责定义和存储数据及数据相关的业务逻辑
  • View——负责响应用户交互Controller——负责联系 Model 和 View

当有用户交互事件到来时,view 通过 target-action 来处理,当需要处理特殊 UI 逻辑或者获得数据源时,View 与代理 Controller 通信。Model 不主动与 Controller 通信,一旦有数据更新需要通知会采用 NSNotification 或 KVO 进行通知。

一个 application 的总体架构如下图

Eventloop 会将用户交互事件交个 UIApplication进行分发,可能是分发给 UIView 或者 AppDelegate。AppDelegate 主要管理 app 的生命周期,如初始化、加载 UIWindow、设置 rootViewController、内存不足时处理、app切换、app终止等。ViewCtroller 的属性 view 类似一个根视图,ViewController 负责管理 view 的生命周期。DataModel 用于存储数据,Document 用于管理 Model,但不是必须的。UIWindow 是 View 层次结构中的最高层,添加一个 contentView 就可以显示视图。View 对象组成和布局 view 的层次结构,其中有特定用户交互的称为 Control。

Main Event Loop 事件循环

MEL 负责处理所有用户交互事件,运行在主线程中。当发生交互事件时,系统会生成交互关联事件,然后通过应用端口放入事件队列,依次由 MEL 执行,MEL 传递给 UIApplication 进行分发。

用户交互事件有四类,在 UIEvent 类中用一个枚举类定义。

typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches, //触摸事件,按钮,手势
    UIEventTypeMotion, // 运动事件,摇一摇,指南针
    UIEventTypeRemoteControl, // 远程遥控,耳机
    UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0), // 3D touch
};

UIEvent 就是一个响应事件的类,会在交互事件发生时被系统实例化,由于可能被系统复用,所以应用不能持有事件对象。

这里以 UIEventTypeTouches 为例,讲解事件传递和响应过程。UIEvent 会持有一些UITouch 对象,UITouch 类用于表示一个具体的手指触摸对象,通过它可以获得 touch event 的一些属性

  • window:触摸时所在的窗口
  • view:触摸时所在视图
  • tapCount:短时间内点击的次数
  • timestamp:触摸产生或变化的时间戳
  • phase:触摸周期内的各个状态
  • locationInView:方法:取得在指定视图的位置
  • previousLocationInView:方法:取得移动的前一个位置

一个 touch event 事件发生后首先会被 UIApplication 先发送到 UIWindow,然后依次按照 view 层次结构向下传递,找到最合适的视图。

寻找最合适视图

一般步骤如下

  • 首先判断 UIWindow 能否接受 touch event,如果能则判断 touch point 是否在 window 内
  • 从后向前遍历子 View,重复第一步
  • 如果遍历完没有找到则自己作为最合适的 view,如果找到则结束

一个 UIView 不能接受 touch event 的可能情况有

  • 不接受用户交互:userInteractionEnabled = NO;
  • 隐藏:hidden = YES;
  • 透明:alpha = 0.0~0.01
  • 没有实现开始触摸方法(注意是touchesBegan:withEvent:而不是移动和结束触摸事件)

具体过程如下,当一个 View 接收到一个 touch event后,会调用 hitTest: withEvent: 方法

- (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;
}

这个方法在 UIView 中定义,UIWindow 继承了这个类,所以也有这个方法。其中有个方法 (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 用于判断触摸点是否在自己身上。如果都满足,会继续遍历自己的 subView,对每一个 subView 调用 hit 方法,从而找到最合适的 view。

响应事件

UIView 继承自 UIResponder 类,UIResponder 类中就定义了具体的响应事件,要注意 UIVIewController 也继承了 UIResponder,也可以响应事件。

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches NS_AVAILABLE_IOS(9_1);

// Generally, all responders which do custom press handling should override all four of these methods.
// Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each
// press it is handling (those presses it received in pressesBegan:withEvent:).
// pressesChanged:withEvent: will be invoked for presses that provide an analog value
// (like thumbsticks or analog push buttons)
// *** You must handle cancelled presses to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

默认的响应方法是将事件按照“响应链”上传,当一个事件发生后首先看 initial view 能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view 的 superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器 view controller,首先判断视图控制器的根视图 view 是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递;(对于视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果 window 还是不能处理此事件则继续交给 application(UIApplication 单例对象)处理,如果最后 application 还是不能处理此事件则将其丢弃。所以如果要自己响应事件,就必须复写这些事件。

其中响应 touch 的方法如下

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;

APP 状态与生命周期

一个应用的运行状态可以包括以下几个

  • Not running:app还没运行
  • Inactive:app运行在foreground但没有接收事件
  • Active:app运行在foreground和正在接收事件
  • Background:运行在background和正在执行代码
  • Suspended:运行在background但没有执行代码

如何在这几个状态中切换就需要 AppDelegate 的参与,确切的说是实现了 UIApplicationDelegate 协议的委托对象来负责。在协议中以下几个方法会被调用

application:willFinishLaunchingWithOptions: - 启动时的第一次执行
application:didFinishLaunchingWithOptions: - 在显示app给用户之前执行最后的初始化操作
applicationDidBecomeActive: - app已经切换到active状态后执行
applicationWillResignActive: - app将要从前台切换到后台时需要执行的操作
applicationDidEnterBackground: - app已经进入后台后需要执行的操作
applicationWillEnterForeground: - app将要从后台切换到前台需要执行的操作,但app还不是active状态
applicationWillTerminate: - app将要结束时需要执行的操作

一个 app 第一次启动时会依次调用 willFinishLaunchingWithOptions、didFinishLaunchingWithOptions、applicationDidBecomeActive三个方法,app 状态由 Not running -> Inactive -> Active。

当 app 被切出的时候,有三种情况

  • 先切换到多任务窗口,会调用 applicationWillResignActive 方法,app 状态 Active -> Inactive,再切换回来会调用 applicationDidBecomeActive 方法
  • 直接退出到 home 、进入其他应用时,依次调用 applicationWillResignActive 和 applicationDidEnterBackground 方法,状态 Active -> Inactive -> Backgroud
  • 中断发生,如锁屏和电话接入,依次调用 applicationWillResignActive 和 applicationDidEnterBackground 方法

当 app 被切回时,依次调用 applicationWillEnterForeground 和 applicationDidBecomeActive 方法。

当 app 被退出时,调用 applicationWillTerminate。

UIViewController 生命周期

UIViewController 是 MVC 架构中的 C,主要负责管理视图、处理 Model 业务和作为 view 的数据源和委托。

UIViewCtroller 可以通过 nib、StoryBoard 或 NSCoding 协议初始化,也可以用代码直接构建。

    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    self.window.backgroundColor = [UIColor whiteColor];
    
    MainTabbarControllerViewController *tabbarController = [[MainTabbarControllerViewController alloc] init];
    self.window.rootViewController = tabbarController;
    [self.window makeKeyAndVisible];

这里是在 AppDelegate 类的 didFinishLaunchingWithOptions 方法内进行的 rootViewCtroller 的初始化工作。

接下来 UIViewController 就开始初始化自己的根视图。

  • loadView:加载view,没有特殊情况只执行一次
  • viewDidLoad:view加载完毕,只会在将要布局时调用一次
  • viewWillAppear:控制器的view将要显示
  • viewWillLayoutSubviews:控制器的view将要布局子控件
  • viewDidLayoutSubviews:控制器的view布局子控件完成
  • viewDidAppear:控制器的view完全显示
  • viewWillDisappear:控制器的view即将消失的时候
  • viewDidDisappear:控制器的view完全消失的时候
  • viewWillUnload: iOS6以上的 UIKit 不会在内存警告时自动释放视图,所以viewWillUnload和viewDidUnload将不再触发
  • viewDidUnload
  • dealloc: 析构,回收内存资源

ViewController 的 view 会采用懒加载方式加载,加载的 view 会一直保持在内存中,即使 app 进入后台。而如果在 TabBarViewController 和 NavigationController 中进行视图切换时,就会循环调用 viewWillDisappear,viewDidDisappear,viewWillAppear,viewDidAppear 这些方法。

UIView 的生命周期

UIView,尤其是 UIViewcontroller 的根 View,其生命周期都在 UIViewController 生命周期内,也可以理解为被 Controller 所持有。

具体来说有以下几个方法

- (instancetype)init

- (void)layoutSubviews

- (void)didAddSubview:(UIView *)subview

- (void)willRemoveSubview:(UIView *)subview

- (void)willMoveToSuperview:(UIView *)newSuperview

- (void)didMoveToSuperview

- (void)willMoveToWindow:(UIWindow *)newWindow

- (void)didMoveToWindow

- (void)removeFromSuperview

- (void)dealloc

通过实践得出以下结论:

  • Controller 在 loadView 方法里会调用 didAddSubview,willMoveToSuperview,didMoveToSuperview 等方法将 子 view 初始化并加入到根 view 中
  • Controller 在 viewWillLayoutSubviews 方法中会调用 willMoveToWindow,didMoveToWindow 等方法,将view 移动到 UIWindow 中
  • Controller 在 viewWillLayoutSubviews 方法中会调用 layoutSubviews 方法,此时子 view 对自己的子 view 向下依次进行布局操作

你可能感兴趣的:(011-Application,VIewController,View的总结)