iOS并发开发简要整理(上)

在多核处理器没有大范围使用开来的时候,就有了多线程的概念,iOS的并发开发也不是新东西了。本篇文章主要是对iOS开发当中经常涉及到的并发内容所做的简要整理,把学过的用过的东西重新整理到笔头上。

0. 并发的初衷

记得大学上专业课的时候,老师曾经经常提问一个问题——“并发”vs.“并行”。简单对比下,并行基本上就是同时进行,而并发不一定保证两个线程同一时刻在同时作业,而可以体现为同一个时间段内各自都完成了一部分任务。

在多核上,做的好的并发可以真正的并行,而在单核上,不过是多个线程对处理器计算执行权的交换更替。为什么要交替进行?这好比一群人排队等着填表做体检,轮到一个人A的时候,进行了一半发现需要等另一人B到达带来一些材料,B迟迟不来,那么在他身后排队的人C(以及C后面的人)肯定不会就这样停下,而是更希望A到一边等B,C和后面的人依次完成任务,知道B来了,A继续。这样的好处就是,体检可以更有效率的进行,总体上缩短这群人完成体检的时间,C和C后面的人不用陪着A一起死等B的到来。计算机也是一样,处理器资源宝贵,如果有现成等待IO完成,其它准备好了的线程可以先来执行。并发既能提高吞吐量,也能让准备好的线程有机会得到比较好的响应。

在之前我整理Java并发文章的时候也提到过这些。

1. iOS平台的并发支持

我这里强调“iOS并发”,是因为我主要是在iOS开发工作中了解到的这些技术,而这些技术并不一定仅局限于iOS系统。从一定意义上讲,iOS是源于苹果的桌面系统OS X的,所以这里提到的很多东西也是在整个苹果开发体系内可用的。

经过了苹果技术体系的不断发展,iOS上对并发的支持从不同层面来看可谓是多种多样。目前苹果官方文档主推的技术是Grand Central Dispatch(GCD)和NSOperation /Operation Queue,这两个都是使用任务队列的方式进行并发任务的执行实现。除了这些,Cocoa(Touch)的Foundation里还有一个NSThread类,这个类相信很多人就比较容易理解的,也很类似java.lang.Thread了,NSObject本身也可以分出NSThread来。在iOS开发当中,C语言的东东也都是可用的,所以像pthread也可以发挥它的作用。

iOS中除了上面提到的这些并发的技术支持外,还有一个概念经常和并发一起出现,Run Loop。后面的文章中,我会整理一下,把我的理解阐述一下。

2. GCD

GCD是苹果提供的一个简单、直接的能够实现并发的方案,需要和block配合使用。在iOS4之后,block的广泛使用,也让GCD成为了一个很好用的工具。之前只用“线程”概念做并发开发的同学,可能不是很容易理解GCD(至少我当初有点迷惑),但如果你做过Java,想想ThreadPoolExecutor就应该明白个大概了。

比较原始的,我们可以创建一个线程,指定它的任务,令其执行。多个任务就开多个线程。但线程不是越多越好,线程会占用资源,而且线程间的切换也会影响执行效率。所以,我们就要考虑如何维护线程。再结合线程安全,线程同步等问题考虑,线程管理的成本还是很高的。而如果线程的维护管理是系统已经为我们实现好的,按照一定的策略进行管理调度,那可以大大减轻我们的维护工作,降低成本。所以。无论是iOS上的GCD,还是Java的ThreadPoolExecutor都是这样做的,我们最后只管往一个队列里放入我们想要执行的任务即可。

  • GCD中,一个队列就是一个 dispatch_queue_t类型的东东,它在GCD中也算是一个Dispatch Object了。队列从类型上来看分串行的(Serial)和并发的(Concurrent)的,区别在于前者必须等一个任务完成后,下一个任务(block)才开始进行,后者则不保证。不过两者都是按顺序向队列里放任务进去的。此外,苹果文档中还单独给出了一个主队列(main queue),主队列其实也是一个串行队列,与主线程关联,由main runloop控制,用来处理UI相关的任务。
  • GCD队列具体怎么创建和获取,可参考苹果文档,我这里不会细说。但是需要说明一点的是,主队列不用自己创建,另外还有全局的(Global)几个不同优先级的队列也可以直接获取到,除非自己需要特殊的串行队列,这些大多数情况下够用了。现在串行的队列和并发的队列都可以通过 dispatch_queue_create来创建,根据参数区分。但自己创建的要自己维护,负责清理释放。
  • 向队列中分派任务,可以以同步的方式进行( dispatch_sync),也可以是异步的( dispatch_async)。同步的方式会在分派任务时阻塞住,直到结束。但考虑可能发生的死锁情况,同步还是尽量不用的好。除非在能保证安全的情况下,又需要直接拿到同步执行的返回值的。
  • dispatch_once是个很不错的东西,可以用来实现单例,也主要用来实现单例。Xcode5.1.1中貌似就可以自动完成用dispatch_once实现的代码块,很赞很方便。
  • 至于dispatch_apply、dispatch_after这种我就不多说了,很容易懂,也没有其它几个常用。dispatch_group则实现了Thread概念下类似Join的功能,等待一个或一组任务完成。
  • GCD任务分派后没法取消,但可以通过GCD队列进行suspend和resume控制。而针对并发队列(Concurrent Queue),可以通过barrier对特定任务进行前后隔离,读写锁实现可考虑。需要注意的是,本段中这几点,对于Global Queue是无效的!
  • GCD队列可组合使用。在GCD队列中的任务出队列时不一定就执行了,也可能进到另一个队列。而Global队列有优先级之分,因此可以通过队列组合指定优先级,方法就是dispatch_set_target_queue()。
