在这篇文章中,我们将为您详细介绍Swift37/90Days-iOS中的设计模式(Swift版本)02的内容,并且讨论关于swift设计原则的相关问题。此外,我们还会涉及一些关于Android4.4(
在这篇文章中,我们将为您详细介绍Swift37/90Days - iOS 中的设计模式 (Swift 版本) 02的内容,并且讨论关于swift设计原则的相关问题。此外,我们还会涉及一些关于Android 4.4 (KitKat) 中的设计模式 - Graphics 子系统、Dubbo 中的设计模式、Highcharts iOS Swift:HIGauge.dial 在 iOS swift 中总是返回 nil、iOS 18发布啦!iOS 18好吗?iOS 18值得更新吗?iOS 18beta版的知识,以帮助您更全面地了解这个主题。
本文目录一览:- Swift37/90Days - iOS 中的设计模式 (Swift 版本) 02(swift设计原则)
- Android 4.4 (KitKat) 中的设计模式 - Graphics 子系统
- Dubbo 中的设计模式
- Highcharts iOS Swift:HIGauge.dial 在 iOS swift 中总是返回 nil
- iOS 18发布啦!iOS 18好吗?iOS 18值得更新吗?iOS 18beta版
Swift37/90Days - iOS 中的设计模式 (Swift 版本) 02(swift设计原则)
(阔别一个多月。。终于完成了。。)
更新声明
翻译自 Introducing iOS Design Patterns in Swift – Part 2/2 ,本教程 objc 版本的作者是 Eli Ganem ,由 vincent Ngo 更新为 Swift 版本。
再续前缘
欢迎来到教程的第二部分!这是本系列教程的最后一部分,在这一章的学习里,我们会更加深入的学习一些 iOS 开发中常见的设计模式:适配器模式 (Adapter),观察者模式 (Observer),备忘录模式 (Memento)。
开始吧少年们!
准备工作
你可以先下载上一章结束时的项目源码 。
在第一部分的教程里,我们完成了这样一个简单的应用:
我们的原计划是在上面的空白处放一个可以横滑浏览专辑的视图。其实仔细想想,这个控件是可以应用在其他地方的,我们不妨把它做成一个可复用的视图。
为了让这个视图可以复用,显示内容的工作都只能交给另一个对象来完成:它的委托。这个横滑页面应该声明一些方法让它的委托去实现,就像是 UITableView
的 UITableViewDelegate
一样。我们将会在下一个设计模式中实现这个功能。
适配器模式 - Adapter
适配器把自己封装起来然后暴露统一的接口给其他类,这样即使其他类的接口各不相同,也能相安无事,一起工作。
如果你熟悉适配器模式,那么你会发现苹果在实现适配器模式的方式稍有不同:苹果通过委托实现了适配器模式。委托相信大家都不陌生。举个例子,如果一个类遵循了 NSCoying
的协议,那么它一定要实现 copy
方法。
如何使用适配器模式
横滑的滚动栏理论上应该是这个样子的:
新建一个 Swift 文件:HorizontalScroller.swift
,作为我们的横滑滚动控件, HorizontalScroller
继承自 UIView
。
打开 HorizontalScroller.swift
文件并添加如下代码:
@objc protocol HorizontalScrollerDelegate { }
这行代码定义了一个新的协议: HorizontalScrollerDelegate
。我们在前面加上了 @objc
的标记,这样我们就可以像在 objc 里一样使用 @optional
的委托方法了。
接下来我们在大括号里定义所有的委托方法,包括必须的和可选的:
// 在横滑视图中有多少页面需要展示 func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> Int // 展示在第 index 位置显示的 UIView func horizontalScrollerViewAtIndex(scroller: HorizontalScroller,index:Int) -> UIView // 通知委托第 index 个视图被点击了 func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller,index:Int) // 可选方法,返回初始化时显示的图片下标,默认是0 optional func initialViewIndex(scroller: HorizontalScroller) -> Int
其中,没有 option
标记的方法是必须实现的,一般来说包括那些用来显示的必须数据,比如如何展示数据,有多少数据需要展示,点击事件如何处理等等,不可或缺;有 option
标记的方法为可选实现的,相当于是一些辅助设置和功能,就算没有实现也有默认值进行处理。
在 HorizontalScroller
类里添加一个新的委托对象:
weak var delegate: HorizontalScrollerDelegate?
为了避免循环引用的问题,委托是 weak
类型。如果委托是 strong
类型的,当前对象持有了委托的强引用,委托又持有了当前对象的强引用,这样谁都无法释放就会导致内存泄露。
委托是可选类型,所以很有可能当前类的使用者并没有指定委托。但是如果指定了委托,那么它一定会遵循 HorizontalScrollerDelegate
里约定的内容。
再添加一些新的属性:
// 1 private let VIEW_PADDING = 10 private let VIEW_DIMENSIONS = 100 private let VIEWS_OFFSET = 100 // 2 private var scroller : UIScrollView! // 3 var viewArray = [UIView]()
上面标注的三点分别做了这些事情:
- 定义一个常量,用来方便的改变布局。现在默认的是显示的内容长宽为100,间隔为10。
- 创建一个
UIScrollView
作为容器。 - 创建一个数组用来存放需要展示的数据
接下来实现初始化方法:
override init(frame: CGRect) { super.init(frame: frame) initializeScrollView() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) initializeScrollView() } func initializeScrollView() { //1 scroller = UIScrollView() addSubview(scroller) //2 scroller.setTranslatesAutoresizingMaskIntoConstraints(false) //3 self.addConstraint(NSLayoutConstraint(item: scroller,attribute: .Leading,relatedBy: .Equal,toItem: self,multiplier: 1.0,constant: 0.0)) self.addConstraint(NSLayoutConstraint(item: scroller,attribute: .Trailing,attribute: .Top,attribute: .Bottom,constant: 0.0)) //4 let tapRecognizer = UITapGestureRecognizer(target: self,action:Selector("scrollerTapped:")) scroller.addGestureRecognizer(tapRecognizer) }
上面的代码做了如下工作:
- 创建一个
UIScrollView
对象并且把它加到父视图中。 - 关闭
autoresizing masks
,从而可以使用AutoLayout
进行布局。 - 给
scrollview
添加约束。我们希望scrollview
能填满HorizontalScroller
。 - 创建一个点击事件,检测是否点击到了专辑封面,如果确实点击到了专辑封面,我们需要通知
HorizontalScroller
的委托。
添加委托方法:
func scrollerTapped(gesture: UITapGestureRecognizer) { let location = gesture.locationInView(gesture.view) if let delegate = self.delegate { for index in 0..<delegate.numberOfViewsForHorizontalScroller(self) { let view = scroller.subviews[index] as UIView if CGRectContainsPoint(view.frame,location) { delegate.horizontalScrollerClickedViewAtIndex(self,index: index) scroller.setContentOffset(CGPointMake(view.frame.origin.x - self.frame.size.width/2 + view.frame.size.width/2,0),animated:true) break } } } }
我们把 gesture
作为一个参数传了进来,这样就可以获取点击的具体坐标了。
接下来我们调用了 numberOfViewsForHorizontalScroller
方法,HorizontalScroller
不知道自己的 delegate
具体是谁,但是知道它一定实现了 HorizontalScrollerDelegate
协议,所以可以放心的调用。
对于 scroll view
中的 view
,通过 CGRectContainsPoint
进行点击检测,从而获知是哪一个 view
被点击了。当找到了点击的 view
的时候,则会调用委托方法里的 horizontalScrollerClickedViewAtIndex
方法通知委托。在跳出 for
循环之前,先把点击到的 view
居中。
接下来我们再加个方法获取数组里的 view
:
func viewAtIndex(index :Int) -> UIView { return viewArray[index] }
这个方法很简单,只是用来更方便获取数组里的 view
而已。在后面实现高亮选中专辑的时候会用到这个方法。
添加如下代码用来重新加载 scroller
:
func reload() { // 1 - Check if there is a delegate,if not there is nothing to load. if let delegate = self.delegate { //2 - Will keep adding new album views on reload,need to reset. viewArray = [] let views: NSArray = scroller.subviews // 3 - remove all subviews views.enumerateObjectsUsingBlock { (object: AnyObject!,idx: Int,stop: UnsafeMutablePointer<ObjCBool>) -> Void in object.removeFromSuperview() } // 4 - xValue is the starting point of the views inside the scroller var xValue = VIEWS_OFFSET for index in 0..<delegate.numberOfViewsForHorizontalScroller(self) { // 5 - add a view at the right position xValue += VIEW_PADDING let view = delegate.horizontalScrollerViewAtIndex(self,index: index) view.frame = CGRectMake(CGFloat(xValue),CGFloat(VIEW_PADDING),CGFloat(VIEW_DIMENSIONS),CGFloat(VIEW_DIMENSIONS)) scroller.addSubview(view) xValue += VIEW_DIMENSIONS + VIEW_PADDING // 6 - Store the view so we can reference it later viewArray.append(view) } // 7 scroller.contentSize = CGSizeMake(CGFloat(xValue + VIEWS_OFFSET),frame.size.height) // 8 - If an initial view is defined,center the scroller on it if let initialView = delegate.initialViewIndex?(self) { scroller.setContentOffset(CGPointMake(CGFloat(initialView)*CGFloat((VIEW_DIMENSIONS + (2 * VIEW_PADDING))),animated: true) } } }
这个 reload
方法有点像是 UITableView
里面的 reloadData
方法,它会重新加载所有数据。
一段一段的看下上面的代码:
- 在调用
reload
之前,先检查一下是否有委托。 - 既然要清除专辑封面,那么也需要重新设置
viewArray
,要不然以前的数据会累加进来。 - 移除先前加入到
scrollview
的子视图。 - 所有的
view
都有一个偏移量,目前默认是100,我们可以修改VIEW_OFFSET
这个常量轻松的修改它。 -
HorizontalScroller
通过委托获取对应位置的view
并且把它们放在对应的位置上。 - 把
view
存进viewArray
以便后面的操作。 - 当所有
view
都安放好了,再设置一下content size
这样才可以进行滑动。 -
HorizontalScroller
检查一下委托是否实现了initialViewIndex()
这个可选方法,这种检查十分必要,因为这个委托方法是可选的,如果委托没有实现这个方法则用0作为默认值。最终设置scroll view
将初始的view
放置到居中的位置。
当数据发生改变的时候,我们需要调用 reload
方法。当 HorizontalScroller
被加到其他页面的时候也需要调用这个方法,我们在 HorizontalScroller.swift
里面加入如下代码:
override func didMovetoSuperview() { reload() }
在当前 view
添加到其他 view
里的时候就会自动调用 didMovetoSuperview
方法,这样可以在正确的时间重新加载数据。
HorizontalScroller
的最后一部分是用来确保当前浏览的内容时刻位于正中心的位置,为了实现这个功能我们需要在用户滑动结束的时候做一些额外的计算和修正。
添加下面这个方法:
func centerCurrentView() { var xFinal = scroller.contentOffset.x + CGFloat((VIEWS_OFFSET/2) + VIEW_PADDING) let viewIndex = xFinal / CGFloat((VIEW_DIMENSIONS + (2*VIEW_PADDING))) xFinal = viewIndex * CGFloat(VIEW_DIMENSIONS + (2*VIEW_PADDING)) scroller.setContentOffset(CGPointMake(xFinal,animated: true) if let delegate = self.delegate { delegate.horizontalScrollerClickedViewAtIndex(self,index: Int(viewIndex)) } }
上面的代码计算了当前视图里中心位置距离多少,然后算出正确的居中坐标并滑动到那个位置。最后一行是通知委托所选视图已经发生了改变。
为了检测到用户滑动的结束时间,我们还需要实现 uiscrollviewdelegate
的方法。在文件结尾加上下面这个扩展:
extension HorizontalScroller: uiscrollviewdelegate { func scrollViewDidEndDragging(scrollView: UIScrollView,willDecelerate decelerate: Bool) { if !decelerate { centerCurrentView() } } func scrollViewDidEndDecelerating(scrollView: UIScrollView) { centerCurrentView() } }
当用户停止滑动的时候,scrollViewDidEndDragging(_:willDecelerate:)
这个方法会通知委托。如果滑动还没有停止,decelerate
的值为 true
。当滑动完全结束的时候,则会调用 scrollViewDidEndDecelerating
这个方法。在这两种情况下,你都应该把当前的视图居中,因为用户的操作可能会改变当前视图。
你的 HorizontalScroller
已经可以使用了!回头看看前面写的代码,你会看到我们并没有涉及什么 Album
或者 AlbumView
的代码。这是极好的,因为这样意味着这个 scroller
是完全独立的,可以复用。
运行一下你的项目,确保编译通过。
这样,我们的 HorizontalScroller
就完成了,接下来我们就要把它应用到我们的项目里了。首先,打开 Main.sstoryboard
文件,点击上面的灰色矩形,设置 Class
为 HorizontalScroller
:
接下来,在 assistant editor
模式下向 ViewController.swift
拖拽生成 outlet ,命名为 scroller
:
接下来打开 ViewController.swift
文件,是时候实现 HorizontalScrollerDelegate
委托里的方法啦!
添加如下扩展:
extension ViewController: HorizontalScrollerDelegate { func horizontalScrollerClickedViewAtIndex(scroller: HorizontalScroller,index: Int) { //1 let prevIoUsAlbumView = scroller.viewAtIndex(currentAlbumIndex) as AlbumView prevIoUsAlbumView.highlightAlbum(didHighlightView: false) //2 currentAlbumIndex = index //3 let albumView = scroller.viewAtIndex(index) as AlbumView albumView.highlightAlbum(didHighlightView: true) //4 showDataForAlbum(index) } }
让我们一行一行的看下这个委托的实现:
- 获取上一个选中的相册,然后取消高亮
- 存储当前点击的相册封面
- 获取当前选中的相册,设置为高亮
- 在
table view
里面展示新数据
接下来在扩展里添加如下方法:
func numberOfViewsForHorizontalScroller(scroller: HorizontalScroller) -> (Int) { return allAlbums.count }
这个委托方法返回 scroll vew
里面的视图数量,因为是用来展示所有的专辑的封面,所以数目也就是专辑数目。
然后添加如下代码:
func horizontalScrollerViewAtIndex(scroller: HorizontalScroller,index: Int) -> (UIView) { let album = allAlbums[index] let albumView = AlbumView(frame: CGRectMake(0,100,100),albumCover: album.coverUrl) if currentAlbumIndex == index { albumView.highlightAlbum(didHighlightView: true) } else { albumView.highlightAlbum(didHighlightView: false) } return albumView }
我们创建了一个新的 AlbumView
,然后检查一下是不是当前选中的专辑,如果是则设为高亮,最后返回结果。
是的就是这么简单!三个方法,完成了一个横向滚动的浏览视图。
我们还需要创建这个滚动视图并把它加到主视图里,但是在这之前,先添加如下方法:
func reloadScroller() { allAlbums = LibraryAPI.sharedInstance.getAlbums() if currentAlbumIndex < 0 { currentAlbumIndex = 0 } else if currentAlbumIndex >= allAlbums.count { currentAlbumIndex = allAlbums.count - 1 } scroller.reload() showDataForAlbum(currentAlbumIndex) }
这个方法通过 LibraryAPI
加载专辑数据,然后根据 currentAlbumIndex
的值设置当前视图。在设置之前先进行了校正,如果小于0则设置第一个专辑为展示的视图,如果超出了范围则设置最后一个专辑为展示的视图。
接下来只需要指定委托就可以了,在 viewDidLoad
最后加入一下代码:
scroller.delegate = self reloadScroller()
因为 HorizontalScroller
是在 StoryBoard
里初始化的,所以我们需要做的只是指定委托,然后调用 reloadScroller()
方法,从而加载所有的子视图并且展示专辑数据。
标注:如果协议里的方法过多,可以考虑把它分解成几个更小的协议。UITableViewDelegate
和 UITableViewDataSource
就是很好的例子,它们都是 UITableView
的协议。尝试去设计你自己的协议,让每个协议都单独负责一部分功能。
运行一下当前项目,看一下我们的新页面:
等下,滚动视图显示出来了,但是专辑的封面怎么不见了?
啊哈,是的。我们还没完成下载部分的代码,我们需要添加下载图片的方法。因为我们所有的访问都是通过 LibraryAPI
实现的,所以很显然我们下一步应该去完善这个类了。不过在这之前,我们还需要考虑一些问题:
-
AlbumView
不应该直接和LibraryAPI
交互,我们不应该把视图的逻辑和业务逻辑混在一起。 - 同样,
LibraryAPI
也不应该知道AlbumView
这个类。 - 如果
AlbumView
要展示封面,LibraryAPI
需要告诉AlbumView
图片下载完成。
看起来好像很难的样子?别绝望,接下来我们会用观察者模式 (Observer Pattern
) 解决这个问题!:]
观察者模式 - Observer
在观察者模式里,一个对象在状态变化的时候会通知另一个对象。参与者并不需要知道其他对象的具体是干什么的 - 这是一种降低耦合度的设计。这个设计模式常用于在某个属性改变的时候通知关注该属性的对象。
常见的使用方法是观察者注册监听,然后再状态改变的时候,所有观察者们都会收到通知。
在 MVC 里,观察者模式意味着需要允许 Model
对象和 View
对象进行交流,而不能有直接的关联。
Cocoa
使用两种方式实现了观察者模式: Notification
和 Key-Value Observing (KVO)
。
通知 - Notification
不要把这里的通知和推送通知或者本地通知搞混了,这里的通知是基于订阅-发布模型的,即一个对象 (发布者) 向其他对象 (订阅者) 发送消息。发布者永远不需要知道订阅者的任何数据。
Apple
对于通知的使用很频繁,比如当键盘弹出或者收起的时候,系统会分别发送 UIKeyboardWillShowNotification/UIKeyboardWillHideNotification
的通知。当你的应用切到后台的时候,又会发送 UIApplicationDidEnterBackgroundNotification
的通知。
注意:打开 UIApplication.swift
文件,在文件结尾你会看到二十多种系统发送的通知。
如何使用通知
打开 AlbumView.swift
然后在 init
的最后插入如下代码:
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification",object: self,userInfo: ["imageView":coverImage,"coverUrl" : albumCover])
这行代码通过 NSNotificationCenter
发送了一个通知,通知信息包含了 UIImageView
和图片的下载地址。这是下载图像需要的所有数据。
然后在 LibraryAPI.swift
的 init
方法的 super.init()
后面加上如下代码:
NSNotificationCenter.defaultCenter().addobserver(self,selector:"downloadImage:",name: "BLDownloadImageNotification",object: nil)
这是等号的另一边:观察者。每当 AlbumView
发出一个 BLDownloadImageNotification
通知的时候,由于 LibraryAPI
已经注册了成为观察者,所以系统会调用 downloadImage()
方法。
但是,在实现 downloadImage()
之前,我们必须先在 dealloc
里取消监听。如果没有取消监听消息,消息会发送给一个已经销毁的对象,导致程序崩溃。
在 LibaratyAPI.swift
里加上取消订阅的代码:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }
当对象销毁的时候,把它从所有消息的订阅列表里去除。
这里还要做一件事情:我们最好把图片存储到本地,这样可以避免一次又一次下载相同的封面。
打开 PersistencyManager.swift
添加如下代码:
func saveImage(image: UIImage,filename: String) { let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)") let data = UIImagePNGRepresentation(image) data.writetoFile(path,atomically: true) } func getimage(filename: String) -> UIImage? { var error: NSError? let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)") let data = NSData(contentsOfFile: path,options: .UncachedRead,error: &error) if let unwrappedError = error { return nil } else { return UIImage(data: data!) } }
代码很简单直接,下载的图片会存储在 Documents
目录下,如果没有检查到缓存文件, getimage()
方法则会返回 nil
。
然后在 LibraryAPI.swift
添加如下代码:
func downloadImage(notification: NSNotification) { //1 let userInfo = notification.userInfo as [String: AnyObject] var imageView = userInfo["imageView"] as UIImageView? let coverUrl = userInfo["coverUrl"] as Nsstring //2 if let imageViewUnWrapped = imageView { imageViewUnWrapped.image = persistencyManager.getimage(coverUrl.lastPathComponent) if imageViewUnWrapped.image == nil { //3 dispatch_async(dispatch_get_global_queue(disPATCH_QUEUE_PRIORITY_DEFAULT,{ () -> Void in let downloadedImage = self.httpClient.downloadImage(coverUrl) //4 dispatch_sync(dispatch_get_main_queue(),{ () -> Void in imageViewUnWrapped.image = downloadedImage self.persistencyManager.saveImage(downloadedImage,filename: coverUrl.lastPathComponent) }) }) } } }
拆解一下上面的代码:
-
downloadImage
通过通知调用,所以这个方法的参数就是NSNotification
本身。UIImageView
和URL
都可以从其中获取到。 - 如果以前下载过,从
PersistencyManager
里获取缓存。 - 如果图片没有缓存,则通过
HTTPClient
获取。 - 如果下载完成,展示图片并用
PersistencyManager
存储到本地。
再回顾一下,我们使用外观模式隐藏了下载图片的复杂程度。通知的发送者并不在乎图片是如何从网上下载到本地的。
运行一下项目,可以看到专辑封面已经显示出来了:
关了应用再重新运行,注意这次没有任何延时就显示了所有的图片,因为我们已经有了本地缓存。我们甚至可以在没有网络的情况下正常使用我们的应用。不过出了问题:这个用来提示加载网络请求的小菊花怎么一直在显示!
我们在下载图片的时候开启了这个白色小菊花,但是在图片下载完毕的时候我们并没有停掉它。我们可以在每次下载成功的时候发送一个通知,但是我们不这样做,这次我们来用用另一个观察者模式: KVO 。
键值观察 - KVO
在 KVO 里,对象可以注册监听任何属性的变化,不管它是否持有。如果感兴趣的话,可以读一读苹果 KVO 编程指南。
如何使用 KVO
正如前面所提及的, 对象可以关注任何属性的变化。在我们的例子里,我们可以用 KVO 关注 UIImageView
的 image
属性变化。
打开 AlbumView.swift
文件,找到 init(frame:albumCover:)
方法,在把 coverImage
添加到 subView
的代码后面添加如下代码:
coverImage.addobserver(self,forKeyPath: "image",options: nil,context: nil)
这行代码把 self
(也就是当前类) 添加到了 coverImage
的 image
属性的观察者里。
在销毁的时候,我们也需要取消观察。还是在 AlbumView.swift
文件里,添加如下代码:
deinit { coverImage.removeObserver(self,forKeyPath: "image") }
最终添加如下方法:
override func observeValueForKeyPath(keyPath: String,ofObject object: AnyObject,change: [NSObject : AnyObject],context: UnsafeMutablePointer<Void>) { if keyPath == "image" { indicator.stopAnimating() } }
必须在所有的观察者里实现上面的代码。在检测到属性变化的时候,系统会自动调用这个方法。在上面的代码里,我们在图片加载完成的时候把那个提示加载的小菊花去掉了。
再次运行项目,你会发现一切正常了:
注意:一定要记得移除观察者,否则如果对象已经销毁了还给它发送消息会导致应用崩溃。
此时你可以把玩一下当前的应用然后再关掉它,你会发现你的应用的状态并没有存储下来。最后看见的专辑并不会再下次打开应用的时候出现。
为了解决这个问题,我们可以使用下一种模式:备忘录模式。
备忘录模式 - Memento
备忘录模式捕捉并且具象化一个对象的内在状态。换句话说,它把你的对象存在了某个地方,然后在以后的某个时间再把它恢复出来,而不会打破它本身的封装性,私有数据依旧是私有数据。
如何使用备忘录模式
在 ViewController.swift
里加上下面两个方法:
//MARK: Memento Pattern func saveCurrentState() { // When the user leaves the app and then comes back again,he wants it to be in the exact same state // he left it. In order to do this we need to save the currently displayed album. // Since it's only one piece of information we can use NSUserDefaults. NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex,forKey: "currentAlbumIndex") } func loadPrevIoUsstate() { currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex") showDataForAlbum(currentAlbumIndex) }
saveCurrentState
把当前相册的索引值存到 NSUserDefaults
里。NSUserDefaults
是 iOS 提供的一个标准存储方案,用于保存应用的配置信息和数据。
loadPrevIoUsstate
方法加载上次存储的索引值。这并不是备忘录模式的完整实现,但是已经离目标不远了。
接下来在 viewDidLoad
的 scroller.delegate = self
前面调用:
loadPrevIoUsstate()
这样在刚初始化的时候就加载了上次存储的状态。但是什么时候存储当前状态呢?这个时候我们可以用通知来做。在应用进入到后台的时候, iOS 会发送一个 UIApplicationDidEnterBackgroundNotification
的通知,我们可以在这个通知里调用 saveCurrentState
这个方法。是不是很方便?
在 viewDidLoad
的最后加上如下代码:
NSNotificationCenter.defaultCenter().addobserver(self,selector:"saveCurrentState",name: UIApplicationDidEnterBackgroundNotification,object: nil)
现在,当应用即将进入后台的时候,ViewController
会调用 saveCurrentState
方法自动存储当前状态。
当然也别忘了取消监听通知,添加如下代码:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }
这样就确保在 ViewController
销毁的时候取消监听通知。
这时再运行程序,随意移到某个专辑上,然后按下 Home 键把应用切换到后台,再在 Xcode 上把 App 关闭。重新启动,会看见上次记录的专辑已经存了下来并成功还原了:
看起来专辑数据好像是对了,但是上面的滚动条似乎出了问题,没有居中啊!
这时 initialViewIndex
方法就派上用场了。由于在委托里 (也就是 ViewController
) 还没实现这个方法,所以初始化的结果总是第一张专辑。
为了修复这个问题,我们可以在 ViewController.swift
里添加如下代码:
func initialViewIndex(scroller: HorizontalScroller) -> Int { return currentAlbumIndex }
现在 HorizontalScroller
可以根据 currentAlbumIndex
自动滑到相应的索引位置了。
再次重复上次的步骤,切到后台,关闭应用,重启,一切顺利:
回头看看 PersistencyManager
的 init
方法,你会发现专辑数据是我们硬编码写进去的,而且每次创建 PersistencyManager
的时候都会再创建一次专辑数据。而实际上一个比较好的方案是只创建一次,然后把专辑数据存到本地文件里。我们如何把专辑数据存到文件里呢?
一种方案是遍历 Album
的属性然后把它们写到一个 plist
文件里,然后如果需要的时候再重新创建 Album
对象。这并不是最好的选择,因为数据和属性不同,你的代码也就要相应的产生变化。举个例子,如果我们以后想添加 Movie
对象,它有着完全不同的属性,那么存储和读取数据又需要重写新的代码。
况且你也无法存储这些对象的私有属性,因为其他类是没有访问权限的。这也就是为什么 Apple 提供了 归档 的机制。
归档 - Archiving
苹果通过归档的方法来实现备忘录模式。它把对象转化成了流然后在不暴露内部属性的情况下存储数据。你可以读一读 《iOS 6 by Tutorials》 这本书的第 16 章,或者看下苹果的归档和序列化文档。
如何使用归档
首先,我们需要让 Album
实现 NSCoding
协议,声明这个类是可被归档的。打开 Album.swift
在 class
那行后面加上 NSCoding
:
class Album: NSObject,NSCoding {
然后添加如下的两个方法:
required init(coder decoder: NSCoder) { super.init() self.title = decoder.decodeObjectForKey("title") as String? self.artist = decoder.decodeObjectForKey("artist") as String? self.genre = decoder.decodeObjectForKey("genre") as String? self.coverUrl = decoder.decodeObjectForKey("cover_url") as String? self.year = decoder.decodeObjectForKey("year") as String? } func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeObject(title,forKey: "title") aCoder.encodeObject(artist,forKey: "artist") aCoder.encodeObject(genre,forKey: "genre") aCoder.encodeObject(coverUrl,forKey: "cover_url") aCoder.encodeObject(year,forKey: "year") }
encodeWithCoder
方法是 NSCoding
的一部分,在被归档的时候调用。相对的, init(coder:)
方法则是用来解档的。很简单,很强大。
现在 Album
对象可以被归档了,添加一些代码来存储和加载 Album
数据。
在 PersistencyManager.swift
里添加如下代码:
func saveAlbums() { var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin") let data = NSKeyedArchiver.archivedDataWithRootObject(albums) data.writetoFile(filename,atomically: true) }
这个方法可以用来存储专辑。 NSKeyedArchiver
把专辑数组归档到了 albums.bin
这个文件里。
当我们归档一个包含子对象的对象时,系统会自动递归的归档子对象,然后是子对象的子对象,这样一层层递归下去。在我们的例子里,我们归档的是 albums
因为 Array
和 Album
都是实现 NScopying
接口的,所以数组里的对象都可以自动归档。
用下面的代码取代 PersistencyManager
中的 init
方法:
override init() { super.init() if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) { let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [Album]? if let unwrappedAlbum = unarchiveAlbums { albums = unwrappedAlbum } } else { createPlaceholderAlbum() } } func createPlaceholderAlbum() { //Dummy list of albums let album1 = Album(title: "Best of Bowie",artist: "David Bowie",genre: "Pop",coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png",year: "1992") let album2 = Album(title: "It's My Life",artist: "No Doubt",coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png",year: "2003") let album3 = Album(title: "nothing Like The Sun",artist: "Sting",coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png",year: "1999") let album4 = Album(title: "Staring at the Sun",artist: "U2",coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png",year: "2000") let album5 = Album(title: "American Pie",artist: "Madonna",coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png",year: "2000") albums = [album1,album2,album3,album4,album5] saveAlbums() }
我们把创建专辑数据的方法放到了 createPlaceholderAlbum
里,这样代码可读性更高。在新的代码里,如果存在归档文件, NSKeyedUnarchiver
从归档文件加载数据;否则就创建归档文件,这样下次程序启动的时候可以读取本地文件加载数据。
我们还想在每次程序进入后台的时候存储专辑数据。看起来现在这个功能并不是必须的,但是如果以后我们加了编辑功能,这样做还是很有必要的,那时我们肯定希望确保新的数据会同步到本地的归档文件。
因为我们的程序通过 LibraryAPI
来访问所有服务,所以我们需要通过 LibraryAPI
来通知 PersistencyManager
存储专辑数据。
在 LibraryAPI
里添加存储专辑数据的方法:
func saveAlbums() { persistencyManager.saveAlbums() }
这个方法很简单,就是把 LibraryAPI
的 saveAlbums
方法传递给了 persistencyManager
的 saveAlbums
方法。
然后在 ViewController.swift
的 saveCurrentState
方法的最后加上:
LibraryAPI.sharedInstance.saveAlbums()
在 ViewController
需要存储状态的时候,上面的代码通过 LibraryAPI
归档当前的专辑数据。
运行一下程序,检查一下没有编译错误。
不幸的是似乎没什么简单的方法来检查归档是否正确完成。你可以检查一下 Documents
目录,看下是否存在归档文件。如果要查看其他数据变化的话,还需要添加编辑专辑数据的功能。
不过和编辑数据相比,似乎加个删除专辑的功能更好一点,如果不想要这张专辑直接删除即可。再进一步,万一误删了话,是不是还可以再加个撤销按钮?
最后的润色
现在我们将添加最后一个功能:允许用户删除专辑,以及撤销上次的删除操作。
在 ViewController
里添加如下属性:
// 为了实现撤销功能,我们用数组作为一个栈来 push 和 pop 用户的操作 var undoStack: [(Album,Int)] = []
然后在 viewDidLoad
的 reloadScroller()
后面添加如下代码:
let undobutton = UIBarButtonItem(barButtonSystemItem: .Undo,target: self,action:"undoAction") undobutton.enabled = false; let space = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace,target:nil,action:nil) let trashButton = UIBarButtonItem(barButtonSystemItem: .Trash,target:self,action:"deletealbum") let toolbarButtonItems = [undobutton,space,trashButton] toolbar.setItems(toolbarButtonItems,animated: true)
上面的代码创建了一个 toolbar
,上面有两个按钮,在 undoStack
为空的情况下, undo
的按钮是不可用的。注意 toolbar
已经在 storyboard
里了,我们需要做的只是配置上面的按钮。
我们需要在 ViewController.swift
里添加三个方法,用来处理专辑的编辑事件:增加,删除,撤销。
先写添加的方法:
func addAlbumAtIndex(album: Album,index: Int) { LibraryAPI.sharedInstance.addAlbum(album,index: index) currentAlbumIndex = index reloadScroller() }
做了三件事:添加专辑,设为当前的索引,重新加载滚动条。
接下来是删除方法:
func deletealbum() { //1 var deletedAlbum : Album = allAlbums[currentAlbumIndex] //2 var undoAction = (deletedAlbum,currentAlbumIndex) undoStack.insert(undoAction,atIndex: 0) //3 LibraryAPI.sharedInstance.deletealbum(currentAlbumIndex) reloadScroller() //4 let barButtonItems = toolbar.items as [UIBarButtonItem] var undobutton : UIBarButtonItem = barButtonItems[0] undobutton.enabled = true //5 if (allAlbums.count == 0) { var trashButton : UIBarButtonItem = barButtonItems[2] trashButton.enabled = false } }
挨个看一下各个部分:
- 获取要删除的专辑。
- 创建一个
undoAction
对象,用元组存储Album
对象和它的索引值。然后把这个元组加到了栈里。 - 使用
LibraryAPI
删除专辑数据,然后重新加载滚动条。 - 既然撤销栈里已经有了数据,那么我们需要设置撤销按钮为可用。
- 检查一下是不是还剩专辑,如果没有专辑了那就设置删除按钮为不可用。
最后添加撤销按钮:
func undoAction() { let barButtonItems = toolbar.items as [UIBarButtonItem] //1 if undoStack.count > 0 { let (deletedAlbum,index) = undoStack.removeAtIndex(0) addAlbumAtIndex(deletedAlbum,index: index) } //2 if undoStack.count == 0 { var undobutton : UIBarButtonItem = barButtonItems[0] undobutton.enabled = false } //3 let trashButton : UIBarButtonItem = barButtonItems[2] trashButton.enabled = true }
照着备注的三个步骤再看一下撤销方法里的代码:
- 首先从栈里
pop
出一个对象,这个对象就是我们当初塞进去的元祖,存有删除的Album
对象和它的索引位置。然后我们把取出来的对象放回了数据源里。 - 因为我们从栈里删除了一个对象,所以需要检查一下看看栈是不是空了。如果空了那就设置撤销按钮不可用。
- 既然我们已经撤消了一个专辑,那删除按钮肯定是可用的。所以把它设置为
enabled
。
这时再运行应用,试试删除和插销功能,似乎一切正常了:
我们也可以趁机测试一下,看看是否及时存储了专辑数据的变化。比如删除一个专辑,然后切到后台,强关应用,再重新开启,看看是不是删除操作成功保存了。
如果想要恢复所有数据,删除应用然后重新安装即可。
小结
最终项目的源代码可以在 BlueLibrarySwift-Final 下载。
通过这两篇设计模式的学习,我们接触到了一些基础的设计模式和概念:Singleton、MVC、Delegation、Protocols、Facade、Observer、Memento 。
这篇文章的目的,并不是推崇每行代码都要用设计模式,而是希望大家在考虑一些问题的时候,可以参考设计模式提出一些合理的解决方案,尤其是应用开发的起始阶段,思考和设计尤为重要。
如果想继续深入学习设计模式,推荐设计模式的经典书籍:Design Patterns: Elements of Reusable Object-Oriented Software。
如果想看更多的设计模式相关的代码,推荐这个神奇的项目: Swift 实现的种种设计模式。
接下来你可以看看这篇:Swift 设计模式中级指南,学习更多的设计模式。
玩的开心。 :]
原文链接:
- Introducing iOS Design Patterns in Swift – Part 2/2
Android 4.4 (KitKat) 中的设计模式 - Graphics 子系统
本文主要从设计模式角度简单地侃下 Android4.4 (KitKat) 的 Graphics 子系统。可以看到在 KitKat 中 Google 对 code 还是整理过的,比如替换了像 SurfaceTexture 这种第一眼看到不知所云的东西,去掉了像 ISurface 这种打酱油的定义,改掉了明明是 SurfaceHolder 类型却死皮白脸叫 surface 的变量。自从修正了这些晦涩逆天的概念后,妈妈再也不用担心我看不懂 Android 的 code 了。当然仍然有不少 legacy code,如果没看过以前版本的话会有些小迷茫,好在无伤大雅。接下来言归正传。作为一个操作系统,Android 需要考虑到灵活性,兼容性,可用性,可维护性等方方面面 ,为了达到这些需求,它需要良好的设计。因此,在 Android 源码中可以看到很多设计模式的身影。光是本文涉及的 Graphics 子系统中,就用到了如 Observer, Proxy, Singleton, Command, Decorator, Strategy, Adapter, Iterator 和 Simple Factory 等模式。如果要学习设计模式,我想 Android 源代码是一个比较好的实例教材。当然很多用法和 GoF 书中的经典示例不一样,但其理念还是值得学习的。本文仍以大体流程为纲,在涉及到时穿插相应的设计模式。这样既涵盖了 Android 工作原理,又能一窥其设计理念,一举两得。这里本意是想自顶向下地展开,因为 Android 源代码庞大,很容易迷失在 code 的海洋中。本着 divide-and-conquer 的原则,本文重点先介绍 SurfaceFlinger 也就是服务端的工作流程,而对于应用程序端的 App UI 部分留到以后再讲。
为了让分析不过于抽象,首先,让我们先找一个可以跑的实例为起点,使得分析更加有血有肉。这里选的是 /frameworks/native/services/surfaceflinger/tests/resize/resize.cpp。选这个测试用例原因有几:一、它是一个 Native 程序,不牵扯它们在 Java 层的那一坨代理和 Jni 调用。二、麻雀虽小,五脏俱全,应用程序在 Native 层的主干它都有。三、程序无废话,简约而不简单,高端洋气上档次。注意这个用例默认编译有错的,不过好在对于码农们不是大问题,改发改发也就过了。
下面是该用例中最核心的几行,我们来看看这样简单的任务后面 Android 到底做了神马。
sp<surfacecomposerclient> client = new SurfaceComposerClient();
sp<surfacecontrol> surfaceControl = client->createSurface(String8(resize),
160, 240, PIXEL_FORMAT_RGB_565, 0);
sp<surface> surface = surfaceControl->getSurface();
SurfaceComposerClient::openGlobalTransaction();
surfaceControl->setLayer(100000);
SurfaceComposerClient::closeGlobalTransaction();
ANativeWindow_Buffer outBuffer;
surface->lock(&outBuffer, NULL);
ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
android_memset16((uint16_t*)outBuffer.bits, 0xF800, bpr*outBuffer.height);
surface->unlockAndPost();</surface></surfacecontrol></surfacecomposerclient>
这儿的大概流程是先创建 SurfaceComposerClient,再通过它创建 SurfaceControl,再得到 Surface,然后通过 lock () 分配图形缓冲区,接着把要渲染的东西(这里是简单的红色)画好后,用 unlockAndPost () 交给 SurfaceFlinger 去放到硬件缓冲区中,也就是画到屏幕上。最后结果是屏幕上应该能看到一个小红色块。这里先不急着挖代码,先放慢脚步直观地理解下这些概念。SurfaceComposer 可以理解为 SurfaceFlinger 的别称,因为 SurfaceFlinger 主要就是用来做各个图层的 Composition 操作的。但 SurfaceFlinger 是在服务端的,作为应用程序要让它为之服务需要先生成它所对应的客户端,也就是 SurfaceComposerClient,因为 SurfaceComposerClient 是懂得和服务端打交道的协议的。SurfaceComposerClient 位于服务端的代理叫 Client,应用程序通过前者发出的远程过程调用就是通过后者来实现的。有了它之后,应用程序就可以通过它来申请 SurfaceFlinger 为自己创建 Surface,这个 Surface 就是要绘制的表面的抽象。去除次要元素,下图简要勾勒了这些类之间的总体结构:
把这些个结构画出来后,优美而对称的 C/S 架构就跃然电脑上了。中间为接口层,两边的服务端和客户端(也就是应用程序)通过接口就可以相互通信。这样两边都可以做到设计中的 “面向接口编程”。只要接口不变,两边实现怎么折腾都行。可以看到,应用程序要通过 SurfaceFlinger 将自己的内容渲染到屏幕上是一个客户端请求服务的过程。其中应用程序端为生产者,提供要渲染的图形缓冲区,SurfaceFlinger 为消费者,拿图形缓冲区去合并渲染。从结构上来看,可以看到应用程序端的东西在服务端都有对应的对象。注意它们的生成顺序是由上到下的,即对于应用程序来说,先有 ComposerService,再有 SurfaceComposerClient,再有 Surface;对于服务端来说,依次有 SurfaceFlinger,Client 和 Layer。
这里至少看到两种设计模式 :Singleton 和 Proxy 模式。当然用法可能和教科书中不一样,但是思想是一致的。ComposerService 用来抽象 SurfaceFlinger,SurfaceFlinger 全局只有一个,所以 ComposerService 是 Singleton 对象。另一方面,应用程序想要让服务端为其做事,但服务端不在同一进程中,这就需要在服务端创建本地对象在服务端的代理(如 Client),这就是 Proxy 模式了。Proxy 模式应用很广,可用于远程对象访问 (remote proxy),虚拟化 (virtual proxy),权限控制 (protection proxy) 和智能指针 (smart reference proxy) 等。这里用的是 remote proxy(当然也有 protection proxy 的因素)。另外 smart reference proxy 模式的例子如 Android 中的智能指针。
大体结构讲完,下面分步讲流程。首先,创建 SurfaceComposerClient 的流程见下面的序列图:
没啥亮点,一笔带过。前戏结束,下面进入正题,应用程序创建 Surface 过程如下:
挺直观的过程,细节从略。要注意的有几点:
一、客户端到服务端的调用都是通过 Binder 来进行的。如果不知道 Binder 是什么也没关系,只要把它看作一个面向系统级的 IPC 机制就行。上面在客户端和服务端之间很多的进程间过程调用就是用它来完成的。
二、现实中,像调用 SurfaceFlinger 的 createLayer () 函数这种并不那么直接,而是采用异步调用方式。这个一会再讲。
三、IGraphicBufferProducer,IGraphicBufferConsumer, BufferQueue, SurfaceTextureLayer 这几个类是一脉相承的。所以图中当服务端创建一个 SurfaceTextureLayer 对象,传到客户端被转成 IGraphicBufferProducer 是完全 OK 的。这种面向接口编程的用法还有很多,其理论基础是里氏替换原则。这里也体现了接口隔离原则,同样的对象在客户端只暴露作为生产者的接口,而在服务端暴露消费者的接口,避免了接口污染。
那么上面的过程中用到了哪些设计模式呢?我觉得比较明显的有以下几个:
Proxy 模式这里被用来解除环形引用和避免对象被回收。ConsumerBase 先创建一个类型为 BufferQueue::ConsumerListerner 的对象 listerner,然后再在外面包了层类型为 QueueBuffer::ProxyConsumerListener 的代理对象 proxy,它拥有一个指向 listerner 的弱指针(因弱引用不影响回收)。一方面,加了这一层代理相当于把相互的强引用变成了单向的强引用,之所以这样做是为了避免对象间的环形引用导致难以回收。另一方面,如果用的强指针,那在 ConsumerBase 构造函数中,一旦函数结束,对于 ConsumerListener (即 ConsumerBase) 的强引用就没有了,那么 onLastStrongRef () 就可能被调用,从而回收这个还有用的对象。前面我们看到 Proxy 模式在远程对象访问中的应用,这里我们看到了另一种用法。
Observer 模式被用做在应用程序渲染完后提交给服务端时的通知机制。首先 consumerConnect () 会建立起 BufferQueue 到 ConsumerBase 对象的引用,放于成员变量 mConsumerListener 之中,接着 setFrameAvailableListener () 建立起 ConsumerBase 对象到 Layer 的引用。这样就形成了下面这样一个链式 Observer 模式:
注意虽是链式,但和 Chain of Responsiblity 模式没啥关系。注册完成之后,日后当应用程序处理完图形缓冲区后调用 queueBuffer () 时,BufferQueue 就会调用这个 listener 的回调函数 onFrameAvailable (),然后途经 ConsumerBase 调用 Layer 的回调函数 onFrameAvailable (),最后调用 signalLayerUpdate () 使 SurfaceFlinger 得到通知。这样就使得 SurfaceFlinger 可以专心干自己的事,当有需求来时自然会被通知到。事实上,当被通知有新帧需要渲染时,SurfaceFlinger 也不是马上停下手上的事,而是先做一个轻量级的处理,也就是把相应消息放入消息队列留到以后处理。这就使得各个应用程序和 SurfaceFlinger 可以异步工作,保证 SurfaceFlinger 的性能不被影响。而这又引入了下面的 Command 模式。
Command 模式被用来使得 SurfaceFlinger 可以和其它模块异步工作。Command 模式常被用来实现 undo 操作或是延迟处理,这里显然是用了后者。简单地概括下就是:当有消息来 (如 INVALIDATE 消息),先把它通过 MessageQueue 存起来(实际是存在 Looper 里),然后 SurfaceFlinger 线程会不断去查询这个消息队列。如果队列不为空且约定处理时间已到,就会取出做相应处理。具体流程后面谈到 SurfaceFlinger 时再说。为什么要这么麻烦呢,主要还是要保证 SurfaceFlinger 的稳定和高效。消息可以随时过来,但 SurfaceFlinger 可以异步处理。这样就可以保证不打乱 SurfaceFlinger 那最摇摆的节奏,从而保证用户体验。
创建不同功能的 BufferQueue 使用的是类似于 Builder 的设计模式。当 BufferQueue 被创建出来后,它拥有默认的参数,客户端为把它打造成想要的 BufferQueue,通过其接口设定一系列的参数即可,就像 Wizard 一样。为什么要这么做呢,先了解下背景知识。一个图形缓冲区从应用程序到屏幕的路上两个地方用到了 BufferQueue。一个用来将应用程序渲染好的图形缓冲传给 SurfaceFlinger,另一个用来把 SurfaceFlinger 合成好的图形缓冲放到硬件图形缓冲区上。
BufferQueue 中核心数据是一个 GraphicBuffer 的队列。而 GraphicBuffer 根据使用场合的不同可以从共享内存 (即 Ashmem,因为这块内存要在应用程序和服务端程序两个进程间共享)或者从硬件图形缓冲区 (即 Framebuffer,因为它是 SurfaceFlinger 渲染完要放到屏幕上的) 中分配。另外因为用途不同,它的格式,大小,以及在 BufferQueue 中的数量都可能是不同的。虽然是同一个类,用于不同场合出身就不同,那又怎么区分哪个是高富帅,哪个是矮穷挫呢。很简单,当 BufferQueue 被创建出来之后,由 Layer 或是 FramebufferSurface 来充当导演的角色,打造相应的 BufferQueue。它们调用一系列的函数(如 setConsumerUsageBits () 和 setDefaultMaxBufferCount () 等)将构建出来的 BufferQueue 变得适用于自己。
另外,Adapter 和 Decorator 模式在代码中也经常会出现。从目的上讲,由于被调用者提供的接口或功能常常不能满足调用者的需求,如果是接口不满足就用 Adapter 模式,如果要增加额外功能就用 Decorator 模式。从结构上讲,Adapter 可以是 Subclassing 结构也可以是 Composition 结构,而 Decorator 一般是 Composition 结构。事实上这两个模式经常混在一起用。实用中我觉得没必要太在意到底是什么模式,能起到作用就行。举例来说,SurfaceFlingerConsumer 是 GLConsumer 的 Wrapper,当 Layer 调用 SurfaceFlingerConsumer 的接口,底层会部分使用 GLConsumer 的相应实现(事实上 SurfaceFlingerConsumer 和 GLConsumer 实现中有重复代码)。我觉得它是用了 subclassing 结构来达到了类似 Decorator 模式的目的。当然这里模式用得不是很清晰,只是借机引下相关模式,例子跳过也罢。
回到我们的测试用例主线上,现在应用程序中 Surface 创建好了,下面几行主要功能是把应用程序所绘图层的 z 轴值设得很大,也就是很牛逼肯定能看到的地方。
SurfaceComposerClient::openGlobalTransaction();
surfaceControl->setLayer(100000);
SurfaceComposerClient::closeGlobalTransaction();
像这种更改屏幕或是应用程序窗口属性的动作需要用 openGlobalTransaction () 和 closeGlobalTransaction () 包起来,这样中间的更改操作就成为一个事务。事务中的操作暂时只在本地,只有当 closeGlobalTransaction () 被调用时才一起通过 Binder 发给 SurfaceFlinger 处理。这主要是由另一个 Singleton 对象 Compoesr 来实现的。属性改变的事务化优化了系统资源,因为更改这些属性的操作往往很 heavy,意味着很多东西需要重新计算,所以这里把这些费时操作打了一个包,避免重复劳动。由于这块并不复杂也不是重点,而且部分流程和后面重复,所以就跳过直接进入高潮 - 写图形缓冲区和交由 SurfaceFlinger 合成输出到屏幕。让我们看看下面这几行背后都做了些什么:
ANativeWindow_Buffer outBuffer;
surface->lock(&outBuffer, NULL);
ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
android_memset16((uint16_t*)outBuffer.bits,0xF800, bpr*outBuffer.height);
surface->unlockAndPost();
代码首先定义了图形缓冲区,它就是应用程序用来画图的缓冲区了。但这里只有元信息,放绘制数据的地方还没分配出来。先看下面这两行绘制缓冲区的语句,它们的目的很简单,就是把图形缓冲区整成红色。注意 Surface 格式是 PIXEL_FORMAT_RGB_565,所以红色对应 0xF800。
ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
android_memset16((uint16_t*)outBuffer.bits,0xF800, bpr*outBuffer.height);
绘制缓冲区前后分别用 lock () 和 unlockAndPost () 包起来,这两个函数主要用途是向服务端申请图形缓冲区,再把绘制好的图形缓冲区交给 SurfaceFlinger 处理。大体流程如下:
这样应用程序就把自己渲染好的图形缓冲区华丽丽地交给 SurfaceFlinger 了。其中最主要的是图形缓冲区的传递处理,图形缓冲区对应的类为 GraphicBuffer。BufferQueue 为它的缓冲队列,可以看作是一个元素为 GraphicBuffer 的队列。插播下背景知识,BufferQueue 中的 GraphicBuffer 有以下几种状态,之间的转换关系如下:
这部分用到的设计模式主要有以下几个:
Memento 模式。从 BufferQueue 传回的 GraphicBuffer 是在共享内存中分配的,而这块内存又是用句柄来表示的。我们知道一个进程的句柄或地址到另一个进程中就不一定合法了。那么为什么 Surface 的 lock () 函数拿到这个 GraphicBuffer 后就直接拿来当本地对象用了呢。这里就要引入 Memento 模式了。GraphicBuffer 继承了 Flattenable 类,实现了 flatten () 和 unflatten 函数。当 GraphicBuffer 被从进程 A 经由 Binder 传递到进程 B 时,先在进程 A 中它的 flatten () 函数被调用,再在进程 B 中 unflatten () 函数被调用。这样就可以保证 GraphicBuffer 跨进程后仍然有效。简要地看下 flatten () 和 unflatten () 的实现。服务端在传递 GraphicBuffer 前调用 flatten () 函数把 GraphicBuffer 的基本信息存在结构体中,应用程序端拿到后调用 unflatten () 将这个结构体拿出来后调用 GraphicBufferMapper::registerBuffer ()(它接着会调用 gralloc 模块中的 gralloc_register_buffer (),接着调用 gralloc_map (),最后用 mmap () 完成映射) 将远端 GraphicBuffer 映射到本地地址空间,然后据此在 BpGraphicBufferProducer::requestBuffer () 中重新构建了一个 GraphicBuffer。这样应用程序端和服务端中的 GraphicBuffer 就被映射到了同一块物理空间,达到了共享的目的,而且对于上层应用这个转化的过程完全是透明的。QueueBufferOutput 和 QueueBufferInput 的处理与 GraphicBuffer 的处理目的相同,都是进程间传递对象,不同之处在于前两者相当于在另一个进程中拷贝了一份。因为它们只包含少量 POD 成员,所以拷贝对性能影响不大。
Iterator 模式在 Android 源码中也很散落地应用着。如 Surface::lock () 中,为了使得 frontBuffer 可以重用(图形渲染一般采用双缓冲,frontBuffer 用于显示输出,backBuffer 用于作图,这样两者可以同时进行互不影响。很多时候其实下一帧和前一帧比只有一小块是需要重新渲染的,如切水果时很多时候其实就水果周围一块区域需要重渲染,这块区域即为脏区域),我们需要知道脏区域。脏区域信息用 Region 表示。而 Region 本身是由多个矩形组成的,而作为客户端要访问 Region 中的这些矩形,不需要知道它们的内在实现。这样在访问这个脏区域时就可以这么写:
Region::const_iterator head(reg.begin());
Region::const_iterator tail(reg.end());
while(head != tail) {
…
}
这样客户端的代码就不依赖于 Region 的实现了,无论 Region 中是数组也好,队列也好,上层代码不变,无比优美。同理还有 BufferQueue::queueBuffer () 中用到的 Fifo::iterator 等。
Strategy 模式用于让系统可以优雅地适应不同平台中的 HAL 模块。尽管这儿部分代码是用 C 写的,和标准的 Strategy 用法略有不同,但精神是一样的。举例来说,由于 gralloc 模块是平台相关的,在不同平台有不同的实现。那么作为上层客户端,如 GraphicBufferMapper 来说,它不希望依赖于这种变化。于是要求所有平台提供的 gralloc 模块提供统一的接口,而这个接口对应的对象的符号统一为 HAL_MODULE_INFO_SYM。这样不管是哪个平台,上层客户端就只要用 dlopen () 和 dlsym () 去载入和查询这个结构体就可以得到这些接口相应的实现了(载入流程见 hw_get_module () ->hw_get_module_by_class () -> load ())。这里涉及到的是 gralloc_module_t 接口,也就是 GraphicBufferMapper 要用到的接口,各个平台提供的 gralloc 都要实现这个接口。同理的还有对同在 HAL 层的 hwcomposer 模块的处理。
另外,我们还看到了前面提到过的设计模式又一次次地出现,如死亡通知是基于 Binder 的 Observer 模式,另外 GraphicBufferMapper 对 gralloc 模块 gralloc_module_t 的封装可看作是 Adapter 模式的应用。
回到主线,前面的流程进行到 Layer 调用 signalLayerUpdate () 通知 SurfaceFlinger 就结束了。下面分析下 SurfaceFlinger 的流程及对于应用程序图形缓冲区的后续处理过程。为了让文章看起来更加完整些,我们还是从 SurfaceFlinger 的初始化开始,然后再接着讲那 signalLayerUpdate () 之后的故事。这里就从 SurfaceFlinger 的创建开始讲起吧。本来 SurfaceFlinger 有两种启动方式,由 SystemServer 以线程方式启动,或是由 init 以进程方式启动。不过 Android 4.4 上好像前者去掉了,反正我是没找到。。。。那故事就从 main_surfaceflinger.cpp 讲起吧。
上半部分是 SurfaceFlinger 的初始化,下半部分是 SurfaceFlinger 对于 VSync 信号的处理,也就是一次合并渲染的过程。由于代码里分支较多,为了说明简便,这里作了几点假设:首先假设平台支持硬件 VSync 信号,这样就不会创建 VSyncThread 来用软件模拟了。另外假设 INVALIDATE_ON_VSYNC 为 1,也就是把 INVALIDATE 操作放到 VSync 信号来时再做。值得注意的是 SurfaceFlinger 线程模型相较之前版本有了较大变化,主要原因是引入了 VSync 的虚拟化。相关的线程有 EventThread (vsyncSrc), EventThread (sfVsyncSrc), EventControlThread 和 DispSyncThread。这个东西比较好玩,所以单独放一篇文章来讲(http://blog.csdn.net/jinzhuojun/article/details/17293325)。
先粗略介绍下几个重要类的基本作用:
EventControlThread 是一个简单地另人发指的线程,用来根据标志位开关硬件 VSync 信号。但为毛要单独放到个线程,难道是开关硬件 VSync 信号的代价很大?
RenderEngine 是对一坨 egl 和 gl 函数的封装。引入它可能是觉得 egl 和 gl 函数混杂在其它模块中里看起来太乱了。它把 GL 相关的东西封装起来了,只暴露出 GL 无关的接口,这样 SurfaceFlinger 等模块作为客户端不需要了解底层细节。
Looper 是一个通用的类,负责将事件和消息存成队列,并提供轮询接口处理队列中的事件和消息。在这里主要用于处理 TRANSACTION, INVALIDATE 和 REFRESH 消息以及 VSync 事件。
MessageQueue 主要操作 Looper。由于 Looper 不是专用于 SurfaceFlinger 的,MessageQueue 封装了一些 SurfaceFlinger 专用的信息,使得 EventThread 和 SurfaceFlinger 等模块可以通过它来间接使用 Looper 类。
SurfaceFlinger,DisplayDevice,FramebufferSurface 和 HWComposer 这几个类之间的关系比较暧昧。SurfaceFlinger 是这部分里最大的客户端。DisplayDevice 抽象了显示设备,封装了用于渲染的 Surface 和 HWComposer 模块等,从而尽可能使得 SurfaceFlinger 只要和它打交道。FramebufferSurface 抽象了 SurfaceFlinger 用来画图的 Surface,该 Surface 对应一个 BufferQueue 用于多缓冲渲染。它和之前提到的应用端用到的 Surface 区别在于它是基于硬件图形缓冲区的,而不是 Ashmem。HWComposer 封装了两个重要的 HAL 模块,即 framebuffer 和 hwcomposer,前者对应硬件缓冲区,后者对应 hwcomposer 硬件。hwcomposer 控制 VSync 信号,管理屏幕信息和操作 framebuffer。HWComposer 主要工作是负责将平台相关的 HAL 模块加载好,并且使得上层模块通过它来使用 HAL 模块。注意两个容易混淆的概念:HWComposer 是类,肯定会有;hwcomposer 是硬件模块,在某些平台可能会不可用。这几个类的大致结构如下:
SurfaceFlinger 的渲染工作主要是由 VSync 信号驱动的。EventThread 负责发出虚拟 VSync 信号(由硬件 VSync 信号偏移指定相位虚拟化得到)。初始化时,MessageQueue 在 setEventThread () 函数中先与 EventThread 建立连接,然后将与 EventThread 之间通信的 socket(BitTube)句柄注册进 Looper,同时也注册了自己的回调函数。另一方面,SurfaceFlinger 会通过 Looper 不断轮询这个句柄,看该句柄上有没有数据。当在该句柄上接收到数据,就会调用 MessageQueue 相应的回调函数。经过一番处理后,最后 MessageQueue 的 Handler 基于收到的消息调用到 SurfaceFlinger 的相应处理函数。就这样,EventThread->Looper->MessageQueue->SurfaceFlinger 的事件消息传递过程形成了。
这里又看到了比较熟悉的设计模式,如 Iterator 模式用于遍历所有图层 (HWComposer::LayerListIterator), Observer 模式用于虚拟 VSync 信号线程向 DispSyncThread 申请虚拟 VSync 事件 (DispSyncThread::EventListener),还有前面提到过的 Command 模式用于消息的延迟处理 (postMessageAsync ())。
除了这些熟悉的身影外,我们还能看到些新面孔。如 RenderEngine::create () 应用了简单工厂模式,它根据 GLES 版本号创建相应的 RenderEngine。这样,作为 RenderEngine 的使用者 SurfaceFlinger 和 Layer 自然就成了 Strategy 模式的受益者,它们不用关心 RenderEngine 在各个不同版本间实现的差异。还有 Mediator 模式在 Service 管理中的应用。SurfaceFlinger 进程中,addService () 函数向 Service Manager 注册了 SurfaceFlinger 服务。这样 Service Manager 作为中介的角色在 Client 和 Service 之间做沟通,它使得一个网状的模块结构变成了一个优美的星形结构。当然在这里因为就一个服务看不出来,因此这个另作章节再讲。
可以看到,当 VSync 信号到来时,SurfaceFlinger 最主要是通过处理 INVALIDATE 和 REFRESH 消息来做合并渲染和输出的工作的。这里的核心思想是能少处理一点是一点,所以在渲染前有很多脏区域的计算工作,这样后面只要处理那些区域的更新就可以了。这样是有现实意义的,一方面由于图层间的遮盖,有些不可见图层不需要渲染。另一方面,因为我们的应用程序中前后帧一般只有一小部分变化,要是每帧都全变估计人都要看吐了。这里主要是调用了这几个函数:
handleMessageTransaction () 主要处理之前对屏幕和应用程序窗口的改动。因这些改动很有可能会改变图层的可见区域,进而影响脏区域的计算。
handleMessageInvalidate () 主要调用 handlePageFlip () 函数。这里 Page Flip 是指从 BufferQueue 中取下一个图形缓冲区内容,就好像是 “翻页” 一样。该函数主要是从各 Layer 对应的 BufferQueue 中拿图形缓冲区数据,并根据内容更新脏区域。
handleMessageRefresh () 就是合并和渲染输出了。作为重头戏,这步步骤多一些,大体框架如下:
文章一开始的测试用例主干部分背后的故事大概就这么些了。篇幅有限,省略了很多细节。我们可以看到,在服务端做了这么多的事,而对于应用程序来说只要先 lock (),再填 buffer,最后 unlockAndPost () 就 OK 了。这也算是 Facade 模式的体现了吧。
总结地说,从 Android 源码中我们可以温习到应用设计模式的基本原则:一是只在合适的地方用。很多时候我们学习设计模式恰恰是为了不用,准确地说是不滥用。二是要用的话不需要过于拘泥于原有的或经典的用法,以解决问题为目的适当自由发挥。
Dubbo 中的设计模式
Dubbo 中的设计模式
最近在看阿里开源 RPC 框架 Dubbo 的源码,顺带梳理了一下其中用到的设计模式。下面将逐个列举其中的设计模式,并根据自己的理解分析这样设计的原因和优劣。
责任链模式
责任链模式在 Dubbo 中发挥的作用举足轻重,就像是 Dubbo 框架的骨架。Dubbo 的调用链组织是用责任链模式串连起来的。责任链中的每个节点实现 Filter
接口,然后由 ProtocolFilterWrapper
,将所有 Filter
串连起来。Dubbo 的许多功能都是通过 Filter
扩展实现的,比如监控、日志、缓存、安全、telnet 以及 RPC 本身都是。如果把 Dubbo 比作一列火车,责任链就像是火车的各车厢,每个车厢的功能不同。如果需要加入新的功能,增加车厢就可以了,非常容易扩展。
观察者模式
Dubbo 中使用观察者模式最典型的例子是 RegistryService
。消费者在初始化的时候回调用 subscribe 方法,注册一个观察者,如果观察者引用的服务地址列表发生改变,就会通过 NotifyListener
通知消费者。此外,Dubbo 的 InvokerListener
、ExporterListener
也实现了观察者模式,只要实现该接口,并注册,就可以接收到 consumer 端调用 refer 和 provider 端调用 export 的通知。Dubbo 的注册 / 订阅模型和观察者模式就是天生一对。
修饰器模式
Dubbo 中还大量用到了修饰器模式。比如 ProtocolFilterWrapper
类是对 Protocol 类的修饰。在 export 和 refer 方法中,配合责任链模式,把 Filter 组装成责任链,实现对 Protocol 功能的修饰。其他还有 ProtocolListenerWrapper
、 ListenerInvokerWrapper
、InvokerWrapper
等。个人感觉,修饰器模式是一把双刃剑,一方面用它可以方便地扩展类的功能,而且对用户无感,但另一方面,过多地使用修饰器模式不利于理解,因为一个类可能经过层层修饰,最终的行为已经和原始行为偏离较大。
工厂方法模式
CacheFactory
的实现采用的是工厂方法模式。CacheFactory
接口定义 getCache 方法,然后定义一个 AbstractCacheFactory
抽象类实现 CacheFactory
,并将实际创建 cache 的 createCache 方法分离出来,并设置为抽象方法。这样具体 cache 的创建工作就留给具体的子类去完成。
抽象工厂模式
ProxyFactory
及其子类是 Dubbo 中使用抽象工厂模式的典型例子。ProxyFactory
提供两个方法,分别用来生产 Proxy
和 Invoker
(这两个方法签名看起来有些矛盾,因为 getProxy 方法需要传入一个 Invoker 对象,而 getInvoker 方法需要传入一个 Proxy
对象,看起来会形成循环依赖,但其实两个方式使用的场景不一样)。AbstractProxyFactory
实现了 ProxyFactory
接口,作为具体实现类的抽象父类。然后定义了 JdkProxyFactory
和 JavassistProxyFactory
两个具体类,分别用来生产基于 jdk 代理机制和基于 javassist 代理机制的 Proxy
和 Invoker
。
适配器模式
为了让用户根据自己的需求选择日志组件,Dubbo 自定义了自己的 Logger 接口,并为常见的日志组件(包括 jcl, jdk, log4j, slf4j)提供相应的适配器。并且利用简单工厂模式提供一个 LoggerFactory
,客户可以创建抽象的 Dubbo 自定义 Logger
,而无需关心实际使用的日志组件类型。在 LoggerFactory 初始化时,客户通过设置系统变量的方式选择自己所用的日志组件,这样提供了很大的灵活性。
代理模式
Dubbo consumer 使用 Proxy
类创建远程服务的本地代理,本地代理实现和远程服务一样的接口,并且屏蔽了网络通信的细节,使得用户在使用本地代理的时候,感觉和使用本地服务一样。
Highcharts iOS Swift:HIGauge.dial 在 iOS swift 中总是返回 nil
如何解决Highcharts iOS Swift:HIGauge.dial 在 iOS swift 中总是返回 nil
当我尝试使用 HIGauge 更改拨号格式时,应用程序崩溃
下面是我的代码:
let speed = HIGauge()
speed.name = "Speed"
speed.data = [480]
speed.tooltip = HITooltip()
speed.tooltip.valueSuffix = " km/h"
speed.dial.backgroundColor = HIColor(uiColor: UIColor.green)
应用程序在“speed.dial.backgroundColor”上崩溃。 拨给零值。
我已经尝试设置
chart.styledMode = true
但应用仍然崩溃。
解决方法
您需要先创建一个对象 HIDial
,然后设置颜色:
speed.dial = HIDial()
speed.dial.backgroundColor = HIColor(uiColor: UIColor.green)
iOS 18发布啦!iOS 18好吗?iOS 18值得更新吗?iOS 18beta版
ios 18 横空出世,带来了一系列激动人心的新功能。您是否好奇 ios 18 的亮点,它是否值得升级?php小编西瓜带来 ios 18 的全面解读,详细介绍了它的新特性、改进和已解决的错误。如果您正在考虑升级到 ios 18,请继续阅读以了解它的优缺点,并决定它是否适合您的设备和需求。
iOS 18 beta版终于发布啦!iOS 18此次更新是否与预期一样呢? iOS 18更新了哪些内容呢?是否真的值得果粉用户升级呢?
iOS 18的更新内容涵盖了多个方面,旨在提升用户体验和个性化设置。以下是iOS 18的更新内容概览:
定制主屏幕:
用户可以自由
移动应用程序,按照个人喜好调整主屏幕布局。 图标支持深色模式,用户可以为图标着色,打造独特的外观。
应用程序可以随意放置,深色模式APP有更深度的适配,且有色系可选,整体可调节成一种色系。
优化控制中心:
控制中心进行了重新设计,新增了多款快捷组件,用户可以根据需要选择和
排列。 控件页面支持多页布局,用户可滑动访问控制中心的其它页面。
控制中心界面设计已扩展为多页布局,允许用户将不常访问的功能移动到次级页面。
隐私与安全:
iOS 18支持给APP上锁,支持面容识别,同时也能隐藏APP,以加强用户的隐私权限。
用户可以专门控制第三方App可以访问哪些通讯录,进一步保障数据安全。
信息应用更新:
发送的字体样式和表情有更多自定义选项。
支持稍后发送功能。
在无网情况下,iPhone 14及后续机型支持卫星直发。
其他内置应用更新:
邮箱应用进行了更新,分类和摘要功能提高了效率。
钱包应用支持两个手机一碰即可相互转账。
地图应用带来了新的地形图。
相册应用引入了智能功能,查找照片和照片分类更加精准。
附上iOS 18升级方法:
※1、刷机前请做好重要数据资料的备份,或勾选“保留用户资料刷机”,防止重要资料丢失;
※2、请确保移动设备未开启激活锁,或者知道 ID 锁帐号、密码,否则刷机后可能会无法激活设备;
※3、设备升级到 iOS 18后,将无法再降级到“苹果已关闭验证”的固件版本,即使之前使用备份了 SHSH 也不能降级。
打开最新版电脑端,用数据线把手机设备连接到电脑。点击上方“智能刷机”进入到“一键刷机”界面,连接成功会自动匹配iOS 18固件,选择“保留用户资料刷机”立即刷机。
以上就是iOS 18发布啦!iOS 18好吗?iOS 18值得更新吗?iOS 18beta版的详细内容,更多请关注php中文网其它相关文章!
今天的关于Swift37/90Days - iOS 中的设计模式 (Swift 版本) 02和swift设计原则的分享已经结束,谢谢您的关注,如果想了解更多关于Android 4.4 (KitKat) 中的设计模式 - Graphics 子系统、Dubbo 中的设计模式、Highcharts iOS Swift:HIGauge.dial 在 iOS swift 中总是返回 nil、iOS 18发布啦!iOS 18好吗?iOS 18值得更新吗?iOS 18beta版的相关知识,请在本站进行查询。
本文标签: