iOS View 编程指导(一)-view和window的底层逻辑和结构详解

本系列文章参考苹果官方文档:View Programming Guide for iOS

本系列文章还包括:

  1. iOS View 编程指导(二)-window
  2. iOS View 编程指导(三)-View
  3. iOS View 编程指导(四)-动画

窗口(Windows) 和 视图(Views)


  • 在iOS中,用Windows和Views来展示UI界面
  • Windows是个容器, 本身不可见, 是用来盛放View的
  • Views定义了填充Window的内容, 比如image,text,shapes, 或者各种可见UI元素的组合, 等等
  • 另外Window继承自View

预览


  • 每个APP至少需要一个window和一个view
  • UIKit提供了一些定义好的view,比如button, text label, table view, scroll view, picker view等,这些view都是可以直接拿来用的
  • 如果有的地方需要的界面, 用UIKit提供的系统view无法满足, 开发者可以自定义view

views是用来管理应用中的UI界面

  • app中的view都是类UIView的实例,表示屏幕上的一块矩形区域
  • view用来绘制内容,响应用户触碰事件,管理view自己的subviews.
  • 绘制内容用到的绘图技术包括Core Graphic, OpenGL ES, UIKit中的画shape,画image,画text
  • View还是touch event的响应者
  • view和它的super view,subviews构成了一个树形结构叫做view hierarchy,本文中我称它为view的层级树

可以将视图看做是构建应用UI界面的砖头,UI界面是使用各种视图堆砌起来的,这时各种视图间的联系构成了view hierarchy.

window用来协调View的显示

  • 一个window是一个UIWindow的实例,它管理view的显示,处理用户事件;配合view一起构建APP的用户界面.
  • 每个App至少有个window,大多数时候App都是只有一个key window(在设备的主屏幕上显示用户界面)的,而且该window通常不会变化的, window创建后保持不变只有window内的view可能会变化.
  • 当设备连接了外置屏幕时,系统会创建第二个window

动画给用户提供了界面变化的视觉反馈

  • iOS提供了丰富的动画如一组view间的modal,transitioning
  • view的许多属性变化也提供了动画,这些属性有:透明度(alpha),位置大小变化(frame),backgroundColor等
  • view的动画可以使用底层的layer来做CoreAnimation动画

Interface Builder的作用

界面的创建除了可以用来代码直接创建,还可以使用一个xcode工具-InterfaceBuilder来构建,这个工具是一个可视化构建工具,可以直接拖拽View等UI元素进入界面,可以直接修改UI元素的属性和位置大小.具体请参考文档Interface Builder User GuideView Controller Programming Guide for iOS

view和window的结构


  • UIView和UIWindow这个类提供非常厉害的工具来管理布局和view的展示.
  • 理解这两个类的底层的结构和工作原理对构建UI界面有益

view的底层结构


  • 在iOS中view指一块在屏幕上的可见矩形区域,view可以在该矩形区域绘制内容,处理用户的触摸事件.
  • view同时也可以是其他视图的父视图,父视图管理子view的位置和大小,这些事情UIView这个类默认帮你干了
  • 如果想要定制view可以继承UIView

在View底层有一个Layer,该layer用来绘制UI内容,UIView上的动画支持也是由底层的layer来实现的.所以说UIView这个类是对CALayer的扩展已提供对用户屏幕事件的响应能力.
开发者对view的操作大部分都是通过UIView中开放的接口进行的,然而有时候也会用到CALayer来进行操作.

通过下图可以帮我们理解view和layer之间的关系,图中显示的是苹果的demo-ViewTransitions一个界面的UIView和底层Layer的结构.
下图包括的view: 有window,一个普通的View容器containerView,一个imageView,toolbar(控件control也是集成自UIView),bar button(不是UIView的子类,但它底层是由UIView构成的,相当于view的容器).每个UIView都有一个Layer,通过UIView的layer属性可以访问.在CALayer的底层又是由Core Animation对象构成的,用于硬件加速和屏幕内容的绘制.

一个界面中的View结构图

使用Core Animation对于提高性能有重要意义. 在实际的视图绘制中,在内容绘制代码(尽量少调用,因为很耗性能)调用后,绘制的内容会被Core Animation缓存好,以备接下来复用. 复用缓存好的内容可以减少视图更新重绘对性能的消耗,特别是在做动画时,coreAnimation的复用机制非常重要.