GCD队列图

GCD队列图(来源于Objc中国)

  • 对于阻塞的调用,尽管脱离开主线程,使得App的UI不受影响,但特定线程的资源还是会得不到充分利用,可考虑使用dispatch source作异步解决方案。

GCD简单易用,是并发开发很好上手的工具,这里就把主要的内容整理至此。

除了上面提到的这些,GCD还有很多其它内容,还有信号量(semaphore)、dispatch data、dispatch io等。除了参考苹果的《Concurrency Programming Guide》所述,《GCD Reference》东西更全。

3. Operation Queue

说完了GCD,Operation Queue理解起来就简单多了。可以说,Operation Queue是在语言上Objective-C的一个包装,实际上也是这样,最终的实现可以认为还是利用了GCD。

既然是包装了GCD,那一定多做了一些事情,最基本的就是面向对象,是Objective-C中NSObject的子类对象,可继承扩展,可封装复用。除此之外,Operation支持KVO、支持优先级、支持completion block,但我最关注的还是另外两点:

  • 支持cancel
  • 支持任务间依赖(注意,依赖高于优先级priority)

有了这两点支持,在某些复杂的业务场景下,用Operation比用GCD省事很多。

按照苹果官方文档所述,NSOperation是“抽象”的(虽然Objective-C中没有真正意义上的抽象概念),需要继承、扩展实现,方可使用。当然,苹果还是给了两个可用的子类,NSBlockOperation和NSInvocationOperation。这些怎么用苹果文档里写得都很清楚,注意NSBlockOperatioan里的block是并发的。

对于自己扩展实现的Operation,最主要的最基本的,把main方法实现了。做好异常处理是好习惯。

关于Operation的执行,既可以放到Operation Queue里(通常是这么用),也可以单独start。放到NSOperationQueue对象里会增加operation的引用计数,同时也保证了异步执行。而如果是调用start进行执行就不一定了,能否异步全看这个Operation是如何实现的。Operation的实现类别分为同步和并发的,可通过isConcurrent区别。默认情况下,这个方法返回NO,意味着调用start方法单独执行时都是在当前线程执行的。如果需要并发,就要负责自己对Operation的并发特性进行实现:

  • 首先,不能再用父类的start方法实现,需要自己负责开启新线程执行自定义的任务。
  • isConcurrent方法自然要修改为返回YES。
  • 同时,对isFinished和isExecuting等方法需要按新的自定义逻辑进行实现,同时保持对KVO的兼容。

关于Operation的取消执行支持,这个是之前提到的Operation的一个特点。而这也是需要开发者参与定义的,在执行构成中的特定点判断是否需要终止执行。需要注意的一点是,被cancel掉的Operation同样也被认为是finished状态的,这意味着一个依赖于其它Operation的Operation,只要它的依赖全部都cancel了,也可以开始start执行了。此外,也可以对一个Operation Queue对象直接做取消操作,取消队列中所有任务。

4. 其它

