ios 设计

转载 2012年03月31日 00:02:33

http://peixun.eol.cn/company/company_article_detail.php?articleid=17657

 

 

10个迷惑新手的Cocoa, Objective-c开发难点和问题
因为有自己的软件要开发,所以我只能忙里偷闲过来接着写这篇教程。大家莫怪。请按上面的最后更新时间算。
原文地址
http://lianxu.me/blog/2011/11/14/10-cocoa-objc-newbie-problems/

首先请谅解我可能使用很多英文,毕竟英文资料将来会是你的主要资料来源。

在你继续深入学习之前,请停下脚步弄清这些问题。如果你是新手,这个教程不要希望一次能看的非常透彻,学一定阶段反回来再看看又会有新的体会的。

1. language background

首先c, c++语言背景,必须。
很多人问 “没有任何语言基础,我不想学c直接学objective-c”
这里简单几句,objc 90%代码是c、众多开源代码是c,c++。你不学好c在unix世界里只能是个二流开发者!也许说得过于严厉,不过自己斟酌把。 

接着English必须。
需要英语!需要英语!需要英语!苹果不会把它们文档都写成中文的。“什么,有人翻译?” 等有人闲着翻译出来了的时候,大家都已经卖了很多软件了。你也是跟着人家屁股后面做开发。

2. Runtime(运行时)

Objective-c是动态语言,  很多新手或者开发人员常常被Runtime这个东西所迷惑。而恰恰这是一个非常重要的概念。 为什么重要呢!?我可以这么问:“如果让你(设计、)实现一个计算机语言,你要如何下手?” 很少程序员这么思考过。但是这么一问,就会强迫你从更高层次思考(1)以前的问题了。 注意我这句话‘设计’括起来了,稍微次要点,关键是实现。

我把实现分成3钟不同的层次:
    1. 传统的面向过程的语言开发,例如c语言。实现c语言编译器很简单,只要按照语法规则实现一个LALR语法分析器就可以了,编译器优化是非常难的topic,不在这里讨论范围内,忽略。 这里我们实现了编译器其中最最基础和原始的目标之一就是把一份代码里的函数名称,转化成一个相对内存地址,把调用这个函数的语句转换成一个jmp跳转指令。在程序开始运行时候,调用语句可以正确跳转到对应的函数地址。 这样很好,也很直白,但是。。。太死板了。everything is pre-determined

    2. 我们希望灵活,于是需要开发面向对象的语言,例如c++。 c++在c的基础上增加了类的部分。但这到底意味着什么呢?我们再写它的编译器要如何考虑呢?其实,就是让编译器多绕个弯,在严格的c编译器上增加一层类处理的机制,把一个函数限制在它处在的class环境里,每次请求一个函数调用,先找到它的对象, 其类型,返回值,参数等等,确定了这些后再jmp跳转到需要的函数。这样很多程序增加了灵活性同样一个函数调用会根据请求参数和类的环境返回完全不同的结果。增加类机制后,就模拟了现实世界的抽象模式,不同的对象有不同的属性和方法。同样的方法,不同的类有不同的行为! 这里大家就可以看到作为一个编译器开发者都做了哪些进一步的思考。但是。。。还是死板, 我们仍然叫c++是static language。 

    3. 希望更加灵活! 于是我们完全把上面哪个类的实现部分抽象出来,做成一套完整运行阶段的检测环境。这次再写编译器甚至保留部分代码里的sytax名称,名称错误检测,runtime环境注册所以全局的类,函数,变量等等信息等等,我们可以无限的为这个层增加必要的功能。调用函数时候,会先从这个运行时环境里检测所以可能的参数再做jmp跳转。这,就是runtime。编译器开发起来比上面更加弯弯绕。但是这个层极大增加了程序的灵活性。  例如当调用一个函数时候,前2种语言,很有可能一个jmp到了一个非法地址导致程序crash, 但是在这个层次里面,runtime就过滤掉了这些可能性。 这就是为什么dynamic langauge更加强壮。 因为编译器和runtime环境开发人员已经帮你处理了这些问题。



好了上面说着这么多,我们再返回来看objective-c.  现在你是不是能理解这样的语句了呢?

复制代码
  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];


看似很简单的语句,但是为了让语言实现这个能力,语言开发者要付出很多努力实现runtime环境。这里运行时环境处理了弱类型、函数存在检查工作。runtime会检测注册列表里是否存在对应的函数,类型是否正确,最后确定下来正确的函数地址,再进行保存寄存器状态,压栈,函数调用等等实际的操作。

复制代码
  1. id knife=[Knife grateKnife];
  2. NSArray *monsterList=[NSArray array];
  3. [monsterList makeObjectsPerformSelector:@selector(killMonster:) withObject:knife];


在c,c++年代去完成这个功能是非常麻烦的,但是动态语言却非常简单。