View的层级树和subview的管理

  • view除了可以显示内容外还可以作为其他view的容器,这时就形成了一种父子关系,作为容器的为superview,容器中的view成为subview,所以这是一种组合设计模式(参考iOS设计模式(二)-21种设计模式),这种设计模式对view层级堆砌有重要的意义
  • 显然,当subview的内容是不透明时会遮住superview的内容.如果subview的内容部分透明时显示的内容是混合的. superview用一个数组来管理它的subviews,所以各个subview之间有先后顺序,后加入的subview会遮住先加入的.
  • 改变superview的size,隐藏superview,改变superview的alpha值,对superview做transform操作,都会相应的改变或者影响它的subview
  • view的层级树也会对用户事件响应产生影响. 事件发生后一般是把该事件给一个合适view处理,如果该view不处理那么会将事件往它的superview传递,如果该superview不处理,事件继续向上传递给它的superview的superview; 也就说,事件的传递是沿着响应链(responder chain)往上传递的; 直到传递给UIApplication对象(一般处理是将事件丢弃掉),就不再传递了.

view的绘制

UIView使用以一种按需绘制(on-demand,类似懒加载)方式进行内容的绘制. 当view第一次加载时,系统调用view的绘制代码开始绘制,然后将绘制的内容保存为一种图片,然后将图片显示. 如果view接下来不更新那么系统不会再绘制而是一直显示那种图片,当view有了更新系统才会再一次绘制并保存一直新的图片后显示.

当view更新重绘时,不是直接调用绘制代码再一次绘制,而是调用UIView的setNeedsDisplay或者setNeedsDisplayInRect:方法来通知系统需要更新view. 系统会等待下次runloop调用(时间为一帧时间,通常为1/60秒),在等待期间系统可以给你进行view的remove,resize,hide,reposition等操作.

注意:view几何上(位置,大小)的更新不会导致view的重绘,而是根据view的contentMode来拉伸或者移动上次绘制的内容图片,不是重新绘制.具体那种contentMode会怎样影响view的更新,下文会提到.

当开始绘制view的内容时,UIKit提供的系统view时直接调用系统私有方法来进行绘制的. 如果是自定义view的话需要覆盖(override)drawRect:方法,也可以直接设置底层layer的contents,但不常用.

Content Modes

每个view都一个contentmode用来控制循环使用view中的内容.当一个view第一次绘制时,会保存一个bitmap来表示内容,当发生集合变动时,view直接使用哪个bitmap来根据view的contentmode来做些改动循环使用.contentMode会对以下view的变动产生影响:

  • 改变view的宽高或者位置(frame和bounds的改变)
  • 给view加一个变换transform

多数view的contentMode的默认值是UIViewContentModeScaleToFill,该模式使得view的content缩放后填充frame设置的大小.下图中显示不同的contentMode对应view的content填充样式,从图中可以看到并不是所有的模式都是充满view的frame,从而使一些内容变形.

contentMode

你可以将view的contentMode设置为UIViewContentModeRedraw,此时view的几何更新会致使系统调用drawRect:重绘view的内容.所以一般不用这种模式,这种模式对性能影响很大.

view的拉伸

view可以通过设置contentStretch来确定view可以进行拉伸的部分内容.contentMode的部分模式(UIViewContentModeScaleToFill, UIViewContentModeScaleAspectFit, UIViewContentModeScaleAspectFill)可以导致view可以拉伸,view的拉伸如下图所示:

view的拉伸

view内置的动画

view底层的layer带来一个好处是view改动带来的动画. 适当的动画设计给APP带来好的用户体验. view的许多属性是支持动画的,要创建view的动画需要干两件事:

  1. 告诉UIKit你想展示一个动画

  2. 改变view的属性
    下面是一些可以做动画的属性:

  3. frame-创建移动和大小的动画变动

  4. bounds-创建大小的动画变动

  5. center-创建移动动画

  6. transfrom-创建变换(缩放,旋转)动画

  7. alpha-创建透明度动画

  8. backgroundColor-创建背景颜色动画

  9. contentStretch-创建拉伸动画

