[转]10个迷惑新手的Cocoa,Objective-c开发难点和问题
这篇博客将描述一些我见到的众多Cocoa开发新手遇到的问题和障碍。在你继续深入学习MacOS编程之前,请停下脚步弄清这些问题。如果你是新手,这个教程不要希望一次能看的非常透彻,学一定阶段反回来再看看又会有新的体会的。
1. language background
首先c, c++语言背景,必须。 很多人问:”没有任何语言基础,我不想学c直接学objective-c。“ 这里我简单说几句,objc是c的超集,也就是说大部分objc代码其实是c、而且众多开源代码是c,c++写成的。你不学好c在unix世界里只能是个二流开发者!也许说得过于严厉,不过自己斟酌把。
接着English,必须。 苹果不会把它们文档都写成中文的。“什么,有人翻译?” 等有人闲着翻译出来了的时候,大家都已经卖了很多软件了。你也是跟着人家屁股后面做开发。
2. runtime(运行时)
Objective-c是动态语言, 很多新手或者开发人员常常被runtime这个东西所迷惑。而恰恰这是一个非常重要的概念。我可以这么问:“如果让你(设计、)实现一个计算机语言,你要如何下手?” 很少程序员这么思考过。但是这么一问,就会强迫你从更高层次思考1以前的问题了。 注意我这句话‘设计’括起来了,稍微次要点,关键是实现。
我把实现分成3种不同的层次:
第一种是传统的面向过程的语言开发,例如c语言。实现c语言编译器很简单,只要按照语法规则实现一个LALR语法分析器就可以了,编译器优化是非常难的topic,不在这里讨论范围内,忽略。 这里我们实现了编译器其中最最基础和原始的目标之一就是把一份代码里的函数名称,转化成一个相对内存地址,把调用这个函数的语句转换成一个jmp跳转指令。在程序开始运行时候,调用语句可以正确跳转到对应的函数地址。 这样很好,也很直白,但是太死板了。Everything is predetermined.
我们希望语言更加灵活,于是有了第二种改进,开发面向对象的语言,例如c++。 c++在c的基础上增加了类的部分。但这到底意味着什么呢?我们再写它的编译器要如何考虑呢?其实,就是让编译器多绕个弯,在严格的c编译器上增加一层类处理的机制,把一个函数限制在它处在的class环境里,每次请求一个函数调用,先找到它的对象, 其类型,返回值,参数等等,确定了这些后再jmp跳转到需要的函数。这样很多程序增加了灵活性同样一个函数调用会根据请求参数和类的环境返回完全不同的结果。增加类机制后,就模拟了现实世界的抽象模式,不同的对象有不同的属性和方法。同样的方法,不同的类有不同的行为! 这里大家就可以看到作为一个编译器开发者都做了哪些进一步的思考。虽然面相对象语言有所改进,但还是死板, 我们仍然叫c++是static language.
希望更加灵活!于是我们完全把上面哪个类的实现部分抽象出来,做成一套完整运行阶段的检测环境,形成第三种,动态语言。这次再写编译器甚至保留部分代码里的sytax名称,名称错误检测,runtime环境注册所以全局的类,函数,变量等等信息等等,我们可以无限的为这个层增加必要的功能。调用函数时候,会先从这个运行时环境里检测所以可能的参数再做jmp跳转。这,就是runtime。编译器开发起来比上面更加弯弯绕。但是这个层极大增加了程序的灵活性。 例如当调用一个函数时候,前2种语言,很有可能一个jmp到了一个非法地址导致程序crash, 但是在这个层次里面,runtime就过滤掉了这些可能性。 这就是为什么dynamic langauge更加强壮。 因为编译器和runtime环境开发人员已经帮你处理了这些问题。
好了上面说着这么多,我们再返回来看objective-c的这些语句:
1
2
3
4
5
6
7
8
9
10
|
|
看似很简单的语句,但是为了让语言实现这个能力,语言开发者要付出很多努力实现runtime环境。这里运行时环境处理了弱类型、函数存在检查工作。runtime会检测注册列表里是否存在对应的函数,类型是否正确,最后确定下来正确的函数地址,再进行保存寄存器状态,压栈,函数调用等等实际的操作。
1
2
3
|
|
用c,c++完成这个功能还是比较非常麻烦的,但是动态语言处理却非常简单并且这些语句让objc语言更加intuitive。
在Objc中针对对象的函数调用将不再是普通的函数调用,[obj function1With:var1];
这样的函数调用将被运行时环境转换成objc_msgSend(target,@selector(function1With:),var1);
。Objc的runtime环境是开源的,所以我们可以拿出一下实现做简单介绍,可以看到objc_msgSend由汇编语言实现,我们甚至不必阅读代码,只需查看注释就可以了解,运行时环境在函数调用前做了比较全面的安全检查,已确保动态语言函数调用不会导致程序crash。对于希望深入学习的朋友可以自行下载Objc-runtime源代码来阅读,这里就不再深入讲解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
|
现在说一下runtime的负面影响: 1. 关于执行效率问题。 “静态语言执行效率要比动态语言高”,这句没错。因为一部分cpu计算损耗在了runtime过程中,而从上面的汇编代码也可以看出,大概损耗在哪些地方。而静态语言生成的机器指令更简洁。正因为知道这个原因,所以开发语言的人付出很大一部分努力为了保持runtime小巧上。所以objecitve-c是c的超集+一个小巧的runtime环境。 但是,换句话说,从算法角度考虑,这点复杂度不算差别的,Big O notation结果不会有差别。( It’s not log(n) vs n2 ) 2. 另外一个就是安全性。动态语言由于运行时环境的需求,会保留一些源码级别的程序结构。这样就给破解带来的方便之门。一个现成的说明就是,java,大家都知道java运行在jre上面。这就是典型的runtime例子。它的执行文件.class全部可以反编译回近似源代码。所以这里的额外提示就是如果你需要写和安全有关的代码,离objc远点,直接用c去。
简单理解:“Runtime is everything between your each function call.”
但是大家要明白,第二点我提到runtime并不只是因为它带来了这些简便的语言特性。而是这些简单的语言特性,在实际运用中需要你从完全不同的角度考虑和解决问题。只是计算1+1,很多语言都是一样的,但是随着问题的复杂,项目的增长,静态语言和动态语言就会演化出完全不同的风景。
3. thread
“thread synchronization another notorious trouble!”
记得上学时候学操作系统这门课,里面都会有专门一章介绍任务调度和生产者消费者的问题。 这就是为今后使用进程、线程开发打基础。概念很简单,但难点在synchronization(同步),因为死锁检测算法不是100%有效,否则就根本没有死锁这个说法了。另一个原因是往往这类错误很隐晦,静态分析很难找到。同时多线程开发抽象度较高需要经验去把握。
总体来说,我见到的在这方面的问题可以分为一下几点:
1. 对系统整体结构认识模糊
不知道多线程开发的几个基点,看别人代码越看越糊涂的。一会NSThread,一会Grand Central Dispatch、block,一会又看到了pthread等等。Apple封装了很多线程的API, 多线程开发的基本结构入下图:
可以看到在多线程开发中你可以选择这上面这4种不同的方式。
Mach是核心的操作系统部分。其实这个我也不是非常熟悉,至少我还没有读到过直接使用mach做多线程的代码。
pthread(POSIX Threads)是传统的多线程标准,灵活、轻巧,但是需要理论基础,开发复杂。需要注意一点,根据apple文档提示,在Cocoa下使用pthread需要先启动至少一个NSThread,确定进入多线程环境后才可以。
NSThread是Mac OS 10.0后发布的多线程API较为高层,但是缺乏灵活性,而且和pthread相比效率低下。
Grand Central Dispatch 是10.6后引入的开源多线程库,它介于pthread和NSThread之间。比NSThread更灵活、小巧,并且不需要像pthread一样考虑很多lock的问题。同时objective-c 2.0发布的新语法特性之一blocks,也正是根据Grand Central Dispatch需求推出的。
所以在你写多线程代码或者阅读多线程代码时候,心理要先明确使用哪种技术。
2. thread和Reference Counting内存管理造成的问题。
线程里面的方法都要放到NSAutoreleasePool里面吗?
这类问题很常见,迷惑的原因是不明白 NSAutoreleasePool 到底是干什么用的。NSAutoreleasePool跟thread其实关系并不显著,它提供一个临时内存管理空间,好比一个沙箱,确保不会有不当的内存分配泄露出来,在这个空间内新分配的对象要向这个pool做一下注册告诉:“pool,我新分配一块空间了”。当pool drain掉或者release,它里面分配过的内存同样释放掉。可见和thread没有很大关系。但是,我们阅读代码的时候经常会看到,新开线程的函数内总是以NSAutoreleasePool开始结束。这又是为什么呢!? 因为thread内恰好是最适合需要它的地方! 线程函数应该计算量大,时间长(supposed to be heavy)。在线程里面可能会有大量对象生成,这时使用autoreleasepool管理更简洁。所以这里的答案是,不一定非要在线程里放NSAutoreleasePool,相对的在cocoa环境下任意地方都可以使用NSAutoreleasePool。如果你在线程内不使用NSAutoreleasePool,要记得在内部alloc和relase配对出现保证没有内存泄露。
3. 线程安全
每个程序都有一个主线程(main thread),它负责处理事件响应,和UI更新。
更新UI问题。很多新手会因为这个问题,导致程序崩溃或出现各种问题。而且逛论坛会看到所以人都会这么告诉你:“不要在后台线程更新你的UI”。其实这个说法不严密,在多线程环境里处理这个问题需要谨慎,而且要了解线程安全特性。
首先我们需要把“UI更新”这个词做一个说明,它可以有2个层次理解,首先是绘制,其次是显示。这里绘制是可以在任何线程里进行,但是要向屏幕显示出来就需要在主线程操作了。我举个例子说明一下,例如现在我们有一个NSImageView,里面设置了一个NSImage, 这时我想给NSImage加个变色滤镜,这个过程就可以理解为绘制。那么我完全可以再另外一个线程做这个比较费时的操作,滤镜增加完毕再通知NSImageView显示一下。另一个例子就是,Twitter客户端会把每一条微博显示成一个cell,但是速度很快,这就是因为它先对cell做了offscreen的渲染,然后再拿出来显示。
所以通过这点我们也可以得到进一步的认识,合理设计view的更新是非常重要的部分。很多新手写得代码片段没错,只是因为放错了地方就导致整个程序出现各种问题。
根据苹果线程安全摘要说明,再其它线程更新view需要使用lockFocusIfCanDraw和unlockFocus锁定,确保不会出现安全问题。
另外还要知道常用容器的线程安全情况。immutable的容器是线程安全的,而mutable容器则不是。例如NSArray和NSMutableArray。
4. Asynchronous(异步) vs. Synchronous(同步)
我在一个view要显示多张web图片,我想问一下,我是应该采用异步一个一个下载的方式,还是应该采用多线程同时下载的方式,还是2个都用,那种方式好呢?
实际上单独用这2个方法都不好。并不是简单的用了更多线程就提高速度。这个问题同时涉及客户端和服务器的情况。
处理这种类型的程序,比较好的结构应该是:非主线程建立一个队列(相当于Asynchronous任务),队列里同时启动n个下载任务(相当于Synchronous任务)。这里的n在2~8左右就够了。这个结构下可以认为队列里面每n个任务之间是异步关系,但是这n个任务之间又是同步关系,每次同时下载2~8张图片。这样处理基本可以满足速度要求和各类服务器限制。
5. thread和runloop
runloop是线程里的一部分,但我觉得有必要单独拿出来写,是因为它涉及的东西比较容易误解,而说明它的文章又不多。
4. runloop
thread和runloop在以前,开发者根本不太当成一个问题。因为没有静态语言里runloop就是固定的线程执行loop。而现在Cocoa新手搞不明白的太多了,因为没有从动态角度看它,首先回想一下第2点介绍的runtime概念,接着出一个思考题。
现在有一个程序片段如下:
1
2
3
4
5
6
7
8
9
10
11
|
|
现在要求,做某些设计,使得当这个线程运行的同时,还可以从其它线程里往它里面随意增加或去掉不同的计算任务。 这,就是NSRunloop的最原始的开发初衷。让一个线程的计算任务更加灵活。 这个功能在c, c++里也许可以做到但是非常难,最主要的是因为语言能力的限制,以前的程序员很少这么去思考。
好,现在我们对上面代码做一个非常简单的进化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
|
注意,这里没有做线程安全处理,记住Mutable container is not thread safe. 这个简单的扩展,让我们看到了如何利用runtime能力让线程灵活起来。当我们从另外线程向targetQueue和actionQueue同时加入对象和方法时候,这个线程函数就有了执行一个额外代码的能力。
有人会问,哪里有runloop? 那个是nsrunloop? 看不出来啊。
1
2
3
|
|
这个结构就叫线程的runloop, 它和NSRunloop这个类虽然名字很像,但完全不是一个东西。以前在使用静态语言开始时候,程序员没有什么迷惑,因为没有NSRunloop这个东西。 我接着来说,这个NSRunloop是如何来得。
第二段扩展代码里面确实没有NSRunloop这个玩意儿,我们接着做第3次改进。 这次我们的目的是把其中动态部分抽象出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
|
走到这里,我们就算是基本把Runloop结构抽象出来了。例如我有一个MyNSThread实例,myThread1。我可以给这个实例的线程添加需要的任务,而myThread1内部的MyNSRunloop对象会管理好这些任务。
1
2
3
4
5
6
7
8
9
|
|
当你看懂了上面的代码也许会感叹,‘原来是这么回事啊!为什么把这么简单的功能搞这么复杂呢?’ 其实就是这么回事,把Runloop抽象出来可以使得线程任务管理更加loose coupling,给设计模式提供更大的空间。这样第三方开发者不需要过深入的涉及线程内部代码而轻松管理线程任务。另外请注意,这里MyNSRunloop, MyNSTimer等类是我写得一个模拟情况,真实的NSRunloop实现肯定不是这么简单。这里为了说明一个思想。这种思想贯穿整个cocoa framework,从界面更新到event管理。
5. delegate, protocol
这个会列出来因为,我感觉问它的数量仅此于内存管理部分,它们用得很频繁,并且这些是设计模式的重要组成部分。
待写…
6. event responder
使用过Xcode的开发者都知道Interface Builder这个开发组件,在Xcode4版本以后该组件已经和xcode整合到一起。它是苹果软件开发中非常重要的部分。ib为开发者减轻了很大一部分界面设计工作。但是其中有一个东西让新接触ib的开发者一头雾水,那就是First Responder, 它是什么东西,为何它会有那么多Actions。这节我会详细介绍如何理解Responder和Cocoa下的事件响应链。
First Responder在IB属性为Placeholders,这意味着它属于一个虚拟实例。就好比TextField里面的string placeholder一样,只是临时显示一下。真正的first responder会被其它对象代替。实际上,任何派生自NSResponder类的对象都可以替代First Responder。而First Responder里面的所有Actions就是NSResponder提供的接口函数,当然你也可以定义自己的响应函数。
MacOS在系统内部会维护一个称为“The Responder Chain”的链表。该列表内容为responder对象实例,它们会对各种系统事件做出响应。最上面的哪个对象就叫做first responder,它是最先接收到系统事件的对象。如果该对象不处理该事件,系统会将这个事件向下传递,直到找到响应事件的对象,我们可以理解为该事件被该这个对象截取了。
The Responder Chain基本结构如下图所示:
在理解了上面的概念之后,我希望使用一个例子让大家对responder有更加具体的认识。大家都知道NSTextField这个控件,它是最常见的控件之一。它最基本功能是显示一个字符串,如果启用可选,那么用户可以选中文本,拷贝文本,如果开启编辑选项,还可以运行用户编辑文本,等等基本操作。
下面展示给大家的例子是创建一个我们自己创建的简单textfield叫LXTextField。它不属于NSTextField而是派生自NSView,具有功能显示字符串,全选字符串,响应用户cmd+c的拷贝操作,三个基本功能。注意NSView派生自NSResponder。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
|