现在说一下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(同步),因为1. There is no 100% deadlock detection algorithm. If there is, no deadlock at all. 2. 往往这类错误很隐晦,静态分析很难找到。 3. 抽象度较高需要经验去把握。  

总体来说,我见到的在这方面的问题可以分为一下几点:
  
1. 不知道多线程开发的几个基点,看别人代码越看越糊涂的。一会NSThread、一会Grand Central Dispatch、block等等。。。Apple封装了很多线程的api, down to core多线程的结构基本是:


 

可以看到在多线程开发中你可以选择这上面这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。

    
    5. Asynchronous(异步) vs. Synchronous(同步)
    
引用
我在一个view要显示多张web图片,我想问一下,我是应该采用异步一个一个下载的方式,还是应该采用多线程同时下载的方式,还是2个都用,那种方式好呢?


实际上单独用这2个方法都不好。不要简单的认为用了多线程就速度很快,如果一下子把100个图片请求全部发出去,这样TCP/IP栈瞬间就满了,整个程序速度反而下降了。

处理这种类型的程序,比较好的结构应该是:非主线程建立一个队列(相当于Asynchronous任务),队列里同时启动n个下载任务(相当于Synchronous任务)。这里的n在(2~8左右就够了)。这个结构就是说,队列里面每n个任务之间是异步关系,但是这n个任务之间又是同步关系,每次同时下载2~8张图片。
  
(非常抱歉,之前质疑问问题的人理解错误。其实是我自己晕了,问得问题很好。)
    

4. runloop
现在说说runloop为何会成为cocoa开发中迷惑的点。因为很多新手没有从动态角度看它。 首先回想一下第2点介绍的runtime的概念。 接着我出一个题思考一下。

现在我有一个程序片段如下:
复制代码
  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. }

现在要求,做某些设计,使得当这个线程运行的同时,还可以从其它线程里往它里面随意增加或去掉不同的计算任务。 这,就是NSRunloop的最原始的开发初衷。让一个线程的计算任务更加灵活。 这个功能在c, c++里也许可以做到但是非常难,最主要的是因为语言能力的限制,以前的程序员很少这么去思考。

好,现在我们对上面代码做一个非常简单的进化:

复制代码
  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. }

注意,这里没有做线程安全处理,记住Mutable container is not thread safe.
这个简单的扩展,让我们看到了如何利用runtime能力让线程灵活起来。当我们从另外线程向targetQueue和actionQueue同时加入对象和方法时候,这个线程函数就有了执行一个额外代码的能力。

但,有人会问,哪里有runloop? 那个是 nsrunloop? 看不出来啊。
复制代码
  1. while (TRUE) {
  2. //break in some condition
  3. }

一个线程内这个结构就叫线程的runloop,   它和NSRunloop这个类虽然名字很像,但完全不是一个东西。以前在使用静态语言开始时候,程序员没有什么迷惑,因为没有NSRunloop这个东西。 我接着来说,这个NSRunloop是如何来得。

第二段扩展代码里面确实没有NSRunloop这个玩意儿,我们接着做第3次改进。 这次我们的目前是把哪个动态部分抽象出来。



5. delegate, protocol
这个会列出来因为,我感觉问它的数量仅此于内存管理部分,它们用得很频繁,并且它们是多钟设计模式的重要组成部分。


6. responder chain


7. Memory Reference Counting(RC) & Automatic Reference Counting(ARC)
这个也许是问得最多的问题了吧。所有这些问题往往来源于3个地方,1、不了解底层机制;2、没有吃透规则; 3、不了解常用container的Reference Counting特性,或着说没有下功夫去看对应文档。

1. 底层机制
大家是否知道从旧时代的RC到ARC机制到底意味着什么呢? 为什么ARC从开发速度,到执行速度和稳定性都要优于rc?

开发速度不言而喻,你少写很多release代码,甚至很少去操心这部分。

执行速度呢?这个还要从runtime说起,还记得我在第2点说得一句话么:“Runtime is everything between your each function call.”  

RC是一个古老的内存管理哲学,谁分配谁释放。通过counting来计数到底该资源有几个使用者。道理很简单,但是往往简单的东西人却会犯错。从来没有一个程序员可以充满信心的说,我写得代码从来没有过内存泄露。这样来看,我们就更需要让程序可以自己处理这个管理机制,这就需要把这个机制放到runtime里。

所以RC->ARC就是把内存管理部分从普通开发者的函数中移到了函数外的runtime中。因为runtime的开发原型简单,逻辑层次更高,所以做这个开发和管理出错的概率更小。实际上编译器开发人员对这部分经过无数次测试,所以可以说用arc几乎不会出错。另外由于编译的额外优化,使得这个部分比程序员自己写得代码要快速很多。而且对于一些通用的开发模式,例如autorelease对象,arc有更优秀的算法保证autoreleasepool里的对象更少。

