注册 登录
主题 : 10个迷惑新手的Cocoa,Objective-c开发难点和问题  2014-11-06 更新protocol, delegate
级别: 圣骑士
UID: 1736
精华: 1
发帖: 98
可可豆: 5438 CB
威望: 5438 点
在线时间: 701(时)
注册时间: 2008-12-27
最后登录: 2017-09-17
0 楼:  发表于: 2011-09-14 23:29    发自: Web Page
来源于 教程源码 分类

10个迷惑新手的Cocoa,Objective-c开发难点和问题  2014-11-06 更新protocol, delegate   

管理提醒: 本帖被 neogui 设置为精华(2011-11-15)
10个迷惑新手的Cocoa,Objective-c开发难点和问题 U2bjFLd"  
Keefo  14 September, 2011 8:01:52 AM PDT :x tXQza"-  
Last update on Fri 23 Nov 2012 10:33:31 PST :yUEkm8  
Last update on Thu 06 Nov 2014 16:40:00 PST "jG}B.l=,  
/YZr~|65  
因为有自己的软件要开发,所以我只能忙里偷闲过来接着写这篇教程。大家莫怪。请按上面的最后更新时间算。 N[s}qmPha  
原文地址 0q&<bV:D  
http://lianxu.me/blog/2012/11/14/10-cocoa-objc-newbie-problems/ 9 FB19  
.zi_[  
1. language background {zMU#=EC  
2. run-time zT!drq:x  
3. thread R8ZK]5{o  
4. run-loop ]H`1F1=  
5. protocol, del-e-gate N<~t3/Nm  
6. event respon-der &*+'>UEe5  
7. mem-ory management e" St_z(  
8. class heritage and category bD/~eIcWL  
8. drawing issue SHe49!RA'{  
10. design pattern vQ;Ex  
_lamn }(x0  
Z'"tB/=W  
首先请谅解我可能使用很多英文,毕竟英文资料将来会是你的主要资料来源。 xai*CY@cQ  
0u;4%}pD  
在你继续深入学习之前,请停下脚步弄清这些问题。如果你是新手,这个教程不要希望一次能看的非常透彻,学一定阶段反回来再看看又会有新的体会的。对于完全看不懂的同学,也不要强求自己。找来《计算机组成原理》《数据结构》《操作系统》《编译原理》《计算机网络》之类的大学课本看看吧。这些基础不可能在这个论坛学到的,至少不可能系统的学到。 9I&xfvD,  
<StN%2WQ1  
1. language background " wNJ  
z6*X%6,8  
首先c, c++语言背景,必须。 7Zlw^'q$:L  
很多人问 “没有任何语言基础,我不想学c直接学objective-c” ,6-:VIHQ  
这里简单几句,objc 90%代码是c、众多开源代码是c,c++。你不学好c在unix世界里只能是个二流开发者!也许说得过于严厉,不过自己斟酌把。 gIjh:_ Pz  
7 S}_F^  
接着English必须。 |S_eDjF  
需要英语!需要英语!需要英语!苹果不会把它们文档都写成中文的。“什么,有人翻译?” 等有人闲着翻译出来了的时候,大家都已经卖了很多软件了。你也是跟着人家屁股后面做开发。 `$ 6rz  
*MKO I'  
2. Runtime(运行时) ibj87K  
0cj>mj1M  
Objective-c是动态语言,  很多新手或者开发人员常常被Runtime这个东西所迷惑。而恰恰这是一个非常重要的概念。 为什么重要呢!?我可以这么问:“如果让你(设计、)实现一个计算机语言,你要如何下手?” 很少程序员这么思考过。但是这么一问,就会强迫你从更高层次思考(1)以前的问题了。 注意我这句话‘设计’括起来了,稍微次要点,关键是实现。 < jJ  
LDPUD'  
我把实现分成3钟不同的层次: "N`[r iq{  
    1. 传统的面向过程的语言开发,例如c语言。实现c语言编译器很简单,只要按照语法规则实现一个LALR语法分析器就可以了,编译器优化是非常难的topic,不在这里讨论范围内,忽略。 这里我们实现了编译器其中最最基础和原始的目标之一就是把一份代码里的函数名称,转化成一个相对内存地址,把调用这个函数的语句转换成一个jmp跳转指令。在程序开始运行时候,调用语句可以正确跳转到对应的函数地址。 这样很好,也很直白,但是。。。太死板了。everything is pre-determined Yt;MV)  
wOU_*uY@6'  
    2. 我们希望灵活,于是需要开发面向对象的语言,例如c++。 c++在c的基础上增加了类的部分。但这到底意味着什么呢?我们再写它的编译器要如何考虑呢?其实,就是让编译器多绕个弯,在严格的c编译器上增加一层类处理的机制,把一个函数限制在它处在的class环境里,每次请求一个函数调用,先找到它的对象, 其类型,返回值,参数等等,确定了这些后再jmp跳转到需要的函数。这样很多程序增加了灵活性同样一个函数调用会根据请求参数和类的环境返回完全不同的结果。增加类机制后,就模拟了现实世界的抽象模式,不同的对象有不同的属性和方法。同样的方法,不同的类有不同的行为! 这里大家就可以看到作为一个编译器开发者都做了哪些进一步的思考。但是。。。还是死板, 我们仍然叫c++是static language。 wB.&}p9p  
G3Z)Z) N  
    3. 希望更加灵活! 于是我们完全把上面哪个类的实现部分抽象出来,做成一套完整运行阶段的检测环境。这次再写编译器甚至保留部分代码里的sytax名称,名称错误检测,runtime环境注册所以全局的类,函数,变量等等信息等等,我们可以无限的为这个层增加必要的功能。调用函数时候,会先从这个运行时环境里检测所以可能的参数再做jmp跳转。这,就是runtime。编译器开发起来比上面更加弯弯绕。但是这个层极大增加了程序的灵活性。  例如当调用一个函数时候,前2种语言,很有可能一个jmp到了一个非法地址导致程序crash, 但是在这个层次里面,runtime就过滤掉了这些可能性。 这就是为什么dynamic langauge更加强壮。 因为编译器和runtime环境开发人员已经帮你处理了这些问题。