不管是GCD还是Operation Queue,都没有对传统线程(Thread)的直接使用,对线程模型进行了包装,避免了一些比较繁琐的问题,降低了对线程对象的维护成本。那么想用好这两种工具,我们需要做的就是对要处理的任务进行合理抽象拆分,交给合适的队列处理。和线程要占用系统资源一样,NSOperation对象也要恰当使用,不是越多越好,要控制在合适的范围。

就像前文提并发的初衷所说,并发是为了更合理更有效率的利用计算资源,而较慢的IO处理是一个阻力。并发和IO通常是紧密关联的两个话题。在实际开发中,即使使用GCD和NSOperation对任务做了并发异步处理,也尽量不要让任务出现阻塞,因为GCD和NSOperation底层也是依赖线程实现的,一个阻塞的动作也会使一个线程对应的资源变得浪费。对于这种情况,我们尽量不要占用线程,尽量利用系统所提供的各项异步IO方案。

5. 参考

学习iOS并发和整理本文时,也都学习了很多,下面是一些参考链接:

https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html

http://my.oschina.net/u/936286/blog/159245

http://mobile.51cto.com/iphone-403490.htm

http://daition340.github.io/blog/2014/06/28/multi-threading-programming-of-ios-part-3/

发表在 iOS, iOS开发基础, 开发, 计算机技术 | 标签为 , , , , , , | 5 条评论

CocoaPods使用整理

按本人当前的使用和理解,CocoaPods是苹果(包括iOS和Mac)开发过程中代码库依赖管理的一个很好的工具。本人是从Java开发转过来的,一直很好奇iOS开发中有没有类似maven的工具,和maven对应的东西是什么,直到有一天了解并开始接触CocoaPods。不像maven那样复杂,CocoaPods最主要的用处还是对第三方库各个版本依赖的一个管理,方便开发者站在巨人(或众人)的肩膀上。本人使用CocoaPods不算早,接触CocoaPods也有一段时间了,简要整理一下使用过程中的问题和理解。像怎么安装,怎么配置这些其它blog已经写得很好的,官方文档已经说得很清楚的就不在这赘述了。

0. 为啥用CocoaPods

我能想到的有这样几个要用CocoaPods的理由:

  • 我们在开发自己的App时,不太可能所有方面都从头做起,第三方的工具资源是很丰富的,即使有时我们有能力一行一行自己亲为也未必必要。因此如果把第三方开源资源引入进来是个需要考虑的问题。比较土的一种办法是把源码文件Down下来或者copy,这种办法可想是很不“优雅”的,更重要的是如果将来开源库有更新,我们挨个文件替换会很麻烦。CocoaPods可以通过Git的方式把开源库的使用和版本管理关联管理起来,比较好的解决这个问题。
  • 如果我们自己开发的App的各个部分需要拆分成独立模块,我们通常要自己手动搭建workspace,导入子工程,这虽不难,却也麻烦。CocoaPods则会根据配置自己创建好Pods子工程和完整的workspace,省去了很多事。
  • 从使用Git做代码版本维护上考量,子工程通常以submodule的形式引入,我们如果想指定某个submodule的版本,需要到Git下切换其commit或tag。这样submodule少则罢了,多了恐怕很难去挨个调整。而CocoaPods通过一个Podfile做好各方配置,一目了然。
  • 即使不考虑上面这3个点,工程庞大了,源码多了,像ARC/MRR兼容的问题配置起来也和麻烦。而CocoaPods可以最大程度上让每个Pod自己把本来需要使用者配置的东西配好了,写在podspec中,而对于使用者对这些通常可以不必关心。

当然,CocoaPods也不是万能的,和世间万物一样,并非完美无缺。如pod repo的建设和pod spec的版本管理,如果没有一套自动化的流程工具来做,还是有点小麻烦的。而且如上面提到的,CocoaPods应该主要是基于Git的,好在苹果开发者大多都已经用Git管理代码了。

1. 自己创建和使用Pod的一般过程

要想使用Pod,就得有Pod。Pod是一块完整的代码模块,我们要把他用Git管理起来,并按照格式配置好这个Pod的基本信息,也就是pod spec。最后,把pod spec放到pod repo中。因此,pod repo按我的理解也就是一个pod spec的库,最重要的作用可以认为是到源码的指向及说明。

pod spec的编写可参见:

Podspec Syntax Reference

而要想使用Pod,你至少要知道使用哪些Pod,版本是什么,将他们列在Podfile中。这有点类似于maven众pom.xml里的<dependencies>。

