第四期

最后更新于:2022-04-01 02:39:29

> 原文出处: http://southpeak.github.io/blog/2015/08/15/ioszhi-shi-xiao-ji-di-si-qi-2015-dot-08-dot-15/ > 作者:南峰子 又欠了一屁股债了。积累了一大堆的问题放在那,就是没有整理。不能怪别人,也能怪自己了,犯起懒来,啥事也不想做,连喜爱的户外运动也给拉下了,掐指一算,居然大半年没出去了。然后经常看到老驴子们出去玩耍,回来就是一通的美图,心里那个痒痒啊。 回到正题吧,这次的知识小集知识点不多,还是三个: 1. ARC与MRC的性能对比 2. Bitcode 3. 在Swift中实现NS_OPTIONS 篇幅超过了预期,大家慢慢看,如有问题还请指正。 ## ARC与MRC的性能对比 MRC似乎已经是一个上古时代的话题了,不过我还是绕有兴致的把它翻出来。因为,今天我被一个问题问住了:ARC与MRC的性能方面孰优劣。确实,之前没有对比过。 先来做个测试吧。首先我们需要一个计时辅助函数,我选择使用mach_absolute_time,计算时间差的函数如下: ~~~ double subtractTimes(uint64_t endTime, uint64_t startTime) { uint64_t difference = endTime - startTime; static double conversion = 0.0; if(conversion == 0.0) { mach_timebase_info_data_t info; kern_return_t err = mach_timebase_info(&info); //Convert the timebaseinto seconds if(err == 0) conversion = 1e-9 * (double) info.numer / (double) info.denom; } return conversion * (double)difference; } ~~~ 然后定义两个测试类,一个是ARC环境下的,一个是MRC环境下的,分别如下: ~~~ // Test1.m + (void)test { uint64_t start,stop; start = mach_absolute_time(); for (int i = 0; i < 1000000; i++) { NSArray *array = [[NSArray alloc] init]; } stop = mach_absolute_time(); double diff = subtractTimes(stop, start); NSLog(@"ARC total time in seconds = %f\n", diff); } // Test2.m // 在target->Build Phases->Compile Sources中,添加编译标识-fno-objc-arc + (void)test { uint64_t start,stop; start = mach_absolute_time(); for (int i = 0; i < 1000000; i++) { NSArray *array = [[NSArray alloc] init]; [array release]; } stop = mach_absolute_time(); double diff = subtractTimes(stop, start); NSLog(@"MRC total time in seconds = %f\n", diff); } ~~~ 多运行几组测试,然后挑两组吧来看看,数据如下: ~~~ // A组 ARC total time in seconds = 0.077761 MRC total time in seconds = 0.072469 // B组 ARC total time in seconds = 0.075722 MRC total time in seconds = 0.101671 ~~~ 从上面的数据可以看到,ARC与MRC各有快慢的情况。即使上升到统计学的角度,ARC也只是以轻微的优势胜出。看来我的测试姿势不对,并没有证明哪一方占绝对的优势。 嗯,那我们再来看看官方文档是怎么说的吧。在[Transitioning to ARC Release Notes](https://developer.apple.com/library/ios/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html)中有这么一段话: > **Is ARC slow?** > > It depends on what you’re measuring, but generally “no.” The compiler efficiently eliminates many extraneous`retain`/`release` calls and much effort has been invested in speeding up the Objective-C runtime in general. In particular, the common “return a retain/autoreleased object” pattern is much faster and does not actually put the object into the autorelease pool, when the caller of the method is ARC code. > > One issue to be aware of is that the optimizer is not run in common debug configurations, so expect to see a lot more `retain`/`release` traffic at `-O0` than at `-Os`. 再来看看别人的数据吧。Steffen Itterheim在[Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)](http://www.learn-cocos2d.com/2013/03/confirmed-arc-slow/)一文中给出了大量的测试数据。这篇文章是2013.3.20号发表的。Steffen Itterheim通过他的测试得出一个结论 > ARC is generally faster, and ARC can indeed be slower 嗯,有些矛盾。不过在文章中,Steffen Itterheim指出大部分情况下,ARC的性能是更好的,这主要得益于一些底层的优化以及autorelease pool的优化,这个从官方文档也能看到。但在一些情况下,ARC确实是更慢,ARC会发送一些额外的retain/release消息,如一些涉及到临时变量的地方,看下面这段代码: ~~~ // this is typical MRC code: { id object = [array objectAtIndex:0]; [object doSomething]; [object doAnotherThing]; } // this is what ARC does (and what is considered best practice under MRC): { id object = [array objectAtIndex:0]; [object retain]; // inserted by ARC [object doSomething]; [object doAnotherThing]; [object release]; // inserted by ARC } ~~~ 另外,在带对象参数的方法中,也有类似的操作: ~~~ // this is typical MRC code: -(void) someMethod:(id)object { [object doSomething]; [object doAnotherThing]; } // this is what ARC does (and what is considered best practice under MRC): -(void) someMethod:(id)object { [object retain]; // inserted by ARC [object doSomething]; [object doAnotherThing]; [object release]; // inserted by ARC } ~~~ 这些些额外的retain/release操作也成了降低ARC环境下程序性能的罪魁祸首。但实际上,之所以添加这些额外的retain/release操作,是为了保证代码运行的正确性。如果只是在单线程中执行这些操作,可能确实没必要添加这些额外的操作。但一旦涉及以多线程的操作,问题就来了。如上面的方法中,object完全有可能在doSoming和doAnotherThing方法调用之间被释放。为了避免这种情况的发生,便在方法开始处添加了[object retain],而在方法结束后,添加了[object release]操作。 如果想了解更多关于ARC与MRC性能的讨论,可以阅读一下[Are there any concrete study of the performance impact of using ARC?](http://stackoverflow.com/questions/12527286/are-there-any-concrete-study-of-the-performance-impact-of-using-arc)与[ARC vs. MRC Performance](http://mjtsai.com/blog/2013/09/10/arc-vs-mrc-performance/),在此就不过多的摘抄了。 实际上,即便是ARC的性能不如MRC,我们也应该去使用ARC,因此它给我们带来的好处是不言而喻的。我们不再需要像使用MRC那样,去过多的关注内存问题(虽然内存是必须关注的),而将更多的时间放在我们真正关心的事情上。如果真的对性能非常关切的话,可以考虑直接用C或C++。反正我是不会再回到MRC时代了。 ### 参考 1. [Are there any concrete study of the performance impact of using ARC?](http://stackoverflow.com/questions/12527286/are-there-any-concrete-study-of-the-performance-impact-of-using-arc) 2. [ARC vs. MRC Performance](http://mjtsai.com/blog/2013/09/10/arc-vs-mrc-performance/) 3. [Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)](http://www.learn-cocos2d.com/2013/03/confirmed-arc-slow/) 4. [Transitioning to ARC Release Notes](https://developer.apple.com/library/ios/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html) ## Bitcode 今天试着用Xcode 7 beta 3在真机(iOS 8.3)上运行一下我们的工程,结果发现工程编译不过。看了下问题,报的是以下错误: > ld: ‘/Users/**/Framework/SDKs/PolymerPay/Library/mobStat/lib**SDK.a(**ForSDK.o)’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64 得到的信息是我们引入的一个第三方库不包含bitcode。嗯,不知道bitcode是啥,所以就得先看看这货是啥了。 ### Bitcode是什么? 找东西嘛,最先想到的当然是先看官方文档了。在[App Distribution Guide – App Thinning (iOS, watchOS)](https://developer.apple.com/library/prerelease/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html#//apple_ref/doc/uid/TP40012582-CH35)一节中,找到了下面这样一个定义: > *Bitcode* is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the store. 说的是bitcode是被编译程序的一种中间形式的代码。包含bitcode配置的程序将会在App store上被编译和链接。bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到App store上。 嗯,看着挺高级的啊。 继续看,在[What’s New in Xcode-New Features in Xcode 7](https://developer.apple.com/library/prerelease/ios/documentation/DeveloperTools/Conceptual/WhatsNewXcode/Articles/xcode_7_0.html)中,还有一段如下的描述 > **Bitcode.** When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary. 当我们提交程序到App store上时,Xcode会将程序编译为一个中间表现形式(bitcode)。然后App store会再将这个botcode编译为可执行的64位或32位程序。 再看看这两段描述都是放在App Thinning(App瘦身)一节中,可以看出其与包的优化有关了。喵大(@onevcat)在其博客[开发者所需要知道的 iOS 9 SDK 新特性](http://onevcat.com/2015/06/ios9-sdk/)中也描述了iOS 9中苹果在App瘦身中所做的一些改进,大家可以转场到那去研读一下。 ### Bitcode配置 在上面的错误提示中,提到了如何处理我们遇到的问题: > You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64 要么让第三方库支持,要么关闭target的bitcode选项。 实际上在Xcode 7中,我们新建一个iOS程序时,bitcode选项默认是设置为YES的。我们可以在”Build Settings”->”Enable Bitcode”选项中看到这个设置。 不过,我们现在需要考虑的是三个平台:iOS,Mac OS,watchOS。 * 对应iOS,bitcode是可选的。 * 对于watchOS,bitcode是必须的。 * Mac OS不支持bitcode。 如果我们开启了bitcode,在提交包时,下面这个界面也会有个bitcode选项: ![image](https://developer.apple.com/library/prerelease/ios/documentation/IDEs/Conceptual/AppDistributionGuide/Art/6_ios_review_dist_profile_submit_2x.png) > 盗图,我的应用没办法在这个界面显示bitcode,因为依赖于第三方的库,而这个库不支持bitcode,暂时只能设置ENABLE_BITCODE为NO。 > > 所以,如果我们的工程需要支持bitcode,则必要要求所有的引入的第三方库都支持bitcode。我就只能等着公司那些大哥大姐们啥时候提供一个新包给我们了。 ### 题外话 如上面所说,bitcode是一种中间代码。LLVM官方文档有介绍这种文件的格式,有兴趣的可以移步[LLVM Bitcode File Format](http://llvm.org/docs/BitCodeFormat.html#llvm-bitcode-file-format)。 ### 参考 1. [App Distribution Guide – App Thinning (iOS, watchOS)](https://developer.apple.com/library/prerelease/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html#//apple_ref/doc/uid/TP40012582-CH35) 2. [What’s New in Xcode-New Features in Xcode 7](https://developer.apple.com/library/prerelease/ios/documentation/DeveloperTools/Conceptual/WhatsNewXcode/Articles/xcode_7_0.html) 3. [开发者所需要知道的 iOS 9 SDK 新特性](http://onevcat.com/2015/06/ios9-sdk/) 4. [LLVM Bitcode File Format](http://llvm.org/docs/BitCodeFormat.html#llvm-bitcode-file-format) ## 在Swift中实现NS_OPTIONS 从Xcode 4.5以后,我们在Objective-C中使用NS_ENUM和NS_OPTIONS来定义一个枚举,以替代C语言枚举的定义方式。其中NS_ENUM用于定义普通的枚举,NS_OPTIONS用于定义选项类型的枚举。 而到了Swift中,枚举增加了更多的特性。它可以包含原始类型(不再局限于整型)以及相关值。正是由于这些原因,枚举在Swift中得到了更广泛的应用。在Foundation中,Objective-C中的NS_ENUM类型的枚举,都会自动转换成Swift中enum,并且更加精炼。以Collection View的滚动方向为例,在Objective-C中,其定义如下: ~~~ typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) { UICollectionViewScrollDirectionVertical, UICollectionViewScrollDirectionHorizontal }; ~~~ 而在Swift中,其定义如下: ~~~ enum UICollectionViewScrollDirection : Int { case Vertical case Horizontal } ~~~ 精练多了吧,看着舒服多了,还能少码两个字。我们自己定义枚举时,也应该采用这种方式。 不过对于Objective-C中NS_OPTIONS类型的枚举,Swift中的实现似乎就没有那么美好了。 我们再来对比一下UICollectionViewScrollPosition的定义吧,在Objective-C中,其定义如下: ~~~ typedef NS_OPTIONS(NSUInteger, UICollectionViewScrollPosition) { UICollectionViewScrollPositionNone = 0, // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions. // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException. UICollectionViewScrollPositionTop = 1 << 0, UICollectionViewScrollPositionCenteredVertically = 1 << 1, UICollectionViewScrollPositionBottom = 1 << 2, // Likewise, the horizontal positions are mutually exclusive to each other. UICollectionViewScrollPositionLeft = 1 << 3, UICollectionViewScrollPositionCenteredHorizontally = 1 << 4, UICollectionViewScrollPositionRight = 1 << 5 }; ~~~ 而在Swift 2.0中,其定义如下: ~~~ struct UICollectionViewScrollPosition : OptionSetType { init(rawValue: UInt) static var None: UICollectionViewScrollPosition { get } // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions. // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException. static var Top: UICollectionViewScrollPosition { get } static var CenteredVertically: UICollectionViewScrollPosition { get } static var Bottom: UICollectionViewScrollPosition { get } // Likewise, the horizontal positions are mutually exclusive to each other. static var Left: UICollectionViewScrollPosition { get } static var CenteredHorizontally: UICollectionViewScrollPosition { get } static var Right: UICollectionViewScrollPosition { get } } ~~~ 额,光看代码,不看实现,这也是化简为繁的节奏啊。 为什么要这样做呢?Mattt给了我们如下解释: > Well, the same integer bitmasking tricks in C don’t work for enumerated types in Swift. An `enum` represents a type with a closed set of valid options, without a built-in mechanism for representing a conjunction of options for that type. An `enum` could, ostensibly, define a case for all possible combinations of values, but for `n > 3`, the combinatorics make this approach untenable. 意思是Swift不支持C语言中枚举值的整型掩码操作的技巧。在Swift中,一个枚举可以表示一组有效选项的集合,但却没有办法支持这些选项的组合操作(“&”、”|”等)。理论上,一个枚举可以定义选项值的任意组合值,但对于n > 3这种操作,却无法有效的支持。 为了支持类NS_OPTIONS的枚举,Swift 2.0中定义了OptionSetType协议【在Swift 1.2中是使用RawOptionSetType,相比较而言已经改进了不少】,它的声明如下: ~~~ /// Supplies convenient conformance to `SetAlgebraType` for any type /// whose `RawValue` is a `BitwiseOperationsType`. For example: /// /// struct PackagingOptions : OptionSetType { /// let rawValue: Int /// init(rawValue: Int) { self.rawValue = rawValue } /// /// static let Box = PackagingOptions(rawValue: 1) /// static let Carton = PackagingOptions(rawValue: 2) /// static let Bag = PackagingOptions(rawValue: 4) /// static let Satchel = PackagingOptions(rawValue: 8) /// static let BoxOrBag: PackagingOptions = [Box, Bag] /// static let BoxOrCartonOrBag: PackagingOptions = [Box, Carton, Bag] /// } /// /// In the example above, `PackagingOptions.Element` is the same type /// as `PackagingOptions`, and instance `a` subsumes instance `b` if /// and only if `a.rawValue & b.rawValue == b.rawValue`. protocol OptionSetType : SetAlgebraType, RawRepresentable { /// An `OptionSet`'s `Element` type is normally `Self`. typealias Element = Self /// Convert from a value of `RawValue`, succeeding unconditionally. init(rawValue: Self.RawValue) } ~~~ 从字面上来理解,OptionSetType是选项集合类型,它定义了一些基本操作,包括集合操作(union, intersect, exclusiveOr)、成员管理(contains, insert, remove)、位操作(unionInPlace, intersectInPlace, exclusiveOrInPlace)以及其它的一些基本操作。 作为示例,我们来定义一个表示方向的选项集合,通常我们是定义一个实现OptionSetType协议的结构体,如下所示: ~~~ struct Directions: OptionSetType { var rawValue:Int init(rawValue: Int) { self.rawValue = rawValue } static let Up: Directions = Directions(rawValue: 1 << 0) static let Down: Directions = Directions(rawValue: 1 << 1) static let Left: Directions = Directions(rawValue: 1 << 2) static let Right: Directions = Directions(rawValue: 1 << 3) } ~~~ 所需要做的基本上就是这些。然后我们就可以创建Directions的实例了,如下所示: ~~~ let direction: Directions = Directions.Left if direction == Directions.Left { // ... } ~~~ 如果想同时支持两个方向,则可以如上处理: ~~~ let leftUp: Directions = [Directions.Left, Directions.Up] if leftUp.contains(Directions.Left) && leftUp.contains(Directions.Up) { // ... } ~~~ 如果leftUp同时包含Directions.Left和Directions.Up,则返回true。 这里还有另外一种方法来达到这个目的,就是我们在Directions结构体中直接声明声明Left和Up的静态常量,如下所示: ~~~ struct Directions: OptionSetType { // ... static let LeftUp: Directions = [Directions.Left, Directions.Up] static let RightUp: Directions = [Directions.Right, Directions.Up] // ... } ~~~ 这样,我们就可以以如下方式来执行上面的操作: ~~~ if leftUp == Directions.LeftUp { // ... } ~~~ 当然,如果单一选项较多,而要去组合所有的情况,这种方法就显示笨拙了,这种情况下还是推荐使用contains方法。 总体来说,Swift中的对选项的支持没有Objective-C中的NS_OPTIONS来得简洁方便。而且在Swift 1.2的时候,我们还是可以使用”&“和”|”操作符的。下面这段代码在Swift 1.2上是OK的: ~~~ UIView.animateWithDuration(0.3, delay: 1.0, options: UIViewAnimationOptions.CurveEaseIn | UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in // ... }, completion: nil) ~~~ 但到了Swift 2.0时,OptionSetType已经不再支持”&“和”|”操作了,因此,上面这段代码需要修改成: ~~~ UIView.animateWithDuration(0.3, delay: 1.0, options: [UIViewAnimationOptions.CurveEaseIn, UIViewAnimationOptions.CurveEaseInOut], animations: { () -> Void in // ... }, completion: nil) ~~~ 不过,慢慢习惯就好。 ### 参考 1. [RawOptionSetType](http://nshipster.com/rawoptionsettype/) 2. [Exploring Swift 2.0 OptionSetTypes](http://www.swift-studies.com/blog/2015/6/17/exploring-swift-20-optionsettypes) 3. [Notes from WWDC 2015: The Enumerated Delights of Swift 2.0 Option Sets](http://www.informit.com/articles/article.aspx?p=2420231)​ 4. 《100个Swift开发必备Tip》— Tip 66\. Options ## 零碎 ### 静态分析中”Potential null dereference”的处理 我们在写一个方法时,如果希望在方法执行出错时,获取一个NSError对象,我们通常会像下面这样来定义我们的方法 ~~~ + (NSString )checkStringLength:(NSString *)str error:(NSError **)error { if (str.length <= 0) { *error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil]; return nil; } return str; } ~~~ 这段代码看着没啥问题,至少在语法上是OK的,所以在编译时,编译器并不会报任何警告。 如果我们用以下方式去调用的话,也是一切正常的: ~~~ NSError *error = nil; [Test checkStringLength:@"" error:&error]; ~~~ 不过我们如果就静态分析器来分析一下,发现会在”*error = …“这行代码处报如下的警告: > Potential null dereference. According to coding standards in ‘Creating and Returning NSError Objects’ the parameter may be null 这句话告诉我们的是这里可能存在空引用。实际上,如果我们像下面这样调用方法的话,程序是会崩溃的: ~~~ [Test checkStringLength:@"" error:NULL]; ~~~ 因为此时在方法中,error实际上是NULL,*error这货啥也不是,对它赋值肯定就出错了。 这里正确的姿式是在使用error之前,先判断它是否为NULL,完整的代码如下: ~~~ + (NSString )checkStringLength:(NSString *)str error:(NSError **)error { if (str.length <= 0) { if (error != NULL) { *error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil]; } return nil; } return str; } ~~~ 实际上,对此这种方式的传值,我们始终需要去做非空判断。 ### Charles支持iOS模拟器 咬咬牙花了50刀买了一个Charles的License。 今天临时需要在模拟器上跑工程,想抓一下数据包,看一下请求Header里面的信息。工程跑起来时,发现Charles没有抓取到数据。嗯,本着有问题先问stackoverflow的原则,跑到上面搜了一下。找到了这个贴子:[How to use Charles Proxy on the Xcode 6 (iOS 8) Simulator?](http://stackoverflow.com/questions/25439756/how-to-use-charles-proxy-on-the-xcode-6-ios-8-simulator)。不过我的处理没有他这么麻烦,基本上两步搞定了: 1.在Charles的菜单中选择Help > SSL Proxying > Install Charles Root Certificate in iOS Simulators,直接点击就行。这时候会弹出一个提示框,点击OK就行。 2.如果这时候还不能抓取数据,就重启模拟器。 这样就OK了。在Keychain里面,真机和模拟器的证书是同一个。 至于stackoverflow里面提到的在3.9.3版本上还需要覆盖一个脚本文件,这个没有尝试过,哈哈,我的是最新的3.10.2。 还有个需要注意的是,在抓取模拟器数据时,如果关闭Charles,那么模拟器将无法再请求到网络数据。这时需要重新开启Charles,或者是重启模拟器。另外如果重置了模拟器的设置(Reset Content and Settings…),Charles也抓取不到模拟器的数据,需要重新来过。 #### 参考 1. [How to use Charles Proxy on the Xcode 6 (iOS 8) Simulator?](http://stackoverflow.com/questions/25439756/how-to-use-charles-proxy-on-the-xcode-6-ios-8-simulator)
';

