学习要点:
1.事件的触发与传递
2.控件的绘制与重绘
事件的触发与传递
我以前自己做游戏,做过整套的UI控件,对UI架构还是比较了解的,现在主流事件触发、处理机制概括来说就是,“自上而下的判断,自下而上的冒泡”。
当发生一个点击事件,首先判断当前交点位于哪个view,view中的哪个子view,子view中那个控件,控件中哪个位置,一层层的判断,然后再反过来一层层的回调处理函数。
当前主流的语言基本都是这样的做法,早期windows/MFC是直接通过消息队列来派发事件,这毫无疑问是一种比较落后的做法,造成消息传递非常困难,现在貌似已经没有语言那么做了。
还有就是消息的定义、归集、分类和派发等一些具体设计,比较难的地方包括焦点的转移,单选框的处理等等,就不具体展开了。
IOS的做法基本是就是遵循这一套原则,如果下面是UIKit的类图,可以看到NSView是从一个叫UIResponder的类派生出来的,这个类里就定义所有的触摸事件和一些转发机制,确保了所有view和从NSView派生出来的控件都有统一的事件回调接口。
控件的绘制与重绘
基本思想
界面的绘制比较复杂,早期windows的做法是简单的直接把控件都绘制在window上,但这样有一个问题,如果要作动画或者scrollView的话就需要重绘所有控件,这样会造成严重的效率问题,这也是为什么Windows平台几乎没有任何的动画特性。
一个基本的优化方法就是创建一个图片缓冲,比方说scrollView,可以把scrollView中的控件先渲染到一个图片,然后再绘制到窗口。在滚动时只需要移动一下图片就行了,仅当scrollView的内容发生改变是再重绘这个图片。
这样做可以极大的提改绘制效率,代价就是图片缓冲的额外开销。
但这也是必须的,做过控件底层的人就会知道控件和文字的绘制实际上是非常慢的,比绘制几万个三角形的模型快不了多少。想要出流畅的动画,这点代价是可以接受的。
还有一个棘手问题就是有时候会碰到控件需要横跨到父View绘图区域外的问题,这种情况不多但也不少,需要特殊处理,虽然不是不能解决,但会破坏代码的结构,所以一般来说大部分控件都不支持这种用法,超出部分将被直接切掉,除了ComboBox,Canvas和Menu等少数控件。
IOS基本也是使用这种绘图模式,你可以使用一个单独图层来缓冲你的控件,也可以不用,看具体需求而且。
IOS中的实现
UIView是iOS系统中界面元素的基础,所有的界面元素都是继承自它。UIView中可以直接绘图,方法是重载drawRect,在方法中获取相应的contextRef并绘图,这种方式绘图将会直接绘制在其父级某个view的缓冲里。
另一种方式是使用CALayer类。在ISO中使用一个叫CALayer(Core Animation Layer)的类来管理缓冲图层,他通常hold在UIView下,但也可以单独使用。
你可以通过设置[view setWantsLayer: YES]来控制是否开启图层功能。
当使用CALayer的时候,UIView就只作为一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性。绘图方式是重载CALayer的drawInContext方法,这种方式将会绘制在CALayer中的缓冲图层中,然后UIView中通过调用updateLayer来将其绘制到父图层之中,你可以通过重载这个函数来做出各种动画效果。
所以你可以使用CALayer也可以不用,关键在于你是否有动画的需求。IOS中没有容器的概念,但基本就是那个意思,我的建议是对于一个整体界面单元使用CALayer,但个体控件就不要用了,毕竟移动设备内存有限。Mac上的话就无所谓了,多使用可以提高一点点效率,但Mac强大的显示能力使你很难感觉出差别。
关于Cell
Cocoa中绘图另一个很有特色的地方就是Cell的概念。Cocoa使用了一种策略模式,把实际的绘图逻辑封装起来,独立出一个绘图接口,CALayer会调用相应的Cell来进行绘图。这带来了很好的重用性并且可以方便的重载。
了解UITableView
IOS中还有一个非常独特的控件就是UITableView。本质是说UITableView就是一个列表控件,从scrollView继承出来,但是有一个问题,就是列表可能会非常长,于是就有可能会产生一个超长的图片缓冲,在内存非常有限的手机上这是不能被接受的(好吧,其实在PC上是也是个问题,一般来说PC显卡能接受的图片素材不超过4096,手机一般要求小于2048)。
解决办法就是动态加载,当且仅当UITableView中的某项被显示时他才会被加载和绘制。但是显然苹果对于UITableView的优化并不到位,是个有名的问题控件,用的时候一定要注意。
其实我觉得优化的办法或许并不复杂,提前的缓冲几个UITableViewCell,就可以有效避免卡顿。
UI的优化
- 少用透明,透明控件重绘时需要重绘被其压在下面的所有控件,实际情况一般会更糟,具体看你的drawRect实现而定,比如有没有充分的分析dirtyRect。所以除非必要少用透明。
- 尽量的重用Cell,Cell带来了很大的灵活性,但也带来了一大堆的Cell实例,如果可能还是尽量的重复使用同一个Cell。