Objective-C: delegate的那点事儿

Delegate算是Objective-C的一大特性, 关于Delegate的基础就不多介绍了, 有兴趣的请参看文档.

这里仅对Delegate使用中的一些问题做点讨论

我们用Delegate很多情况下是基于多线程的,比如我们有一个ViewController在这个Controller里面进行了一个下载图片的操作,下载成功后需要通过protocol来现实下载成功, 但是当ViewController已经被release,而下载工作才结束, 那么下载工作的[delegate didFinishDownload] (暂且就这么命名吧) 就会产生一个异常,因为你给一个deallocated的对象发送了一个消息.

那么,如何解决这个问题呢,首先我们可能想到用if (delegate == nil) 来判断delegate是否存在,但其实这是不行的,因为已经dealloc的对象并不是nil.要知道Objective-C中给nil发送消息是可以的,所以如果这种方法可行,其实我们就根本不需要if这句,[delegate didFinishDownload] 给nil发送了一个消息也不会出现异常,因此这种方法只是重复了上面的错误.

还有一个叫[delegate respondsToSelector:SEL]来判断delegate是否响应一个Selector, 根据上一段的描述,我们也可以判断出这个也是不行的.这里额外提一点关于respondsToSelector的东西,要使用这个方法,必须有@protocol MyProtocol <NSObject>,因为respondsToSelector是NSObject的一个protocol方法.

既然要防止delegate被release,那么retain这个delegate是否可行呢?这么做虽然避免错误的发生,但是也产生了另一个问题,这就关系到Objective-C内存管理中的Retain Circle, 即:有A,B两个Object, A中有一个B的实例变量,B中又有一个A的实例变量,要release A就必须releaseA中的B,而要release B有必须release B中的A,这样就产生了一个Retain Circle,A B都不能被dealloc.解决Retain Circle的方法就是使用弱引用(weak reference),弱引用没有被引用的那个Object的所有权,也就不需要release它,从而解决了Retain Circle问题.为了防止Retain Circle的发生, delegate通常都是弱引用的, 因此我们一般不应该retain一个delegate.但是似乎有一个例外:NSURLConnection, 网上对其的讨论结果是:NSURLConnection会retain它的delegate,详细可以参考StackOverflow上的这个问题

似乎没有简单可行的方法来解决这个问题(至少在本文发表时我还没有找到),那么我们只能在通过程序结构的设计来解决这一问题了,对应不同的程序自然也就有不同的解决方法,我想到的一种就是在这个ViewConrtoller被release的时候,把下载方法中的delegate设置成nil即可(目前测试可行, 如有错误还请指正).

更新几种解决方法:

–以上–

UITableViewCell的selecte与deselect

首先要从一个我遇到的问题谈起,一个基于NavigationBar的App,开始时我有一个UITableViewController,其中每个UITableViewCell点击后都会push另一个ViewController,每次点击Cell的时候,Cell都会被选中,当从push的ViewController返回的时候选中的Cell便会自动取消选中(有动画效果)。后来由于某些原因我把这个UITableViewController改成了UIViewController,之后就产生了一个问题:每次返回到TableView的时候,之前选中的Cell不能自动取消选中,经过查找得知:

UITableViewController有一个clearsSelectionOnViewWillAppear的property,

而当把UITableViewController修改成UIViewController后,这个属性自然就不存在了,因此我们必须手动添加取消选中的功能,方法很简单,在viewWillAppear方法中加入:

[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];

即可,估计UITableViewController也是用类似的方法来实现取消选中的功能的。

–以上–

XCode SVN 设置指南

经初步测试,使用本文的方法,可以解决文件丢失,commit错误(Error: 155005  Working copy not locked..等)等问题

  • 修改svn配置文件