Podfile的编写可参见:

The Podfile

2. CocoaPods的大致原理

Cocoa是ruby写的工具,包含CocoaPod、Core、Xcodeproj等部分。

对于一次pod install,CocoaPods通常先读取Podfile配置文件,进行分析,并把pod repo中的spec拉齐,紧接着把pod spec指向的source拉齐到 ~/Library/Caches/CocoaPods,并clone到Pods中,创建Pods工程,为每个Pod生成4个配置文件,安装三方库,写入Podfile.lock和Manifest.lock,记录安装结果,对xcodeproj文件做处理。我们最后还能看到生成了xcworkspace,为我们配好了工作空间。

我们打开就可以看到,CocoaPods把我们需要的Pod都配好了,主工程也采用了其中的一些settings,并且Pods最终汇总生成一个统一的libPods-xxx.a供主工程使用。

3. 印象较深刻的几个问题

其实,使用CocoaPods的过程中,还是会遇到各种各样的问题的,下面简要整理和列了些说明,希望能有帮助。

  • pod不是新的。最新的修改没有集成进主工程,用pod update,这个会更新pod repo的。
  • lock问题(沙盒文件与 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),一般是Podfile.lock和Manifest.lock不一致造成。粗暴直接的办法是,删除Podfile.lock和Pods目录,重新安装。
  • pod顺序相关的问题,注意Podfile里的顺序不同,意义是不完全一样的,如其中有复杂的依赖关系,而且有版本号未限定死的情况,会有问题。如下图。所以要注意版本号和依赖顺序。64b44dd490e60c7ce05e527d2fb84787
  • Linker flags问题。在Pod里提供了vendored library或者framework,但主工程link时却说Undefined symbols,这个很有可能是Link flags里出了问题,可参见主工程的Other Linker Flags是否包含了Pods中的配置,如未包含,则添加下$(inherited)试试。
  • Architecture相关问题。在Build的时候,link阶段出如下问题,“ld: library not found for -lPods-xxx”,但各项配置看起来“没错”,实际上这很可能是Architecture相关settings出了问题,此类往往还一同伴有如“Pods-xxx was rejected as an implicit dependency for ‘libPods-xxx.a’ because its architectures ‘armv7s’ didn’t contain all required architectures ‘armv7 armv7s’”字样的warning。
  • 代码安装过程有问题。可清理 ~/Library/Caches/CocoaPods/Git后重试。libCache
  • Invalid byte sequence问题,这个我在之前的谋篇文章临时的解释了下,可参见:http://www.molotang.com/articles/1590.html

4. 其它

整理本文前,除了翻一翻自己的惨痛经历外,还参考了一些不错的材料:

http://objccn.io/issue-6-4/

http://www.atatech.org/article/detail/17927/0

http://geeklu.com/2013/06/cocoapods-101/

http://ryantang.me/blog/2014/01/05/cocoapods/

http://code4app.com/article/cocoapods-install-usage

欢迎补充,欢迎交流。

祝大家中秋节快乐!

发表在 iOS, 开发, 计算机技术 | 标签为 , , , , | 2 条评论

深入理解Objective-C的Block

最近时间少,也变得懒了,好久没在这里写文章了,眼看就到8月末了,还是整理一篇酝酿已久的吧。之前的文章中整理过用ObjectiveC开发中常用到的Block代码块,其中也提到了一个和block使用不当的crash例子。接着这个问题,本篇文章将更深一步,对Block的内存使用相关的内容简要整理一下,解释其中的道理和使用Block需要注意的问题

0. 问题所在

下面给出一段代码:

- (NSArray*) getBlockArray
{
    int num = 916;
    return [[NSArray alloc] initWithObjects:
            ^{ NSLog(@"this is block 0:%i", num); },
            ^{ NSLog(@"this is block 1:%i", num); },
            ^{ NSLog(@"this is block 2:%i", num); },
            nil];
}

- (void) forTest
{
    int a = 10;
    int b = 20;
}

- (void)test
{
    NSArray* obj = [self getBlockArray];
    [self forTest];
    void (^blockObject)(void);
    blockObject = [obj objectAtIndex:2];
    blockObject();
}

