17 6月

Swift语言那些鲜为人知的特性

文 / Russ Bishop:全能型程序员,使用C#,Objective-C和Swift语言编程,开发了奇特的应用Storm Sim Free。

原文链接:http://www.russbishop.net/more-swift-attributes

Swift语言有各种各样缺乏(或没有)文档记录的特性(attribute)放在那里等着被使用。让我们一起看看其中的一些特性:

@inline

这个特性为编译器提供了内联提示。有效的取值是__always和never。除非我认为必须要用这两个值,否则就不会使用它(特别是__always)。到目前为止与其相关的规则还不是很明确,在有限的测试下,它可以正常地工作,但还要视具体情况而定。

进一步的解释:尽管底层虚拟机(Low Level Virtual Machine, LLVM)有强制内联的概念,但我们目前还不知道这个@inline特性是否与其直接映射,也不知道是否存在大小方面的限制,但这将会导致编译器忽略这一点而跳过内联。理论上说应该是这样的,但我不保证一定是。

注意(当优化设置关闭时)在调试模式下的构建将忽略@inline。

@transparent

我最初并未将这个特性列出来。该特性会导致编译器在管道(pipeline)中更早地将函数内联。它用于“像+(Int, Int)这样非常原始的函数”,而“不应该用于独立函数”

甚至在没有优化设置的调试模式下@transparent特性函数就会被内联,所以在调用“1+1”这样的函数的时候并不会特别慢。另外这个特性与@inline(__always)非常类似。

@availability

这个特性可以用来标识某些函数只在某些平台或版本上可用。第一个参数是平台,可以用星号(*)代表一切可用,还可以是iOS或OSX。因为如果需要针对不同的平台,就要指定多个@availability属性。

如果需要表示该函数在某个给定的平台完全不可用时,可以将第二个参数置为unavailable。此外,还可以用introduced,deprecated和obsoleted来指定一个或是多个版本的组合:obsoleted意味着该项已经删除,deprecated仅仅表示如果使用就会给予警告。最后你可以设置message的值,如果该项被使用了就由编译器输出。下面是一些例子:

  1. @availability(*, unavailable)
  2. func foo() {}
  3. @availability(iOS, unavailable, message=“you can’t call this”)
  4. func foo2() {}
  5. @availability(OSX, introduced=10.4, deprecated=10.6, obsoleted=10.10)
  6. @availability(iOS, introduced=5.0, deprecated=7.0)
  7. func foo3() {}

@noreturn

正如该特性所描述的那样:编译器可以假定这个函数是一个永远循环运行的起点,例如while true { },或者假定是函数abort或者exit进程的情况。

评论者Marco Masser指出,如果调用另一个被标志为@noreturn的函数,那么编译器会忽略掉当前函数中缺失的返回值(missing return values),因为编译器理解程序的控制流。

@asmname

该属性给出了函数、方法或属性实现的符号名称。如果你已经知道对应的函数参数及其类型,那么就可以直接调用Swift的内部标准库函数,甚至不用头文件,也可以方便地调用C语言编写的函数:

  1. @asmname(“function”) func f()

@unsafe_no_objc_tagged_pointer

上面这个仍然是个谜,但我猜测它是在告诉Swift与Objective-C联系的时候不要使用tagged pointer

@semantics

这又是另一个谜。参数看起来像是array.mutate_unknown或array.init这样的字符串数组。想必这是要告诉编译器(或静态分析器)函数是如何工作的。

结论

谁还需要乏味老套的 @objc和@autoclosure呢?还是算了吧!

我今年会去参加苹果全球开发者大会(WWDC),你也一定要去呀!

http://www.csdn.net/article/2015-06-08/2824887-more-swift-attributes

09 6月

ARC中retain cycle揭秘

文 / Ignacio Nieto Carvajal:自由开发者,精通iOS和Mac OSX应用程序开发,还包括Web服务和UX/UI设计。

原文链接:http://digitalleaves.com/blog/2015/05/demystifying-retain-cycles-in-arc/

ARC中的retain cycle就像日本B级恐怖电影一样。开始使用Cocoa或Cocoa Touch做开发时,你甚至不会在意它的存在。直到有一天应用程序由于内存泄漏而出现了崩溃现象,你才意识到它们的存在,看到像幽灵一样的retain cycle无处不在。随着岁月流逝,你学会适应它们,发现它们,避免它们……但最终恐慌还在,无孔不入。

包括我在内,对于许多开发人员来说,ARC的最令人失望之处莫过于苹果公司让ARC来管理内存。不幸的是ARC没有循环引用检测器,因此很容易出现retain cycle现象,从而迫使开发人员在编码时要采取特殊的预防措施。