还有就是view的过度动画(transition),比如controller间的modal,navigationController间的视图切换都是view的transition
另外动画可以通过UIKit创建也可以通过CoreAnimation创建动画,core Animation动画直接操作layer层,这样可以创建的动画可以更加丰富(比如控制动画的timing等),想要更加深入的学习Core Animation可以浏览Apple官方文档Core Animation 编程指导Core Animation指南

视图几何表示和坐标系


UIKit中的坐标系规定原点在屏幕的左上方.另外每一个window和view都自己的坐标系,所以view中的subview的位置相对于该view的坐标系而不是屏幕的坐标系. 下图展示了UIKit中的坐标系:

iOS中屏幕坐标系

因为每个view或者window都有自己的坐标系,所以在做几何变动或者绘制图形时都要考虑到你需要参考的坐标系,比如在view中绘制图形时,你参考的是该view的坐标系.类UIWindowUIView都提供了一些方法供开发来切换不同的坐标系操作.

注意:在iOS中不同框架中对坐标系定义(原点,x/y轴的方向)有区别.比如在Core Graphic和OpenGL ES中使用的坐标系和UIKit中不同,它们定义的坐标系是:原点在屏幕的左下角,x轴向右,y轴向上.所以在用core graphic绘制图形时,要考虑到此时的坐标系和UIKit中不同(主要是上下颠倒了)

Frame,Bounds和Center这些属性之间的关系

view通过frame,bounds,center这几个属性来记录它的位置和大小:

  • frame,记录view在superview中(superview的坐标系)的大小和位置信息
  • bounds,记录view自己的坐标系原点位置,和大小
  • center,记录自己在中心点在superview中的位置

framecenter主要用来操作view的几何改变(位置,大小).你使用这两个属性来构建你层级树或更新位置和大小.如果你只是单单改变位置,center是你的首先,因为center一直有效,而frame在view做了transform变换后,frame就会失效,而center不会.

而属性bounds的使用场景是在view中进行图形绘制. 因为bounds表示view自己的坐标系,原点默认为(0,0),大小为指view的size.
如下图所示图片在父视图中的位置是(40,40),大小是(240,380),所以图片的frame是(40,40,240,380),而bounds是(0,0,240,380)

view的frame和bounds

改变frame,bounds,center中任意一个会影响其他属性:

  • 当你设置frame时,bounds中的size会和frame中一致,center也会相应的改变
  • 当你设置center时,frame中origin会改变,但bounds不变
  • 当你设置bounds的size时,frame中的size会变,center不变
  • 当你设置bounds的origin时,frame,center都不变,但view中的subview受到影响(可以写个demo自测一下,subview在屏幕上的位置会移动)

默认子view超出父view的边界不会被裁剪. 当superview的属性clipsToBounds设置为YES时,view超出superview的frame部分会被裁剪掉.但是touch event不会传递给边界外的子view.

坐标系的变换

坐标系变换可以使view(和里面的内容)更快地的变换. affine transform(仿射变换,旋转,缩放,位移)是一个矩阵. 在内容绘制时,可以使用仿射变换来快速修改画好的内容:

  • 如果想给整个view进行变换那么直接给属性transform设置一个CGAffineTransform的值
  • 如果自定义view时,在drawRect:中使用仿射变换对图形进行修改

通常通过仿射变换给view进行选择的动画,如果想对view进行永久性位移和大小的改变则可以通过view的frame和center

当进行仿射变换时,是基于view的center,也就是说进行放射变换center是不变的,bounds也不会变

在方法drawRect:中,使用放射变换来移动和改变方向绘画操作,而不是固定绘画对象在一个位置; 在开始绘制前,通常是先将绘制的对象固定到一个点(通常为(0,0)),然后给对象做一次相应的位移变换;通过这种方式,如果改变绘制对象在view中,那么只需要通过再做一次放射变换,这样比再创建一个新的绘图对象更快更节省系统资源. 通过方法CGContextGetCTM来获取绘图上下文(graphic context)中的当前transform,通过core graphic中方法来设置和修改绘画对象的transform.

给view进行几何变换时,此时的current transform存放在view的属性transform中,而在drawRect:中,transform是和绘画上下文graphic context绑定在一起的.