如上两个方法实现的代码并不难理解,其中第三个方法我们要去调用。它会调用第一个方法,并返回一个数组,数组中的元素是block代码块。那么在特定的场景下,调用test会发生crash(闪退)。说明这样的调用存在问题,恐怕能看到的应该就是EXC_BAD_ACCESS错误,通常这可以理解为一个“野指针”错误,访问了内存中不该访问的内容。

问题在哪?从“野指针”错误,我们很直接能想到的就是block对象引用到的地址内容已经不是我们想要的了,简单说就是block无效了。可block是对象类型的啊,为什么放在数组对象中回传失效了呢,加入NSArray的对象本身就应该retain过啊。

问题就在这里,下面我们先来看简单下Block与对象的关系。

1. Block与对象

首先我们先反思几个问题:

  • block到底是不是对象?
  • 如果是对象,和某个已定义的类的实例对象在使用上是不是一样的?
  • 如果不一样,主要的区别是什么?

对于第一个问题,苹果的Objective-C官方文档中在“Working with Blocks”明确说明:

Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary.  ”

可见,Block是Objective-C语言中的对象

苹果在block的文档中也提过这么一句:

As an optimization, block storage starts out on the stack—just like blocks themselves do.

Clang的文档中也有说明:

The initial allocation is done on the stack,but the runtime provides a Block_copy function” (Block_copy在下面我会说)

凭这一点,我们就可以回答剩下的两个问题。Block对象与一般的类实例对象有所不同,一个主要的区别就是分配的位置不同,block默认在栈上分配,一般类的实例对象在堆上分配。

而这正是导致本文最初提到的那个问题发生的根本原因。Block对象在栈上分配,block的引用指向栈帧内存,而当方法调用过后,指针指向的内存上写的是什么数据就不确定了。但是到此,retain的疑问还是没有解开。

我们想一想Objective-C引用计数的原理,retain是对一个在堆中分配内存的对象的引用计数做了增加,执行release操作的时候检查计数是否为1,如果是则释放堆中内存。而对于在栈上分配的block对象,这一点显然有所不同,如果方法调用返回,栈帧上的数据自然会作废处理,不像堆上内存,需要单独release,就算NSArray对block对象本身做了retain也无济于事。

Clang文档中提到:

Block pointers may be converted to type id; block objects are laid out in a way that makes them compatible with Objective-C objects. There is a builtin class that all block objects are considered to be objects of; this class implements retain by adjusting the reference count, not by calling Block_copy.

那么要是想如本文开头那样,用一个方法对block数组做初始化是否有可行方案呢。答案是肯定的,不过需要真正了解block的使用,至少要会用Block_copy()和Block_release()。

2. Block的类型和使用

我这里有对某个Block数组的一段Console Log显示,如下:

<__NSArrayI 0x937f240>(
<__NSGlobalBlock__: 0x126750>,
<__NSStackBlock__: 0xbfffc788>,
<__NSMallocBlock__: 0x937f1c0>,
<__NSMallocBlock__: 0x937f1e0>,
<__NSMallocBlock__: 0x937f200>,
<__NSMallocBlock__: 0x937f220>,
<__NSGlobalBlock__: 0x126818>
)

可以看得出,这些对象都是block,而且还分了3种不同的类型。

其实在Clang的文档中,只定义了两个Block类型: _NSConcreteGlobalBlock 和 _NSConcreteStackBlock 。而在Console中的Log我们看到的3个类型应该是处理过的显示,这些字样在苹果的文档和Clang/LLVM的文档中实难找到。通过字面上来看,可以认为 _NSConcreteGlobalBlock对应于 __NSGlobalBlock__ ,_NSConcreteStackBlock对应于 __NSStackBlock__ ,而__NSMallocBlock__则是另一种情况。(实际上也正是如此)

NSGlobalBlock,我们只要实现一个没有对周围变量没有引用的Block,就会显示为是它。而如果其中加入了对定义环境变量的引用,就是NSStackBlock。那么NSMallocBlock又是哪来的呢?malloc一词其实大家都熟悉,就是在堆上分配动态内存时。没错,如果你对一个NSStackBlock对象使用了Block_copy()或者发送了copy消息,就会得到NSMallocBlock。这一段中的几项结论可从代码实验得出。

因此,也就得到了下面对block的使用注意点。

