2011年11月28日 星期一

IOS delegate你必須知道的事情

ios delegate你必須知道的事情

當你開始寫iOS程式不久,應該開始面對到很多的delegate,
不管是用別人的library或是自己寫library,可能都逃不了delegate。
為了怕有些人不知道什麼是delegate,在這邊簡單的介紹一下,
delegate中文叫做委託,通常會用在class內部把一些事件處理"委託"給別人去完成。
舉個例子,XML Parser可能他知道怎麼parse xml,但是parse到的東西要怎麼處理xml parser可能不知道。
所以NSXMLParser就提供了一個NSXMLParserDelegate給client去實作,
當parse到某個element的時候,就callback delegate所定義的message,
讓他client自己去決定怎麼去處理這個element。
好吧,我承認我解釋的很模糊,不過我這篇本來就不是要你搞懂什麼是delegate,
而是針對使用或是設計delegate的時候,可能會要注意的事情。

在我們的class中設計delegate的時候,我們通常會有幾個注意事項。
假設我的class叫做MyClass,那我們可能會有定義一個MyClassDelegate這個protocol當作我的delegate protocol。
而MyClass中我們可能是這樣寫。
@protocol MyClassDelegate  - (void) myClassOnSomeEvent:(MyClass*)myClass; @end  @interface MyClass {     id _delegate; } @property (nonatomic, assign) delegate; @end 
上面的code我們注意到delegate此property是定義為@property (assign)
為什麼我們不用retain而要用assign呢?
原因就是在於iOS的reference counting的環境中,我們必須解決circular count的問題。
讓我們來寫寫我們平常都怎麼用delegate的,下面的code我想大家應該不陌生
- (void)someAction {    myClass = [MyClass new];    myClass.delegate = self;    .... } 
這邊很快的就出現circular reference了
假設上面的code是寫在一個myViewController的物件當中,
之後一旦myViewController的reference count變成1的時候,
myViewController跟myClass這兩個兄弟兩只剩下互相retain,那就變成了孤島,也就因此造成了memory leak!!!


也因為這樣,iOS官方文件才會要建議我們所以的delegate都要用assign property。
也就是所謂"weak reference"的property,他的特色就是雖然會持有對方的reference,但是不會增加retain count。
如此下來,當myViewController的retain count變成0,則會dealloc。
同時在dealloc中,也一併把myClass release,則myClass也跟著被release。
- (void)dealloc {    [myClass release];    [super dealloc]; } 


事情就結束了嗎? 還沒有唷...
這邊還有一個大家常常忘記的重點,那就是上面的dealloc這樣寫會有潛在危險。
應該要改成這樣
- (void)dealloc {    myClass.delegate = nil;    [myClass release];    [super dealloc]; } 
你可能會很納悶,myClass不是馬上就會被release了嗎? 幹嘛要先把他的delegate設成nil?
那是因為我們假設myClass會馬上會被dealloc,但是現實狀況這個是不一定的,
有可能裡面內部有建個NSURLConnection,或是正在做某件事情而讓其他物件也retain myClass。
如果myClass沒有馬上dealloc,那他的myClass.delegate不就正指向一個不合法的位置了嗎? (此種pointer稱作dangling pointer)


解決方法是在MyViewController的dealloc中,在release myClass之前,
要先把原本指向自己的delegate改設成nil,這樣才可以避免crash發生。
在我之前寫的project,很大一部份的crash都是這樣造成的,因為這個問題通常不是每次都發生,
但是發生的時候確很難在重新複製,所以不可不慎啊。


但是很興奮的是到了iOS5中的Automatic Reference Counting這個問題可以有所改善。
在ARC中提出了一個新的weak reference的概念來取代原本的assign,
weak reference指到的物件若是已經因retain count歸零而dealloc了,則此weak reference也自動設成nil。
而原本舊的這種assign的作法,在ARC中叫做__unsafe_unretained,這只是為了相容iOS4以下的版本。

回顧重點:
如果你是寫library給別人用的,記得把你的delegate設成assign property,這樣才不會造成circular reference
當你是要始用別人的library,記得在你自己dealloc的時候,把delegate設成nil,以避免crash的事情發生。

source: http://webcache.googleusercontent.com/search?q=cache:dv0DZ9ilW3oJ:popcornylu.blogspot.com/2011/07/delegate.html+&cd=4&hl=zh-TW&ct=clnk&lr=lang_zh-CN%7Clang_zh-TW

2011年11月7日 星期一

IOS UITableView 重用机制浅析

iphone重用机制是苹果为了实现大量数据显示而采用的一种节省内存的机制,但是在实际使用过程中,会有以下问题:

1、使用addSubView在每项上添加视图的时候会有重叠的现象。例如,UITableView中的Cell ,如果在cell上添加视图,则在使用苹果的重用机制的时候,会重现重叠的现象。刚开始学习的时候,觉得这个重用不太合适,原因:在数据不是很多的时候,个人觉得使用不使用重用机制都可以,于是乎,干脆放弃使用这套机制。代码如下:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell != nil) {
[cell release];
}