svn的配置文件在~/.subversions/config
修改方法:
1.找到 global-ignores 一行,去掉注释用的“#”,编辑(添加)成 global-ignores = build *~.nib *.so *.pbxuser *.mode *.perspective*
2.找到 enable-auto-props = yes 把注释去掉(这步似乎可以忽略)
3. 在[auto-props] Section添加以下内容
     *.mode* = svn:mime-type=text/X-xcode
     *.pbxuser = svn:mime-type=text/X-xcode
     *.perspective* = svn:mime-type=text/X-xcode
     *.pbxproj = svn:mime-type=text/X-xcode
 
关于修改配置文件的更多讨论在这里
 
  • SCM Config
XCode 菜单中 SCM >> Configure Repositories
 
按 “+” 添加一项
     Name:任意
     URL:svn://[email protected]/opt/ibmtc/SUBPATH 然后按tab切换 下面那几个除了Password都自动填好了
     Scheme: svn
     Host:svn.footoo.org
     Path:/opt/ibmtc/SUBPATH
     User: USERNAME
     Password: PASSWD
下面有个指示灯,如果显示绿色并有Authenticated字样则表示设置成功
  • 添加项目
1. 如果不是新建的项目 直接到第二步 SCM >> Repositories 选择好对应目录 点Import选择想要进行版本控制的项目(最好是“干净”的项目 即没有.svn文件夹 否则可能有一些错误)
2. 在刚才的窗口选择刚添加的项目 Check out,相当于用svn对项目进行初始化,加入一些控制信息。这一步很重要,缺失了将不能进行版本控制
3. 打开Check out下来的项目 打开项目属性窗口 >> General >> Config Roots & SCM…  下拉按钮选择Subversion 下面的table中的Repository选择你之前在Configure Repositories中添加的那个,一般都会有Recommended字样
 此时在菜单 SCM中就多出了很多项目
在左边的项目树中,邮件点击表头,选择SCM可以多出一栏现实项目修改信息
文件的标示状态简介如下: 

Cocoa中的weak reference

以下所有内容翻译自官方文档,如有不当之处还请各位指正。

Weak Reference to Objects(弱引用)

————————————————————————————————————

retain 一个 object 创建一个 “Strong” reference,一个object在直到它所有的strong reference都release了之后才能dealloc,因此object的生命周期是受它的Strong reference的所有者控制的。某些情况下,这种行为不是我们所期待的,你也许想有一个不会阻止一个object dealloc的reference,这种情况下,你就拥有一个“Weak” reference,weak reference是通过存储一个不retain相应的object的指针所创建的。

 
weak reference在会有循环reference要创建时是必要的。比如,如果Object A和 B之间互相Communicate,它们各自需要一个对方的reference。如果它们各自retain了对方,那么直到它们之间的Connection切断之前它们之中任何一个都不会dealloc,但是它们之间的Connection要等待它们其中任意一个dealloc了之后才能切断,这就产生了一个矛盾。要解决这个问题,其中一个object作为一个次级(subordinate)角色并且拥有对方的一个weak reference。一个具体的例子,在一个view继承体系中,一个parent view拥有并因此retain他的childe views,但是一个child view不拥有它的parent;而又必须知道它的parent是谁,所以就有一个指向它的parent的weak reference。
 
Cocoa中另一些weak reference的例子包括但不局限于这些:table data sources,outline view items,notification observers,各种targets和delegates。(比如一个NSTableView不会retain它的data source,一个NSApplication不会retain它的delegate)
 
当你需要向一个你只拥有weak reference的object发送消息时,就要小心,那个object是不是已经dealloc了,如这这样会使你的程序Crash。
后面又是一些例子(delegate 和notification),不翻译了。

动态调整UITableViewCell高度的实现方法

有时我们需要动态调整UITableViewCell的高度,根据内容的不同设置不同的高度,以前看到一种实现方法,写得有点麻烦,具体地址找不到了,这里有个更好的(至少我认为),分享一下部分代码。