` @`CG[-9  
RZXjgddL  
H{Wu]C<@p  
好了上面说着这么多,我们再返回来看objective-c.  现在你是不是能理解这样的语句了呢? E=nIRG|g  
e)d`pQ6  
复制代码
  1. id obj=self;
  2. if ([obj respondsToSelector:@selector(function1:)) {
  3. }
  4. if ([obj isKindOfClass:[NSArray class]] ) {
  5. }
  6.         
  7. if ([obj conformsToProtocol:@protocol(myProtocol)]) {
  8. }
  9.         
  10. if ([[obj class] isSubclassOfClass:[NSArray class]]) {
  11. }
  12. [obj someNonExistFunction];
<J) ]mh dm  
?l9XAW t\  
看似很简单的语句,但是为了让语言实现这个能力,语言开发者要付出很多努力实现runtime环境。这里运行时环境处理了弱类型、函数存在检查工作。runtime会检测注册列表里是否存在对应的函数,类型是否正确,最后确定下来正确的函数地址,再进行保存寄存器状态,压栈,函数调用等等实际的操作。 #d6)#:uss  
YNQY4\(  
复制代码
  1. id knife=[Knife grateKnife];
  2. NSArray *monsterList=[NSArray array];
  3. [monsterList makeObjectsPerformSelector:@selector(killMonster:) withObject:knife];
h&KO<>  
aDU<wxnSvO  
用c,c++完成这个功能还是比较非常麻烦的,但是动态语言处理却非常简单并且这些语句让objc语言更加intuitive。 5>[u `  
E|iQc8gr&  
现在说一下runtime的负面影响: sB7# ~p A  
1. 关于执行效率问题。 “静态语言执行效率要比动态语言高”,这句没错。因为一部分cpu计算损耗在了runtime过程中。而静态语言生成的机器指令更简洁。正因为知道这个原因,所以开发语言的人付出很大一部分努力为了保持runtime小巧上。所以objecitve-c是c的超集+一个小巧的runtime环境。 但是,换句话说,从算法角度考虑,这点复杂度不算差别的,Big O notation结果不会有差别。( It’s not log(n) vs n2 ) i<#QW'R(  
2. 另外一个就是安全性。动态语言由于运行时环境的需求,会保留一些源码级别的程序结构。这样就给破解带来的方便之门。一个现成的说明就是,java,大家都知道java运行在jre上面。这就是典型的runtime例子。它的执行文件.class全部可以反编译回近似源代码。所以这里的额外提示就是如果你需要写和安全有关的代码,离objc远点,直接用c去。 N sXHO  
'Gj3:-xqL  
简单理解:“Runtime is everything between your each function call.” Q+[n91ey**  
M/b Sud?@%  
但是大家要明白,第二点我提到runtime并不只是因为它带来了这些简便的语言特性。而是这些简单的语言特性,在实际运用中需要你从完全不同的角度考虑和解决问题。只是计算1+1,很多语言都是一样的,但是随着问题的复杂,项目的增长,静态语言和动态语言就会演化出完全不同的风景。 GPN]9  
8Vr%n2M  
3. thread t'n pG}`tE  
6 (]Dh;gC  
"thread synchronization another notorious trouble!" e"|efE  
l3)} qu  
记不记得上学时候学得操作系统这门课,里面都会有专门一章介绍任务调度和生产者消费者的问题。 这就是为了今后使用进程、线程开发打基础。概念很简单,但是心知肚明的人很少。难点在synchronization(同步),因为1. There is no 100% deadlock detection algorithm. If there is, no deadlock at all. 2. 往往这类错误很隐晦,静态分析很难找到。 3. 抽象度较高需要经验去把握。   `sn^ysp  
hgPa6Kd  
总体来说,我见到的在这方面的问题可以分为一下几点: '=b/6@&  
   0Tx6zO  
1. 不知道多线程开发的几个基点,看别人代码越看越糊涂的。一会NSThread、一会Grand Central Dispatch、block等等。。。Apple封装了很多线程的api, down to core多线程的结构基本是: Z?h~{Mg  
Ayxkv)%:@)  

IxY|>5z  
d YJ(!V&  
可以看到在多线程开发中你可以选择这上面这4种不同的方式。 r>>%2Z-P  
c2l@6<Ww  
Mach是核心的操作系统部分。其实这个我也不是非常熟悉,至少我还没有读到过直接使用mach做多线程的代码。 = ;Au<|  
H?yK~bGQ  
pthread(POSIX Threads)是传统的多线程标准,灵活、轻巧,但是需要理论基础,开发复杂。需要注意一点,根据apple文档提示,在Cocoa下使用pthread需要先启动至少一个NSThread,确定进入多线程环境后才可以。 Te"io U?.  
%)1y AdG 8  
NSThread是Mac OS 10.0后发布的多线程API较为高层,但是缺乏灵活性,而且和pthread相比效率低下。 NPy&OcRl  
z&zP)>Pv  
Grand Central Dispatch 是10.6后引入的开源多线程库,它介于pthread和NSThread之间。比NSThread更灵活、小巧,并且不需要像pthread一样考虑很多lock的问题。同时objective-c 2.0发布的新语法特性之一blocks,也正是根据Grand Central Dispatch需求推出的。 ,E S0NA  
H#,W5EJzM  
所以在你写多线程代码或者阅读多线程代码时候,心理要先明确使用哪种技术。 >qnko9V  
0X6YdW_2X  
    2. thread和Reference Counting内存管理造成的问题。 J')o|5S1N  
  线程里面的方法都要放到NSAutoreleasePool里面吗这类问题很常见,主要原因是 NSAutoreleasePool 到底是干什么用得不明白。 NSAutoreleasePool跟thread其实关系并不显著,它提供一个临时内存管理空间,好比一个沙箱,确保不会有不当的内存分配泄露出来,在这个空间内新分配的对象要向这个pool做一下注册告诉:“pool,我新分配一块空间了”。当pool drain掉或者release,它里面分配过的内存同样释放掉。可见和thread没有很大关系。但是,我们阅读代码的时候经常会看到,新开线程的函数内总是以NSAutoreleasePool开始结束。这又是为什么呢!? 因为thread内恰好是最适合需要它的地方! 线程函数应该计算量大,时间长(supposed to be heavy)。在线程里面可能会有大量对象生成,这时使用autoreleasepool管理更简洁。所以这里的答案是,不一定非要在线程里放NSAutoreleasePool,相对的在cocoa环境下任意地方都可以使用NSAutoreleasePool。如果你在线程内不使用NSAutoreleasePool,要记得在内部alloc和relase配对出现保证没有内存泄露。 ~vm%6CABM  
ag [ZW  
    3. 线程安全 ]cHgleHQ  
   每个程序都有一个主线程(main thread),它负责处理事件相应,和UI更新。 )_YX DU  
?9 <:QE;I>  
更新UI问题。很多新手会因为这个问题,导致程序崩溃或出现各种问题。而且逛论坛会看到所以人都会这么告诉你:“不要在后台线程更新你的UI”。其实这个说法不严密,在多线程环境里处理这个问题需要谨慎,而且要了解线程安全特性。 f6hnTbJ  
^aItoJq  
首先我们需要把“UI更新”这个词做一个说明,它可以有2个层次理解,首先是绘制,其次是显示。这里绘制是可以在任何线程里进行,但是要向屏幕显示出来就需要在主线程操作了。我举个例子说明一下,例如现在我们有一个NSImageView,里面设置了一个NSImage, 这时我想给NSImage加个变色滤镜,这个过程就可以理解为绘制。那么我完全可以再另外一个线程做这个比较费时的操作,滤镜增加完毕再通知NSImageView显示一下。另一个例子就是,Twitter客户端会把每一条微博显示成一个cell,但是速度很快,这就是因为它先对cell做了offscreen的渲染,然后再拿出来显示。 e"{{ TcNk  
T(id^ w  
所以通过这点我们也可以得到进一步的认识,合理设计view的更新是非常重要的部分。很多新手写得代码片段没错,只是因为放错了地方就导致整个程序出现各种问题。 'DP1,7  
y0L_"e/  
根据苹果线程安全摘要说明,再其它线程更新view需要使用lockFocusIfCanDraw和unlockFocus锁定,确保不会出现安全问题。 ,Vc6Gwm  
gb[5&> (#  
另外还要知道常用容器的线程安全情况。immutable的容器是线程安全的,而mutable容器则不是。例如NSArray和NSMutableArray。 5_GYrR2  
b RFLcM  
     ,wQ5.U,  
    5. Asynchronous(异步) vs. Synchronous(同步) L/$H"YOv  
    
引用
我在一个view要显示多张web图片,我想问一下,我是应该采用异步一个一个下载的方式,还是应该采用多线程同时下载的方式,还是2个都用,那种方式好呢?
11 Q1AN  
<cps2*'  
实际上单独用这2个方法都不好。并不是简单的用了更多线程就提高速度。这个问题同时涉及客户端和服务器的情况。 ~Y^+M*   
p|U?86 t  
处理这种类型的程序,比较好的结构应该是:非主线程建立一个队列(相当于Asynchronous任务),队列里同时启动n个下载任务(相当于Synchronous任务)。这里的n在2~8左右就够了。这个结构下可以认为队列里面每n个任务之间是异步关系,但是这n个任务之间又是同步关系,每次同时下载2~8张图片。这样处理基本可以满足速度要求和各类服务器限制。 fK>L!=Q  
   9+Np4i@  
(非常抱歉,之前质疑问问题的人理解错误。其实是我自己晕了,问得问题很好。) ~!B \(@GU  
"H'B*vc-  
4. runloop -_=nDH  
现在说说runloop为何会成为cocoa开发中迷惑的点。因为很多新手没有从动态角度看它。 首先回想一下第2点介绍的runtime的概念。 接着我出一个题思考一下。 3dg1DR;  
}T(D7|^R  
现在我有一个程序片段如下: ?gA 8x  
复制代码
  1. - (void)myThread:(id)sender
  2. {
  3.     NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
  4.     while (TRUE) {
  5.         
  6.         //do some jobs
  7.        //break in some condition
  8.         
  9.         usleep(10000);
  10.         
  11.         [pool drain];
  12.     }
  13.     
  14.     [pool release];
  15. }
PxvyN_B#>  
现在要求,做某些设计,使得当这个线程运行的同时,还可以从其它线程里往它里面随意增加或去掉不同的计算任务。 这,就是NSRunloop的最原始的开发初衷。让一个线程的计算任务更加灵活。 这个功能在c, c++里也许可以做到但是非常难,最主要的是因为语言能力的限制,以前的程序员很少这么去思考。 ZgTW.<.%2  
8{^kQ/]'|  
好,现在我们对上面代码做一个非常简单的进化: n\mO6aJ  
kMIcK4.MH  
复制代码
  1. NSMutableArray *targetQueue;
  2. NSMutableArray *actionQueue;
  3. - (void)myThread:(id)sender
  4. {
  5.     NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
  6.     while (TRUE) {
  7.         
  8.         //do some jobs
  9.         //break in some condition
  10.         int n=[targetQueue count];
  11.         assert(n==[actionQueue count]);
  12.         for(int i=0;i<n;i++){
  13.             id target=[targetQueue objectAtIndex:i];
  14.             SEL action=NSSelectorFromString([actionQueue objectAtIndex:i]);
  15.             if ([target respondsToSelector:action]) {
  16.                 [target performSelector:action withObject:nil];
  17.             }
  18.         }
  19.                 
  20.         usleep(10000);
  21.         
  22.         [pool drain];
  23.     }
  24.     
  25.     [pool release];
  26. }
,0 M_ Bk"  
注意,这里没有做线程安全处理,记住Mutable container is not thread safe. f\|w '  
这个简单的扩展,让我们看到了如何利用runtime能力让线程灵活起来。当我们从另外线程向targetQueue和actionQueue同时加入对象和方法时候,这个线程函数就有了执行一个额外代码的能力。 xQ f*  
?1~`*LE  
但,有人会问,哪里有runloop? 那个是 nsrunloop? 看不出来啊。 B-*+r`@Bd  
复制代码
  1. while (TRUE) {
  2. //break in some condition
  3. }
Ua:}Vn&!  
一个线程内这个结构就叫线程的runloop,   它和NSRunloop这个类虽然名字很像,但完全不是一个东西。以前在使用静态语言开始时候,程序员没有什么迷惑,因为没有NSRunloop这个东西。 我接着来说,这个NSRunloop是如何来得。 )1?y 8_B  
t% d Z-Ym  
第二段扩展代码里面确实没有NSRunloop这个玩意儿,我们接着做第3次改进。 这次我们的目的是把其中动态部分抽象出来。 ejSji-Qd  
YL!P0o13r  
复制代码
  1. @interface MyNSTimer : NSObject
  2. {
  3.   id target;
  4.   SEL action;
  5.   float interval;
  6.   CFAbsoluteTime lasttime;
  7. }
  8. - (void)invoke;
  9. @end
  10. @implementation MyNSTimer
  11. - (void)invoke;
  12. {
  13.   if ([target respondsToSelector:action]) {
  14.         [target performSelector:action withObject:nil];
  15.     }
  16. }
  17. @end
g];!&R-  
h0g8*HY+}  
W=~ ~5jFX  
复制代码
  1. @interface MyNSRunloop : NSObject
  2. {
  3.   NSMutableArray *timerQueue;
  4. }
  5. - (void)addTimer:(MyNSTimer*)t;
  6. - (void)executeOnce;
  7. @end
  8. @implementation MyNSRunloop
  9. - (void)addTimer:(MyNSTimer*)t;
  10. {
  11.   @synchronized(timerQueue){
  12.       [timerQueue addObject:t];
  13.     }
  14. }
  15. - (void)executeOnce;
  16. {
  17.   CFAbsoluteTime currentTime=CFAbsoluteTimeGetCurrent();
  18.   @synchronized(timerQueue){
  19.       for(MyNSTimer *t in timerQueue){
  20.           if(currentTime-t.lasttime>t.interval){
  21.               t.lasttime=currentTime;
  22.                   [t invoke];
  23.           }
  24.       }
  25.   }
  26. }
  27. @end
;AG8C #_  
`KZm0d{H  
复制代码
  1. @interface MyNSThread : NSObject
  2. {
  3.   MyNSRunloop *runloop;
  4. }
  5. - (void)main:(id)sender;
  6. @end
  7. @implementation MyNSThread
  8. - (void)main:(id)sender
  9. {
  10.     NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
  11.     while (TRUE) {
  12.         //do some jobs
  13.         //break in some condition
  14.         [runloop executeOnce];
  15.         usleep(10000);
  16.         [pool drain];
  17.     }
  18.     [pool release];
  19. }
  20. @end
5'OrHk;u  
Cjn#00  
走到这里,我们就算是基本把Runloop结构抽象出来了。例如我有一个MyNSThread实例,myThread1。我可以给这个实例的线程添加需要的任务,而myThread1内部的MyNSRunloop对象会管理好这些任务。 b6,iZ+]  
 x]}^v#  