对于iOS开发人员来说,retain cycle是个难点。在网上有很多误导信息[1][2],人们所给出的这些错误信息和修复方法甚至会导致应用出现新的问题,甚至崩溃掉,基于这样的情况,本文中我会阐明主题,给读者一些启发。

相关理论一瞥

Cocoa框架内存管理可以追溯到MRR(Manual Retain Release),在MRR中,开发人员在创建对象的时候,要为每个内存中的对象声明所有权。并且,当不再需要该对象时,要放弃所有权。MRR通过引用计数系统来实现这种所有权机制。每个对象都被分配一个计数器指示被“拥有”了多少次,每次加一,释放对象的时候每次减一。当引用计数变成零的时候,该对象将不复存在。对于开发人员来说,不得不手动维护引用计数真的是很烦人的事情,于是苹果公司引入了自动引用计数(Automated Reference Counting, ARC)机制,免得开发人员手动添加保留(retain)和释放(release)指令,让他们专注于解决应用程序的问题。在ARC环境下,开发人员要将一个变量定义为“strong”或“weak”。使用weak的话应用程序中被声明的对象不会被retain,而使用strong声明的对象将会被retain,并且其引用计数加一。

为什么要在乎?

ARC的问题在于容易导致retain cycle,它发生在两个不同的对象间彼此包含强引用的时候。试想一个Book对象包含一系列的Page对象,每个Page对象有个属性指向该页所在的这本书。当你释放掉指向Book和Page的变量时,Book和Page之间还存在着强引用。因此,即使没有变量指向Book和Page了,Book和Page及其所占用的内存也不会被释放掉。

不幸之处在于并非所有retain cycle都很容易被发现。对象之间的传递关系(A引用B,B转而引用C,C引用A)会导致retain cycle。更糟糕的是Objective-C里的块(block)和Swift里的闭包(closure)都被认为是独立的内存对象。因此,任何在块或闭包内对像的引用都将会对其变量做retain操作。因此,如果对象仍然retain这个块的话,就会导致潜在的retain cycle发生。

retain cycle可以成为应用程序潜在的危害,导致内存消耗过高,性能低下和崩溃。但还没有来自于苹果公司的文档,针对retain cycle可能发生的不同场景,以及如何避免进行描述。这导致了一些误解并形成了不良的编程习惯。

用例场景

那么闲话少说,我们一起来分析一些场景,确定它们是否会导致retain cycle以及如何避免:

父子对象关系

这是retain cycle的典型例子。不幸的是这也是苹果公司唯一给出相关解决方案文档的例子。就是我上面描述的Book和Page对象的例子。这种情况的典型解决方案是把Child类里面的代表父类的变量定义成weak,这样就可以避免retain cycle。

  1. class Parent {
  2.    var name: String
  3.    var child: Child?
  4.    init(name: String) {
  5.       self.name = name
  6.    }
  7. }
  8. class Child {
  9.    var name: String
  10.    weak var parent: Parent!
  11.    init(name: String, parent: Parent) {
  12.       self.name = name
  13.       self.parent = parent
  14.    }
  15. }

在Swift语言中,代表父类的变量是个弱变量的事实迫使我们将其定义为可选类型。不使用可选类型的另一种做法是将父类型对象声明为“unowned”(意味着我们不会对变量声明进行内存管理或声明所有权)。然而在这种情况下,我们必须非常仔细地确保只有一个Child实例指向Parent,Parent就不能是nil,否则程序就会崩溃:

  1. class Parent {
  2.    var name: String
  3.    var child: Child?
  4.    init(name: String) {
  5.       self.name = name
  6.    }
  7. }
  8. class Child {
  9.    var name: String
  10.    unowned var parent: Parent
  11.    init(name: String, parent: Parent) {
  12.       self.name = name
  13.       self.parent = parent
  14.    }
  15. }
  16. var parent: Parent! = Parent(name: “John”)
  17. var child: Child! = Child(name: “Alan”, parent: parent)
  18. parent = nil
  19. child.parent <== possible crash here!

一般来说公认的做法是父对象必须拥有(强引用)其子对象,这些子对象对其父对象应该只保持一个弱引用。这同样适用于集合,集合必须拥有其所包含的对象。

包含在实例变量中的块和闭包

另一个经典的例子虽然不是那么直观,但正如我们之前所说的那样,闭包和块是独立的内存对象,并retain了它们所引用的对象。因此如果我们有一个包含闭包变量的类,这个变量又恰好引用了其所拥有对象的属性或方法,由于闭包通过创建一个强引用而“捕获”了自己,就会有retain cycle发生。

  1. class MyClass {
  2.    lazy var myClosureVar = {
  3.       self.doSomething()
  4.    }
  5. }