每个subview在创建时是根据它们的superview的坐标系.如果给superview加上一个仿射变换后,那么subviews相对于屏幕来说也变动了,但相对于superview来说它们是没有变化的,因为做放射变换时bounds不会变.

下图展示了分别给superview和subview做旋转变换的效果.在view的drawRect:方法中,给view中矩形旋转45°,然后再给view旋转45°,结果是view中的矩形好像旋转了90°,其实相对于view来说矩形只旋转了45°.

放射变换对view中内容产生的影响

注意:如果view的transform不是identity transform的话,view的frame是无效的你需要忽略掉它.如果你对一个view做了放射变换那么你可以用bounds或者center来获取view的位置和大小. view中的subview的frame是有效的

如果想学习更多关于放射变换的知识可以参考Apple文档Drawing and Printing Guide for iOS

点和像素

在iOS中,坐标值和距离的单位是点(points),在代码中用float表示. 但每一的物理表示(一点含有多个少个像素)和设备有关.点points确定了内容绘制的结构(屏幕大小).
下表中列举了不同的设备的点数:

设备 屏幕大小(点)
Iphone 5s,4英寸 320 x 568
iPhone 4s,3.5英寸 320 x 480
ipad 768 x 1024

points都是用来给view的坐标系做单位的,用与绘制内容. 像素是屏幕的分辨率的单位,有的设备一个点等于一个像素,但有的高清屏幕上一个点等于多个像素, 所以你要牢记:点不一定和屏幕像素一一对应. 在绘制内容时,不管是UIKit还是CoreGraphic都是用点为单位的,虽然底层系统会将点转换为屏幕像素来显示内容,但我们在绘制代码中不需要考虑,也就是说绘图是不需要考虑当前设备屏幕分辨率.

在iOS中像OpenGL ES这种绘图技术就是基于屏幕像素的,这是在绘图代码中就需要考虑屏幕像素. 还有像iOS中的图片显示就需要考虑屏幕的分辨率,在资源文件中iOS根据图片的后缀(@2x,@3x)来自动转换图片来适配不同的分辨率的屏幕. UIView也提供了scale信息来基于像素绘图技术的使用,如果想进一步学习该技术可以参考苹果文档Drawing and Printing Guide for iOS.中一节内容:Supporting High-Resolution Screens In Views

View的runtime交互模型


任意时刻屏幕交互和代码产生view的变化,在底层UIKit一系列复杂的操作来响应改变或者更新,在该系列操作过程中,UIKit暴露了一些点供开发者加入个性代码.理解这些暴露点可以帮助理解view的底层逻辑.下图展示了屏幕产生的touch事件后,view处理该事件将流程图(代码产生的view事件也是同样的流程):

UIKit和view交互的流程图

通过上图的展示,我们可以看到在个状态下发生了什么和开发者需要如何参与该交互流程以开发自己的应用:

  1. 用户触摸屏幕

  2. 硬件将触摸事件报告给UIKit框架

  3. UIKit将触摸事件封装成UIEvent对象,然后将该对象派送到合适的view.(如果想进一步了解事件的传递和响应,请看Apple文档: Event Handling Guide for iOS)

  4. 你需要在view中处理event,你的事件处理代码可以如下:

    • 改变view或者view的subview的属性(frame,bounds,alpha,等)
    • 调用view或者view的subview的setNeedsLayout来标记该view需要更新布局
    • 调用view或者view的subview的setNeedsDisplay或者setNeedsDisplayInRect:来标记该view需要重绘
    • 通知controller更新部分数据
      上面的动作根据个人需求来进行实施
  5. 如果view的在几何上有更新,UIKit将会根据下面规则来更新view的subviews:

    1. 如果view使用了autoresizing, UIKit根据自动布局的规则来调整subviews.如果你想学习自动布局的工作原理请看Apple的文档Handling Layout Changes Automatically Using Autoresizing Rules
    2. 如果你的view重写了layoutSubViews方法,UIKit将会调用它
      • 自定义view时,在layoutSubViews中可以对该view中的subviews进行调整(比如,大小,位置). 举个列子,有个这样的view,该view很大需要滚动显示. 那么我们可以创建几个subview作为可复用的"砖头"(tiles)来部分显示内容,而不是一下创建整个要显示的内容,不管怎么样,这样做不利于内存的利用. 这时重写layoutSubViews时,需要隐藏屏幕外的砖头或将他们移动新的位置用他们来重绘将要显示的内容,需要重绘的砖头的布局代码此时也就失效了,需要重新设置布局.
  6. 如果view标记了需要重绘,那么UIKit会要求view重绘.

    • 自定义view重写drawRect:,UIKit会自动调用该方法.需要注意的是在方法中只能做跟视觉更新有关的事情,不能干其他事情,比如更新数据,更新布局等,需要快速的完成视觉上的更新. 系统提供的view通常不会实现drawRect:方法,但会在drawRect调用时来管理他们的内容绘制.
  7. 任何需要更新的view会一起和应用中的其他视觉内容打包压缩给设备硬件

  8. 然后绘图硬件将渲染好的内容显示到屏幕上