复制代码
  1. MyNSTimer *timer1=[MyNSTimer scheduledTimerWithTimeInterval:1
  2.                                                    target:obj1
  3.                                                  selector:@selector(download1:)];
  4. [myThread1.runloop addTimer:timer1];
  5. MyNSTimer *timer2=[MyNSTimer scheduledTimerWithTimeInterval:2
  6.                                                    target:obj2
  7.                                                  selector:@selector(download2:)];
  8. [myThread1.runloop addTimer:timer2];
/CrSu  
Vr3Zu{&2  
当你看懂了上面的代码也许会感叹,‘原来是这么回事啊!为什么把这么简单的功能搞这么复杂呢?’ 其实就是这么回事,把Runloop抽象出来可以使得线程任务管理更加loose coupling,给设计模式提供更大的空间。这样第三方开发者不需要过深入的涉及线程内部代码而轻松管理线程任务。另外请注意,这里MyNSRunloop, MyNSTimer等类是我写得一个模拟情况,真实的NSRunloop实现肯定不是这么简单。这里只为了说明一个思想。这种思想贯穿整个cocoa framework从界面更新到event管理。 rDdoOb]B  
.Y tKS  
5. delegate, protocol 4>wP7`/+y  
这个会列出来因为,我感觉问它的数量仅此于内存管理部分,它们用得很频繁,并且这些是设计模式的重要组成部分。 ; 5*&xz  
=Qy<GeY  
先从字面解释一下。protocol 是名词协议。delegate 有2个词性,名词是代表,动词是委托。这2个东西是虚、实的对应体。打个现实的比方,protocol 就好比法律,delegate 就好比法院。法律是虚的,是概念,只能通过法院来执行和体现。法院是法律的具体体现,法院要按照法律规定的来操作,绝对不能自己随意发挥。 IPS4C[v  
j*|VctM  
有了上面的说明后,很多东西就顺理成章了。下面先写一个 protocol: 8SMxw~9$  
yuh *  
复制代码
  1. @protocol XYZPieChartViewDataSource
  2. - (NSUInteger)numberOfSegments;
  3. - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
  4. - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
  5. @end
s:n6rG  
S,88*F(<^q  
这段代码定义了一个叫 XYZPieChartViewDataSource 的协议。里面包含 3 个方法声明。 L^1NY3=$  
/:cd\ A}  
我见到过一些学习编程的人,他们总是潜意识的认为程序代码就是用来计算的,那么这个 protocol 怎么计算呢?其实,protocol 的实际计算作用为 0,主要是工程作用。也就是说,它不做任何计算,主要是给程序员阅读用的,就像编辑器左边显示的行数差不多。是一个标记。下面我们来看如何使用这个标记。 2=*H 8'k  
/2&c$9=1  
复制代码
  1. @interface XYZPieChartView : NSView
  2. @property (weak) id <XYZPieChartViewDataSource> dataSource;
  3. ...
  4. @end
Tf>bX_L?  
Cwv9 a^  
这里的类 XYZPieChartView 里包含一个属性 dataSource 它的类型定义是 id <XYZPieChartViewDataSource> 这个类型翻译成人类语言就是,dataSource 是一个遵循 XYZPieChartViewDataSource 协议的对象。至于“遵循 XYZPieChartViewDataSource 协议” 到底意味着什么,看看协议定义就可以了。dataSource 这个对象内部必定需要实现 #|uCgdi  
k R?qb6  
复制代码
  1. - (NSUInteger)numberOfSegments;
  2. - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
  3. - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
0CHH)Bku  
)*$lp'~7N  
这 3 个方法。 g_;\iqxL  
^ gdaa>L  
NDN7[7E  
一个遵循协议的对象,其规定的方法实现有 2 种可能性。第一种,是 Cocoa 框架内部帮你实现了,2 是 Apple 只定义了协议,你必须自己实现这些方法。下面列出 2 个实际开发种的例子给大家对比学习: 6_(&6]}66  
P0;n9>g  
NSTableView and NSTableViewDataSource Protocol  on4HKeO  
\;,_S+Fz8  
W_JlOc!y  
表格是最常见的控件。苹果的 NSTableView 就是最典型的例子。玩过 NSTableView 的朋友肯定见过 NSTableViewDataSource 协议。这就是苹果定义协议,你需要自己实现代码的例子。NSTableView 里有一个属性是  z<MsKD0Q  
IBOutlet id<NSTableViewDataSource> datasource 当你把一个对象赋值给 tableview.datasource 时,你必须给这个对象内部实现 NSTableViewDataSource 协议规定的方法。因为 tableview 在显示数据的时候,要通过你提高的对象获取数据,而获取数据的方法是由这个 NSTableViewDataSource 协议定义下来的。 * ` JYC  
xVw9v6@`h  
NSCoding Protocol l[dK[4  
&T#;-`'  
&)QX7*H  
待续… D(~U6SR  
&sl0W-;0  
j`EXlc~  
6. event responder >R'F,  
?>VLTp8]  
esJ~;~[@(r  

dn& s*  
p'Y^ X  
使用过Xcode的开发者都知道Interface Builder这个开发组件,在Xcode4版本以后该组件已经和xcode整合到一起。它是苹果软件开发中非常重要的部分。ib为开发者减轻了很大一部分界面设计工作。但是其中有一个东西让新接触ib的开发者一头雾水,那就是First Responder, 它是什么东西,为何它会有那么多Actions。这节我会详细介绍如何理解Responder和Cocoa下的事件响应链。 !Lu2  
Fn wJ+GTu  
First Responder在IB属性为Placeholders,这意味着它属于一个虚拟实例。就好比TextField里面的string placeholder一样,只是临时显示一下。真正的first responder会被其它对象代替。实际上,任何派生自NSResponder类的对象都可以替代First Responder。而First Responder里面的所有Actions就是NSResponder提供的接口函数,当然你也可以定义自己的响应函数。 5tl< 3g `  
0 j^Kgx  
MacOS在系统内部会维护一个称为“The Responder Chain”的链表。该列表内容为responder对象实例,它们会对各种系统事件做出响应。最上面的哪个对象就叫做first responder,它是最先接收到系统事件的对象。如果该对象不处理该事件,系统会将这个事件向下传递,直到找到响应事件的对象,我们可以理解为该事件被该这个对象截取了。 8=!D$t\3  
wi!?BCseq  
The Responder Chain基本结构如下图所示: x[cL Bc<  

d9k0F OR1  
zrvF]|1UP  
AzPu)  
在理解了上面的概念之后,我希望使用一个例子让大家对responder有更加具体的认识。大家都知道NSTextField这个控件,它是最常见的控件之一。它最基本功能是显示一个字符串,如果启用可选,那么用户可以选中文本,拷贝文本,如果开启编辑选项,还可以运行用户编辑文本,等等基本操作。 E fqX y>W  
21n? =[  
下面展示给大家的例子是创建一个我们自己创建的简单textfield叫LXTextField。它不属于NSTextField而是派生自NSView,具有功能显示字符串,全选字符串,响应用户cmd+c的拷贝操作,三个基本功能。注意NSView派生自NSResponder。 :a!^   
,<.V7(|t)  
irZ])a  
复制代码
  1. @interface LXTextField : NSView
  2. {
  3.     NSString *stringValue;
  4.     BOOL selectAll;
  5. }
  6. @property(retain,nonatomic) NSString *stringValue;
  7. @end
  8. @implementation LXTextField
  9. @synthesize stringValue;
  10. - (void)awakeFromNib
  11. {
  12.     selectAll = NO;
  13. }
  14. - (id)initWithFrame:(NSRect)frameRect
  15. {
  16.     if( self = [super initWithFrame:frameRect] ){
  17.         selectAll = NO;
  18.     }
  19.     return self;
  20. }
  21. - (BOOL)acceptsFirstResponder
  22. {
  23.     return YES;
  24. }
  25. - (BOOL)becomeFirstResponder
  26. {
  27.     return YES;
  28. }
  29. - (BOOL)resignFirstResponder
  30. {
  31.     selectAll=NO;
  32.     [self setNeedsDisplay:YES];
  33.     return YES;
  34. }
  35. - (void)setStringValue:(NSString *)string{
  36.     stringValue = string;
  37.     [self setNeedsDisplay:YES];
  38. }
  39. - (void)drawRect:(NSRect)dirtyRect
  40. {
  41.     if (selectAll) {
  42.         NSRect r = NSZeroRect;
  43.         r.size = [stringValue sizeWithAttributes:nil];
  44.         [[NSColor selectedControlColor] set];
  45.         NSRectFill(r);
  46.     }
  47.     [stringValue drawAtPoint:NSZeroPoint withAttributes:nil];
  48. }
  49. - (IBAction)selectAll:(id)sender;
  50. {
  51.     selectAll=YES;
  52.     [self setNeedsDisplay:YES];
  53. }
  54. - (IBAction)copy:(id)sender;
  55. {
  56.     NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
  57.     [pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil];
  58.     [pasteBoard setString:stringValue forType:NSStringPboardType];
  59. }
  60. - (void)mouseDown:(NSEvent *)theEvent
  61. {
  62.     if ([theEvent clickCount]>=2) {
  63.         selectAll=YES;
  64.     }
  65.     [self setNeedsDisplay:YES];
  66. }
  67. - (void)keyDown:(NSEvent *)theEvent
  68. {
  69. }
  70. @end
