美文网首页
控制器加载原理和UI初始化过程

控制器加载原理和UI初始化过程

作者: 佟小胆胆小 | 来源:发表于2018-05-25 18:20 被阅读0次

    UIViewController的加载过程探究

    1. 新建一个 UIViewController 并勾选Also create Xib file


      image.png
    2. 在FirstViewController.m文件里重写-init和-initWithNibName: bundle:函数,并打印他们的函数名
    //FirstViewController.m
    - (instancetype)init{
        NSLog(@"%s",__func__);
        
        self = [super init];
        
        if(self){}
        
        return self;
    }
    
    -(instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
        
        NSLog(@"%s",__func__);
        
        self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
        
        if (self) {}
        
        return self;
    }
    
    1. 在ViewController中使用init 和 initWithNibName两种方式创建FirstViewController体验两种创建方式的区别
    //ViewController.m
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        FirstViewController * firstVc = [[FirstViewController alloc] init];
        [self.navigationController pushViewController:firstVc animated:YES];
    }
    
    

    通过日志我们看到FirstViewController先调用了-init函数 然后调用-initWithNibName


    -init 日志

    接下来我们看一下通过-initWithNibName创建方式

    //ViewController.m
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
    //    FirstViewController * firstVc = [[FirstViewController alloc] init];
        FirstViewController * firstVc  = [[FirstViewController alloc] initWithNibName:@"FirstViewController" bundle:nil];
        [self.navigationController pushViewController:firstVc animated:YES];
    }
    
    -initWithNibName创建方式

    这时我们发现只调用了-initWithNibName函数

    • 小结:init函数内部调用了initWithNibName

    小技巧
    默认系统新建的ViewController是没有NavigationController的我们可以选中要添加NavigationController的控制器通过Xcode菜单的Editor -> Embed In -> NavigationController 为ViewController添加NavigationController


    系统默认生成
    添加NavigationController 生成完成

    UIViewController与view的关系

    1. UIViewController的view是懒加载

    还是刚刚的例子这次我们添加打印-viewDidLoad函数的代码

        //FirstViewController.m
    - (void)viewDidLoad {
        NSLog(@"%s",__func__);
        
        [super viewDidLoad];
    }
    
    

    我们在-initWithNibName:bundle:函数内部打一个断点并使用po打印FirstViewController的view对象属性,发现调用了-viewDidLoad函数,证明控制器的view是懒加载的


    image.png
    1. -loadView函数作用将View添加到控制器
      我们在重写loadView函数并且不调用[super loadView];
        //FirstViewController.m
    - (void)loadView{
        
        NSLog(@"%s",__func__);
        
    //    [super loadView];
        
    }
    

    运行一下查看结果


    simulator
    console
    • 首先控制器由于没有view被加载显示黑色
    • 由于控制器没有加载到view会不停调用-loadView 和-viewDidLoad

    小结: 系统会在loadView加载控制器的view,如果我们想替换view或者初始化多个view可以在这个方法里进行处理

    -viewWillLayoutSubviews函数才能获取到view的最终frame

    1. -viewWillLayoutSubviews调用时机
      我们重写-viewWillLayoutSubviews、-viewWillAppear函数
       //FirstViewController.m
    - (void)viewWillAppear:(BOOL)animated {
        
        NSLog(@"%s",__func__);
        
        [super viewWillAppear:animated];
        
    }
    
    - (void)viewWillLayoutSubviews {
        
        NSLog(@"%s",__func__);
        
        [super viewWillLayoutSubviews];
        
    }
    

    观察日志函数调用顺序


    i函数调用顺序
    • 首先调用 initWithNibName:bundle: 加载nib文件
    • loadView加载view
    • viewDidLoad已加载view
    • viewWillAppear view将要显示
    • viewWillLayoutSubviews 布局子View

    由于iPhone从5开始之后需要进行屏幕适配所以viewDidLoad加载的Frame可能是不准备的,我们要在-viewWillLayoutSubviews函数进行view的frame设置

    演示一个小例子说明一下 :

    • 我们在FirstViewController.xib中拖一个View进去

    • 修改 View as 为IPhone SE设备(由于我们运行采用iPhone8、这样能看出适配效果)

    • 然后使用Autoresizing适配一下


      布局准备
    • 我们在-viewDidload函数内在橙色view下面添加一个大小一样的蓝色view、并打印橙色View的宽度

    - (void)viewDidLoad {
        
        NSLog(@"%s",__func__);
        
        [super viewDidLoad];
        
        UIView *blueView = [[UIView alloc]init];
        blueView.backgroundColor = [UIColor blueColor];
        blueView.frame = CGRectMake(_orangeView.frame.origin.x,
                                      _orangeView.frame.origin.y + _orangeView.frame.size.height,
                                      _orangeView.frame.size.width,
                                      _orangeView.frame.size.height);
        
        
        [self.view addSubview:blueView];
        
        NSLog(@"%lf",_orangeView.frame.size.width);
        
        
    }
    

    我们看一下运行效果


    Simulator

    咦!我设置的frame和橙色View一样为什么宽度变窄了
    别急 我们看一下日志分析一下


    console log

    宽度是320


    FirstViewController.xib
    这不正是iPhone SE的宽度么
    我们将-viewDidLoad代码剪切到-viewWillLayoutSubviews函数试试
    -viewWillLayoutSubviews设置frame

    神奇的解决了我们的问题、宽度也是iPhone8的屏幕宽度375

    小结:-viewDidLoad设置frame由于屏幕适配导致有时从xib或storyboard创建view的frame可能不准确的,我们尽量将设置frame的代码放到-viewWillLayoutSubviews中可以安全的进行布局

    探究纯代码和Xib创建View

    1.我们新建一个FirstView 继承UIView并重写下面这几个函数并将其添加到ViewController
    -init
    -initWithFrame:
    -initWithCoder:
    -awakeFromNib
    -layoutSubviews

    //FirstView.m
    
    - (instancetype)init{
        
        NSLog(@"%s", __func__);
        self = [super init];
        if (self) {
            
        }
        return self;
        
    }
    
    - (instancetype)initWithFrame:(CGRect)frame{
        
        NSLog(@"%s", __func__);
        self = [super initWithFrame:frame];
        if (self) {
            
        }
        return self;
    }
    
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        
        NSLog(@"%s", __func__);
        self = [super initWithCoder:aDecoder];
        if (self) {
            
        }
        return self;
    }
    
    - (void)awakeFromNib{
        
        NSLog(@"%s", __func__);
        [super awakeFromNib];
    }
    
    - (void)layoutSubviews{
        
        NSLog(@"%s", __func__);
        
        [super layoutSubviews];
        
    }
    
    
    
    
    1. 观察init方式创建会调用哪些函数
    //========== ViewController.m ==========
    
    FirstView *firstView = [[FirstView alloc] init];
        firstView.frame = CGRectMake(0, 0, 50, 50);
        [self.view addSubview:firstView];
    

    通过查看日志发现调用了下面三个方法
    -init
    -initWithFrame:
    -layoutSubviews


    init方式

    3.我们在看一下从xib中加载view的方式

    //========== ViewController.m ==========
        FirstView *firstView = [[[NSBundle mainBundle] loadNibNamed:@"FirstView" owner:nil options:nil] lastObject];
        firstView.frame = CGRectMake(0, 0, 50, 50);
        [self.view addSubview:firstView];
    

    nib中加载View的方式调用了
    -initWithCoder:
    -awakeFromNib
    -layoutSubviews


    xib加载

    小结:
    xib加载View的方式需要从xib文件解档对象所以会调用-initWithCoder:
    与UIViewController相同 安全布局同样需要在-layoutSubviews中执行

    使用setNeedsLayout与setNeedsDisplay更新布局

    上面说到UIView和UIViewController都建议在-layoutSubviews中进行frame设置,如果我们想主动调用-layoutSubviews可以通过setNeedsLayout与setNeedsDisplay间接调用-layoutSubviews
    但是他们都是异步的需要等到下一个Runloop才会调用-layoutSubviews
    我们验证一下。
    1.我们给FirstView添加一个count属性,并在ViewController中-touchesBegan:withEvent:中调用FirstView 的 -setNeedsLayout函数观察前后count值变化

    //FirstView.h
    @property(nonatomic,assign) NSInteger count;
    //FirstView.m
    - (void)layoutSubviews{
        
        NSLog(@"%s", __func__);
        self.count++;
        [super layoutSubviews];
        
    }
    //ViewController.m
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"before: %ld",self.firstView.count);
        [self.firstView setNeedsLayout];
        NSLog(@"after: %ld",self.firstView.count);
        
    }
    

    我们点击UIVIewController的界面查看log,发现count值并没有变化


    log

    我们怎么才能让-setNeedsLayout调用后立即调用-layoutSubviews呢?
    我们只需要调用-layoutIfNeeded方法就可以让View立即执行-layoutSubviews了

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"before: %ld",self.firstView.count);
        [self.firstView setNeedsLayout];
        [self.firstView layoutIfNeeded];
        NSLog(@"after: %ld",self.firstView.count);
        
    }
    

    我们查看一下日志。这才是我们真正想要的结果


    log

    2.好了 ,用过了setNeedsLayout我们在体验一下setNeedsDisplay

    //ViewController.m
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        NSLog(@"before: %ld",self.firstView.count);
        [self.firstView setNeedsDisplay];
        [self.firstView layoutIfNeeded];
        NSLog(@"after: %ld",self.firstView.count);
    }
    

    我们查看一下日志,发现我们的方法不灵了只能等到下一个runloop事件到来


    log

    相关文章

      网友评论

          本文标题:控制器加载原理和UI初始化过程

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