2012.03.11更新:一年后回来审视儿时的代码,发现heightForRowAtIndexPath那个实现方法确实不太好,会dequeue掉一个可以reuse的cell导致经常都要新创建cell,会导致效率方面的问题,最好用NSString的sizeWithFont:forWidth:lineBreakMode:这一系列的方法计算label的高度。


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
		UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
		label.tag = 1;
		label.lineBreakMode = UILineBreakModeWordWrap;
		label.highlightedTextColor = [UIColor whiteColor];
		label.numberOfLines = 0;
		label.opaque = NO; // 选中Opaque表示视图后面的任何内容都不应该绘制
		label.backgroundColor = [UIColor clearColor];
		[cell.contentView addSubview:label];
		[label release];
    }

    UILabel *label = (UILabel *)[cell viewWithTag:1];
	NSString *text;
	text = [textArray objectAtIndex:indexPath.row];
    CGRect cellFrame = [cell frame];
	cellFrame.origin = CGPointMake(0, 0);

	label.text = text;
	CGRect rect = CGRectInset(cellFrame, 2, 2);
	label.frame = rect;
	[label sizeToFit];
	if (label.frame.size.height > 46) {
		cellFrame.size.height = 50 + label.frame.size.height - 46;
	}
	else {
		cellFrame.size.height = 50;
	}
	[cell setFrame:cellFrame];

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
	UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
        //UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath];
	return cell.frame.size.height;
}

–以上–

用NSZombieEnabled解决恼人的EXC_BAD_ACCESS错误

更新Xcode 4设置NSZombieEnabled方法:
按住Option点Run按钮,在出现的窗口中选择Arguments,在Environment Variables里面添加NSZombieEnabled,Value下面添YES

以下为XCode 3的设置方法

这个方法也许很多人都知道了,但是毕竟有不知道的,比如在写这篇文章之前10分钟的我。先说明情况:昨天遇到了这样的问题,程序突然Crash,出现EXC_BAD_ACCESS错误,但是不是每次都Crash,有几次RP高了就好使,Debug时最终基本停在objc-msgsend这里,但也不是每次,是大部分(90%)。十分令人费解,搞了几个小时无果,求助google和stackoverflow终于解决。

首先解释下EXC_BAD_ACCESS,当你向已经释放的对象发送消息时就会出现这种错误。

至于NSZombieEnabled,就是当设置NSZombieEnabled环境变量后,一个对象销毁时会被转化为_NSZombie,个人感觉和线程的那几个状态有些相似,设置NSZombieEnabled后,当你向一个已经释放的对象发送消息,这个对象就不会向之前那样Crash或者产生一个难以理解的行为,而是放出一个错误消息,然后以一种可预测的可以产生debug断点的方式消失(原文是die),因此我们就可以找到具体或者大概是哪个对象被错误的释放了。

设置NSZombieEnabled的方法如下

  1. 在XCode左边那个Groups & Files栏中找到Executables,双击其中的一项,或者右键Get Info;
  2. 切换到Arguments
  3. 这里一共有两个框,在下面那个Variables to be set in the environment:点+号添加一项,Name里填NSZombieEnabled,Value填Yes,要保证前面的钩是选中的。

好了,赶紧去找哪里出了问题吧,至于取消NSZombieEnabled,就是吧刚才提到的那个钩取消即可。

最后总结一下感想,首先是我对EXC_BAD_ACCESS这个错误还没有足够的认识,其次是我没有尽快的去搜索一下解决方法,这两点导致我浪费了大量时间做各种各样奇怪的调试。幸好现在解决了,可以继续工作了。

2012.02.15更新:

在debug过程中,你可能发现启用NSZombieEnabled后,程序不再crash,而一旦去掉NSZombieEnabled,程序再次crash。此时NSZombieEnabled已经无法解决你的问题,只能遵照内存管理原则仔细查找问题出处,至于这种问题的产生原因,个人认为可能是NSZombieEnabled在一定程度上延长了一个object的生命周期,而延长的这段时间恰好突破了EXC_BAD_ACCESS的临界点,从而避免了EXC_BAD_ACCESS的发生。

–以上–

Cocoa: Drag And Drop 简单实现