F|`Hm  
|44Ploz2b  
运行实例,可以看到随着LXTextField收到系统发送的becomeFirstResponder消息,LXTextField变成responder chain中的frist responder, 这时候可以理解为IB里的哪个First Responder虚拟实例被该LXTextField取代。这时候mainMenu上哪些菜单项,例如:全选(cmd+a), 拷贝(cmd+a)等事件都会最先发给当前这个LXTextField。一旦你的LXTextField实现了NSResponder的哪些默认函数,那么该对象就会截取系统事件。当然这些事件具体如何实现还是需要你自己写代码实现。例如这里的 - (IBAction)copy:(id)sender; 显然我手动实现了textfield的copy能力。 ^vZS UfS  
%:i7s-0w  
注意上述代码中我实现了一个空函数- (void)keyDown:(NSEvent *)theEvent 这意味着我们希望LXTextField截取键盘事件而不再传递给responder chain后续对象。当然,如果我们希望LXTextField响应特定键盘事件,而其他事件继续传给其他响应对象,我们可以编写如下代码。 :@yEQ#nFp  
zOJ%}  
复制代码
  1. - (void)keyDown:(NSEvent *)theEvent
  2. {
  3.     if(condition){
  4.         do something;
  5.     }else{
  6.         [super keyDown:theEvent];
  7.     }
  8. }
\UA[  
QRw"H 8nW  
7. mem-ory management kBS9tKBWg  
这个也许是问得最多的问题了吧。所有这些问题往往来源于3个地方,1、不了解底层机制;2、没有吃透规则; 3、不了解常用container的Reference Counting特性,或着说没有下功夫去看对应文档。 kj Jn2c:y  
B.=FSow  
1. 底层机制 aHD]k8 m z  
大家是否知道从旧时代的RC到ARC机制到底意味着什么呢? 为什么ARC从开发速度,到执行速度和稳定性都要优于rc? Lw1Yvtn  
9p]QM)M  
开发速度不言而喻,你少写很多release代码,甚至很少去操心这部分。 <3n Mx^  
ldf\;Qk  
执行速度呢?这个还要从runtime说起,还记得我在第2点说得一句话么:“Runtime is everything between your each function call.”   6W/`07 '  
d z|or9&  
RC是一个古老的内存管理哲学: 谁分配谁释放。 通过counting来确定一个资源有几个使用者。道理很简单,但是往往简单的东西人却会犯错。从来没有一个程序员可以充满信心的说,我写得代码从来没有过内存泄露。这样来看,我们就更需要让程序可以自己处理这个管理机制,这就需要把这个机制放到runtime里。 jV i) Efy  
)705V|v  
所以RC->ARC就是把内存管理部分从普通开发者的函数中移到了函数外的runtime中。因为runtime的开发原型简单,逻辑层次更高,所以做这个开发和管理出错的概率更小。实际上编译器开发人员对这部分经过无数次测试,所以可以说用arc几乎不会出错。另外由于编译的额外优化,使得这个部分比程序员自己写得代码要快速很多。而且对于一些通用的开发模式,例如autorelease对象,arc有更优秀的算法保证autoreleasepool里的对象更少。 TP* hd  
<|HV. O/!  
所以RC->ARC就是把内存释放语句从普通开发者的代码中去掉,然后交给编译器让它在编译阶段自动增加该有的释放语句。 因为是编译器分析,所以出错的概率更小。实际上编译器开发人员对这部分经过无数次测试,所以可以说用arc几乎不会出错。另外由于交给编译器处理,使得这个部分比程序员自己写得代码要快速很多。 h0EEpL|\  
#`^}PuQ  
ARC另一个特点就是,强制程序员遵守owner规则。例如, 旧时很多程序员开发,喜欢在需要用的时候临时建立一个windowController,窗口关闭后controller自己release掉。但是这个思路在arc下面是行不通的。因为这种模式打破了owner规则(一个实例需要至少有1个拥有者)一个临时建立的windowController如果没有owner,那么它会在当前函数退出前就被arc释放掉。有些程序员认为这是arc非常讨厌的地方,当然也有一些程序员认为这样更规范。 'B |JAi?  
;[ZEDF5H  
H8=N@l  
2. RC规则 p:&8sO!m  
首先说一下rc是什么,r-Reference参照,引用 c-counting计数, rc就是引用计数。俗话说就是记录使用者的数量。 例如现在我有一个房间空着,大家可以进去随意使用,但是你进门前,需要给门口的计数牌子+1, 出门时候-1。 这时候这个门口的牌子就是该房间里的人数。一但这个牌子变为0我就可以把房间关闭。 GC-5X`Sq  
7^avpf)>  
这个规则可以让NSObject决定是不是要释放内存。当一个对象alloc时候,系统分配其一块内存并且object自动计数retainCount=1 这时候每当[object retain]一次retainCount+1(这里虽然简写也是rc不过是巧合或者当时开发人员故意选的retain这个词吧)每次[object release]时候retainCount-1 当retainCount==0时候object就真正把这快内存还给系统。 `>o{P/HN  
"69s) ~  
3. 常用container的Reference Counting特性 KR} ?H#%  
这个规则很简单把。但是这块确实让新手最头疼的地方。 问题出在,新手总想去验证rc规则,又总是发现和自己的期望不符合。   [+Iz@0q  
无数次看到有人写下如下句子 fuW\bo3  
复制代码
  1. NSLog(@"%d",[object retainCount]);
U4'#T%*  
!t"4!3  
复制代码
  1. while([object retainCount]>0){
  2.       [object release];
  3. }
jRa43ck  
hW<%R]^|  
当然了,我也做过类似的动作,那种希望一切尽在掌握中的心态。但是你会看到其他人告诉这么做完全没有意义。rc does not work this way.   也许这样的暴力释放会起作用,但是retainCount并不是用来做这个的。每个数字意味着有其它对象引用该资源,这样的暴力释放很容易导致程序崩溃。这个数字也许并不是你心目中的哪个。因为你很难跟踪到底哪些对象引用的该资源。你用代码建立的资源不光只有你的代码才会用到,你调用的各种Framework,Framework调用的Framework,都有可能改变这个资源的retainCount.  所以去验证rc规则不是明智之举。 RbB.q p  
XPc^Tq  
你能做的就是理解规则,使用规则,读文档了解container的引用特性。或者干脆移到arc上面,让runtime环境处理这些问题。 p%ki>p )E|  
i$Ul(?  
引用
我有一个NSMutableArray里面保存了1000个NSString对象,我在release的时候需要循环释放1000个string么?还是只需要release NSMutableArray。
@F AA2 d  
RU|Q ]Ymx  
就像上面提到的,如果你了解container的引用特性,这个问题自然就解决了。“NSMutableArray在添加、插入objects时会做retain操作。” 通过这一句话就分析出,用户不否需要帮助NSMutableArray释放1000个string。回忆上面提到的管理哲学,“谁分配谁释放” 编写NSMutableArray的程序员非常熟悉这个规则,NSMutableArray内部retain了,NSMutableArray自然要负责release。但是NSMutableArray才不会管你在外面什么地方引用了这1000个string,它只管理好内部的rc就够了。所以如果你在NSMutableArray外面对1000个string retain了,你自然需要release。相应的,你作为创建这个NSMutableArray的程序员,你只管release这个NSMutableArray就可以了。 x>K Or,f  
JtE M,tK  
最后说一下不用arc的情况。目前情况来看,有不少第三方的库并未支持arc,所以如果你的旧项目使用了这些库,请检查是否作者发布了新版本,或者你需要自己修正支持arc。 gb1V~  
ijv(9mR  
8. class heritage and category /|}EL%a  
p T?}Kc  
在这节里说明2个比较重要的问题,第一是subclass和category的区别。第二是新手最容易忽略的学习步骤。 +|3@=.V  
RH W]Z Pr<  
Objective-C的Object-oriented programming特性提供subclass和category这2个比较非常重要的部分。subclass应该反复被各种编程书籍介绍过。它是oop继承特性的关键语法,它给类添加了延续并且多样化自己的方法。可以说没有继承就没有oop这玩意。而category相对于subclass就不那么出名了。其实category思想出世于smalltalk,所以它不能算是一个新生事物。 `bq<$e  
U gat1Pz  
先说一下这2个特性最主要的区别。简单可以这么理解,subclass体现了类的上下级关系,而category是类间的平级关系。 MpT8" /.]A  
D\v+wp.  

+Ze} B*0  
ic:zsuEm  
如上图所示,左侧是subclass,可以看到class, subclass1, subclass2是递进关系。同时下面的子类完全继承父类的方法,并且可以覆盖父类的方法。subclass2拥有function1,function2,function3三个函数方法。function1的执行代码来自subclass1, function2的执行代码来自于subclass2。 8S TvCH"Z_  
'@v\{ l  
右侧是category。可以看到,无论如何扩展类的category,最终就只有一个类class。category可以说是类的不同方法的小集合,它把一个类的方法划分成不同的区块。请注意观察,每个category块内的方法名称都没有重复的。这正是category的重要要求。 #\{l"-  
38B2|x  
经过上面简单解释了解了这2点的基本区别,现在深入说一下category。 H*n-_{h"t  
#^0R&) T  
在Objective-c语言设计之初一个主要的哲学观点就是尽量让一个程序员维护庞大的代码集。(对于庞大的项目‘原则’和‘协议’是非常重要的东西。甚至编写良好的文件名都是非常重要的开发技巧)根据结构化程序设计的经验出发,把一个大块代码划分成一些小块的代码更便于程序员管理。于是objc借用了smalltalk的categories概念。允许程序员把一系列功能相近的方法组织到一个单独的文件内,使得这些代码更容易识别。 C[cbbp  
b=C*W,Q_#  
更进一步的,和c,c++这种静态语言相比。objc把class categories功能集成到了run-time里面。因此,objc的categories允许程序员为已经存在的类添加新的方法而不需要重新编译旧的类。一旦一个category加入,它可以访问该类所有方法和实例变量,包括私有变量。 "S[450%  
`v!urE/gg%  
category不仅可以为原有class添加方法,而且如果category方法与类内某个方法具有同样的method signature,那么category里的方法将会替换类的原有方法。这是category的替换特性。利用这个特性,category还可以用来修复一些bugs。例如已经发布的Framework出现漏洞,如果不便于重新发布新版本,可以使用category替换特性修复漏洞。另外,由于category有run-time级别的集成度,所以使得cocoa程序安全性有所下降。许多黑客就是利用这个特性(和posting技术2)劫持函数、破解软件,或者为软件增加新功能。一个很典型的例子就是,我发布的QQ表情管理器。 9c bd~mM{  
WCZjXDiwJ  
值得注意的一点是,由于一个类的categories之间是平级关系。所以如果不同categories拥有相同的方法,这个调用结果是未知的。所以: qz_7%c]K[  
]h`&&Bqt  
引用
Category methods should not override existing methods (class or instance). P\tB~SZ*  
Two different categories implementing the same method results in undefined behavior e+7"/icK  
w{8xpAqm  
_;S-x  
Objc中Categories有其局限的部分,就是你不能为原有的class添加变量,只能添加方法。当然方法里可以添加局部变量。在这个局限基础上就有其它语言做了进一步改进,例如TOM语言就为Categories增加了添加类变量的能力。 l:~/<`o  
/8'NG6"H`  
总上所属,如果你开发时候遇到无论如何都需要为类添加变量的情况,最好的选择就是subclass。相反如果你只希望增加一些函数簇。Categories是最好的选择。 k=$TGqQY?  
HQdxL*N%^  
并且我个人给大家的建议是,每个Cocoa程序员都应该收集整理自己的一套NS类函数的Categories扩展库。这对你今后程序开发效率和掌控情况都有很大提高。 ELoDd&d8  
XSB"{H>&  
l\H=m3Bg  
9. drawing Issues BKCiIfkZ  
,&A7iO  
大家都知道,MacOS是一个非常注重UI的系统。所以在MacOS编程里绘制是一个非常重要的部分。第10部分,我会从2点介绍MacOS下绘制编程。首先是绘制技术分类;其次是绘制代码结构。 s[>,X#7 y  
8Al{+gx@?  
从绘制技术分类上看,Cocoa程序员能接触的几种绘制技术列表如下: r8?gD&c}  
g{)dP!}  
1. Cocoa Drawing(NS-prefix) CZwXTHe  
2. Core Graphics(CG-prefix, called Quazrtz 2D) u {cW:  
3. Core Animation g/d<Zfq<{  
4. Core Image {lzWrUGO  
5. OpenGL #lo6c;*m5  
4i;{!sT  
在这里我不打算给大家介绍每一种都是如何绘制具体的图像。只是介绍一下,它们大概长什么样子,并且有什么优势和限制。 U+jOTq8M  
dES"@?!^  
###Cocoa Drawing `,(4]tlL  
Cocoa Drawing应该是学习Cocoa程序开发最先接触的绘制技术。也是目前大多数MacOS程序所使用的绘制技术,其底层使用Quazrtz 2D(Core Graphics)。苹果对应文档为 [Cocoa Drawing Guide](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CocoaDrawingGuide/Introduction/Introduction.html)。Cocoa Drawing并没有统一的绘制函数,所有绘制函数分散在几个主要的NS类的下面。例如, NSImage, NSBezierPath, NSString, NSAttributedString, NSColor, NSShadow,NSGradient … 5DU6rks%  
Pbn*_/H  
所以很简单,当你看到如下代码就可以判断,使用的是Cocoa Drawing方法 y-b%T|p9  
)u&|_&g{}J  
复制代码
  1. [anImage drawInRect:rect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
  2. [@"some text" drawAtPoint:NSZeroPoint withAttributes:attrs];
  3. NSBezierPath *p=[NSBezierPath bezierPathWithRect:rect];
  4. [[NSColor redColor] set];
  5. [p fill];
d'gfQlDny  
n+9=1Oo"  
这种代码多出现在NSView的drawRect函数内。Cocoa Drawing 的渲染上下文是 NSGraphicsContext,我不断的看到很多新手把 NSGraphicsContext 和 CoreGraphics 的 CGContextRef 搞混。虽然它们很像并且也确实是有关系的,不过如果你不了解当绘制时候的 render context 很多时候将得到一个空白页面的结果。 HVCe;eI  
NN{?z!  
###Core Graphics C3f' {}  
Core Graphics 是 Cocoa Drawing layer 的底层技术,在 iOS 开发中非常普遍,因为 iOS 系统中并不存在 Cocoa layer 所以网上可以找到的多是 Core Graphics 绘制代码段子,这给那些不了解 Mac 开发的新手来说造成了很大困扰。Cocoa 是 Mac OS 下的 application framework 而 iOS 下的 application framework 则是 UIKit.framework又叫 Cocoa Touch,它们分享部分代码基础但又不完全一样。例如,Cocoa Touch 下的 UIView 的渲染上下文会使用 UIGraphicsGetCurrentContext() 取得,它得到的是一个 CGContextRef 指针,而在 NSView 里多用 [NSGraphicsContext currentContext] 取得渲染上下文。它得到的是一个 NSGraphicsContext 对象。当然 NSView 里也可以通过 CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; 来取得一个 Core Graphics 渲染上下文。 可见 Mac OS 下的开发更为灵活一些。因为 iOS 中的 UIKit 开发初期就瞄准了显卡硬件加速,所有 UIView 都是默认 layer-backed 的。iOS 开发者必须使用 Core Graphics 和 Core Animation 这几个相对底层的绘制技术。 >h9I M$2  
J1U/.`Oy  
请看下面等价代码,作用是绘制一个白色矩形。但是分别使用 Core Graphics 和 Cocoa Drawing: !?jrf] A@  
W+c<2?d:  
复制代码
  1. const CGFloat white[]={ 1.0, 1.0, 1.0, 1.0 };
  2. CGContextSetFillColor(cgContextRef, white);
  3. CGContextSetBlendMode(cgContextRef, kCGBlendModeNormal);
  4. CGContextFillRect(cgContextRef, CGRectMake(0, 0, width, height));
  5. [[NSColor whiteColor] set];
  6. NSRectFillUsingOperation(NSMakeRect(0, 0, width, height), NSCompositeSourceOver);
x j)F55e?  
9rX&uP)j^#  
可以看出,这是2种*风格完全不同*的绘制技术。Cocoa Drawing 是分散式的绘制函数,而 Core Graphics 是传统的类似 OpenGL 的集成式的绘制方式。其实 Cocoa Drawing 下层是 Core Graphics, Core Graphics 的下层是 OpenGL。 *KF#'wi  
3*XNV  
u@) U"FZ  
###Core Animation NR6#g,+7  
如果说 Core Graphics 和 Cocoa Drawing 是通用的 UI 绘制框架的话,那么 CA 显然是界面动画绘制的高级技术。 qBQ?HLK-  
Core Animation 的对应 Cocoa Animation 部分应该是 NSAnimation 和 NSViewAnimation,但这2个差距比较大。NSAnimation 出现与 OS X 10.4,Core Animation 是 10.5 后出现的。NSViewAnimation 功能和使用相对简单。 r|8d 4  
net@j#}j-  
简单来说,Core Animation 的作用对象是 CALayer, NSAnimation 的作用对象是 NSView。 n38p!oS  
Qy<P463A(l  
了解你的程序界面是在处理那种对象很重要。 3ZPWze6  
8 FK/~,I  
*待写...* <$$yw=ef  
Q$W  
p` dU2gV  
10. Design pattern u~:y\/Y6  
Et_bH%0  
待写... s\(k<Ks  
&BLJT9Frx  
F,F4nw<W  
(1)这里其实很有意思,为何我用“更高层次思考”,而不是“更底层次。作为一个编译器和语言开发人员,面对的问题确实更底层没错,但是他们思考的维度更高,更抽象,这样子。一个不算恰当的比方就好像一个三维世界的人处理二维世界的一条线的问题。  qA7>vi%  
(2)Posting技术在10.5以后deprecated,并且64bit run-time也不再支持 -7[@R;FS  
!-x$L>1$  
[ 此帖被keefo在2014-11-07 09:14重新编辑 ]

欢迎到 http://www.beyondcow.com 下载我的软件。
到个人博客 http://lianxu.me/下载开源代码。
或者关注我的 新浪微博
级别: 新手上路
UID: 24109
精华: 0
发帖: 10
可可豆: 100 CB
威望: 100 点
在线时间: 37(时)
注册时间: 2010-07-07
最后登录: 2014-01-22
1 楼:  发表于: 2011-09-14 23:40    发自: Web Page
纯粹支持楼主
级别: 精灵王
UID: 15279
精华: 0
发帖: 489
可可豆: 3869 CB
威望: 4654 点
在线时间: 2240(时)
注册时间: 2010-03-12
最后登录: 2017-03-13
2 楼:  发表于: 2011-09-14 23:47    发自: Web Page
  
CocoaChina微群 :http://q.t.sina.com.cn/226567
级别: 精灵王

状态: 连续签到 - [6天]
UID: 28879
精华: 0
发帖: 664
可可豆: 6951 CB
威望: 5748 点
在线时间: 975(时)
注册时间: 2010-08-27
最后登录: 2019-04-14
3 楼:  发表于: 2011-09-15 00:00    发自: Web Page
  此贴必火哇! 留名虚心学习
fangyp116@gmail.com
级别: 新手上路
UID: 56750
精华: 0
发帖: 44
可可豆: 440 CB
威望: 440 点
在线时间: 42(时)
注册时间: 2011-03-13
最后登录: 2016-03-27
4 楼:  发表于: 2011-09-15 01:12    发自: Web Page
mark,学习下,希望楼主继续更新1
级别: 圣骑士

UID: 50373
精华: 0
发帖: 465
可可豆: 3579 CB
威望: 3579 点
在线时间: 651(时)
注册时间: 2011-02-11
最后登录: 2017-07-24
5 楼:  发表于: 2011-09-15 09:00    发自: Web Page
学习 学习  期待楼主更新
什么时候才能成精灵王呀
级别: 风云使者

状态: 连续签到 - [309天]
UID: 83747
精华: 0
发帖: 3865
可可豆: 19534 CB
威望: 19405 点
在线时间: 2397(时)
注册时间: 2011-07-27
最后登录: 2018-11-05
6 楼:  发表于: 2011-09-15 09:46    发自: Web Page
楼主辛苦,佩服佩服。
级别: 圣骑士
UID: 14031
精华: 0
发帖: 380
可可豆: 3669 CB
威望: 3669 点
在线时间: 1680(时)
注册时间: 2010-02-10
最后登录: 2016-08-06
7 楼:  发表于: 2011-09-15 09:47    发自: Web Page
keefo fans comes.
级别: 侠客
UID: 45762
精华: 0
发帖: 110
可可豆: 993 CB
威望: 993 点
在线时间: 143(时)
注册时间: 2011-01-07
最后登录: 2016-12-01
8 楼:  发表于: 2011-09-15 09:57    发自: Web Page
楼主写的很不错,期待更新。
级别: 精灵王

状态: 连续签到 - [106天]
UID: 52331
精华: 0
发帖: 1420
可可豆: 2450 CB
威望: 10164 点
在线时间: 1120(时)
注册时间: 2011-02-21
最后登录: 2018-12-27
9 楼:  发表于: 2011-09-15 10:08    发自: Web Page
     mark 学习啦 %?1ew  

特别能吃苦”这5个字,我想了想,我只做到了前四个。

CocoaChina社区转载内容已尽可能注明出处,如未能核实来源或转发内容图片有权利瑕疵的,请及时联系社区进行修改或删除【联系方式QQ : 3442093904 邮箱:support@cocoachina.com】文章内容为作者独立观点,不代表CocoaChina社区立场。版权归原作者所有,如申请授权请联系作者,因文章侵权CocoaChina社区不承担任何法律及连带责任。

描述
快速回复

关注本帖(如果有新回复会站内信通知您)

发帖、回帖都会得到可观的积分奖励。查看论坛积分规则

按"Ctrl+Enter"直接提交
    顶部