注意:上面的更新模型一般只适用于使用标准的系统view和标准的绘图技术的应用. 如果你的应用中使用的OpenGL技术来绘图话,情况就不一样了,具体请看文档OpenGL ES Programming Guide

在前面的步骤中,自定义view中的需要关注的几个点有:

  • event-handing事件处理方法:
    • touchesBegan:withEvent:
    • touchesMoved:withEvent:
    • touchesEnded:withEvent:
    • touchesCancelled:withEvent:
  • layoutSubViews方法
  • drawRect:方法

上面那些方法是view提供给我们重写的,你可以根据特定业务需求来重写部分方法.比如你使用手势时,event-handing方法就不需要重写了,同样view中不含subview,或者它size不变,那么就不需要重写layoutSubviews方法,像drawRect:方法都是用来用重绘view中的内容的,当view的内容不需要更新时就不需要重写该方法.
除了上述几个主要的重写方法,UIView还提供了其他方法供用户重写,具体请看UIView这个类.

关于view性能方面的几个建议


再使用自定义view时,需要关注view的性能问题,在优化view性能之前应该先收集数据,然后在确定问题点之后才是优化.

不是所有的view都有一个controller与之对应

controller管理view的层级树,通常controller由很多个view组成.在iPhone中一个view层级树通常充满整个屏幕,iPad中有部分view的层级树只是占屏幕的一部分.
你需要明确你应用中controller的作用. controller提供了许多功能,比如调整view的显示,view移除和添加,切换,低内存警告时,controller会释放无用的view和对象,当设备旋转时controller会旋转view的方向以便显示正常. 了解更多关于controller的信息请看Apple文档View Controller Programming Guide for iOS

尽量少地绘制内容

view的内容绘制非常耗性能,所以不要绘制多余的东西,绘制算法要足够好. 如果许多不同view中的内容可以结合在一个view绘制,那么最好将其结合绘制.如果系统的view能解决问题,那么久没必要集成UIView后重写drawRect:方法.

充分利用view的Content Modes

content mode能够减少view绘制的时间. 默认情况下view的contentMode是UIViewContentModeScaleToFill,避免使用UIViewContentModeRedraw

尽量将view设为不透明(Opaque设置NO)

view的属性opaque控制了view的透明度.如果将它设置YES的话,UIKit需要花资源去将几个不同view内容混合渲染来显示最终结构,这样很性能.所以能使view不透明就不透明.

当你view在滚动时,view的绘图策略需要调整

view在滚动时在短时间内会导致view大量重绘.所以如果你的重绘代码的算法不好的,就会产生性能问题. 举个列子:在滚动时你可以占时将内容的绘制的质量降低或者改变content mode,当滚动结束后,将view更新到之前的状态并且更新内容.

不要在自定义控件中内嵌subviews

虽然理论上可以自定义控件,但苹果建议你不要这样干. 控件的自定义可以通过控件本身提供的接口来实现,这些接口是经过了良好的设计的; 就像UIButton,你想改变它的背景图片,你可以通过UIButton中方法setBackgroundImage: forState:来实现.使用UIKit定义好的接口能保证代码运行正确,如果你自己添加一个imageView进去控件中的话,可能会导致一些错误(可以看苹果本文档的英文原文).

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容