第三期

最后更新于:2022-04-01 02:39:26

> 原文出处:http://southpeak.github.io/blog/2015/06/30/ioszhi-shi-xiao-ji-di-san-qi-2015-dot-06-dot-30/ > 作者:南峰子 Swift2出来了,还是得与时俱进啊,不然就成老古董了。再者它开源了,又有事情要做了。当个程序猿真是累啊,一直在追,可从来没追上,刚有那么点念想了,人家又踩了脚油门。 一个月又要过去了,说好的一月两到三篇的,看来希望也是有点渺茫了。本来想好好整理下僵尸对象的内容,看看时间也不多了,也只好放到后面了。这一期没啥好内容,质量也不高,大家凑合着看吧,有疏漏还请大家指出,我一定好好改正。 这一期主要有三个内容: 1. Tint Color 2. Build Configurations in Swift 3. 键盘事件 ## Tint Color 在iOS 7后,UIView新增加了一个tintColor属性,这个属性定义了一个非默认的着色颜色值,其值的设置会影响到以视图为根视图的整个视图层次结构。它主要是应用到诸如app图标、导航栏、按钮等一些控件上,以获取一些有意思的视觉效果。 tintColor属性的声明如下: ~~~ var tintColor: UIColor! ~~~ 默认情况下,一个视图的tintColor是为nil的,这意味着视图将使用父视图的tint color值。当我们指定了一个视图的tintColor后,这个色值会自动传播到视图层次结构(以当前视图为根视图)中所有的子视图上。如果系统在视图层次结构中没有找到一个非默认的tintColor值,则会使用系统定义的颜色值(蓝色,RGB值为[0,0.478431,1],我们可以在IB中看到这个颜色)。因此,这个值总是会返回一个颜色值,即我们没有指定它。 与tintColor属性相关的还有个tintAdjustmentMode属性,它是一个枚举值,定义了tint color的调整模式。其声明如下: ~~~ var tintAdjustmentMode: UIViewTintAdjustmentMode ~~~ 枚举UIViewTintAdjustmentMode的定义如下: ~~~ enum UIViewTintAdjustmentMode : Int { case Automatic // 视图的着色调整模式与父视图一致 case Normal // 视图的tintColor属性返回完全未修改的视图着色颜色 case Dimmed // 视图的tintColor属性返回一个去饱和度的、变暗的视图着色颜色 } ~~~ 因此,当tintAdjustmentMode属性设置为Dimmed时,tintColor的颜色值会自动变暗。而如果我们在视图层次结构中没有找到默认值,则该值默认是Normal。 与tintColor相关的还有一个tintColorDidChange方法,其声明如下: ~~~ func tintColorDidChange() ~~~ 这个方法会在视图的tintColor或tintAdjustmentMode属性改变时自动调用。另外,如果当前视图的父视图的tintColor或tintAdjustmentMode属性改变时,也会调用这个方法。我们可以在这个方法中根据需要去刷新我们的视图。 ### 示例 接下来我们通过示例来看看tintColor的强大功能(示例盗用了Sam Davies写的一个例子,具体可以查看[iOS7 Day-by-Day :: Day 6 :: Tint Color](https://www.shinobicontrols.com/blog/posts/2013/09/27/ios7-day-by-day-day-6-tint-color),我就负责搬砖,用swift实现了一下,代码可以在[这里](https://github.com/southpeak/iOS-Dev-Examples/tree/master/UIKit/UIView/1.%20TintColorExample)下载)。 先来看看最终效果吧(以下都是盗图,请见谅,太懒了): ![image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6cd218cf53.jpg) 这个界面包含的元素主要有UIButton, UISlider, UIProgressView, UIStepper, UIImageView, ToolBar和一个自定义的子视图CustomView。接下来我们便来看看修改视图的tintColor会对这些控件产生什么样的影响。 在ViewController的viewDidLoad方法中,我们做了如下设置: ~~~ override func viewDidLoad() { super.viewDidLoad() println("\(self.view.tintAdjustmentMode.rawValue)") // 输出:1 println("\(self.view.tintColor)") // 输出:UIDeviceRGBColorSpace 0 0.478431 1 1 self.view.tintAdjustmentMode = .Normal self.dimTintSwitch?.on = false // 加载图片 var shinobiHead = UIImage(named: "shinobihead") // 设置渲染模式 shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate) self.tintedImageView?.image = shinobiHead self.tintedImageView?.contentMode = .ScaleAspectFit } ~~~ 首先,我们尝试打印默认的tintColor和tintAdjustmentMode,分别输出了[UIDeviceRGBColorSpace 0 0.478431 1 1]和1,这是在我们没有对整个视图层次结构设置任何tint color相关的值的情况下的输出。可以看到,虽然我们没有设置tintColor,但它仍然返回了系统的默认值;而tintAdjustmentMode则默认返回Normal的原始值。 接下来,我们显式设置tintAdjustmentMode的值为Normal,同时设置UIImageView的图片及渲染模式。 当我们点击”Change Color”按钮时,会执行以下的事件处理方法: ~~~ @IBAction func changeColorHandler(sender: AnyObject) { let hue = CGFloat(arc4random() % 256) / 256.0 let saturation = CGFloat(arc4random() % 128) / 256.0 + 0.5 let brightness = CGFloat(arc4random() % 128) / 256.0 + 0.5 let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) self.view.tintColor = color updateViewConstraints() } private func updateProgressViewTint() { self.progressView?.progressTintColor = self.view.tintColor } ~~~ 这段代码主要是随机生成一个颜色值,并赋值给self.view的tintColor属性,同时去更新进度条的tintColor值。 *注:有些控件的特定组成部件的tint color由特定的属性控制,例如进度就有2个tint color:一个用于进度条本身,另一个用于背景。* 点击”Change Color”按钮,可得到以下效果: ![image](https://www.shinobicontrols.com/media/371246/tint_color_image_2_350x621.jpg) 可以看到,我们在示例中并有没手动去设置UIButton, UISlider, UIStepper, UIImageView, ToolBar等子视图的颜色值,但随着self.view的tintColor属性颜色值的变化,这些控件的外观也同时跟着改变。也就是说self.view的tintColor属性颜色值的变化,影响到了以self.view为根视图的整个视图层次结果中所有子视图的外观。 看来tintColor还是很强大的嘛。 在界面中还有个UISwitch,这个是用来开启关闭dim tint的功能,其对应处理方法如下: ~~~ @IBAction func dimTimtHandler(sender: AnyObject) { if let isOn = self.dimTintSwitch?.on { self.view.tintAdjustmentMode = isOn ? .Dimmed : .Normal } updateViewConstraints() } ~~~ 当tintAdjustmentMode设置Dimmed时,其实际的效果是整个色值都变暗(此处无图可盗)。 另外,我们在子视图CustomView中重写了tintColorDidChange方法,以监听tintColor的变化,以更新我们的自定义视图,其实现如下: ~~~ override func tintColorDidChange() { tintColorLabel.textColor = self.tintColor tintColorBlock.backgroundColor = self.tintColor } ~~~ 所以方框和”Tint color label”颜色是跟着子视图的tintColor来变化的,而子视图的tintColor又是继承自父视图的。 在这个示例中,比较有意思的是还是对图片的处理。对图像的处理比较简单粗暴,对一个像素而言,如果它的alpha值为1的话,就将它的颜色设置为tint color;如果不为1的话,则设置为透明的。示例中的忍者头像就是这么处理的。不过我们需要设置图片的imageWithRenderingMode属性为AlwaysTemplate,这样渲染图片时会将其渲染为一个模板而忽略它的颜色信息,如代码所示: ~~~ var shinobiHead = UIImage(named: "shinobihead") // 设置渲染模式 shinobiHead = shinobiHead?.imageWithRenderingMode(.AlwaysTemplate) ~~~ ### 题外话 插个题外话,跟主题关系不大。 在色彩理论(color theory)中,一个tint color是一种颜色与白色的混合。与之类似的是shade color和tone color。shade color是将颜色与黑色混合,tone color是将颜色与灰色混合。它们都是基于Hues色调的。这几个色值的效果如下图所示: ![image](http://www.craftsy.com/blog/wp-content/uploads/2013/04/Screen-Shot-2013-04-30-at-12.46.43-PM.png) 一些基础的理论知识可以参考[Hues, Tints, Tones and Shades: What’s the Difference?](http://www.craftsy.com/blog/2013/05/hues-tints-tones-and-shades/)或更专业的一些文章。 ### 小结 如果我们想指定整个App的tint color,则可以通过设置window的tint color。这样同一个window下的所有子视图都会继承此tint color。 当弹出一个alert或者action sheet时,iOS7会自动将后面视图的tint color变暗。此时,我们可以在自定义视图中重写tintColorDidChange方法来执行我们想要的操作。 有些复杂控件,可以有多个tint color,不同的tint color控件不同的部分。如上面提到的UIProgressView,又如navigation bars, tab bars, toolbars, search bars, scope bars等,这些控件的背景着色颜色可以使用barTintColor属性来处理。 ### 参考 1. [UIView Class Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/index.html#//apple_ref/occ/instp/UIView/tintColor) 2. [iOS7 Day-by-Day :: Day 6 :: Tint Color](https://www.shinobicontrols.com/blog/posts/2013/09/27/ios7-day-by-day-day-6-tint-color) 3. [Appearance and Behavior](https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TransitionGuide/AppearanceCustomization.html) 4. [Tints and shades](https://en.wikipedia.org/wiki/Tints_and_shades) 5. [Hues, Tints, Tones and Shades: What’s the Difference?](http://www.craftsy.com/blog/2013/05/hues-tints-tones-and-shades/) ## Build Configurations in Swift 在Objective-C中,我们经常使用预处理指令来帮助我们根据不同的平台执行不同的代码,以让我们的代码支持不同的平台,如: ~~~ #if TARGET_OS_IPHONE #define MAS_VIEW UIView #elif TARGET_OS_MAC #define MAS_VIEW NSView #endif ~~~ 在swift中,由于对C语言支持没有Objective-C来得那么友好(暂时不知swift 2到C的支持如何),所以我们无法像在Objective-C中那样自如而舒坦地使用预处理指令。 不过,swift也提供了自己的方式来支持条件编译,即使用build configurations(构建配置)。Build configurations已经包含了字面量true和false,以及两个平台测试函数os()和arch()。 其中os()用于测试系统类型,可传入的参数包含OSX, iOS, watchOS,所以上面的代码在swift可改成: ~~~ #if os(iOS) typealias MAS_VIEW = UIView #elseif os(OSX) typealias MAS_VIEW = NSView #endif ~~~ *注:在WWDC 2014的[“Sharing code between iOS and OS X”](https://developer.apple.com/videos/wwdc/2014/)一节(session 233)中,Elizabeth Reid将这种方式称为Shimming* 遗憾的是,os()只能检测系统类型,而无法检测系统的版本,所以这些工作只能放在运行时去处理。关于如何检测系统的版本,Mattt Thompson老大在它的[Swift System Version Checking](http://nshipster.com/swift-system-version-checking/)一文中给了我们答案。 我们再来看看arch()。arch()用于测试CPU的架构,可传入的值包括x86_64, arm, arm64, i386。需要注意的是arch(arm)对于ARM 64的设备来说,不会返回true。而arch(i386)在32位的iOS模拟器上编译时会返回true。 如果我们想自定义一些在调试期间使用的编译配置选项,则可以使用-D标识来告诉编译器,具体操作是在”Build Setting”–>“Swift Compiler-Custom Flags”–>“Other Swift Flags”–>“Debug”中添加所需要的配置选项。如我们想添加常用的DEGUB选项,则可以在此加上”-D DEBUG”。这样我们就可以在代码中来执行一些debug与release时不同的操作,如 ~~~ #if DEBUG let totalSeconds = totalMinutes #else let totalSeconds = totalMinutes * 60 #endif ~~~ 一个简单的条件编译声明如下所示: ~~~ #if build configuration statements #else statements #endif ~~~ 当然,statements中可以包含0个或多个有效的swift的statements,其中可以包括表达式、语句、和控制流语句。另外,我们也可以使用&&和||操作符来组合多个build configuration,同时,可以使用!操作符来对build configuration取反,如下所示: ~~~ #if build configuration && !build configuration statements #elseif build configuration statements #else statements #endif ~~~ 需要注意的是,在swift中,条件编译语句必须在语法上是有效的,因为即使这些代码不会被编译,swift也会对其进行语法检查。 ### 参考 1. [Cross-platform Swift](http://www.giorgiocalderolla.com/cross-platform-swift.html) 2. [Shimming in Swift](http://stackoverflow.com/questions/24403551/shimming-in-swift) 3. [Swift System Version Checking](http://nshipster.com/swift-system-version-checking/) 4. [Interacting with C APIs](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html#//apple_ref/doc/uid/TP40014216-CH8-XID_21) ## 键盘事件 在涉及到表单输入的界面中,我们通常需要监听一些键盘事件,并根据实际需要来执行相应的操作。如,键盘弹起时,要让我们的UIScrollView自动收缩,以能看到整个UIScrollView的内容。为此,在UIWindow.h中定义了如下6个通知常量,来配合键盘在不同时间点的事件处理: ~~~ UIKeyboardWillShowNotification // 键盘显示之前 UIKeyboardDidShowNotification // 键盘显示完成后 UIKeyboardWillHideNotification // 键盘隐藏之前 UIKeyboardDidHideNotification // 键盘消息之后 UIKeyboardWillChangeFrameNotification // 键盘大小改变之前 UIKeyboardDidChangeFrameNotification // 键盘大小改变之后 ~~~ 这几个通知的object对象都是nil。而userInfo字典都包含了一些键盘的信息,主要是键盘的位置大小信息,我们可以通过使用以下的key来获取字典中对应的值: ~~~ // 键盘在动画开始前的frame let UIKeyboardFrameBeginUserInfoKey: String // 键盘在动画线束后的frame let UIKeyboardFrameEndUserInfoKey: String // 键盘的动画曲线 let UIKeyboardAnimationCurveUserInfoKey: String // 键盘的动画时间 let UIKeyboardAnimationDurationUserInfoKey: String ~~~ 在此,我感兴趣的是键盘事件的调用顺序和如何获取键盘的大小,以适当的调整视图的大小。 从定义的键盘通知的类型可以看到,实际上我们关注的是三个阶段的键盘的事件:显示、隐藏、大小改变。在此我们设定两个UITextField,它们的键盘类型不同:一个是普通键盘,一个是数字键盘。我们监听所有的键盘事件,并打印相关日志(在此就不贴代码了),直接看结果。 1) 当我们让textField1获取输入焦点时,打印的日志如下: ~~~ keyboard will change keyboard will show keyboard did change keyboard did show ~~~ 2) 在不隐藏键盘的情况下,让textField2获取焦点,打印的日志如下: ~~~ keyboard will change keyboard will show keyboard did change keyboard did show ~~~ 3) 再收起键盘,打印的日志如下: ~~~ keyboard will change keyboard will hide keyboard did change keyboard did hide ~~~ 从上面的日志可以看出,不管是键盘的显示还是隐藏,都会发送大小改变的通知,而且是在show和hide的对应事件之前。而在大小不同的键盘之间切换时,除了发送change事件外,还会发送show事件(不发送hide事件)。 另外还有两点需要注意的是: 1. 如果是在两个大小相同的键盘之间切换,则不会发送任何消息 2. 如果是普通键盘中类似于中英文键盘的切换,只要大小改变了,都会发送一组或多组与上面2)相同流程的消息 了解了事件的调用顺序,我们就可以根据自己的需要来决定在哪个消息处理方法中来执行操作。为此,我们需要获取一些有用的信息。这些信息是封装在通知的userInfo中,通过上面常量key来获取相关的值。通常我们关心的是UIKeyboardFrameEndUserInfoKey,来获取动画完成后,键盘的frame,以此来计算我们的scroll view的高度。另外,我们可能希望scroll view高度的变化也是通过动画来过渡的,此时UIKeyboardAnimationCurveUserInfoKey和UIKeyboardAnimationDurationUserInfoKey就有用了。 我们可以通过以下方式来获取这些值: ~~~ if let dict = notification.userInfo { var animationDuration: NSTimeInterval = 0 var animationCurve: UIViewAnimationCurve = .EaseInOut var keyboardEndFrame: CGRect = CGRectZero dict[UIKeyboardAnimationCurveUserInfoKey]?.getValue(&animationCurve) dict[UIKeyboardAnimationDurationUserInfoKey]?.getValue(&animationDuration) dict[UIKeyboardFrameEndUserInfoKey]?.getValue(&keyboardEndFrame) ...... } ~~~ 实际上,userInfo中还有另外三个值,只不过这几个值从iOS 3.2开始就已经废弃不用了。所以我们不用太关注。 最后说下表单。一个表单界面看着比较简单,但交互和UI总是能想出各种方法来让它变得复杂,而且其实里面设计到的细节还是很多的。像我们金融类的App,通常都会涉及到大量的表单输入,所以如何做好,还是需要花一番心思的。空闲时,打算总结一下,写一篇文章。 ### 参考 1. [UIWindow Class Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIWindow_Class/#//apple_ref/doc/constant_group/Keyboard_Notification_User_Info_Keys) ## 零碎 ### 自定义UIPickerView的行 UIPickerView的主要内容实际上并不多,主要是一个UIPickerView类和对应的UIPickerViewDelegate,UIPickerViewDataSource协议,分别表示代理和数据源。在此不细说这些,只是解答我们遇到的一个小需求。 通常,UIPickerView是可以定义多列内容的,比如年、月、日三列,这些列之间相互不干扰,可以自已滚自己的,不碍别人的事。不过,我们有这么一个需求,也是有三列,但这三列需要一起滚。嗯,这个就需要另行处理了。 在UIPickerViewDelegate中,声明了下面这样一个代理方法: ~~~ - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view ~~~ 我们通过这个方法就可以来自定义行的视图。时间不早,废话就不多说了,直接上代码吧: ~~~ - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view { PickerViewCell *pickerCell = (PickerViewCell *)view; if (!pickerCell) { NSInteger column = 3; pickerCell = [[PickerViewCell alloc] initWithFrame:(CGRect){CGPointZero, [UIScreen mainScreen].bounds.size.width, 45.0f} column:column]; } [pickerCell setLabelTexts:@[...]]; return pickerCell; } ~~~ 我们定义了一个PickerViewCell视图,里面根据我们的传入的column参数来等分放置column个UILabel,并通过setLabelTexts来设置每个UILabel的文本。当然,我们也可以在PickerViewCell去定义UILabel的外观显示。就是这么简单。 不过,还有个需要注意的就是,虽然看上去是显示了3列,但实际上是按1列来处理的,所以下面的实现应该是返回1: ~~~ - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 1; } ~~~ #### 参考 1. [UIPickerViewDelegate Protocol Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIPickerViewDelegate_Protocol/) ### Constructing an object of class type ‘**’ with a metatype value must use a ‘required’ initializer. 参考 1. [Implementing NSCopying in Swift with subclasses](http://stackoverflow.com/questions/28144475/implementing-nscopying-in-swift-with-subclasses) ### Swift中”[AnyObject]? does not have a member named generator” 问题的处理 有个小需求,需要遍历当前导航控制器栈的所有ViewController。UINavigationController类自身的viewControllers属性返回的是一个[AnyObject]!数组,不过由于我的导航控制器本身有可能是nil,所以我获取到的ViewController数组如下: ~~~ var myViewControllers: [AnyObject]? = navigationController?.viewControllers ~~~ 获取到的myViewControllers是一个[AnyObject]?可选类型,这时如果我直接去遍历myViewControllers,如下代码所示 ~~~ for controller in myViewControllers { ... } ~~~ 编译器会报错,提示如下: ~~~ [AnyObject]? does not have a member named "Generator" ~~~ 实际上,不管是[AnyObject]?还是其它的诸如[String]?类型,都会报这个错。其原因是可选类型只是个容器,它与其所包装的值是不同的类型,也就是说[AnyObject]是一个数组类型,但[AnyObject]?并不是数组类型。我们可以迭代一个数组,但不是迭代一个非集合类型。 在[stackoverflow](http://stackoverflow.com/questions/26852656/loop-through-anyobject-results-in-does-not-have-a-member-named-generator)上有这样一个有趣的比方,我犯懒就直接贴出来了: ~~~ To understand the difference, let me make a real life example: you buy a new TV on ebay, the package is shipped to you, the first thing you do is to check if the package (the optional) is empty (nil). Once you verify that the TV is inside, you have to unwrap it, and put the box aside. You cannot use the TV while it's in the package. Similarly, an optional is a container: it is not the value it contains, and it doesn't have the same type. It can be empty, or it can contain a valid value. ~~~ 所有,这里的处理应该是: ~~~ if let controllers = myViewControllers { for controller in controllers { ...... } } ~~~ #### 参考 1. [Loop through [AnyObject]? results in does not have a member named generator](http://stackoverflow.com/questions/26852656/loop-through-anyobject-results-in-does-not-have-a-member-named-generator)
';