对于Global的Block,我们无需多处理,不需retain和copy,因为即使你这样做了,似乎也不会有什么两样。对于Stack的Block,如果不做任何操作,就会向上面所说,随栈帧自生自灭。而如果想让它获得比stack frame更久,那就调用Block_copy(),让它搬家到堆内存上。而对于已经在堆上的block,也不要指望通过copy进行“真正的copy”,因为其引用到的变量仍然会是同一份,在这个意义上看,这里的copy和retain的作用已经非常类似。

“The runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1. The paired function is Block_release, which decreases the reference count by 1 and destroys the object if the count reaches zero and is on the heap.

在类中,如果有block对象作为property,可以声明为copy。

3. 其它

如果注释掉其中看似无关的[self forTest]调用,用当前的Xcode版本(我用的是5.1.1)build后,crash是不会发生的,这看起来很有意思。因为forTest方法本身并没有在逻辑上对数组的构建造成什么影响。

实际上这是因为上一个方法调用的栈帧没有被新的数据覆盖,仍然保留原来block数据的原因所致。这样显然是不安全的,是不能保证block数据可用的。

在ARC情况下,我们会发现一个有意思的情况,那就是返回的Block Array,只有元素0是执行过copy的。比如block数组中的第0个block是stack的,那么返回之后在数组index为0处取到的block变成了malloc的。与此同时,其它的block都如同没有执行过copy一样,如上述各段所述。这是一个现象,或者说是一个结论。至于为什么这样,众说纷纭,很多人认为这是编译器的一个bug,欢迎大家多多讨论,给出见解。

在苹果官方的《Transitioning to ARC Release Notes》文档中,写了这样一段话,大家理解一下,尤其是其中的“just work”。

“How do blocks work in ARC?

Blocks ‘just work’ when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more.”

4. 参考

以上整理了对Block的理解,在开发中注意到这些点足以解决block的特殊性带来的各类问题。要想继续深入,可参看LLVM文档中对block的介绍:

http://clang.llvm.org/docs/Block-ABI-Apple.html

http://clang.llvm.org/docs/AutomaticReferenceCounting.html?highlight=class

补充:如下几篇文章讲解得也很细致,可以参看。

《block没那么难(一)》

《block没那么难(二)》

《block没那么难(三)》

发表在 iOS, iOS开发基础, Objective-C, 开发, 计算机技术 | 标签为 , , , , , , | 34 条评论

说说Objective-C语言

由于最近一直在忙于各种事情,好久没有在小站整理文章了,WWDC2014都结束了。说到WWDC2014,虽然没提到新的硬件产品,在第一天一个新语言就炒热了开发者圈子,那就是Swift,很多人开始开玩笑说,“招Swift工程师,要求工作经验一天。。。”,一时间好像不知道Swift就不是技术圈的人一样。苹果同时提供了一本950页的电子书《The Swift Programming Language》,还没来得及仔细研习。有一种说法,“Swift is Objective-C without the C”,可见Objective C奠定了苹果软件技术的基础。本篇就回头对Objective-C语言本身、编译和运行时的理解小结整理一下。本文不会,本站可能也不会整理Objective-C的基本语法,有这方面需要的可以直接到苹果开发者站点上去参看。

0. Objective-C语言概述

Objective-C是个神奇的语言(注意Objective-C的标准写法,是带连字符的),1983年由Brad Cox和Tom Love设计诞生,后经历NeXT的发展变化,最后在乔布斯回归苹果时被整合进Mac OS X平台的开发工具当中。Objective-C和早期的Smalltalk有很多相似的地方,面向对象,单继承,动态绑定。

Objective-C走过了30多年的历程,但却一直没有太大的变化,延续到了今天,以至于很多人觉得其语法非常怪异。现在Objective-C基本上就是苹果一家在用,尤其在从iPhone和iPad等苹果新产品的成功之后,Objective-C变得非常受欢迎,下面一图是TIOBE 2014年六月份的一个图:

TIOBE Programming Language Index 2014.6
TIOBE Programming Language Index 2014.6

从上图可以看到,2009年之后,绿色的线快速飙升。可见iPhone等产品的成功,对Objective-C这一个语言的影响是非常大的。

用Objective-C做iOS开发也有很长一段时间了,对于封装、多态、集成等面向对象的基本特征以及动态绑定等内容,Objective-C和其它语言可以说是大同小异,没什么可说的。

个人印象比较深的两个特点是CategoryMessage机制。