但是在数据多的时候,感觉使用重用机制会好对你的程序的内存使用和优化都有很重要的作用。 但是这样的话,如果想再cell上添加东西的话,重叠现象会很严重。偶然间,使用xib给cell添加视图,发现不会出现重叠的现象。

具体步骤:

(1)新建一个基于UITableViewCell的类A和一个空白的xib。

(2)在A类中声明要添加的视图,例如IBOutlet UILabel *nameLabel,*timeLabel; ,注意:一要是使用IBOutlet。

(3)将xib中的view删除,拖一个 UITableViewCell,然后将这个UITableViewCell的类改为基于A。再把相应的视图添在UITableViewCell上,并且与A类内定义的变量进行连接。这样准备工作就完成了。

(4)使用方法:

A* cell = (A*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray* nib = [[NSBundle mainBundle] loadNibNamed:@"VideoCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}

然后只需在下面改变cell相应视图的属性就可以了。

补充:在使用地图MKMapView一会使用到重用机制,如果想要在MKPinAnnotationView添加视图的话,最好放弃那个重用机制,要不然效果会乱七八糟的~~

2、我相信很多人都会好奇这套重用机制怎么实现的?我也很好奇,一直没找到相关的文档。不过有些认为就是建一个队列或者池子,如果UITableViewCell过多的话,它就会自动清除一些cell,然后如果比较少,它就会自动在这个队列或池子里面添加元素,这样避免了许多使用相同的Style 的Cell。

PS:还是好好研究下C语言的内存管理吧,我觉得那个才是真真意义上的内存管理。OC内存管理还是比较简单的。

更正:今天查别的问题的时候偶尔看到一个老外写的UITableView的代码,发现了一种新的实现方式。实现方式如下:

1、cell的释放使用默认的释放:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[
UITableViewCell alloc] init....] autorelease];

UITextField *field = ...;//初始化

[cell addSubView:field];//添加

[field release];//释放

}

2、通过遍历修改UILabel属性

UITextField*field = nil;

for(UIView *v in cell.contentView.subviews)

{

if([v isMemberOfClass:[UILabel class]])

field = (UITextField *)v;

}

......//修改field的属性