第二期

最后更新于:2022-04-01 02:39:24

> 原文出处: http://southpeak.github.io/blog/2015/05/31/ioszhi-shi-xiao-ji-di-er-qi-2015-dot-05-dot-31/ 作者: 南峰子 换了个厂子,还不到1个月。哎,着实是累啊,基本上是996.5的节奏,只会更多。加班把我快加吐了,但人在江湖,身不由已啊。为了讨口饭吃,命也不要了。谁让咱只是个臭写代码的呢。不过加班是多,只是长得太丑,所有没办法,没时间也得抽时间来学习。不然,饭都没得吃了,还得养家糊口呢。 本期总结的内容不是很多,主要有以下几个问题: 1. 使用UIVisualEffectView为视图添加特殊效果 2. Nullability Annotations 3. weak的生命周期 ## 使用UIVisualEffectView为视图添加特殊效果 在iOS 8后,苹果开放了不少创建特效的接口,其中就包括创建毛玻璃(blur)的接口。 通常要想创建一个特殊效果(如blur效果),可以创建一个UIVisualEffectView视图对象,这个对象提供了一种简单的方式来实现复杂的视觉效果。这个可以把这个对象看作是效果的一个容器,实际的效果会影响到该视图对象底下的内容,或者是添加到该视图对象的contentView中的内容。 我们举个例子来看看如果使用UIVisualEffectView: ~~~ let bgView: UIImageView = UIImageView(image: UIImage(named: "visual")) bgView.frame = self.view.bounds self.view.addSubview(bgView) let blurEffect: UIBlurEffect = UIBlurEffect(style: .Light) let blurView: UIVisualEffectView = UIVisualEffectView(effect: blurEffect) blurView.frame = CGRectMake(50.0, 50.0, self.view.frame.width - 100.0, 200.0) self.view.addSubview(blurView) ~~~ 这段代码是在当前视图控制器上添加了一个UIImageView作为背景图。然后在视图的一小部分中使用了blur效果。其效果如下所示: ![image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ccb7b8c85.jpg) 我们可以看到UIVisualEffectView还是非常简单的。需要注意是的,不应该直接添加子视图到UIVisualEffectView视图中,而是应该添加到UIVisualEffectView对象的contentView中。 另外,尽量避免将UIVisualEffectView对象的alpha值设置为小于1.0的值,因为创建半透明的视图会导致系统在离屏渲染时去对UIVisualEffectView对象及所有的相关的子视图做混合操作。这不但消耗CPU/GPU,也可能会导致许多效果显示不正确或者根本不显示。 我们在上面看到,初始化一个UIVisualEffectView对象的方法是UIVisualEffectView(effect: blurEffect),其定义如下: ~~~ init(effect effect: UIVisualEffect) ~~~ 这个方法的参数是一个UIVisualEffect对象。我们查看官方文档,可以看到在UIKit中,定义了几个专门用来创建视觉特效的,它们分别是UIVisualEffect、UIBlurEffect和UIVibrancyEffect。它们的继承层次如下所示: ~~~ NSObject | -- UIVisualEffect | -- UIBlurEffect | -- UIVibrancyEffect ~~~ UIVisualEffect是一个继承自NSObject的创建视觉效果的基类,然而这个类除了继承自NSObject的属性和方法外,没有提供任何新的属性和方法。其主要目的是用于初始化UIVisualEffectView,在这个初始化方法中可以传入UIBlurEffect或者UIVibrancyEffect对象。 一个UIBlurEffect对象用于将blur(毛玻璃)效果应用于UIVisualEffectView视图下面的内容。如上面的示例所示。不过,这个对象的效果并不影响UIVisualEffectView对象的contentView中的内容。 UIBlurEffect主要定义了三种效果,这些效果由枚举UIBlurEffectStyle来确定,该枚举的定义如下: ~~~ enum UIBlurEffectStyle : Int { case ExtraLight case Light case Dark } ~~~ 其主要是根据色调(hue)来确定特效视图与底部视图的混合。 与UIBlurEffect不同的是,UIVibrancyEffect主要用于放大和调整UIVisualEffectView视图下面的内容的颜色,同时让UIVisualEffectView的contentView中的内容看起来更加生动。通常UIVibrancyEffect对象是与UIBlurEffect一起使用,主要用于处理在UIBlurEffect特效上的一些显示效果。接上面的代码,我们看看在blur的视图上添加一些新的特效,如下代码所示: ~~~ let vibrancyView: UIVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(forBlurEffect: blurEffect)) vibrancyView.setTranslatesAutoresizingMaskIntoConstraints(false) blurView.contentView.addSubview(vibrancyView) var label: UILabel = UILabel() label.setTranslatesAutoresizingMaskIntoConstraints(false) label.text = "Vibrancy Effect" label.font = UIFont(name: "HelveticaNeue-Bold", size: 30) label.textAlignment = .Center label.textColor = UIColor.whiteColor() vibrancyView.contentView.addSubview(label) ~~~ 其效果如下图所示: ![image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ccb82db9f.jpg) vibrancy特效是取决于颜色值的。所有添加到contentView的子视图都必须实现tintColorDidChange方法并更新自己。需要注意的是,我们使用UIVibrancyEffect(forBlurEffect:)方法创建UIVibrancyEffect时,参数blurEffect必须是我们想加效果的那个blurEffect,否则可能不是我们想要的效果。 另外,UIVibrancyEffect还提供了一个类方法notificationCenterVibrancyEffect,其声明如下: ~~~ class func notificationCenterVibrancyEffect() -> UIVibrancyEffect! ~~~ 这个方法创建一个用于通知中心的Today扩展的vibrancy特效。 ### 参考 1. [UIVisualEffectView Class Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIVisualEffectView/) 2. [UIVisualEffect Class Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIVisualEffect_class/index.html#//apple_ref/occ/cl/UIVisualEffect) 3. [UIBlurEffect Class Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIVisualEffect_class/index.html#//apple_ref/occ/cl/UIVisualEffect) 4. [UIVibrancyEffect Class Reference](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIVibrancyEffect/) 5. [UIVisualEffect – Swift Tutorial](http://swiftoverload.com/tag/uivisualeffectview/) 6. [iOS 8: UIVisualEffect](http://idrawcode.tumblr.com/post/101925733632/ios-8-uivisualeffect) ## Pointer is missing a nullability type specifier (**nonnull or **nullable)问题的处理 — Nullability Annotations 最近在用Xcode 6.3写代码,一些涉及到对象的代码会报如下编译器警告: ~~~ Pointer is missing a nullability type specifier (__nonnull or __nullable) ~~~ 于是google了一下,发现这是Xcode 6.3的一个新特性,即**nullability annotations**。 ### Nullability Annotations 我们都知道在swift中,可以使用!和?来表示一个对象是optional的还是non-optional,如view?和view!。而在Objective-C中则没有这一区分,view即可表示这个对象是optional,也可表示是non-optioanl。这样就会造成一个问题:在Swift与Objective-C混编时,Swift编译器并不知道一个Objective-C对象到底是optional还是non-optional,因此这种情况下编译器会隐式地将Objective-C的对象当成是non-optional。 为了解决这个问题,苹果在Xcode 6.3引入了一个Objective-C的新特性:nullability annotations。这一新特性的核心是两个新的类型注释:**__nullable**和**__nonnull**。从字面上我们可以猜到,**__nullable**表示对象可以是NULL或nil,而**__nonnull**表示对象不应该为空。当我们不遵循这一规则时,编译器就会给出警告。 我们来看看以下的实例, ~~~ @interface TestNullabilityClass () @property (nonatomic, copy) NSArray * items; - (id)itemWithName:(NSString * __nonnull)name; @end @implementation TestNullabilityClass ... - (void)testNullability { [self itemWithName:nil]; // 编译器警告:Null passed to a callee that requires a non-null argument } - (id)itemWithName:(NSString * __nonnull)name { return nil; } @end ~~~ 不过这只是一个警告,程序还是能编译通过并运行。 事实上,在任何可以使用const关键字的地方都可以使用__nullable和__nonnull,不过这两个关键字仅限于使用在指针类型上。而在方法的声明中,我们还可以使用不带下划线的nullable和nonnull,如下所示: ~~~ - (nullable id)itemWithName:(NSString * nonnull)name ~~~ 在属性声明中,也增加了两个相应的特性,因此上例中的items属性可以如下声明: ~~~ @property (nonatomic, copy, nonnull) NSArray * items; ~~~ 当然也可以用以下这种方式: ~~~ @property (nonatomic, copy) NSArray * __nonnull items; ~~~ 推荐使用nonnull这种方式,这样可以让属性声明看起来更清晰。 ### Nonnull区域设置(Audited Regions) 如果需要每个属性或每个方法都去指定nonnull和nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针对象都被假定为nonnull,因此我们只需要去指定那些nullable的指针。如下代码所示: ~~~ NS_ASSUME_NONNULL_BEGIN @interface TestNullabilityClass () @property (nonatomic, copy) NSArray * items; - (id)itemWithName:(nullable NSString *)name; @end NS_ASSUME_NONNULL_END ~~~ 在上面的代码中,items属性默认是nonnull的,itemWithName:方法的返回值也是nonnull,而参数是指定为nullable的。 不过,为了安全起见,苹果还制定了几条规则: 1. typedef定义的类型的nullability特性通常依赖于上下文,即使是在Audited Regions中,也不能假定它为nonnull。 2. 复杂的指针类型(如id *)必须显示去指定是nonnull还是nullable。例如,指定一个指向nullable对象的nonnull指针,可以使用”__nullable id * __nonnull”。 3. 我们经常使用的NSError **通常是被假定为一个指向nullable NSError对象的nullable指针。 ### 兼容性 因为Nullability Annotations是Xcode 6.3新加入的,所以我们需要考虑之前的老代码。实际上,苹果已以帮我们处理好了这种兼容问题,我们可以安全地使用它们: 1. 老代码仍然能正常工作,即使对nonnull对象使用了nil也没有问题。 2. 老代码在需要和swift混编时,在新的swift编译器下会给出一个警告。 3. nonnull不会影响性能。事实上,我们仍然可以在运行时去判断我们的对象是否为nil。 事实上,我们可以将nonnull/nullable与我们的断言和异常一起看待,其需要处理的问题都是同一个:违反约定是一个程序员的错误。特别是,返回值是我们可控的东西,如果返回值是nonnull的,则我们不应该返回nil,除非是为了向后兼容。 ### 参考 1. [Nullability and Objective-C](https://developer.apple.com/swift/blog/?id=25) ## weak的生命周期 我们都知道weak表示的是一个弱引用,这个引用不会增加对象的引用计数,并且在所指向的对象被释放之后,weak指针会被设置的为nil。weak引用通常是用于处理循环引用的问题,如代理及block的使用中,相对会较多的使用到weak。 之前对weak的实现略有了解,知道它的一个基本的生命周期,但具体是怎么实现的,了解得不是太清晰。今天又翻了翻《Objective-C高级编程》关于__weak的讲解,在此做个笔记。 我们以下面这行代码为例: **代码清单1:示例代码** ~~~ { id __weak obj1 = obj; } ~~~ 当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在Clang中的声明如下: ~~~ id objc_initWeak(id *object, id value); ~~~ 其具体实现如下: ~~~ id objc_initWeak(id *object, id value) { *object = 0; return objc_storeWeak(object, value); } ~~~ 示例代码轮换成编译器的模拟代码如下: ~~~ id obj1; objc_initWeak(&obj1, obj); ~~~ 因此,这里所做的事是先将obj1初始化为0(nil),然后将obj1的地址及obj作为参数传递给objc_storeWeak函数。 objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,或者指向一个有效的对象。 如果value是一个空指针或者其指向的对象已经被释放了,则object是zero-initialized的。否则,object将被注册为一个指向value的__weak对象。而这事应该是objc_storeWeak函数干的。objc_storeWeak的函数声明如下: ~~~ id objc_storeWeak(id *location, id value); ~~~ 其具体实现如下: ~~~ id objc_storeWeak(id *location, id newObj) { id oldObj; SideTable *oldTable; SideTable *newTable; ...... // Acquire locks for old and new values. // Order by lock address to prevent lock ordering problems. // Retry if the old value changes underneath us. retry: oldObj = *location; oldTable = SideTable::tableForPointer(oldObj); newTable = SideTable::tableForPointer(newObj); ...... if (*location != oldObj) { OSSpinLockUnlock(lock1); #if SIDE_TABLE_STRIPE > 1 if (lock1 != lock2) OSSpinLockUnlock(lock2); #endif goto retry; } if (oldObj) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } if (newObj) { newObj = weak_register_no_lock(&newTable->weak_table, newObj,location); // weak_register_no_lock returns NULL if weak store should be rejected } // Do not set *location anywhere else. That would introduce a race. *location = newObj; ...... return newObj; } ~~~ 我们撇开源码中各种锁操作,来看看这段代码都做了些什么。在此之前,我们先来了解下weak表和SideTable。 weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了某个对象相关的的所有的弱引用信息。其定义如下(具体定义在[objc-weak.h](http://www.opensource.apple.com/source/objc4/objc4-646/runtime/objc-weak.h)中): ~~~ struct weak_table_t { weak_entry_t *weak_entries; size_t num_entries; ...... }; ~~~ 其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下: ~~~ struct weak_entry_t { DisguisedPtr<objc_object> referent; union { struct { weak_referrer_t *referrers; uintptr_t out_of_line : 1; ...... }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }; }; ~~~ 其中referent是被引用的对象,即示例代码中的obj对象。下面的union即存储了所有指向该对象的弱引用。由注释可以看到,当out_of_line等于0时,hash表被一个数组所代替。另外,所有的弱引用对象的地址都是存储在weak_referrer_t指针的地址中。其定义如下: ~~~ typedef objc_object ** weak_referrer_t; ~~~ SideTable是一个用C++实现的类,它的具体定义在[NSObject.mm](http://opensource.apple.com/source/objc4/objc4-532.2/runtime/NSObject.mm)中,我们来看看它的一些成员变量的定义: ~~~ class SideTable { private: static uint8_t table_buf[SIDE_TABLE_STRIPE * SIDE_TABLE_SIZE]; public: RefcountMap refcnts; weak_table_t weak_table; ...... } ~~~ RefcountMap refcnts,大家应该能猜到这个做什么用的吧?看着像是引用计数什么的。哈哈,貌似就是啊,这东东存储了一个对象的引用计数的信息。当然,我们在这里不去探究它,我们关注的是weak_table。这个成员变量指向的就是一个对象的weak表。 了解了weak表和SideTable,让我们再回过头来看看objc_storeWeak。首先是根据weak指针找到其指向的老的对象: ~~~ oldObj = *location; ~~~ 然后获取到与新旧对象相关的SideTable对象: ~~~ oldTable = SideTable::tableForPointer(oldObj); newTable = SideTable::tableForPointer(newObj); ~~~ 下面要做的就是在老对象的weak表中移除指向信息,而在新对象的weak表中建立关联信息: ~~~ if (oldObj) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } if (newObj) { newObj = weak_register_no_lock(&newTable->weak_table, newObj,location); // weak_register_no_lock returns NULL if weak store should be rejected } ~~~ 接下来让弱引用指针指向新的对象: ~~~ *location = newObj; ~~~ 最后会返回这个新对象: ~~~ return newObj; ~~~ objc_storeWeak的基本实现就是这样。当然,在objc_initWeak中调用objc_storeWeak时,老对象是空的,所有不会执行weak_unregister_no_lock操作。 而当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下: 1. 调用objc_release 2. 因为对象的引用计数为0,所以执行dealloc 3. 在dealloc中,调用了_objc_rootDealloc函数 4. 在_objc_rootDealloc中,调用了object_dispose函数 5. 调用objc_destructInstance 6. 最后调用objc_clear_deallocating 我们重点关注一下最后一步,objc_clear_deallocating的具体实现如下: ~~~ void objc_clear_deallocating(id obj) { ...... SideTable *table = SideTable::tableForPointer(obj); // clear any weak table items // clear extra retain count and deallocating bit // (fixme warn or abort if extra retain count == 0 ?) OSSpinLockLock(&table->slock); if (seen_weak_refs) { arr_clear_deallocating(&table->weak_table, obj); } ...... } ~~~ 我们可以看到,在这个函数中,首先取出对象对应的SideTable实例,如果这个对象有关联的弱引用,则调用arr_clear_deallocating来清除对象的弱引用信息。我们来看看arr_clear_deallocating具体实现: ~~~ PRIVATE_EXTERN void arr_clear_deallocating(weak_table_t *weak_table, id referent) { { weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == NULL) { ...... return; } // zero out references for (int i = 0; i < entry->referrers.num_allocated; ++i) { id *referrer = entry->referrers.refs[i].referrer; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable @ %p holds %p instead of %p\n", referrer, *referrer, referent); } } } weak_entry_remove_no_lock(weak_table, entry); weak_table->num_weak_refs--; } } ~~~ 这个函数首先是找出对象对应的weak_entry_t链表,然后挨个将弱引用置为nil。最后清理对象的记录。 通过上面的描述,我们基本能了解一个weak引用从生到死的过程。从这个流程可以看出,一个weak引用的处理涉及各种查表、添加与删除操作,还是有一定消耗的。所以如果大量使用__weak变量的话,会对性能造成一定的影响。那么,我们应该在什么时候去使用weak呢?《Objective-C高级编程》给我们的建议是只在避免循环引用的时候使用__weak修饰符。 另外,在clang中,还提供了不少关于weak引用的处理函数。如objc_loadWeak, objc_destroyWeak, objc_moveWeak等,我们可以在苹果的开源代码中找到相关的实现。等有时间,我再好好研究研究。 ### 参考 1. 《Objective-C高级编程》1.4: __weak修饰符 2. [Clang 3.7 documentation – Objective-C Automatic Reference Counting (ARC)](http://clang.llvm.org/docs/AutomaticReferenceCounting.html) 3. [apple opensource – NSObject.mm](http://opensource.apple.com/source/objc4/objc4-532.2/runtime/NSObject.mm) ## 零碎 ### CAGradientLayer CAGradientLayer类是用于在其背景色上绘制一个颜色渐变,以填充层的整个形状,包括圆角。这个类继承自CALayer类,使用起来还是很方便的。 与Quartz 2D中的渐变处理类似,一个渐变有一个起始位置(startPoint)和一个结束位置(endPoint),在这两个位置之间,我们可以指定一组颜色值(colors,元素是CGColorRef对象),可以是两个,也可以是多个,每个颜色值会对应一个位置(locations)。另外,渐变还分为轴向渐变和径向渐变。 我们写个实例来看看CAGradientLayer的具体使用: ~~~ CAGradientLayer *layer = [CAGradientLayer layer]; layer.startPoint = (CGPoint){0.5f, 0.0f}; layer.endPoint = (CGPoint){0.5f, 1.0f}; layer.colors = [NSArray arrayWithObjects:(id)[UIColor blueColor].CGColor, (id)[UIColor redColor].CGColor, (id)[UIColor greenColor].CGColor, nil]; layer.locations = @[@0.0f, @0.6f, @1.0f]; layer.frame = self.view.layer.bounds; [self.view.layer insertSublayer:layer atIndex:0]; ~~~ #### 参考 1. [CAGradientLayer Class Reference](https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAGradientLayer_class/) ### Xcode中Ineligible Devices的处理 换了台新电脑,装了个Xcode 6.3,整了个新证书和profile,然后打开Xcode,连上手机。额,然后发现设备居然被标识为Ineligible Devices,没认出来。情况类似于下图: ![image](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-21_55d6ccb865d8b.png) 电脑是受信任的,证书和profile也都是OK的。试了几次重启Xcode和重新连接手机,无效。设备就是选不了。最后是在Product->Destination里面才选中这个设备的。不过在工具栏还是不能选择,郁闷,求解。 ### iOS 7后隐藏UITextField的光标 新项目只支持iOS 7后,很多事情变得简单多了,就像隐藏UITextField的光标一样,就简单的一句话: ~~~ textFiled.tintColor = [UIColor clearColor]; ~~~ 通常我们用UIPickerView作为我们的UITextField的inputView时,我们是需要隐藏光标的。当然,如果想换个光标颜色,也是这么处理。 这么处理的有个遗留问题是:通常我们使用UIPickerView作为UITextField的inputView时, 并不希望去执行各种菜单操作(全选、复制、粘帖),但只是去设置UITextField的tintColor时,我们仍然可以执行这边操作,所以需要加额外的处理。这个问题,我们可以这样处理:在textFieldShouldBeginEditing:中,我们把UITextField的userInteractionEnabled设置为NO,然后在textFieldShouldEndEditing:,将将这个值设置回来。如下: ~~~ - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { textField.userInteractionEnabled = NO; return YES; } - (BOOL)textFieldShouldEndEditing:(UITextField *)textField { textField.userInteractionEnabled = YES; return YES; } ~~~ 这样就OK了。当然这只是我们当前使用的一种处理方式,还有其它的方法,直接google或者stackoverflow吧。 ### iOS 7后UIAlertView中文字左对齐问题 在iOS 7之前,如果我们想要让UIAlertView中的文字居左显示的话,可以使用以下这段代码来处理: ~~~ for (UIView *view in alert.subviews) { if([[view class] isSubclassOfClass:[UILabel class]]) { ((UILabel*)view).textAlignment = NSTextAlignmentLeft; } } ~~~ 但很遗憾的是,在iOS 7之后,苹果不让我们这么干了。我们去取UIAlertView的subviews时,获得的只是一个空数组,我们没有办法获取到我们想要的label。怎么办?三条路:告诉产品经理和UED说这个实现不了(当然,这个是会被鄙视的,人家会说你能力差);自己写;找第三方开源代码。嘿嘿,不过由于最近时间紧,所以我决定跟他们说实现不了,哈哈。不过在github上找了一个开源的,[Custom iOS AlertView](https://github.com/wimagguc/ios-custom-alertview),star的数量也不少,看来不错,回头好好研究研究。
';