Category翻译过来就是“分类”,它使得开发者能够直接对系统库中的某些类进行补充,在不继承不修改类源码的基础上增添很多新功能,极大地提供了已有类的可扩展性和可维护性,同时让同一个类的功能分组更加清晰,也是类封装隐藏的一种方式。

消息发送,实际上也就是方法调用。能够按照面向对象通常的方式使得父类引用找到子类实现的动态绑定没有什么可说的,但消息发送机制可以让我们对方法的绑定方式和路径查找更加灵活,使用起来更加方便。

而这些,都与Objective-C的编译、连接、运行时支持有着不可分割的关系。

1. Objective-C代码的编译

在我们日常的iOS开发工作当中,我们不需要关注编译和运行的细节,只需按照苹果提供的Guide去做就好。Apple提供了Xcode这个强大的IDE,帮我们处理了非常多的细节,Build Settings的默认配置也使得我们可以对其透明看待。

其实无论是从Xcode工程中的Build Phases中看,还是去翻Build Log,我们都可以看到总体的编译过程和其他语言——尤其是偏底层的语言的编译过程并没有太多差异。

介绍Objective C语言的一般在开头就会介绍到,Objective-C是C的超集,基于C。C和Java等语言相比就是直接编译成机器指令,从传统的角度来看特点就是“执行快”,Objective C也是如此。对于iOS开发的代码,最终会直接根据arm指令集build出目标代码。

刚刚提到了Xcode build过程中的Log,我们就不得不看看Objective-C是用什么编译的了。回想当年,刚学C/C++的时候用的是什么?嗯,是Windows下的Visual Studio,后来渐渐将开发环境转移到Linux系统中,用什么?嗯,GCC。(印象中,GCC就是狭义上的GNU C Compiler和广义上的GNU Compiler Collection的缩写了)

那Apple呢? Apple用的是LLVM,这一点我们在Build settings的各个选项中可以看得出来。那么苹果为什么没有用鼎鼎大名的GCC呢,这个具体细节就不清楚了,不过客观来看,主要有以下两点原因,GCC是GPL协议下的开源软件,而苹果作为一个商业公司,未必能够愿意在实际生产中遵照这个协议,另外从苹果用Clang代替GCC的编译器前端的情况来看,开源软件的发展方向无法满足苹果的商业需求。

说了这么多,下面进入最重要的部分,运行时runtime吧。

2. Objectvie-C的Runtime

我个人接触的第一门计算机语言是Logo,当时是有小海龟在画图,可以把画图和相关的计算逻辑整理成一个函数,必要的时候去调用,但这个需要Logo在DOS环境下启动起来才行。(此处省略无数字)到了大学,老师讲语言可以这样分类:编译执行的(C)、解释执行的(Javascript)。当遇到了Java,这个算啥嘞。。。(不编译,.java怎么变成.class的;不解释,那jvm是干嘛的)

在做iOS之前,做了一段时间Java开发,Java的虚拟机就是对class中的类信息、字节码进行解释,最终在目标处理器上执行,Java也正是靠这个做到了平台无关性。当然,JVM是非常了不起的,它会帮你把多次解释执行的内容变成编译执行。也就是说,Java是编译执行+解释执行,编译器将源代码转换成中间代码,保留了运行时所需的信息,最后交给Java的运行时(JVM)来处理执行。

我们尝试把这个思路用在Objective-C上,那我们用Xcode build出来的到底还是不是机器码?既然Objective-C有运行时系统,是不是也有类似Java虚拟机的东东在解释中间码?这个问题在最初确实困扰了我。其实把源代码编译和在运行时解析这两个部分不一定非要有类似.class的“中间码”,可以是直接的机器码,只要运行时信息可以在编译阶段以特定的方式保留下来,有特定库中的特定函数能够解析出来并按照这个信息所表达的内容去处理执行即可。所以,编译器和运行时处理器也不一定是分开的,不一定非要有JVM这样的东西。

Objective-C有一个运行时动态链接库,我们在编译的时候,也有中间码,但编译过程完成的时候,中间码也变成了机器码,但这机器码如果离开了objc runtime,也是玩不转的,因为里面包含了很多Objective-C的类描述信息和对objc_msgSend的调用(机器码里的体现应该就是地址跳转之类的了,地址指向某个库)。