2. RC规则
首先说一下rc是什么,r-Reference参照,引用 c-counting计数, rc就是引用计数。俗话说就是记录使用者的数量。 例如现在我有一个房间空着,大家可以进去随意使用,但是你进门前,需要给门口的计数牌子+1, 出门时候-1。 这时候这个门口的牌子就是该房间里的人数。一但这个牌子变为0我就可以把房间关闭。

这个规则可以让NSObject决定是不是要释放内存。当一个对象alloc时候,系统分配其一块内存并且object自动计数retainCount=1 这时候每当[object retain]一次retainCount+1(这里虽然简写也是rc不过是巧合或者当时开发人员故意选的retain这个词吧)每次[object release]时候retainCount-1 当retainCount==0时候object就真正把这快内存还给系统。

3. 常用container的Reference Counting特性
这个规则很简单把。但是这块确实让新手最头疼的地方。 问题出在,新手总想去验证rc规则,又总是发现和自己的期望不符合。  
无数次看到有人写下如下句子
复制代码
  1. NSLog(@"%d",[object retainCount]);


复制代码
  1. while([object retainCount]>0){
  2.       [object release];
  3. }


当然了,我也做过类似的动作,那种希望一切尽在掌握中的心态。但是你会看到其他人告诉这么做完全没有意义。rc does not work this way.   也许这样的暴力释放会起作用,但是retainCount并不是用来做这个的。每个数字意味着有其它对象引用该资源,这样的暴力释放很容易导致程序崩溃。这个数字也许并不是你心目中的哪个。因为你很难跟踪到底哪些对象引用的该资源。你用代码建立的资源不光只有你的代码才会用到,你调用的各种Framework,Framework调用的Framework,都有可能改变这个资源的retainCount.  所以去验证rc规则不是明智之举。

你能做的就是理解规则,使用规则,读文档了解container的引用特性。或者干脆移到arc上面,让runtime环境处理这些问题。

最后说一下不用arc的情况。目前情况来看,有不少第三方的库并未支持arc,所以如果你的旧项目使用了这些库,请检查是否作者发布了新版本,或者你需要自己修正支持arc。

8. class heritage 


9. Design pattern


10. thinking before asking

其实剩下这个有好几点要说,但综合一下把。思路有些相似
例如刚看到这个问题:
引用
现在有A *a;A*b
[NSMutableArray addObject : a];
[NSMutableArray replaceObjectAtIndex:0 withObject:b]
执行完这两个之后,拿可变数组里面的0 的位置 就是b元素了,那这个时候a到哪里去了??是否还占用着内存,如果占用内存的话,又如何去释放??

It's kind of silly.  我并不是想讽刺问问题的朋友。其实如果你真的了解了上面这些知识点,就不会再问这种问题的。 为什么不多思考一层呢,在问这个问题之前想想,到底为什么会问出这个问题? ”如果让你给NSMutableArray实现一个replaceObjectAtIndex函数你会怎么写?“  难道连个[obj release]都考虑不到么?然后根据ARC,它到底释放了没不言自明了把。

其实这种问题论坛里很多的。不妨在迷惑的时候,先问问自己为什么会迷惑。


(1)这里其实很有意思,为何我用“更高层次思考”,而不是“更底层次。作为一个编译器和语言开发人员,面对的问题确实更底层没错,但是他们思考的维度更高,更抽象,这样子。一个不算恰当的比方就好像一个三维世界的人处理二维世界的一条线的问题。 

 

相关文章推荐

iOS设计模式

  • 2017年11月15日 17:41
  • 55.79MB
  • 下载

IT网址导航(前端、设计、产品、java、运维、ios、android)

名站推荐 『 优站精选,汇聚国内外知名技术站点,实现一站式技术分类导航 』 CSDN综合社区博客园技术博客慕课网在线视频W3school在线文档GitHub项目管理Stack Overflo...

安卓iOS毕业设计

  • 2015年06月24日 09:04
  • 747KB
  • 下载

IOS之Objective-C编程设计模式

  • 2013年05月05日 09:14
  • 28.32MB
  • 下载

2016年全新Apple iOS设计规范指南!

原文地址:http://www.360doc.com/content/16/0525/13/33476169_562167037.shtml      iOS推出很多年,已经升级到iOS9!现在...

iOS图标设计模板

  • 2012年05月29日 13:26
  • 10.82MB
  • 下载

Android UI设计之<二>自定义SwitchButton开关,实现类似IOS中UISwitch的动画效果

做IOS开发的都知道,IOS提供了一个具有动态开关效果的UISwitch组件,这个组件很好用效果相对来说也很绚丽,当我们去点击开关的时候有动画效果,但遗憾的是Android上并没有给我们提供类似的组件...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:ios 设计
举报原因:
原因补充:

(最多只允许输入30个字)