先吐个槽:iOS开发在国内还是蛮火的,但是Cocoa的中文资料是在是很少,虽然国内有那么几个论坛,但是总体实力与神马StackOverflow啥的还是差多了,一些问题还是到国外的网站查才是王道。

今天遇到了这样的问题:如何实现向ImageWell中拖入文件,然后显示文件的图标并取得文件路径。

一番查找后没有太大收获,决定回归文档,如果你有同样的问题强烈建议仔细阅读一下文档中的Introduction to Drag and Drop,虽然都是英文,但是其实内容并不多,基本一个小时左右完全可以看完,另外结合一下Cocoa DragAndDrop这个官方样例(文档里也有),保证你能弄清楚Drag and Drop的实现方法。

废话不多说了,喜欢自学的就不用往下看了,懒得看文档的可以听我唠叨两句,但是还要结合文档才能真正弄懂,事先说明一下,我也只是个业余Cocoa爱好者,属于需要什么看什么的类型,所以如果犯了什么错误还请留言指正。

贴代码(点击右边箭头展开)(为了简便以下代码仅实现取得文件路径功能)

 


#import 

@protocol DragAndDropImageViewDelegate

- (void)dragFinished:(NSString *)filePath :(int) tag;

@end

@interface DragAndDropImageView : NSImageView {
	id  delegate;
}

- (id)initWithCoder:(NSCoder *)coder;

@property(nonatomic,assign) id  delegate;

@end

 


#import "DragAndDropImageView.h"

@implementation DragAndDropImageView

@synthesize delegate;

- (id)initWithCoder:(NSCoder *)coder {
	NSLog(@"initWithCoder");
	if (self = [super initWithCoder:coder]) {
		[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]];
	}
	return self;
}

- (NSDragOperation)draggingEntered:(id )sender {
	NSLog(@"draggingEntered");
	NSPasteboard *pboard;
	NSDragOperation sourceDragMask;

	sourceDragMask = [sender draggingSourceOperationMask];
	pboard = [sender draggingPasteboard];

	if ([[pboard types] containsObject:NSFilenamesPboardType]) {
		if (sourceDragMask & NSDragOperationLink) {
			NSLog(@"return NSDragOperationLink");
			return NSDragOperationLink;
		}
	}
	return NSDragOperationNone;
}

- (BOOL)performDragOperation:(id )sender {
	NSLog(@"performDragOperation");

	NSPasteboard *pboard;
	NSDragOperation sourceDragMask;
	int tag = [self tag];

	sourceDragMask = [sender draggingSourceOperationMask];
	pboard = [sender draggingPasteboard];

	if ([sender draggingSource] != self) {
		if ([[pboard types] containsObject:NSFilenamesPboardType]) {
			NSLog(@"ready to modify");
			NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
			NSString *filePath = [files objectAtIndex:0];
			[delegate dragFinished:filePath :tag];
			// modify here to continue
		}
	}
	return YES;
}

@end

@protocol如果没有相应需求可以忽略。

简单解释一下吧,initWithCoder是给InterfaceBuilder用的,其中的registerForDraggedTypes作用是声明Drag and Drop响应的文件类型,当有文件拖入时就会调用draggingEntered方法,该方法中判断是不是要对拖动进行响应,如果响应则继续进入performDragOperation方法,完成处理。还有些中间过程以及详细的解释,以上代码没有涉及,详情请参阅文档。

–以上–

XCode技巧之UserScripts

XCode作为一款强大的IDE,当然也支持脚本功能。通过添加自己的脚本我们可以很方便的完成我们的工作。

在XCode中点击 Edit User Scripts即可方便的添加脚本,而且XCode自带的脚本也相当于给我们提供了丰富的样例。

比如我们添加一个这样的脚本,并为其添加一个快捷键⇧⌘P(快捷键添加方法参考Comments分类中的Un/Comment Selection脚本)

 

#!/bin/sh

echo "%%%{PBXSelection}%%%"
echo "#pragma mark -"
echo "#pragma mark %%%{PBXSelectedText}%%%"
echo "%%%{PBXSelection}%%%"