这种情况下的解决方法是将自身定义成“weak”版本,并且将此弱引用赋给闭包或块。在Objective-C语言中,要定义个新的变量:

 

  1. – (id) init() {
  2.    __weak MyClass * weakSelf = self;
  3.    self.myClosureVar = ^{
  4.       [weakSelf doSomething];
  5.    }
  6. }

而在Swift语言中,我们只需要指定“[weak self] in”作为闭包的启动参数:

  1. var myClosureVar = {
  2.    [weak self] in
  3.    self?.doSomething()
  4. }

这样一来,当闭包快执行完毕时,self变量不会被强制retain,因此会得到释放,打破循环。注意,当声明为weak时,self在闭包内是如何变成可选类型的。

GCD中的dispatch_async

与一贯的认识相反,dispatch_async本身并不会导致retain cycle。

  1. dispatch_async(queue, { () -> Void in
  2.    self.doSomething();
  3. });

在这里,闭包对self强引用,但是类(self)的实例对闭包没有任何强引用,因此一旦闭包结束,它将被释放,也不会有环出现,然而,有的时候会被(错误地)认为这种情况会导致retain cycle。一些开发人员甚至一针见血地指出将块或闭包内所有对“self”的引用都声明为weak:

  1. dispatch_async(queue, {
  2.    [weak self] in
  3.    self?.doSomething()
  4. })

在我看来,对每种情况都这样做并不是好的做法。我们假设某个对象启动了一个长时间的后台任务(比如从网络上下载一些东西),然后调用了一个“self”方法。如果对self传递一个弱引用的话,那么类会在闭包结束之前完成它的生命周期。因此,当调用 doSomething()的时候,类的实例已经不存在了,所以这个方法永远不会被执行。这种情况下,(苹果公司)建议的解决方案是对闭包内的弱引用(???)声明一个强引用:

  1. dispatch_async(queue, {
  2.    [weak self] in
  3.    if let strongSelf = self {
  4.       strongSelf.doSomething()
  5.    }
  6. })

我不仅发现语法冗长单调,缺乏直观性,甚至令人感到厌恶,而且使闭包作为独立的处理实体的打算也落空了。我认为要理解对象的生命周期,确切地明白什么时候应该为实例声明一个内部的weak版,还要知道对象存续期间会有哪些影响。但话又说回来,这正是我解决应用程序的问题时分散我注意力的地方,如果Cocoa框架中没有使用ARC的话,这些都是没有必要写的代码。

局部的闭包和块

没有引用或包含任何实例或类变量的函数局部闭包和块本身不会导致retain cycle。常见的例子就是UIView的animateWithDuration方法:

  1. func myMethod() {
  2.    …
  3.    UIView.animateWithDuration(0.5, animations: { () -> Void in
  4.       self.someOutlet.alpha = 1.0
  5.       self.someMethod()
  6.    })
  7. }

对于dispatch_async和其他GCD相关的方法,我们不用担心没有被类实例强引用的局部闭包和块,它们不会发生retain cycle。

代理方案

代理(delegation)是使用弱引用避免retain cycle的一个典型场景。将委托声明为weak一直是一种不错的做法(并且还算安全)。在Objective-C中:

  1. @property (nonatomic, weak) id <MyCustomDelegate> delegate;

Swift中:

  1. weak var delegate: MyCustomDelegate?

在大多数情况下,对象的代理实例化了该对象,或者被认为比对象存在的时间更长久(并且对代理方法做出反应)。因此,在一个设计得很好的类中,我们不会找到与对象声明周期相关的任何问题。

使用Instruments来调试retain cycle

不管我如何努力避免retain cycle,忘记去引入一个弱引用并意外地创建一个(多谢ARC!)这样的事情迟早还有发生。幸运的是XCode套件中的Instruments应用程序是很不错的工具,用于检测和定位retain cycle。一旦开发阶段结束,在提交苹果商店之前就分析(profile)你的应用是个不错的习惯。Instruments有很多模版用来分析应用的不同方面,但我们感兴趣的是“Leaks”选项。

一旦打开Instruments,你就应该启动应用程序并作一些交互操作,特别是在要测试的区域或试图控制器上。任何检测到的泄漏都将会在“Leaks”部分出现一条红线。辅助视图包含一个区域,Instruments用来显示发生泄漏处的堆栈追踪,用来找到问题所在,甚至使你可以直接定位到造成问题的代码。

http://www.csdn.net/article/2015-05-27/2824782-demystifying-retain-cycles-in-arc