既然Objective-C的runtime这么神奇,大家还是关注下怎么更灵活地使用它吧。按照苹果的文档,分为三个层次:

  • 第一层,也是最基本的,我们使用Objective-C的面向对象,定义父类和子类,动态绑定和执行子类的方法,这就是对objc runtime的利用了
  • 第二层,也比较简单,如果我们定义了protocol,而且还有@optional的方法,可以确定某个对象(的类)是否实现了它,或者这个对象属不属于某个类,使用methodForSelector获取实现等NSObject提供的方法
  • 而最高级的用法,也就是直接调用objc_msgSend、objc_getClass之类的了,这个很强大,用好了有很大价值,但也不要乱用

而大家可能想,那Java里的.class文件中除了byte code还有类型信息呢。没错,其实我们只要用otool对目标文件的objc segment查看就一目了然了。

说了这么多,很多是个人对这块的一个简单理解,希望能够帮助有同样疑问或者遇到同样问题的朋友。

本文相关的内容,有几篇质量比较高的参考,分享给大家,链接在这里:

苹果的runtime文档

唐巧Blog中的《Objective-C对象模型及应用》

Objc中国的文章

另外,关于Swift,预发依然有很怪异的地方,不过TIOBE预计下个版本的排名,它可以冲到前20,拭目以待。

发表在 iOS, Objective-C, 开发, 计算机技术 | 标签为 , , , | 20 条评论

iOS中block的使用

在面向对象语言中,类封装了数据和这些数据相关的行为。然而有些情况下,一个简单的任务和已通过一段代码块和少数几个变量来完成。在iOS4中有了代码块block的概念,这篇文章就对block的使用做一个简单的整理。

0. Block概述

Block是和iOS4一同的,它是C语言级别和运行时方面的一个特征。Block封装了一段代码逻辑,也用{}括起,和标准C语言中的函数/函数指针很相似,此外就是blokc能够对定义环境中的变量可以引用到。这一点和其它各种语言中所说的“闭包”是非常类似的概念。

在iOS4中,block有很多应用场景,比如对代码封装作为参数传递。这在使用dispatch并发(Operation中也有BlockOperation)和completion异步回调等处都广泛应用。

1. Block的基本使用

声明:(返回类型)(^声明的block名称)(参数列表);

实现:^(参数列表){代码块}

赋值的例子:

double (^multiplyTwoValues)(double, double) =
                              ^(double firstValue, double secondValue) {
                                  return firstValue * secondValue;
                              };

有时为了方便,用typedef给出定义,在苹果的官方文档里,建议出现多次的block使用typedef定义。

而block的调用,则十分简单:block名称(参数列表);

2. 使用中的注意点

有如下一些注意点:

  • 对定义环境的变量使用。默认是以const的方式使用,这有点像函数的const参数传递,如过需要block内修改可变,则使用__block,这样做存储就实现了共享,包括块中的递归应用和定义环境上下文中的多个block使用。block通常定义在栈帧当中,而当所处的栈帧被销毁的时候,block以及引用到的__block变量将会依然有效。
  • 引用类型问题。block中的引用默认都是强引用,必要的时候需要使用__weak,同delegate使用的注意一样,避免循环引用。此外,苹果文档中还给出了对instanceVariable和对localVariable引用不同的例子,注意体会下。
  • copy。在类属性中,要使用copy。此外,对block进行copy要使用Block_copy()/Block_release()。
  • 苹果文档中几种需要避免使用的方式。《Blocks Programming Topics》中Using Blocks中的例子,实际上就是要注意block定义的位置与其上下文的关系。
  • 有关Block的地址/引用。注意这篇文章中最后例子中的问题:http://www.cnblogs.com/kesalin/archive/2013/04/30/ios_block.html                        我的理解是Block的地址发生了变化,最终的问题是对Block引用地址释放时的野指针错误。

3. 联想Java中匿名类使用

Java7以及之前的各个版本中,没有“闭包”的概念(感兴趣的可以看今年3月Oracle发布的Java8),回调(callback)使用内部类实现。在方法定义中使用匿名内部类,需要注意的一点是匿名类中对外部方法参数的使用,要求参数只能是final的。

其实在iOS中,对于block使用外部方法的参数,也只能是const的,不能对参数进行__block要求。

更多内容,可以参考苹果官方文档:

Working with Blocks

Blocks Programming Topics

发表在 iOS, iOS开发基础, Java, Objective-C, 开发, 计算机技术 | 标签为 , , , , , | 3 条评论
第 5 页,共 23 页« 最新...34567...121518...最旧 »