第一期

最后更新于:2022-04-01 02:39:21

> 原文出处: http://southpeak.github.io/blog/2015/05/10/ioszhi-shi-xiao-ji-di-%5B%3F%5D-qi-2015-dot-05-dot-10/ 作者: 南峰子 一直想做这样一个小册子,来记录自己平时开发、阅读博客、看书、代码分析和与人交流中遇到的各种问题。之前有过这样的尝试,但都是无疾而终。不过,每天接触的东西多,有些东西不记下来,忘得也是很快,第二次遇到同样的问题时,还得再查一遍。好记性不如烂笔头,所以又决定重拾此事,时不时回头看看,温故而知新。 这里面的每个问题,不会太长。或是读书笔记,或是摘抄,亦或是验证,每个问题的篇幅争取在六七百字的样子。笔记和摘抄的出处会详细标明。问题的个数不限,凑齐3500字左右就发一篇。争取每月至少发两篇吧,权当是对自己学习的一个整理。 本期主要记录了以下几个问题: 1. NSString属性什么时候用copy,什么时候用strong? 2. Foundation中的断言处理 3. IBOutletCollection 4. NSRecursiveLock递归锁的使用 5. NSHashTable ## NSString属性什么时候用copy,什么时候用strong? 我们在声明一个NSString属性时,对于其内存相关特性,通常有两种选择(基于ARC环境):strong与copy。那这两者有什么区别呢?什么时候该用strong,什么时候该用copy呢?让我们先来看个例子。 ### 示例 我们定义一个类,并为其声明两个字符串属性,如下所示: ~~~ @interface TestStringClass () @property (nonatomic, strong) NSString *strongString; @property (nonatomic, copy) NSString *copyedString; @end ~~~ 上面的代码声明了两个字符串属性,其中一个内存特性是strong,一个是copy。下面我们来看看它们的区别。 首先,我们用一个不可变字符串来为这两个属性赋值, ~~~ - (void)test { NSString *string = [NSString stringWithFormat:@"abc"]; self.strongString = string; self.copyedString = string; NSLog(@"origin string: %p, %p", string, &string); NSLog(@"strong string: %p, %p", _strongString, &_strongString); NSLog(@"copy string: %p, %p", _copyedString, &_copyedString); } ~~~ 其输出结果是: ~~~ origin string: 0x7fe441592e20, 0x7fff57519a48 strong string: 0x7fe441592e20, 0x7fe44159e1f8 copy string: 0x7fe441592e20, 0x7fe44159e200 ~~~ 我们要以看到,这种情况下,不管是strong还是copy属性的对象,其指向的地址都是同一个,即为string指向的地址。如果我们换作MRC环境,打印string的引用计数的话,会看到其引用计数值是3,即strong操作和copy操作都使原字符串对象的引用计数值加了1。 接下来,我们把string由不可变改为可变对象,看看会是什么结果。即将下面这一句 ~~~ NSString *string = [NSString stringWithFormat:@"abc"]; ~~~ 改成: ~~~ NSMutableString *string = [NSMutableString stringWithFormat:@"abc"]; ~~~ 其输出结果是: ~~~ origin string: 0x7ff5f2e33c90, 0x7fff59937a48 strong string: 0x7ff5f2e33c90, 0x7ff5f2e2aec8 copy string: 0x7ff5f2e2aee0, 0x7ff5f2e2aed0 ~~~ 可以发现,此时copy属性字符串已不再指向string字符串对象,而是深拷贝了string字符串,并让_copyedString对象指向这个字符串。在MRC环境下,打印两者的引用计数,可以看到string对象的引用计数是2,而_copyedString对象的引用计数是1。 此时,我们如果去修改string字符串的话,可以看到:因为_strongString与string是指向同一对象,所以_strongString的值也会跟随着改变(需要注意的是,此时_strongString的类型实际上是NSMutableString,而不是NSString);而_copyedString是指向另一个对象的,所以并不会改变。 ### 结论 由于NSMutableString是NSString的子类,所以一个NSString指针可以指向NSMutableString对象,让我们的strongString指针指向一个可变字符串是OK的。 而上面的例子可以看出,当源字符串是NSString时,由于字符串是不可变的,所以,不管是strong还是copy属性的对象,都是指向源对象,copy操作只是做了次**浅拷贝**。 当源字符串是NSMutableString时,strong属性只是增加了源字符串的引用计数,而copy属性则是对源字符串做了次**深拷贝**,产生一个新的对象,且copy属性对象指向这个新的对象。另外需要注意的是,这个copy属性对象的类型始终是NSString,而不是NSMutableString,因此其是不可变的。 这里还有一个性能问题,即在源字符串是NSMutableString,strong是单纯的增加对象的引用计数,而copy操作是执行了一次深拷贝,所以性能上会有所差异。而如果源字符串是NSString时,则没有这个问题。 所以,在声明NSString属性时,到底是选择strong还是copy,可以根据实际情况来定。不过,一般我们将对象声明为NSString时,都不希望它改变,所以大多数情况下,我们建议用copy,以免因可变字符串的修改导致的一些非预期问题。 关于字符串的内存管理,还有些有意思的东西,可以参考[NSString特性分析学习](http://blog.cnbluebox.com/blog/2014/04/16/nsstringte-xing-fen-xi-xue-xi/)。 ### 参考 1. [NSString copy not copying?](http://stackoverflow.com/questions/2521468/nsstring-copy-not-copying) 2. [NSString特性分析学习](http://blog.cnbluebox.com/blog/2014/04/16/nsstringte-xing-fen-xi-xue-xi/) 3. [NSString什么时候用copy,什么时候用strong](http://blog.csdn.net/itianyi/article/details/9018567) ## Foundation中的断言处理 经常在看一些第三方库的代码时,或者自己在写一些基础类时,都会用到断言。所以在此总结一下Objective-C中关于断言的一些问题。 Foundation中定义了两组断言相关的宏,分别是: ~~~ NSAssert / NSCAssert NSParameterAssert / NSCParameterAssert ~~~ 这两组宏主要在功能和语义上有所差别,这些区别主要有以下两点: 1. 如果我们需要确保方法或函数的输入参数的正确性,则应该在方法(函数)的顶部使用NSParameterAssert / NSCParameterAssert;而在其它情况下,使用NSAssert / NSCAssert。 2. 另一个不同是介于C和Objective-C之间。NSAssert / NSParameterAssert应该用于Objective-C的上下文(方法)中,而NSCAssert / NSCParameterAssert应该用于C的上下文(函数)中。 当断言失败时,通常是会抛出一个如下所示的异常: ~~~ *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'true is not equal to false' ~~~ Foundation为了处理断言,专门定义了一个NSAssertionHandler来处理断言的失败情况。NSAssertionHandler对象是自动创建的,用于处理失败的断言。当断言失败时,会传递一个字符串给NSAssertionHandler对象来描述失败的原因。**每个线程都有自己的NSAssertionHandler对象**。当调用时,一个断言处理器会打印包含方法和类(或函数)的错误消息,并引发一个NSInternalInconsistencyException异常。就像上面所看到的一样。 我们很少直接去调用NSAssertionHandler的断言处理方法,通常都是自动调用的。 NSAssertionHandler提供的方法并不多,就三个,如下所示: ~~~ // 返回与当前线程的NSAssertionHandler对象。 // 如果当前线程没有相关的断言处理器,则该方法会创建一个并指定给当前线程 + (NSAssertionHandler *)currentHandler // 当NSCAssert或NSCParameterAssert断言失败时,会调用这个方法 - (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)object lineNumber:(NSInteger)fileName description:(NSString *)line, format,... // 当NSAssert或NSParameterAssert断言失败时,会调用这个方法 - (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... ~~~ 另外,还定义了一个常量字符串, ~~~ NSString * const NSAssertionHandlerKey; ~~~ 主要是用于在线程的threadDictionary字典中获取或设置断言处理器。 关于断言,还需要注意的一点是在Xcode 4.2以后,在release版本中断言是默认关闭的,这是由宏NS_BLOCK_ASSERTIONS来处理的。也就是说,当编译release版本时,所有的断言调用都是无效的。 我们可以自定义一个继承自NSAssertionHandler的断言处理类,来实现一些我们自己的需求。如Mattt Thompson的[NSAssertion​Handler](http://nshipster.com/nsassertionhandler/)实例一样: ~~~ @interface LoggingAssertionHandler : NSAssertionHandler @end @implementation LoggingAssertionHandler - (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... { NSLog(@"NSAssert Failure: Method %@ for object %@ in %@#%i", NSStringFromSelector(selector), object, fileName, line); } - (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... { NSLog(@"NSCAssert Failure: Function (%@) in %@#%i", functionName, fileName, line); } @end ~~~ 上面说过,每个线程都有自己的断言处理器。我们可以通过为线程的threadDictionary字典中的NSAssertionHandlerKey指定一个新值,来改变线程的断言处理器。 如下代码所示: ~~~ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSAssertionHandler *assertionHandler = [[LoggingAssertionHandler alloc] init]; [[[NSThread currentThread] threadDictionary] setValue:assertionHandler forKey:NSAssertionHandlerKey]; // ... return YES; } ~~~ 而什么时候应该使用断言呢?通常我们期望程序按照我们的预期去运行时,如调用的参数为空时流程就无法继续下去时,可以使用断言。但另一方面,我们也需要考虑,在这加断言确实是需要的么?我们是否可以通过更多的容错处理来使程序正常运行呢? Mattt Thompson在[NSAssertion​Handler](http://nshipster.com/nsassertionhandler/)中的倒数第二段说得挺有意思,在此摘抄一下: ~~~ But if we look deeper into NSAssertionHandler—and indeed, into our own hearts, there are lessons to be learned about our capacity for kindness and compassion; about our ability to forgive others, and to recover from our own missteps. We can't be right all of the time. We all make mistakes. By accepting limitations in ourselves and others, only then are we able to grow as individuals. ~~~ ### 参考 1. [NSAssertion​Handler](http://nshipster.com/nsassertionhandler/) 2. [NSAssertionHandler Class Reference](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSAssertionHandler_Class/) ## IBOutletCollection 在IB与相关文件做连接时,我们经常会用到两个关键字:IBOutlet和IBAction。经常用xib或storyboard的童鞋应该用这两上关键字非常熟悉了。不过UIKit还提供了另一个伪关键字**IBOutletCollection**,我们使用这个关键字,可以将界面上一组相同的控件连接到同一个数组中。 我们先来看看这个伪关键字的定义,可以从UIKit.framework的头文件UINibDeclarations.h找到如下定义: ~~~ #ifndef IBOutletCollection #define IBOutletCollection(ClassName) #endif ~~~ 另外,在Clang源码中,有更安全的定义方式,如下所示: ~~~ #define IBOutletCollection(ClassName) __attribute__((iboutletcollection(ClassName))) ~~~ 从上面的定义可以看到,与IBOutlet不同的是,IBOutletCollection带有一个参数,该参数是一个类名。 通常情况下,我们使用一个IBOutletCollection属性时,属性必须是strong的,且类型是NSArray,如下所示: ~~~ @property (strong, nonatomic) IBOutletCollection(UIScrollView) NSArray *scrollViews; ~~~ 假定我们的xib文件中有三个横向的scrollView,我们便可以将这三个scrollView都连接至scrollViews属性,然后在我们的代码中便可以做一些统一处理,如下所示: ~~~ - (void)setupScrollViewImages { for (UIScrollView *scrollView in self.scrollViews) { [self.imagesData enumerateObjectsUsingBlock:^(NSString *imageName, NSUInteger idx, BOOL *stop) { UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(CGRectGetWidth(scrollView.frame) * idx, 0, CGRectGetWidth(scrollView.frame), CGRectGetHeight(scrollView.frame))]; imageView.contentMode = UIViewContentModeScaleAspectFill; imageView.image = [UIImage imageNamed:imageName]; [scrollView addSubview:imageView]; }]; } } ~~~ 这段代码会影响到三个scrollView。这样做的好处是我们不需要手动通过addObject:方法将scrollView添加到scrollViews中。 不过在使用IBOutletCollection时,需要注意两点: 1. IBOutletCollection集合中对象的顺序是不确定的。我们通过调试方法可以看到集合中对象的顺序跟我们连接的顺序是一样的。但是这个顺序可能会因为不同版本的Xcode而有所不同。所以我们不应该试图在代码中去假定这种顺序。 2. 不管IBOutletCollection(ClassName)中的控件是什么,属性的类型始终是NSArray。实际上,我们可以声明是任何类型,如NSSet,NSMutableArray,甚至可以是UIColor,但不管我们在此设置的是什么类,IBOutletCollection属性总是指向一个NSArray数组。 关于第二点,我们以上面的scrollViews为例,作如下修改: ~~~ @property (strong, nonatomic) IBOutletCollection(UIScrollView) NSSet *scrollViews; ~~~ 实际上我们在控制台打印这个scrollViews时,结果如下所示: ~~~ (lldb) po self.scrollViews <__NSArrayI 0x1740573d0>( <UIScrollView: 0x12d60d770; frame = (0 0; 320 162); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1740574f0>; layer = <CALayer: 0x174229480>; contentOffset: {0, 0}; contentSize: {0, 0}>, <UIScrollView: 0x12d60dee0; frame = (0 0; 320 161); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x174057790>; layer = <CALayer: 0x1742297c0>; contentOffset: {0, 0}; contentSize: {0, 0}>, <UIScrollView: 0x12d60e650; frame = (0 0; 320 163); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1740579a0>; layer = <CALayer: 0x1742298e0>; contentOffset: {0, 0}; contentSize: {0, 0}> ) ~~~ 可以看到,它指向的是一个NSArray数组。 另外,IBOutletCollection实际上在iOS 4版本中就有了。不过,现在的Objective-C已经支持object literals了,所以定义数组可以直接用@[],方便了许多。而且object literals方式可以添加不在xib中的用代码定义的视图,所以显得更加灵活。当然,两种方式选择哪一种,就看我们自己的实际需要和喜好了。 ### 参考 1. [IBAction / IBOutlet / IBOutlet​Collection](http://nshipster.com/ibaction-iboutlet-iboutletcollection/) 2. [IBOutletCollection.m](http://www.opensource.apple.com/source/clang/clang-318.0.45/src/tools/clang/test/Index/IBOutletCollection.m) ## NSRecursiveLock递归锁的使用 NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。我们先来看一个示例: ~~~ NSLock *lock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveMethod)(int); RecursiveMethod = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value = %d", value); sleep(2); RecursiveMethod(value - 1); } [lock unlock]; }; RecursiveMethod(5); }); ~~~ 这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息: ~~~ value = 5 *** -[NSLock lock]: deadlock (<NSLock: 0x1700ceee0> '(null)') *** Break on _NSLockError() to debug. ~~~ 在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。 所以,对上面的代码进行一下改造, ~~~ NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; ~~~ 这样,程序就能正常运行了,其输出如下所示: ~~~ value = 5 value = 4 value = 3 value = 2 value = 1 ~~~ NSRecursiveLock除了实现NSLocking协议的方法外,还提供了两个方法,分别如下: ~~~ // 在给定的时间之前去尝试请求一个锁 - (BOOL)lockBeforeDate:(NSDate *)limit // 尝试去请求一个锁,并会立即返回一个布尔值,表示尝试是否成功 - (BOOL)tryLock ~~~ 这两个方法都可以用于在多线程的情况下,去尝试请求一个递归锁,然后根据返回的布尔值,来做相应的处理。如下代码所示: ~~~ NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveMethod)(int); RecursiveMethod = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value = %d", value); sleep(2); RecursiveMethod(value - 1); } [lock unlock]; }; RecursiveMethod(5); }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(2); BOOL flag = [lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1]]; if (flag) { NSLog(@"lock before date"); [lock unlock]; } else { NSLog(@"fail to lock before date"); } }); ~~~ 在前面的代码中,我们又添加了一段代码,增加一个线程来获取递归锁。我们在第二个线程中尝试去获取递归锁,当然这种情况下是会失败的,输出结果如下: ~~~ value = 5 value = 4 fail to lock before date value = 3 value = 2 value = 1 ~~~ 另外,NSRecursiveLock还声明了一个name属性,如下: ~~~ @property(copy) NSString *name ~~~ 我们可以使用这个字符串来标识一个锁。Cocoa也会使用这个name作为错误描述信息的一部分。 ### 参考 1. [NSRecursiveLock Class Reference](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSRecursiveLock_Class/) 2. [Objective-C中不同方式实现锁(二)](http://www.tanhao.me/pieces/643.html/) ## NSHashTable 在看KVOController的代码时,又看到了NSHashTable这个类,所以就此整理一下。 NSHashTable效仿了NSSet(NSMutableSet),但提供了比NSSet更多的操作选项,尤其是在对弱引用关系的支持上,NSHashTable在对象/内存处理时更加的灵活。相较于NSSet,NSHashTable具有以下特性: 1. NSSet(NSMutableSet)持有其元素的强引用,同时这些元素是使用hash值及isEqual:方法来做hash检测及判断是否相等的。 2. NSHashTable是可变的,它没有不可变版本。 3. 它可以持有元素的弱引用,而且在对象被销毁后能正确地将其移除。而这一点在NSSet是做不到的。 4. 它的成员可以在添加时被拷贝。 5. 它的成员可以使用指针来标识是否相等及做hash检测。 6. 它可以包含任意指针,其成员没有限制为对象。我们可以配置一个NSHashTable实例来操作任意的指针,而不仅仅是对象。 初始化NSHashTable时,我们可以设置一个初始选项,这个选项确定了这个NSHashTable对象后面所有的行为。这个选项是由NSHashTableOptions枚举来定义的,如下所示: ~~~ enum { // 默认行为,强引用集合中的对象,等同于NSSet NSHashTableStrongMemory = 0, // 在将对象添加到集合之前,会拷贝对象 NSHashTableCopyIn = NSPointerFunctionsCopyIn, // 使用移位指针(shifted pointer)来做hash检测及确定两个对象是否相等; // 同时使用description方法来做描述字符串 NSHashTableObjectPointerPersonality = NSPointerFunctionsObjectPointerPersonality, // 弱引用集合中的对象,且在对象被释放后,会被正确的移除。 NSHashTableWeakMemory = NSPointerFunctionsWeakMemory }; typedef NSUInteger NSHashTableOptions; ~~~ 当然,我们还可以使用NSPointerFunctions来初始化,但只有使用NSHashTableOptions定义的这些值,才能确保NSHashTable的各个API可以正确的工作—包括拷贝、归档及快速枚举。 个人认为NSHashTable吸引人的地方在于可以持有元素的弱引用,而且在对象被销毁后能正确地将其移除。我们来写个示例: ~~~ // 具体调用如下 @implementation TestHashAndMapTableClass { NSMutableDictionary *_dic; NSSet *_set; NSHashTable *_hashTable; } - (instancetype)init { self = [super init]; if (self) { [self testWeakMemory]; NSLog(@"hash table [init]: %@", _hashTable); } return self; } - (void)testWeakMemory { if (!_hashTable) { _hashTable = [NSHashTable weakObjectsHashTable]; } NSObject *obj = [[NSObject alloc] init]; [_hashTable addObject:obj]; NSLog(@"hash table [testWeakMemory] : %@", _hashTable); } ~~~ 这段代码的输出结果如下: ~~~ hash table [testWeakMemory] : NSHashTable { [6] <NSObject: 0x7fa2b1562670> } hash table [init]: NSHashTable { } ~~~ 可以看到,在离开testWeakMemory方法,obj对象被释放,同时对象在集合中的引用也被安全的删除。 这样看来,NSHashTable似乎比NSSet(NSMutableSet)要好啊。那是不是我们就应用都使用NSHashTable呢?Peter Steinberger在[The Foundation Collection Classes](http://www.objc.io/issue-7/collections.html)给了我们一组数据,显示在添加对象的操作中,NSHashTable所有的时间差不多是NSMutableSet的2倍,而在其它操作中,性能大体相近。所以,如果我们只需要NSSet的特性,就尽量用NSSet。 另外,Mattt Thompson在[NSHash​Table & NSMap​Table](http://nshipster.com/nshashtable-and-nsmaptable/)的结尾也写了段挺有意思的话,在此直接摘抄过来: ~~~ As always, it's important to remember that programming is not about being clever: always approach a problem from the highest viable level of abstraction. NSSet and NSDictionary are great classes. For 99% of problems, they are undoubtedly the correct tool for the job. If, however, your problem has any of the particular memory management constraints described above, then NSHashTable & NSMapTable may be worth a look. ~~~ ### 参考 1. [NSHashTable Class Reference](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSHashTable_class/) 2. [NSHash​Table & NSMap​Table](http://nshipster.com/nshashtable-and-nsmaptable/) 3. [NSHashTable & NSMapTable](http://billwang1990.github.io/blog/2014/03/31/nshashtable-and-nsmaptable/) 4. [The Foundation Collection Classes](http://www.objc.io/issue-7/collections.html) ## 零碎 ### (一) “Unknown class XXViewController in Interface Builder file.”“ 问题处理 最近在静态库中写了一个XXViewController类,然后在主工程的xib中,将xib的类指定为XXViewController,程序运行时,报了如下错误: ~~~ Unknown class XXViewController in Interface Builder file. ~~~ 之前也遇到这个问题,但已记得不太清楚,所以又开始在stackoverflow上找答案。 其实这个问题与Interface Builder无关,最直接的原因还是相关的symbol没有从静态库中加载进来。这种问题的处理就是在Target的”Build Setting”–>“Other Link Flags”中加上”-all_load -ObjC”这两个标识位,这样就OK了。 ### (二)关于Unbalanced calls to begin/end appearance transitions for …问题的处理 我们的某个业务有这么一个需求,进入一个列表后需要立马又push一个web页面,做一些活动的推广。在iOS 8上,我们的实现是一切OK的;但到了iOS 7上,就发现这个web页面push不出来了,同时控制台给了一条警告消息,即如下: ~~~ Unbalanced calls to begin/end appearance transitions for ... ~~~ 在这种情况下,点击导航栏中的返回按钮时,直接显示一个黑屏。 我们到stackoverflow上查了一下,有这么一段提示: ~~~ occurs when you try and display a new viewcontroller before the current view controller is finished displaying. ~~~ 意思是说在当前视图控制器完成显示之前,又试图去显示一个新的视图控制器。 于是我们去排查代码,果然发现,在viewDidLoad里面去做了次网络请求操作,且请求返回后就去push这个web活动推广页。此时,当前的视图控制器可能并未显示完成(即未完成push操作)。 ~~~ Basically you are trying to push two view controllers onto the stack at almost the same time. ~~~ 当几乎同时将两个视图控制器push到当前的导航控制器栈中时,或者同时pop两个不同的视图控制器,就会出现不确定的结果。所以我们应该确保同一时间,对同一个导航控制器栈只有一个操作,即便当前的视图控制器正在动画过程中,也不应该再去push或pop一个新的视图控制器。 所以最后我们把web活动的数据请求放到了viewDidAppear里面,并做了些处理,这样问题就解决了。 #### 参考 1. [“Unbalanced calls to begin/end appearance transitions for DetailViewController” when pushing more than one detail view controller](http://stackoverflow.com/questions/9088465/unbalanced-calls-to-begin-end-appearance-transitions-for-detailviewcontroller) 2. [Unbalanced calls to begin/end appearance transitions for UITabBarController](http://stackoverflow.com/questions/8563473/unbalanced-calls-to-begin-end-appearance-transitions-for-uitabbarcontroller)
';