这种方法不敢说有多好,但是给我们提供了一种解决方案,看大家的喜好了``` 例如不喜欢xib的,可以尝试下.

source: http://hi.baidu.com/%CB%E6%B7%E7_1989/blog/item/077c8a944ae7a69ca877a41d.html

IOS内存管理

内存管理

我一直惊叹iOS和Objective-C内存处理能力,例如iPad版本的iWork,Pages应用就是一个内存处理技术应用的鬼斧神工之作。想想256M内存的iPad,可以带来如此的华丽的界面同时获得如此流畅的用户体验,真是不简单。原因就是iOS一直提倡开发者在有限硬件资源内写出最优化的代码,使用CPU最少,占用内存最小。(以下代码适用于iOS SDK 4.1, 由于新SDK 4.2对内存使用有新改动,所以可能有不同。。。)

  1. 尽量少的UIView层
    通常我们喜欢把很多控件层(UILabel,UIButton,UIView等)一起放到一个大的UIView容器来显示我们的内容,这个方法一般是可以的,但是如果要经常重新刷新内容的大区域界面,多数发生在iPad的应用中,这个方法会带来过多的内存使用和动画的延迟(比较卡),例如,scrollview的动画比较卡,又或者,经常收到内存警告。其中一个重要原因是每个控件,特别是透明底的,会多次重新绘制(drawRect)过多。其解决办法是,尽量将几个控件合并到一个层上来显示,这样系统会减少系统调用drawRect,从而带来性能上的提升。
    很简单的一个例子,就是iNotes提供手写功能,用户可以在iPad屏幕上写出不同的笔画,开始的设计是,用户每写一划,iNotes就会生成一个新的透明底UIView来保持这个笔画,用户写了10笔,系统就生产了10个UIView,每个view的大小都是整个屏幕的,以便用户的undo操作。这个方案带来严重的内存问题,因为系统将每个层都保持一个bitmap图,一个像素需要4bit来算,一个层的大小就是 4x1024x768 ~ 3M, 10个层就是 10x3M = 30M,很明显,iPad很快爆出内存警告。
    这个例子最后的方案是,所有笔画都画在同一个层,iNotes可以保存笔画的点进行undo操作。这样的方案就是无论用户画多少笔画,界面重画需要的资源都是一样的。
  2. 显示最佳尺寸的图片
    很多程序员比较懒,网络上拿下来的图片,直接就用UIImageView将其显示给用户,这样的后果就是,程序需要一直保存着大尺寸的图片到内存。解决办法应该是先将图片缩小到需要显示的尺寸,释放大尺寸图片的内存,然后再显示到界面给用户。
  3. 尽量使用图片pattern,而不是一张大的图片
    例如,很多界面设计者喜欢在界面上放一个大底图,但这个底图是老是占用着内存的,最佳方案是,设计出一个小的pattern图,然后用这个方案显示成底图。
    UIImage *smallImage = [[UIImage alloc] initWithContentsOfFile:path];
    backgroundView.backgroundColor = [UIColor colorWithPatternImage:smallImage];
    [smallImage release];
  4. 使用完资源后,立即释放
    一般objective-c的习惯是,用完的资源要立即释放,因为明白什么时候用完某个资源的是程序员你自己。例如,我们要读较大的图片,把它缩小后,显示到界面去。当大图片使用完成后,应该立即释放。代码如下:
    UIImage *fullscreenImage = [[UIImage alloc] initWithContentOfFile:path];
    UIImage *smallImage = [self resizeImage:fullscreenImage];
    [fullscreenImage release];
    imageView.image = smallImage;


    ......
  5. 循环中大量生成的自动释放autorelease对象,可以考虑使用autorelease pool封装。代码范例:
    for(UIView *subview in bigView.subviews) {
    // 使用autorelease pool自动释放对象池
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    UIImageView *imageView = (UIImageView *)subview;

    // subview处理代码
    .......

    // 销毁自动释放对象
    [pool drain];
    }

    自动释放对象池把每个循环内生成的临时对象使用完后立即释放

以上的意见是本人多年来编写iPad/iPhone程序的经验,另外iOS4.0的multi-tasking特性发布后,程序可以被调入后台运行,苹果 工程师的意见是,进入后台运行时,你的应用应该释放掉能释放的对象,尽量保持在16M左右,这样别的程序运行时才不容易把你的应用挤掉。

source: http://www.cocoachina.com/iphonedev/sdk/2011/0621/2962.html

2011年11月6日 星期日

iPhone/Mac Objective-C內存管理教程和原理剖析

前言
初学objectice-C的朋友都有一个困惑,总觉得对objective-C的内存管理机制琢磨不透,程序经常内存泄漏或莫名其妙的崩溃。我在这里总结了自己对objective-C内存管理机制的研究成果和经验,写了这么一个由浅入深的教程。希望对大家有所帮助,也欢迎大家一起探讨。
此文涉及的内存管理是针对于继承于NSObject的Class。
一 基本原理
Objective-C的内存管理机制与.Net/Java那种全自动的垃圾回收机制是不同的,它本质上还是C语言中的手动管理方式,只不过稍微加了一些自动方法。
1 Objective-C的对象生成于堆之上,生成之后,需要一个指针来指向它。
ClassA *obj1 = [[ClassA alloc] init];
2 Objective-C的对象在使用完成之后不会自动销毁,需要执行dealloc来释放空间(销毁),否则内存泄露。
[obj1 dealloc];
这带来了一个问题。下面代码中obj2是否需要调用dealloc?
ClassA *obj1 = [[ClassA alloc] init];
ClassA *obj2 = obj1;
[obj1 hello]; //输出hello
[obj1 dealloc];
[obj2 hello]; //能够执行这一行和下一行吗?
[obj2 dealloc];
不能,因为obj1和obj2只是指针,它们指向同一个对象,[obj1 dealloc]已经销毁这个对象了,不能再调用[obj2 hello]和[obj2 dealloc]。obj2实际上是个无效指针。
如何避免无效指针?请看下一条。
3 Objective-C采用了引用计数(ref count或者retain count)。对象的内部保存一个数字,表示被引用的次数。例如,某个对象被两个指针所指向(引用)那么它的retain count为2。需要销毁对象的时候,不直接调用dealloc,而是调用release。release会让retain count减1,只有retain count等于0,系统才会调用dealloc真正销毁这个对象。
ClassA *obj1 = [[ClassA alloc] init]; //对象生成时,retain count = 1
[obj1 release]; //release使retain count减1,retain count = 0,dealloc自动被调用,对象被销毁
我们回头看看刚刚那个无效指针的问题,把dealloc改成release解决了吗?
ClassA *obj1 = [[ClassA alloc] init]; //retain count = 1
ClassA *obj2 = obj1; //retain count = 1
[obj1 hello]; //输出hello
[obj1 release]; //retain count = 0,对象被销毁
[obj2 hello];
[obj2 release];
[obj1 release]之后,obj2依然是个无效指针。问题依然没有解决。解决方法见下一条。
4 Objective-C指针赋值时,retain count不会自动增加,需要手动retain。
ClassA *obj1 = [[ClassA alloc] init]; //retain count = 1
ClassA *obj2 = obj1; //retain count = 1
[obj2 retain]; //retain count = 2
[obj1 hello]; //输出hello
[obj1 release]; //retain count = 2 – 1 = 1
[obj2 hello]; //输出hello
[obj2 release]; //retain count = 0,对象被销毁
问题解决!注意,如果没有调用[obj2 release],这个对象的retain count始终为1,不会被销毁,内存泄露。(1-4可以参考附件中的示例程序memman-no-pool.m)
这样的确不会内存泄露,但似乎有点麻烦,有没有简单点的方法?见下一条。