注意在Output下拉列表中选择Replace Selection。这个脚本的作用是方便我们建立#pragma,首先提供pragma的名字,然后选中这个名字,按相应快捷键,#pragma就自动完成了,很方便。

这个脚本也可以这样写,我们就不用每行都写echo了

 

#!/bin/sh

cat << EOF
%%%{PBXSelection}%%%
#pragma mark -
#pragma mark %%%{PBXSelectedText}%%%
%%%{PBXSelection}%%%
EOF

除了bash脚本,XCode还支持Ruby,Python等多种语言的脚本,比如这个Ruby脚本

 

#!/usr/bin/env ruby -w

# Source: http://allancraig.net/blog/?p=315

properties = ''
synthesize = ''
release    = ''

STDIN.read.each do |line|
	line.gsub!(/\*/, '').strip!
	words = line.split(/\s+/)
	
	label = words.size > 2 ? words[1] : words[0]
	variable = words[-1]
	properties << "@property (nonatomic, retain) IBOutlet #{label} *#{variable}\n"
	synthesize << "@synthesize #{variable}\n"
	release << "[#{variable.chop} release];\n"
end

synthesize << release.chomp

`echo '#{synthesize.chomp}' | pbcopy`
print properties.chomp

该脚本的作用是帮助我们添加@property、@synthesize、还有dealloc方法中相应的release。使用方法是选中.h文件中的成员变量,比如我们选中UIButton *aButton;,复制到要添加@property的位置,再次选中,按快捷键执行脚本,@property就添加完成了,然后到相应的.m文件中,在要添加@synthesize的位置按⌘V粘贴,@synthesize也添加好了,同时复制过来的还有[aButton release];,选中这行,剪切粘贴到dealloc方法中,大功告成。使用这个脚本不仅可以快速添加@property @synthesize,同时也避免了变量名写错所造成的一些问题。

XCode技巧之自定义TextMacros

这篇文章一样,以下基本都是从becoming productive in xcode中取得的

===Text  Macros===

添加自己的TextMacros(XCode 3.2.5测试通过,XCode 4 未测试)

在~/Library/Application Support/Developer/Shared/Xcode/下面新建Specifications文件夹,建立与/Developer/Applications/Xcode.app/Contents/PlugIns/TextMacros.xctxtmacro/Contents/Resources文件夹下类似的*.xctxtmacro文件即可

例子

 

(
  {
    Identifier = objc.hello;
    BasedOn = objc;
    IsMenuItem = YES;
    OnlyAtBOL= YES;
    Name = "Hello";
    TextString = "Hello, XCode!";
    CompletionPrefix = "hello";
    IncludeContexts = ("xcode.lang.objc");
  },
  {
    Identifier = objc.property;
    BasedOn = objc;
    IsMenuItem = YES;
    OnlyAtBOL= YES;
    Name = "@property Definition";
    TextString = "@property (nonatomic, retain) IBOutlet <#type#> *<#variable#>;";
    CompletionPrefix = "prop";
    IncludeContexts = ("xcode.lang.objc.interface");
  },
  {
    Identifier = objc.rectmake;
    BasedOn = objc;
    IsMenuItem = YES;
    OnlyAtBOL= YES;
    Name = "RectMake";
    TextString = "CGRect aRect = \n\tCGRectMake(<#x#>, <#y#>, <#width#>, <#height#>);";
    CompletionPrefix = "cgrm";
    IncludeContexts = ("xcode.lang.objc.block");
  }
)

上面的代码添加了三个textmacro:

第一个仅仅是测试用,代码中输入hello即有自动补全提示,补全成Hello,Xcode!。

第二个效果是输入prop,然后 ⌃. 自动补全成@property (nonatomic, retain) IBOutlet <#type#> *<#variable#>;的形式,具体实验一下就知道了。

第三个类似,不多说了。

首先注意一下OnlyAtBOL这一行,在新版本的XCode中如果不加上这一行则textmacro无法生效。具体请看这一段的解释:

There is a bug on TextMacros in the latest versions of Xcode (I encourage you to send a bugreport to Apple as I did)

In fact, only TextMacros that have the value “YES” for the key “OnlyAtBOL” will work. If this key is not present (or set to NO), the macro will not respond to autocompletion.
Actually, the new “OnlyAtBOL” key means “Only at Beginning Of Line”, so the autocompletion for this macro will only work if it is triggered in the beggining of a line. But in fact, quite every macro I need everyday comply to this constraint (ifelse, nslog, nss, …) so this is acceptable.
If you want those macros (that does not work because of the lack of OnlyAtBOL set to YES) to work again, you need to add/override their specifications in your “Application Support” directory (in a custom TextMacro file that override them), until the Xcode team fixes that bug.
 
 

XCode快捷键及小技巧

首先推荐个视频:Becoming Productive in XCode  链接里有个demo版的,完全版是收费的,处于对源作者的尊重,不放出下载地址,各位同学自行解决,推荐有能力的购买一下,还是物有所值的,下面这些基本都是视频中提到的

—Workspace—

⌘N          New project

⌘,             Preference

 

⌘E          Zoom editor in

⌥⇧⌘E       Zoom editor in fully

 

—文件切换—

⌘← →     历史编辑点切换

⌘D          Open Quickly,直接输入文件名,支持查看各种.h .m …

⌘↑          .h与.m切换

 

—File Navigation—

← →         按word移动光标,向右移动到word尾部,向左移动到word头部

⌘← →         移动到该行头/尾,

⇧⌥← →      高亮word

⌘← →      高亮行

⌘↑ ↓            移动到文件头/尾

L               光标所在行放置在编辑器中间

⌘L               Goto(行号)

 

2               打开method列表(应该叫Function Popup)(与系统的Spaces切换有冲突,看自己习惯了)

⌘D              BookMark

4               BookMark列表(BookMark Popup)

⌘M          打开Bookmarks Smart Group

 

⌘F              搜索

⌘G ⌘G   (搜索中)下一个/上一个

⌘F           跨文件搜索

+双击        以浮动窗口形式查看定文档

⌘+双击     查看相应文档

 

—Editing Source Code—

TAB             确认当前补全

  ,        补全列表

/               切换到下一个Placeholder

 

⌘←   →    折叠、展开代码段

⌘↑ ↓         折叠、展开所有method段

⌃⌥⌘F        打开、关闭当前代码段高亮功能

 

⌘/               快速注释、取消注释

⌘T           同时编辑当前文件中所有同一变量名

 

—Refactoring—

⌘J           Refractor对话框

 

—Building and Running—

⌘B              Build

⌘B           Build Result窗口

⌘=  ⌘=    查看warnings、errors

⌘Enter         Build & Go

⌘R           打开Console

⌃⌥⌘R        Clear Console      

 

⌥⇧⌘/        打开文档(这是快捷键么)

 

====Tips====

 

—Workspace–

分组(Group)是任意的,并不会真的产生这个目录

Smart Group很实用

右上角那个搜索是支持通配符和正则表达式的,点击放大镜边上的箭头

 

—File Navigation—

类似列表的东西都是支持键盘输入开头的几个字符来选择相应项目的

#pragma mark [something]     类似分隔线的东西,效果在Function Popup中可见,尤其是 #pragma mark – 的时候

 

// TODO: [something]

// MARK: [something]

// FIXME: [something]

// !!!:

// ???:

以上均可在Function Popup有相应效果

 

Editor部分右上角那个右边带箭头的C按钮 打开可查看Superclass(Navigator popup) 默认无快捷键

 

—Refactoring—

Extract:Refactor那个对话框的另一个功能是Extract 支持自动把当前函数中的一段提取到一个新的函数中,对代码重构很有用。使用方法:选择要提取的部分,按⌘J,选择Ectract,然后修改一下函数名即可,系统已经自动把所需的参数添加好了。 

 

—Building and Running—

Preference 》Debugging 》On Start 选择 Show Console 可以默认打开Console