iOS开发进阶


iOS多线程编程之NSThread的使用

1、简介:


1.1 iOS有三种多线程编程的技术,分别是:

1.、 NSThread 

2、Cocoa NSOperation (iOS多线程编程之NSOperation和NSOperationQueue的使用

3、GCD  全称:Grand Central Dispatch( iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用

这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的。

这篇我们主要介绍和使用NSThread,后面会继续2、3 的讲解和使用。


1.2 三种方式的有缺点介绍:



NSThread:

优点:NSThread 比其他两个轻量级

缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销

NSThread实现的技术有下面三种:

Technology

Description

Cocoa threads

Cocoa implements threads using the NSThread class. Cocoa also provides methods on NSObject for spawning new threads and executing code on already-running threads. For more information, see “Using NSThread” and “Using NSObject to Spawn a Thread.”

POSIX threads

POSIX threads provide a C-based interface for creating threads. If you are not writing a Cocoa application, this is the best choice for creating threads. The POSIX interface is relatively simple to use and offers ample flexibility for configuring your threads. For more information, see “Using POSIX Threads”

Multiprocessing Services

Multiprocessing Services is a legacy C-based interface used by applications transitioning from older versions of Mac OS. This technology is available in OS X only and should be avoided for any new development. Instead, you should use the NSThread class or POSIX threads. If you need more information on this technology, see Multiprocessing Services Programming Guide.

一般使用cocoa thread 技术。

Cocoa operation 

优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。Cocoa operation 相关的类是 NSOperation ,NSOperationQueue。NSOperation是个抽象类,使用它必须用它的子类,可以实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。创建NSOperation子类的对象,把对象添加到NSOperationQueue队列里执行。


GCD

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。在iOS4.0开始之后才能使用。GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术。现在的iOS系统都升级到6了,所以不用担心该技术不能使用。


介绍完这三种多线程编程方式,我们这篇先介绍NSThread的使用。



2、NSThread的使用


2.1 NSThread 有两种直接创建方式:

- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument

+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

第一个是实例方法,第二个是类方法

1、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];
2、NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                        selector:@selector(doSomething:)
                                        object:nil];
[myThread start];

2.2参数的意义:

selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。

target  :selector消息发送的对象

argument:传输给target的唯一参数,也可以是nil

第一种方式会直接创建线程并且开始运行线程,
第二种方式是先创建线程对象,然后再运行线程操作,在运行线程操作前可以设置线程的优先级等线程信息

2.3 PS:不显式创建线程的方法:

用NSObject的类方法  performSelectorInBackground:withObject: 创建一个线程:
[Obj performSelectorInBackground:@selector(doSomething) withObject:nil];



2.4 下载图片的例子:

2.4.1  新建singeView app

新建项目,并在xib文件上放置一个imageView控件。
按住control键拖到viewControll er.h文件中创建imageView IBOutlet 

ViewController.m中实现:

//
//  ViewController.m
//  NSThreadDemo
//
//  Created by rongfzh on 12-9-23.
//  Copyright (c) 2012年 rongfzh. All rights reserved.
//

#import "ViewController.h"
#define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"
@interface ViewController ()

@end

@implementation ViewController

-(void)downloadImage:(NSString *) url{
    NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
    UIImage *image = [[UIImage alloc]initWithData:data];
    if(image == nil){
       
    }else{
        [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
    }
}

-(void)updateUI:(UIImage*) image{
    self.imageView.image = image;
}


- (void)viewDidLoad
{
    [super viewDidLoad];
   
//    [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL];
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL];
    [thread start];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

2.4.2线程间通讯

线程下载完图片后怎么通知主线程更新界面呢?

[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];

performSelectorOnMainThread是NSObject的方法,除了可以更新主线程的数据外,还可以更新其他线程的比如:

用:performSelector:onThread:withObject:waitUntilDone: 

运行下载图片:

2.3 线程同步

我们演示一个经典的卖票的例子来讲NSThread的线程同步:

.h
#import <UIKit/UIKit.h>

@class ViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>
{
    int tickets;
    int count;
    NSThread* ticketsThreadone;
    NSThread* ticketsThreadtwo;
    NSCondition* ticketsCondition;
    NSLock *theLock;
}
@property (strong, nonatomic) UIWindow *window;

@property (strong, nonatomic) ViewController *viewController;

@end


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   
    tickets = 100;
    count = 0;
    theLock = [[NSLock alloc] init];
    // 锁对象
    ticketsCondition = [[NSCondition alloc] init];
    ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadone setName:@"Thread-1"];
    [ticketsThreadone start];
   
   
    ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadtwo setName:@"Thread-2"];
    [ticketsThreadtwo start];
   
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

- (void)run{
    while (TRUE) {
        // 上锁
//        [ticketsCondition lock];
        [theLock lock];
        if(tickets >= 0){
            [NSThread sleepForTimeInterval:0.09];
            count = 100 - tickets;
            NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,[[NSThread currentThread] name]);
            tickets--;
        }else{
            break;
        }
        [theLock unlock];
//        [ticketsCondition unlock];
    }
}

如果没有线程同步的lock,卖票数可能是-1.加上lock之后线程同步保证了数据的正确性。
上面例子我使用了两种锁,一种NSCondition ,一种是:NSLock。 NSCondition我已经注释了。

线程的顺序执行

他们都可以通过

[ticketsCondition signal]; 发送信号的方式,在一个线程唤醒另外一个线程的等待。


比如:
#import "AppDelegate.h"

#import "ViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   
    tickets = 100;
    count = 0;
    theLock = [[NSLock alloc] init];
    // 锁对象
    ticketsCondition = [[NSCondition alloc] init];
    ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadone setName:@"Thread-1"];
    [ticketsThreadone start];
   
    ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadtwo setName:@"Thread-2"];
    [ticketsThreadtwo start];
   
    NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil];
    [ticketsThreadthree setName:@"Thread-3"];
    [ticketsThreadthree start];   
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

-(void)run3{
    while (YES) {
        [ticketsCondition lock];
        [NSThread sleepForTimeInterval:3];
        [ticketsCondition signal];
        [ticketsCondition unlock];
    }
}

- (void)run{
    while (TRUE) {
        // 上锁
        [ticketsCondition lock];
        [ticketsCondition wait];
        [theLock lock];
        if(tickets >= 0){
            [NSThread sleepForTimeInterval:0.09];
            count = 100 - tickets;
            NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,[[NSThread currentThread] name]);
            tickets--;
        }else{
            break;
        }
        [theLock unlock];
        [ticketsCondition unlock];
    }
}

wait是等待,我加了一个 线程3 去唤醒其他两个线程锁中的wait


其他同步

我们可以使用指令 @synchronized 来简化 NSLock的使用,这样我们就不必显示编写创建NSLock,加锁并解锁相关代码。
- (void)doSomeThing:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}
还有其他的一些锁对象,
比如:循环锁NSRecursiveLock,条件锁NSConditionLock,分布式锁NSDistributedLock等等,可以自己看官方文档学习



使用 NSOperation的方式有两种,

一种是用定义好的两个子类:

NSInvocationOperation 和 NSBlockOperation。

另一种是继承NSOperation


如果你也熟悉Java,NSOperation就和java.lang.Runnable接口很相似。
和Java的Runnable一样,NSOperation也是设计用来扩展的,只需继承重写NSOperation的一个方法main。相当与java 中Runnalbe的Run方法。然后把NSOperation子类的对象放入NSOperationQueue队列中,该队列就会启动并开始处理它。

NSInvocationOperation例子:

和前面一篇博文一样,我们实现一个下载图片的例子。新建一个Single View app,拖放一个ImageView控件到xib界面。

实现代码如下:

#import "ViewController.h"
#define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self
                                                                           selector:@selector(downloadImage:)
                                                                             object:kURL];
   
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:operation];
     // Do any additional setup after loading the view, typically from a nib.
}

-(void)downloadImage:(NSString *)url{
    NSLog(@"url:%@", url);
    NSURL *nsUrl = [NSURL URLWithString:url];
    NSData *data = [[NSData alloc]initWithContentsOfURL:nsUrl];
    UIImage * image = [[UIImage alloc]initWithData:data];
    [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
}
-(void)updateUI:(UIImage*) image{
    self.imageView.image = image;
}
  1. viewDidLoad方法里可以看到我们用NSInvocationOperation建了一个后台线程,并且放到NSOperationQueue中。后台线程执行downloadImage方法。
  2. downloadImage 方法处理下载图片的逻辑。下载完成后用performSelectorOnMainThread执行主线程updateUI方法。
  3. updateUI 并把下载的图片显示到图片控件中。

第二种方式继承NSOperation 

在.m文件中实现main方法,main方法编写要执行的代码即可。

如何控制线程池中的线程数?

队列里可以加入很多个NSOperation, 可以把NSOperationQueue看作一个线程池,可往线程池中添加操作(NSOperation)到队列中。线程池中的线程可看作消费者,从队列中取走操作,并执行它。

通过下面的代码设置:
[queue setMaxConcurrentOperationCount:5];
线程池中的线程数,也就是并发操作数。默认情况下是-1,-1表示没有限制,这样会同时运行队列中的全部的操作。




介绍:

Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统。这建立在任务并行执行的线程池模式的基础上的。它首次发布在Mac OS X 10.6 ,iOS 4及以上也可用。


设计:

GCD的工作原理是:让程序平行排队的特定任务,根据可用的处理资源,安排他们在任何可用的处理器核心上执行任务。

一个任务可以是一个函数(function)或者是一个block。 GCD的底层依然是用线程实现,不过这样可以让程序员不用关注实现的细节。

GCD中的FIFO队列称为dispatch queue,它可以保证先进来的任务先得到执行
dispatch queue分为下面三种:


Serial      

又称为private dispatch queues,同时只执行一个任务。Serial queue通常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然它们各自是同步执行的,但Serial queue与Serial queue之间是并发执行的。


Concurrent

又称为global dispatch queue,可以并发地执行多个任务,但是执行完成的顺序是随机的。


Main dispatch queue

它是全局可用的serial queue,它是在应用程序主线程上执行任务的。

我们看看dispatch queue如何使用


1、常用的方法dispatch_async

为了避免界面在处理耗时的操作时卡死,比如读取网络数据,IO,数据库读写等,我们会在另外一个线程中处理这些操作,然后通知主线程更新界面。

用GCD实现这个流程的操作比前面介绍的NSThread  NSOperation的方法都要简单。代码框架结构如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 耗时的操作
    dispatch_async(dispatch_get_main_queue(), ^{
        // 更新界面
    });
});
如果这样还不清晰的话,那我们还是用上两篇博客中的下载图片为例子,代码如下:
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];
        NSData * data = [[NSData alloc]initWithContentsOfURL:url];
        UIImage *image = [[UIImage alloc]initWithData:data];
        if (data != nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                self.imageView.image = image;
             });
        }
    });

是不是代码比NSThread  NSOperation简洁很多,而且GCD会自动根据任务在多核处理器上分配资源,优化程序。

系统给每一个应用程序提供了三个concurrent dispatch queues。这三个并发调度队列是全局的,它们只有优先级的不同。因为是全局的,我们不需要去创建。我们只需要通过使用函数dispath_get_global_queue去得到队列,如下:


dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  

这里也用到了系统默认就有一个串行队列main_queue

dispatch_queue_t mainQ = dispatch_get_main_queue();  

虽然dispatch queue是引用计数的对象,但是以上两个都是全局的队列,不用retain或release。

2、dispatch_group_async的使用

dispatch_group_async可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了。下面是一段例子代码:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"group1");
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"group2");
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"group3");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"updateUi");
    });
    dispatch_release(group);

dispatch_group_async是异步的方法,运行后可以看到打印结果:

2012-09-25 16:04:16.737 gcdTest[43328:11303] group1
2012-09-25 16:04:17.738 gcdTest[43328:12a1b] group2
2012-09-25 16:04:18.738 gcdTest[43328:13003] group3
2012-09-25 16:04:18.739 gcdTest[43328:f803] updateUi

每个一秒打印一个,当第三个任务执行后,upadteUi被打印。



3、dispatch_barrier_async的使用

dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行

例子代码如下:

   
 dispatch_queue_t queue = dispatch_queue_create("gcdtest.rongfzh.yc", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"dispatch_async1");
    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:4];
        NSLog(@"dispatch_async2");
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"dispatch_barrier_async");
        [NSThread sleepForTimeInterval:4];

    });
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"dispatch_async3");
    });
打印结果:

2012-09-25 16:20:33.967 gcdTest[45547:11203] dispatch_async1

2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_async2

2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_barrier_async

2012-09-25 16:20:40.970 gcdTest[45547:11303] dispatch_async3

请注意执行的时间,可以看到执行的顺序如上所述。


4、dispatch_apply 

执行某个代码片段N次。

dispatch_apply(5, globalQ, ^(size_t index) {
    // 执行5次
});



 iOS的应用程序的生命周期,还有程序是运行在前台还是后台,应用程序各个状态的变换,这些对于开发者来说都是很重要的。 iOS系统的资源是有限的,应用程序在前台和在后台的状态是不一样的。在后台时,程序会受到系统的很多限制,这样可以提高电池的使用和用户体验。

//开发app,我们要遵循apple公司的一些指导原则,原则如下:


1、应用程序的状态

状态如下:

Not running  未运行  程序没启动

Inactive          未激活        程序在前台运行,不过没有接收到事件。在没有事件处理情况下程序通常停留在这个状态

Active             激活           程序在前台运行而且接收到了事件。这也是前台的一个正常的模式

Backgroud     后台           程序在后台而且能执行代码,大多数程序进入这个状态后会在在这个状态上停留一会。时间到之后会进入挂起状态(Suspended)。有的程序经过特殊的请求后可以长期处于Backgroud状态

Suspended    挂起           程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。


下图是程序状态变化图:


各个程序运行状态时代理的回调:


- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
告诉代理进程启动但还没进入状态保存

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
告诉代理启动基本完成程序准备开始运行

- (void)applicationWillResignActive:(UIApplication *)application
    当应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件,比如来电话了

- (void)applicationDidBecomeActive:(UIApplication *)application 
     当应用程序入活动状态执行,这个刚好跟上面那个方法相反

- (void)applicationDidEnterBackground:(UIApplication *)application
    当程序被推送到后台的时候调用。所以要设置后台继续运行,则在这个函数里面设置即可

- (void)applicationWillEnterForeground:(UIApplication *)application
当程序从后台将要重新回到前台时候调用,这个刚好跟上面的那个方法相反。

- (void)applicationWillTerminate:(UIApplication *)application
当程序将要退出是被调用,通常是用来保存数据和一些退出前的清理工作。这个需要要设置
UIApplicationExitsOnSuspend的键值。

- (void)applicationDidFinishLaunching:(UIApplication*)application
当程序载入后执行

在上面8个方法对应的方法中键入NSLog打印。

现在启动程序看看执行的顺序:

启动程序
lifeCycle[40428:11303] willFinishLaunchingWithOptions
lifeCycle[40428:11303] didFinishLaunchingWithOptions
lifeCycle[40428:11303] applicationDidBecomeActive

按下home键

lifeCycle[40428:11303] applicationWillResignActive
lifeCycle[40428:11303] applicationDidEnterBackground

双击home键,再打开程序

lifeCycle[40428:11303] applicationWillEnterForeground
lifeCycle[40428:11303] applicationDidBecomeActive


2、应用程序的生命周期

2.1、加载应用程序进入前台


2.2、加载应用程序进入后台



2.3、关于main函数


main函数是程序启动的入口,在iOS app中,main函数的功能被最小化,它的主要工作都交给了UIKit framework

#import <UIKit/UIKit.h>

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class]));
    }
}
UIApplicationMain函数有四个参数,你不需要改变这些参数值,不过我们也需要理解这些参数和程序是如何开始的。

argc 和argv参数包含了系统带过来的启动时间。 
第三个参数确定了主要应用程序类的名称,这个参数指定为nil,这样UIKit就会使用默认的程序类UIApplication。
第四个参数是程序自定义的代理类名,这个类负责系统和代码之间的交互。它一般在Xcode新建项目时会自动生成。


另外 UIApplicationMain函数加载了程序主界面的文件。虽然这个函数加载了界面文件,但是没有放到应用程序的windows上,你需要在Delegate的 application:willFinishLaunchingWithOptions方法中加载它。


一个应用程序可以有一个主的storyboard文件或者有一个主的nib文件,但不能同时有两个存在。

如果程序在启动时没有自动加载主要的故事版或nib文件,你可以在application:willFinishLaunchingWithOptions方法里准备windows的展示。


3、响应中断

3.1 当一个基于警告式的中断发生时,比如有电话打进来了,这是程序会临时进入inactive状态,这用户可以选择如何处理这个中断,流程如下图:


在iOS5,通知不会把程序变成为激活状态,通知会显示在状态栏上,如果你;拉下状态栏,程序会变成inactive,把状态栏放回去,程序变回active。

按锁屏键也是另外一种程序的中断,当你按下锁屏键,系统屏蔽了所有触摸事件,把app放到了后台,这时app状态是 inactive,并进入后台。


3.2 当有这些中断时,我们的app该怎么办呢?我们应该在applicationWillResignActive:方法中:

  • 停止timer 和其他周期性的任务
  • 停止任何正在运行的请求
  • 暂停视频的播放
  • 如果是游戏那就暂停它
  • 减少OpenGL ES的帧率
  • 挂起任何分发的队列和不重要的操作队列(你可以继续处理网络请求或其他时间敏感的后台任务)。
当程序回到active状态 ,     applicationDidBecomeActive:    方法应该上面提到的任务重新开始,比如重新开始timer, 继续分发队列,提高OpenGL ES的帧率。不过游戏要回到暂停状态,不能自动开始。


4、转到后台运行

4.1 如图所示:


PS:只有在IOS4以上系统或者支持多任务的设备才能后台运行。不然会直接结束状态。


4.2 当应用程序进入后台时,我们应该做写什么呢?

  • 保存用户数据或状态信息,所有没写到磁盘的文件或信息,在进入后台时,最后都写到磁盘去,因为程序可能在后台被杀死,
  • 释放尽可能释放的内存
applicationDidEnterBackgound: 方法有大概5秒的时间让你完成这些任务。如果超过时间还有未完成的任务,你的程序就会被终止而且从内存中清除。如果还需要长时间的运行任务,可以调用   beginBackgroundTaskWithExpirationHandler  方法去请求后台运行时间和启动线程来运行长时间运行的任务。

4.3 应用程序在后台时的内存使用
在后台时,每个应用程序都应该释放最大的内存。系统努力的保持更多的应用程序在后台同时 运行。不过当内存不足时,会终止一些挂起的程序来回收内存,那些内存最大的程序首先被终止。
事实上,应用程序应该的对象如果不再使用了,那就应该尽快的去掉强引用,这样编译器可以回收这些内存。如果你想缓存一些对象提升程序的性能,你可以在进入后台时,把这些对象去掉强引用。

下面这样的对象应该尽快的去掉强引用:
  • 图片对象
  • 你可以重新加载的 大的视频或数据文件
  • 任何没用而且可以轻易创建的对象
在后台时,为了减少程序占用的内存,系统会自动在回收一些系统帮助你开辟的内存。比如:
系统回收Core Animation的后备存储。
去掉任何系统引用的缓存图片
去掉系统管理数据缓存强引用

5 、返回前台运行

流程如图所示:

当app处于挂起状态时,它是不能执行任何代码的。因此它不能处理在挂起期间发过来的通知,比如方向改变,时间改变,设置的改变还有其他影响程序展现的或状态的通知。在程序返回后台或前台是,程序都要正确的处理这些通知。


6、程序的终止

程序只要符合以下情况之一,只要进入后台或挂起状态就会终止:
iOS4.0以前的系统
app是基于iOS4.0之前系统开发的。
设备不支持多任务
在Info.plist文件中,程序包含了  UIApplicationExitsOnSuspend  键。
app如果终止了  ,系统会调用app的代理的方法  applicationWillTerminate:   这样可以让你可以做一些清理工作。你可以保存一些数据或app的状态。这个方法也有5秒钟的限制。超时后方法会返回程序从内存中清除。
注意:用户可以手工关闭应用程序。


7、 The Main Run Loop  主运行循环

Main Run Loop负责处理用户相关的事件。UIApplication对象在程序启动时启动main run Loop,它处理事件和更新视图的界面。看Main Run Loop就知道,它是运行在程序的主线程上的。这样保证了接收到用户相关操作的事件是按顺序处理的。

Main Run Loop  处理事件的架构图:

用户操作设备,相关的操作事件被系统生成并通过UIKit的指定端口分发。事件在内部排成队列,一个个的分发到Main run loop 去做处理。UIApplication对象是第一个接收到时间的对象,它决定事件如何被处理。触摸事件分发到主窗口,窗口再分发到对应出发触摸事件的View。其他的事件通过其他途径分发给其他对象变量做处理。

大部分的事件可以在你的应用里分发,类似于触摸事件,远程操控事件(线控耳机等)都是由app的  responder objects 对象处理的。Responder objects 在你的app里到处都是,比如:UIApplication 对象。view对象,view controller 对象,都是resopnder objects。大部分事件的目标都指定了resopnder object,不过事件也可以传递给其他对象。比如,如果view对象不处理事件,可以传给父类view或者view controller。




Cocoa框架是iOS应用程序的基础,了解Cocoa框架,对开发iOS应用有很大的帮助。


1、Cocoa是什么?

Cocoa是OS X和 iOS操作系统的程序的运行环境。

是什么因素使一个程序成为Cocoa程序呢?不是编程语言,因为在Cocoa开发中你可以使用各种语言;也不是开发工具,你可以在命令行上就可以创建Cocoa程序。Cocoa程序可以这么说,它是由一些对象组成,而这些对象的类最后都是继承于它们的根类 :NSObject。而且它们都是基于Objective-C运行环境的。


1.1、Cocoa框架

iOS中,Cocoa众多框架中最重要最基本的两个框架是:Foundation 和 UIKit。

Foundation 和界面无关,也可以说和界面无关的类基本是Foundation框架的,和界面相关的是UIKit框架。

这两个框架在系统中处于的位置如图:



1.2、Foundation框架

好吧,那我们看看两个框架的类组织架构图,第一个先看Foundation的,三个图,包括了Foundation所以的类,图中灰色的是iOS不支持的,灰色部分是OS X系统的。




将上图Foundation框架中的类进行逻辑分类如下:

  1. 值对象
  2. 集合 
  3. 操作系统服务 包括下面三个:文件系统和URL   进程间通讯。 这个范畴中的大部分类代表不同的系统端口、套接字、和名字服务器,对实现底层的IPC很有用。NSPipe代表一个BSD管道,即一种进程间的单向通讯通道。   线程和子任务。 NSThread类使您可以创建多线程的程序,而各种锁(lock)类则为彼此竞争的线程在访问进程资源时提供各种控制机制。通过NSTask,您的程序可以分出      一个子进程来执行其它工作或进行进度监控。
  4. 通知
  5. 归档和序列化
  6. 表达式和条件判断
  7. Objective-C语言服务

1.3 UIKit框架

应用程序可以通过三种方式使用UIKit创建界面
  1.   在用户界面工具(interface Buidler)从对象库里拖拽窗口,视图或者其他的对象使 用。
  2.   用代码创建
  3.   通过继承UIView类或间接继承UIView类实现自定义用户界面

框架类组织架构图:



在图中可以看出,responder 类是图中最大分支的根类,UIResponder为处理响应事件和响应链,定义了界面和默认行为。当用户用手指滚动列表或者在虚拟键盘上输入时,UIKit就生成时间传送给UIResponder响应链,直到链中有对象处理这个事件。相应的核心对象,比如:UIApplication  ,UIWindow,UIView都直接或间接的从UIResponder继承。


2、Cocoa对象

2.1 Objective-C是面向对象的语言

Objective-C和Java C++一样,有封装,继承,多态,重用。但是它不像C++那样有重载操作法、模版和多继承,也没有Java的垃圾回收机制。

2.2 Objective-C的优点

Objective-C语言有C++ Java等面向对象的特点,那是远远不能体现它的优点的。Objective-C的优点是它是动态的。动态能力有三种:

动态类-运行时确定类的对象

动态绑定-运行时确定要调用的方法

动态加载--运行时为程序加载新的模块

2.3 动态能力相关的isa指针

每个Objective-C对象都有一个隐藏的数据结构,这个数据结构是Objective-C对象的第一个成员变量,它就是isa指针。这个指针指向哪呢?它指向一个类对象(class object  记住它是个对象,是占用内存空间的一个变量,这个对象在编译的时候编译器就生成了,专门来描述某个类的定义),这个类对象包含了Objective-C对象的一些信息(为了区分两个对象,我把前面提到的对象叫Objective-C对象),包括Objective-C对象的方法调度表,实现了什么协议等等。这个包含信息就是Objective-C动态能力的根源了。

那我们看看isa指针类型的数据结构是什么样的?如果抛开NSObject对象的其他的成员数据和变量,NSObject可以看成这样:

@interface NSObject <NSObject> {
     Class    isa;



不考虑@interface关键字在编译时的作用,可以把NSObject更接近C语言结构表示为:
struct NSObject{
   Class isa;
}

Class是用typedef 定义的
typedef struct objc_class *Class;

那NSObject可以这么写了
struct NSObject{
  objc_class *isa
}

那objc_class的结构是什么样的呢?大概是这样的:
struct objc_class {
     Class isa;
    
     Class super_class;
    
     const char *name;
    
     long version;
     long info;
    
     long instance_size;
     struct objc_ivar_list *ivars;
     struct objc_method_list **methodLists;
    
     struct objc_cache *cache;
     struct objc_protocol_list *protocols;  
}

这里会看到,在这个结构体里还有一个isa指针,又是一重指向,是不是有种到了盗梦空间的感觉。不用紧张,take easy,不会有那么多层次的,这里的isa指针指向的是元类对象(metaclass object),带有元字,证明快到头了。那元对象有啥用呢?它用来存储的关于类的版本,名字,类方法等信息。所有的元类对象(metaclass object)都指向 NSObject的元类对象,到头还是NSObject。一共三次:类对象->元类对象->NSObject元类对象。

       为了得到整个类组织架构的信息,objc_class结构里定义了第二个成员变量Class super_class,它指向父类的类对象。说了这么多,可能关系缕不清楚,有道是一张图胜过千言万语


图中可以看出,D3继承D2,D2继承D1,D1最终继承NSObject。
下图从D3的一个对象开始,排列出D3 D2 D1 NSObject 类对象,元类对象等关系。


图中的箭头都是指针的指向。


2.4 根类 NSObject

NSObject是大部分Objective-C类的根类,它没有父类。其它类继承NSObject,访问Objective-C运行时系统的基本接口,这样其他类的实例可以获得运行时的能力。


2.4.1 根类和根类协议

NSObject不但是个类名,NSObject也是个协议的名称,参考NSObject协议 , NSObject协议指定了根类必须实现的接口。


2.4.2 根类的主要方法:

  •     分配、初始化、和复制:
alloc和allocWithZone:方法用于从某内存区域中分配一个对象内存,并使对象指向其运行时的类定义。
init方法是对象初始化。
new是一个将简单的内存分配和初始化结合起来的方法。
copy和copyWithZone:

  • 对象的保持和清理:
retain方法增加对象的保持次数。
release方法减少对象的保持次数。
autorelease方法也是减少对象的保持次数,但是以推迟的方式。
retainCount方法返回对当前的保持次数。
dealloc方法由需要释放对象的实例变量以及释放动态分配的内存的类实现。

  • 内省和比较

NSObjec有很多方法可以查询对象的运行时信息。这些内省方法有助于找出对象在类层次中的位置,确定对象是否实现特定的方法,以及测试对象是否遵循某种协议。下面是部分方法 
superclass和class方法(实现为类和实例方法)分别以Class对象的形式返回接收者的父类和类。
您可以通过isKindOfClass:和isMemberOfClass:方法来确定对象属于哪个类。后者用于测试接收者是否为指定类的实例。isSubclassOfClass:类方法则用于测试类的继承性。
respondsToSelector:方法用于测试接收者是否实现由选择器参数标识的方法。instancesRespondToSelector:类方法则用于测试给定类的实例是否实现指定的方法。
conformsToProtocol:方法用于测试接收者(对象或类)是否遵循给定的协议。
isEqual:和hash方法用于对象的比较。
description方法允许对象返回一个内容描述字符串;这个方法的输出经常用于调试(“print object”命令),以及在格式化字符串中和“%@”指示符一起表示对象。

  • 对象的编码和解码

下面的方法和对象的编解码(作为归档过程的一部分)有关:
encodeWithCoder:和initWithCoder:是NSCoding协议仅有的方法。前者使对象可以对其实例变量进行编码,后者则使对象可以根据解码过的实例变量对自身进行初始化。
NSObject类中声明了一些于对象编码有关的方法:classForCoder:、replacementObjectForCoder:、和awakeAfterUsingCoder:。

  • 消息的转发

 forwardInvocation:允许一个对象将消息转发给另一个对象。

  • 消息的派发 

在performSelector开头的一些方法允许你延迟后派发指定消息,而且可以将消息(同步或异步的消息)从辅助线程派发到主线程。


2.5 Cocoa对象生命周期

对象的四种内存管理方式,如下图所示

  •  对象的生命周期—简化视图


  • 保持接收到的对象


  • 拷贝接收到的对象


  • 自动释放池







1、Cocoa对象的创建

我们都知道创建一个对象有两步:alloc 和 init(对象分配和初始化),两步缺一不可。
初始化一般都是紧接着对象分配的后面进行,但是这两个操作的作用是完全不同的。

分配对象:就是Cocoa从应用程序的虚拟内存中为对象分配一块内存。Cocoa会根据对象的实例变量(类型和变量的排列顺序)计算内存大小并分配内存。为了分配内存,你需要向 类对象类对象上篇详细讲了它的由来和作用)发送alloc 或者allocWithZone:发送消息。消息返回一个未初始化的类实例。那发送分配消息除了分配内存外,还做了其他的一些很重要的工作:
  • 对象的保持(retain)数设置为1.
  • 分配的对象的isa指针指向类对象。
  • 把对象所有的实例变量初始化为0.也可以理解成0的等价类型:nil  NULL
这样所有的对象都有了isa指针,而且指向它们对应的类对象,这样对象就可以找到它运行时的信息。比如对象在继承层次机构上所在的位置(哪个是父类,哪个是子类等信息),它实现的协议,还有能响应什么消息。
即便如此,alloc之后的对象还不是一个可用的对象,对象必须初始化。

1.1初始化对象

初始化过程就是把对象的实例变量设置成有效合理的值,或者说你想要的数据。如果你的类没有实现初始化方法,它会调用父类的。

初始化方法的形式

初始化方法是实例方法,返回的是id类型的对象。初始化方法是讲究形式的,不能乱写。方法你可以有参数,多个也行,但是必须是init开头,比如:
- (id)initWithArray:(NSArray *)array; (from NSSet)
参数形式:WithType: 

初始化的问题

初始化也有问题?啥问题!?有时候初始化返回的并不是一个新的对象。什么时候呢? 比如:我们熟悉的单例模式的时候。还有保持对象某个属性唯一的时候。账户类的id唯一性,如果初始化一个id是已存在的id,那就要返回已存在id对应的账户对象。
这时候我们需要:
  • 释放刚刚分配的对象(是不是感觉很浪费,刚分配了又要释放,都没用了呢,没办法的事情啊)
  • 返回已存在的账户对象
有时候你初始化对象失败了,也需要有一些操作。怎么会失败呢?比如:initFromFile: 这个初始化方法,它是要从一个文件初始化,万一这个文件不存在,那就是初始化失败,初始化失败了怎么办:
  • 释放刚刚分配的对象
  • 返回nil
对象不能重复初始化,不然会产生NSInvalidArgumentException异常。

实现初始化方法

自定义类可能就需要自己写初始化的方法 了,可以有一个或多个初始化方法,看你设计的类的需要。不过实现初始化方法需要遵循以下步骤:
  1. 先要调用父类的初始化方法
  2. 检查父类初始化返回的对象,如果是nil则初始化失败,也返回nil
  3. 在初始化实例变量时,如果它们是其他对象的引用,必要时要进行retain和copy
  4. 如果返回一个已存在的对象,那首先释放新分配的对象(刚才提到的账号的类)
  5. 遇到问题初始化不成功(比如初始化文件失败),返回nil
  6. 如果没有问题,返回self。初始化完成
下面这个例子能说明这几个步骤,请看:
- (id)initWithAccountID:(NSString *)identifier {
    if ( self = [super init] ) {
        Account *ac = [accountDictionary objectForKey:identifier];
        if (ac) { // object with that ID already exists
            [self release];
            return [ac retain];
        }
        if (identifier) {
            accountID = [identifier copy]; // accountID is instance variable
            [accountDictionary setObject:self forKey:identifier];
            return self;
        } else {
            [self release];
            return nil;
        }
    } else
        return nil;
}

注意:子类初始化时,必须先调用父类的初始化方法,以保证继承链中父类的实例变量得到正确的赋值。

下图解释继承链的初始化过程:




1.2 dealloc方法

dealloc和init方法是相呼应的。dealloc确保的是对象的实例变量和动态分配的内存被正确的释放。和init方法相反,父类的dealloc是在释放了其他的之后最后调用的。
- (void)dealloc {
    [accountDictionary release];
    if ( mallocdChunk != NULL )
        free(mallocdChunk);
    [super dealloc];
}

1.3 工厂类方法

工厂类方法把分配对象和初始化合二为一,返回创建对象,而且还自动释放。这些方法的形式一般是:+ (type)className...
NSDate工厂类方法:
+ (id)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (id)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)secs;
+ (id)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;

测试下第一个代码:
    NSDate *now = [NSDate dateWithTimeIntervalSinceNow: 0];
    NSLog(@"now:%@",now);

打印出来now:2012-10-23 06:39:25 +0000

引当前时间为基准,0是当前时间,+0000表示是时区,咱们是8时区,+8是14:39。如果参数是24*60*60是明天的时间,如果是负数那就是昨天的时间。

NSData提供下面的工厂方法:
+ (id)dataWithBytes:(const void *)bytes length:(unsigned)length;
+ (id)dataWithBytesNoCopy:(void *)bytes length:(unsigned)length;
+ (id)dataWithBytesNoCopy:(void *)bytes length:(unsigned)length
        freeWhenDone:(BOOL)b;
+ (id)dataWithContentsOfFile:(NSString *)path;
+ (id)dataWithContentsOfURL:(NSURL *)url;
+ (id)dataWithContentsOfMappedFile:(NSString *)path;

dataWithContentsOfURL的例子,下载图片,毫无压力。
        NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];
        NSData * data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [[UIImage alloc]initWithData:data];

2、运行时内省的能力

内省(Introspection)是面向对象语言和环境的重要特性,Objective-C和Cocoa在这方面做的很好。内省是对象自己检查自己做为运行时对象详细信息的一种能力。这些详细信息包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息。NSObject协议和类定义了很多内省方法,用于查询运行时信息,以便根据对象的特征进行识别。

灵活的使用内省能力可以让你的程序更稳定强大。内省可以避免错误地进行消息派发、对象相等的错误判断等问题。下面介绍内省的一些实用方法:

2.1 定位继承关系

NSObject协议声明了几个方法,用于确定对象在类层次中的位置。class返回类的Class对象。superclass返回父类的Class对象。看下面例子:
while ( id anObject = [objectEnumerator nextObject] ) {
    if ( [self class] == [anObject superclass] ) {
        // do something appropriate...
    }
}

返回的两个Class对象看是否相等。
检查类对象的从属关系:isKindOfClass:判断是否是这个类的或这个类的子类的实例。isMemberOfClass: 这个更严格些,判断是否是这个类的实例。例子:
if ([item isKindOfClass:[NSData class]]) {
    const unsigned char *bytes = [item bytes];
    unsigned int length = [item length];
    // ...
}


2.2 判断方法的实现或者是否遵循某个协议

NSObject还有两个功能更加强大的内省方法,即respondsToSelector:和conformsToProtocol:。两个是实例方法。respondsToSelector判读对象是否实现某个的方法,conformsToProtocol判断是否遵循指定的正式协议(正是协议的意思是实现该协议的所有方法)。所有继承NSObject的类都有有这两个方法。
respondsToSelector例子:
- (void)doCommandBySelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {
        [self performSelector:aSelector withObject:nil];
    } else {
        [_client doCommandBySelector:aSelector];
    }
}


2.3 对象的比较

hash和isEqual:方法都在NSObject协议中声明,且彼此关系紧密。实现hash方法会返回一个整型数。两个对象相等意味着它们有相同的哈希值。如果您的对象可能被包含在象NSSet这样的集合中,则需要定义hash方法,并确保该方法在两个对象相等的时候返回相同的哈希值。不过NSObject类中缺省的isEqual实现只是简单地检查指针是否相等。

isEqual方法例子:
- (void)saveDefaults {
    NSDictionary *prefs = [self preferences];
    if (![origValues isEqual:prefs])
        [Preferences savePreferencesToDefaults:prefs];
}
如果子类增加了实例变量,比较子类需要对子类的实例变量也做比较才能确定对象是否相等时,需要重载isEqual方法:
- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToWidget:other];
}

- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
    if (self == aWidget)
        return YES;
    if (![(id)[self name] isEqual:[aWidget name]])
        return NO;
    if (![[self data] isEqualToData:[aWidget data]])
        return NO;
    return YES;
}

3、对象可变性(mutable)

3.1 为什么要有可变与不可变对象

创建对象的时候,选可变的对象还是选不可变的对象呢?怎么决定呢。先看看为什么要有可变与不可变这两种对象的存在。
可变的对象的类前面都有 Mutable的关键字,这些类有:
  • NSMutableArray
  • NSMutableDictionary
  • NSMutableSet
  • NSMutableIndexSet
  • NSMutableCharacterSet
  • NSMutableData
  • NSMutableString
  • NSMutableAttributedString
  • NSMutableURLRequest
它们都是对应的不可变类的子类。
如果对象都是可变的,那在某些场景中是很不安全和不可靠的。比如你的某个对象当做参数传给了某个方法,你不希望你的对象被改变。这时这个方法却你的变量改变了,这是你不想要的结果。而在另外一些场景却相反。OK,为了对应不同的场景,对象就必须有可变与不可变之分了。

3.2 什么时候用可变对象

当需要在对象创建之后频繁或不断地对其内容进行修改时,请使用可变对象
有些时候,用一个不可变对象取代另一个可能更好。比如,大多数保留字符串的实例变量都应该被赋值为一个不可变的NSString对象,而这些对象则用“setter”方法来进行替换。
依靠返回类型来进行可变性提示。
如果你不能确定一个对象是可变的,则将它当成不可变的处理。

4、创建单例

创建单例的步骤:
  • 声明一个单例对象的静态实例,并初始化为nil。
  • 在该类的类工厂方法(名称类似于“sharedInstance”或“sharedManager”)中生成该类的一个实例,但仅当静态实例为nil的时候。
  • 重载allocWithZone:方法,确保当用户试图直接(而不是通过类工厂方法)分配或初始化类的实例时,不会分配出另一个对象。
  • 实现基本协议方法:copyWithZone:、release、retain、retainCount、和autorelease ,以保证单例的状态。
实现单例的代码例子:
static MyGizmoClass *sharedGizmoManager = nil;
+ (MyGizmoClass*)sharedManager
{
    @synchronized(self) {
        if (sharedGizmoManager == nil) {
            [[self alloc] init]; // assignment not done here
        }
    }
    return sharedGizmoManager;
}

+ (id)allocWithZone:(NSZone *)zone
{
    @synchronized(self) {
        if (sharedGizmoManager == nil) {
            sharedGizmoManager = [super allocWithZone:zone];
            return sharedGizmoManager;  // assignment and return on first allocation
        }
    }
    return nil; //on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain
{
    return self;
}

- (unsigned)retainCount
{
    return UINT_MAX;  //denotes an object that cannot be released
}

- (void)release
{
    //do nothing
}

- (id)autorelease
{
    return self;
}


  虽然iOS 5.0版本之后加入了ARC机制,由于相互引用关系比较复杂时,内存泄露还是可能存在。所以了解原理很重要。

这里讲述在没有ARC的情况下,如何使用Instruments来查找程序中的内存泄露,以及 NSZombieEnabled设置的使用。

1、运行Demo。

先下载一个实现准备好的内存泄露的Demo吧:leak app

下载下来,打开运行,程序是一个寿司的列表,列出各种寿司卷。试着选择里面的几行,应该是选第二行的时候就崩溃了。崩溃截图:


在崩溃的地方断住了,知道crash的地方了,但是不知道具体crash的原因。


2、设置NSZombieEnabled

这是一个 “EXC_BAD_ACCESS”错误。我们打开XCode的选项:“NSZombieEnabled” 。在crash时可能会给你更多的一些提示信息。

设置步骤:1



2:勾上红色框里的


运行,按刚才的操作选中其中的cell。再次crash,这次在output窗口会看到多了一项错误信息:

2012-11-28 13:22:08.911 PropMemFun[2132:11303] *** -[CFString respondsToSelector:]: message sent to deallocated instance 0x713ebc0

大概意思是:向已释放的内存发送消息。也就是说使用了已释放的内存,在C语言相当于使用了“野指针”


看了下crash的这个语句,sushiString应该是没问题的,它是从stringWithFormat初始化出来的。那就是_lastSushiSelected的问题。

_lastSushiSelected指向了sushiString,sushiString是一个autorelease变量。 在第二次点击时,使用的是sushiString已经被释放,所以crash了。那为_lastSushiSelected保留一下,就可以用了。代码修改如下:

_lastSushiSelected = [sushiString retain];  

运行,这时候不崩溃。

3、分析内存泄露(shift+command+b)

app不crash了,那看看有没有内存泄露。用XCode的Analyze就能分析到哪里有内存泄露



分析之后可以看到:


这里提示alertView没被释放,有内存泄露,那我们释放

    [alertView release];

再分析,这个问题解决了。


4、使用Instruments的leaks工具

分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。
 
按上面操作,build成功后跳出 Instruments工具,选择Leaks选项,这时候寿司程序也运行起来了,选中list中的项,拖动等操作后,工具显示效果如下:


大家可能都能猜到,红色的柱子表示内存泄露了。怎么通过这个工具看到在哪泄露了呢?
先在工具栏按下红色的圆形按钮,把工具监视内存的活动停下来。选择Leak,然后点中间十字交叉那,选择Call Tree.

这时候左下角的Call Tree的可选项可以选了。选中Invert Call Tree 和Hide System Libraries,显示如下:


这时候内存泄露的具体代码找到了,在右边的红色框框里指定了哪个方法出现了内存泄露。
你只要在这些方法上双击,就会跳转到具体的代码,哈哈,是不是很方便。

这里应该是提示100%内存会泄露。

6、解决内存泄露问题

问题找到了,那就解决吧

关于:tableView:didSelectRowAtIndexPath ,分析下它的内存过程:

  1. sushiString变量通过autorelease创建,它的引用计数是1.   
  2. 这行代码使得引用计数增加到2, _lastSushiSelected = [sushiString retain];
  3. 这个方法结束时,sushiString的autorelease生效了,这个变量的引用计数减少为1
  4. 当再次执行tableView:didSelectRowAtIndexPath这个方法时,_lastSushiSelected被赋值了新指针,老的_lastSushiSelected的引用计数还是1,没有被释放,产生了内存泄露。

怎么解决呢?

_lastSushiSelected  = [sushiString  retain ];之前把原来的release就ok了:
[_lastSushiSelected release];      
_lastSushiSelected = [sushiString retain];  

关于:tableView:cellForRowAtIndexPath


这个比较明显, sushiString被alloc和init之后就没有释放,可以用stringWithFormat来调用autorelease,代码如下:

NSString *sushiString = [NSString stringWithFormat:@ "%d: %@" , indexPath.row, sushiName];  



单例模式在iOS开发过程中经常用到,苹果提供过objective c单例的比较官方的写法:
static MyGizmoClass *sharedGizmoManager = nil;

+ (MyGizmoClass*)sharedManager
{
    @synchronized(self) {
        if (sharedGizmoManager == nil) {
            [[self alloc] init]; // assignment not done here
        }
    }
    return sharedGizmoManager;
}

+ (id)allocWithZone:(NSZone *)zone
{
    @synchronized(self) {
        if (sharedGizmoManager == nil) {
            sharedGizmoManager = [super allocWithZone:zone];
            return sharedGizmoManager;  // assignment and return on first allocation
        }
    }
    return nil; //on subsequent allocation attempts return nil
}

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain
{
    return self;
}

- (unsigned)retainCount
{
    return UINT_MAX;  //denotes an object that cannot be released
}

- (void)release
{
    //do nothing
}

- (id)autorelease
{
    return self;
}

现在iOS5之后普遍使用了ARC技术,原来这个写法就显得很累赘了,release已经不使用了。

咱们可以结合GCD来实现单例模式:

+ (id)sharedInstance
{
  static dispatch_once_t pred = 0;
  __strong static id _sharedObject = nil;
  dispatch_once(&pred, ^{
    _sharedObject = [[self alloc] init]; // or some other init method
  });
  return _sharedObject;
}
使用dispatch_once,这个方法的意思是在应用程序生命周期内,这方法只执行一次,这就是ARC下结合GCD的一个单例的代码

因为我们可能需要多个单例的类,每个都写一次比较麻烦,可以更方便一些,把这些代码写成宏的形式,只需要传入类名就可以创建一个单例的类了。

通过传入类名创建头文件中单例方法的名称:
#define DEFINE_SINGLETON_FOR_HEADER(className) \
\
+ (className *)shared##className;

通过传入类名创建单例方法的实现
#define DEFINE_SINGLETON_FOR_CLASS(className) \
\
+ (className *)shared##className { \
static className *shared##className = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
shared##className = [[self alloc] init]; \
}); \
return shared##className; \
}
使用宏:

在项目中新建了一个类testSingleton,并且有.h 和.m文件。

#import "testSingleton.h"

@implementation testSingleton
DEFINE_SINGLETON_FOR_CLASS(testSingleton)
@end

#define DEFINE_SINGLETON_FOR_HEADER(className) \
\
+ (className *)shared##className;

#define DEFINE_SINGLETON_FOR_CLASS(className) \
\
+ (className *)shared##className { \
static className *shared##className = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
shared##className = [[self alloc] init]; \
}); \
return shared##className; \
}

#import <Foundation/Foundation.h>

@interface testSingleton : NSObject
DEFINE_SINGLETON_FOR_HEADER(testSingleton);
@end
你可以把宏单独放到项目管理宏的文件中。这里是为了方便演示放到和testSingleton.h文件了。
获取单例:
- (void)test
{
    testSingleton *testShare = [testSingleton sharedtestSingleton];
}

动画效果提供了状态或页面转换时流畅的用户体验,在iOS系统中,咱们不需要自己编写绘制动画的代码,Core Animation提供了丰富的api来实现你需要的动画效果。

    UIKit只用UIView来展示动画,动画支持UIView下面的这些属性改变:

1、commitAnimations方式使用UIView动画
- (void)viewDidLoad
{
    [super viewDidLoad];
   
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
   
    [button setTitle:@"改变" forState:UIControlStateNormal];
    button.frame = CGRectMake(10, 10, 60, 40);
    [button addTarget:self action:@selector(changeUIView) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
   
}

- (void)changeUIView{
    [UIView beginAnimations:@"animation" context:nil];
     [UIView setAnimationDuration:1.0f];
     [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:YES];
    [UIView commitAnimations];
}

下面是点击改变后的效果(两种):


动画的常量有一下四种
   UIViewAnimationTransitionNone,
   UIViewAnimationTransitionFlipFromLeft,
   UIViewAnimationTransitionFlipFromRight,
   UIViewAnimationTransitionCurlUp,
   UIViewAnimationTransitionCurlDown,

1.2 交换本视图控制器中2个view位置


 [self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];

先添加两个view ,一个redview  一个yellowview

- (void)viewDidLoad
{
    [super viewDidLoad];
    UIView *redView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];
   
    UIView *yellowView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    yellowView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:yellowView];
   
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button setTitle:@"改变" forState:UIControlStateNormal];
    button.frame = CGRectMake(10, 10, 300, 40);
    [button addTarget:self action:@selector(changeUIView) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
   
    UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button1 setTitle:@"改变1" forState:UIControlStateNormal];
    button1.frame = CGRectMake(10, 60, 300, 40);
    [button1 addTarget:self action:@selector(changeUIView1) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button1];
   
}

- (void)changeUIView1{
    [UIView beginAnimations:@"animation" context:nil];
     [UIView setAnimationDuration:1.0f];
     [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationTransition:UIViewAnimationTransitionCurlDown forView:self.view cache:YES];
    //  交换本视图控制器中2个view位置
    [self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
    [UIView commitAnimations];
}


这样看起来就像两页一样了。


1.3 、   [UIView setAnimationDidStopSelector:@selector(animationFinish:)];

在commitAnimations消息之前,可以设置动画完成后的回调,设置方法是:

    [UIView setAnimationDidStopSelector:@selector(animationFinish:)];

2、使用:CATransition

- (void)changeUIView2{
    CATransition *transition = [CATransition animation];
    transition.duration = 2.0f;
      transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromTop;
    [self.view exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
    [self.view.layer addAnimation:transition forKey:@"animation"];
}

transition.type 的类型可以有

淡化、推挤、揭开、覆盖

NSString * const kCATransitionFade;

NSString * const kCATransitionMoveIn;

NSString * const kCATransitionPush;

NSString * const kCATransitionReveal;

这四种,
transition.subtype 
也有四种

NSString * const kCATransitionFromRight;

NSString * const kCATransitionFromLeft;

NSString * const kCATransitionFromTop;

NSString * const kCATransitionFromBottom;




2.2 私有的类型的动画类型:

立方体、吸收、翻转、波纹、翻页、反翻页、镜头开、镜头关

               animation.type = @"cube"
               animation.type = @"suckEffect";    
               animation.type = @"oglFlip";//不管subType is "fromLeft" or "fromRight",official只有一种效果
               animation.type = @"rippleEffect";
               animation.type = @"pageCurl";
               animation.type = @"pageUnCurl"
               animation.type = @"cameraIrisHollowOpen ";
               animation.type = @"cameraIrisHollowClose ";

下图是第一个cube立方体的效果:

2.3 CATransition的 startProgress  endProgress属性

这两个属性是float类型的。
可以控制动画进行的过程,可以让动画停留在某个动画点上,值在0.0到1.0之间。endProgress要大于等于startProgress。
比如上面的立方体转到,可以设置endProgress= 0.5,让动画停留在转动一般的位置。
上面这些私有的动画效果,在实际应用中要谨慎使用。因为在app store审核时可能会以为这些动画效果而拒绝通过。

3、UIView的 + (void)animateWithDuration

:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
方法。
这个方法是在iOS4.0之后才支持的。
比 1 里的UIView的方法简洁方便使用。
DidView里添加moveView。

 moveView = [[UIView alloc] initWithFrame:CGRectMake(10, 180, 200, 40)];
    moveView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:moveView];

- (void)changeUIView3{
    [UIView animateWithDuration:3 animations:^(void){
        moveView.frame = CGRectMake(10, 270, 200, 40);
    }completion:^(BOOL finished){
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 40, 40)];
        label.backgroundColor = [UIColor blackColor];
        [self.view addSubview:label];
    }];
}

然后用UIView animateWithDuration动画移动,移动动画完毕后添加一个Label。

3.2、 animateWithDuration的嵌套使用


- (void)changeUIView3{

   
    [UIView animateWithDuration:2
                          delay:0
                        options:UIViewAnimationOptionCurveEaseOut animations:^(void){
        moveView.alpha = 0.0;
    }completion:^(BOOL finished){
        [UIView animateWithDuration:1
                              delay:1.0
                            options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
                         animations:^(void){
                             [UIView setAnimationRepeatCount:2.5];
                             moveView.alpha = 1.0;
                         }completion:^(BOOL finished){
                            
                         }];
       
    }];
}
这个嵌套的效果是先把view变成透明,在从透明变成不透明,重复2.5次透明到不透明的效果。


Core Animation可以翻译为核心动画,它为图形渲染和动画提供了基础。使用核心动画,你只需要设置一些参数比如起点和终点,剩下的帧核心动画为你自动完成。核心动画使用硬件加速,不用消耗cpu资源。其实平时咱们开发的iOS应用都在有意无意的使用了核心动画。动画不会替代View,而是和View一起提供更好的性能。Core Animation通过缓存view上的内容到bitmap,这样bitmap就可以直接在图形硬件上操作。从而提高了性能。

核心动画所在的位置:



1、关于层类

Layer Classes是core animation的基础。Layer Classes提供了一个抽象的概念,这个概念对于那些使用NSview和UIview的开发者来说是很熟悉的。基础层是由CAlayer类提供的,CAlayer是所有Core Animation层的父类。    
同一个视图类的实例一样,一个CAlayer实例也有一个单独的superlayer和上面所有的子层(sublayers),它创建了一个有层次结构的层,我们称之为layer tree。layers的绘制就像views一样是从后向前绘制的,绘制的时候我们要指定其相对与他们的superlayer的集合形状,同时还需要创建一个局部的坐标系。layers可以做一些更复杂的操作,例如rotate(旋转),skew(倾斜),scale(放缩),和project the layer content(层的投影)。   
图层的内容提供
(1)直接设置层的content属性到一个core graphics图,或者通过delegation来设置
(2)提供一个代理直接绘制到Core Graphics image context(核心图形的上下文)
(3)设置任意数量的所有层共有的可视的风格属性。例如:backgroundColor(背景色),opacity(透明度)和masking(遮罩)。max os x应用通过使用core image filters来达到这种可视化的属性。
(4)子类化CAlayer,同时在更多的封装方式中完成上面的任意技术。 

 1.1 CALayer的子类和他们的使用场景

Class

Usage

CAEmitterLayer

Used to implement a Core Animation–based particle emitter system. The emitter layer object controls the generation of the particles and their origin.

CAGradientLayer

Used to draw a color gradient that fills the shape of the layer (within the bounds of any rounded corners).

CAEAGLLayer/CAOpenGLLayer

Used to set up the backing store and context needed to draw using OpenGL ES (iOS) or OpenGL (OS X).

CAReplicatorLayer

Used when you want to make copies of one or more sublayers automatically. The replicator makes the copies for you and uses the properties you specify to alter the appearance or attributes of the copies.

CAScrollLayer

Used to manage a large scrollable area composed of multiple sublayers.

CAShapeLayer

Used to draw a cubic Bezier spline. Shape layers are advantageous for drawing path-based shapes because they always result in a crisp path, as opposed to a path you draw into a layer’s backing store, which would not look as good when scaled. However, the crisp results do involve rendering the shape on the main thread and caching the results.

CATextLayer

Used to render a plain or attributed string of text.

CATiledLayer

Used to manage a large image that can be divided into smaller tiles and rendered individually with support for zooming in and out of the content.

CATransformLayer

Used to render a true 3D layer hierarchy, rather than the flattened layer hierarchy implemented by other layer classes.

QCCompositionLayer

Used to render a Quartz Composer composition. (OS X only)

1.2、 anchorPoint、 position

anchorPoint又称锚点,锚点对动画是有很大影响的。下图描述了基于锚点的三个示例值:


1.3、 图层的 frame、bounds、position 和 anchorPoint 关系如下图所示: 


在该示例中,anchorPoint 默认值为(0.5,0.5),位于图层的中心点。图层的 position 值为(100.0,100.0),bounds 为(0.0,0.0,120,80.0)。通过计算得到图层的 frame为(40.0,60.0,120.0,80.0)。

如果你新创建一个图层,则只有设置图层的 frame 为(40.0,60.0,120.0,80.0),相应的 position 属性值将会自动设置为(100.0,100.0),而 bounds 会自动设置为 (0.0,0.0,120.0,80.0)。下图显示一个图层具有相同的 frame(如上图),但是在该图中它的 anchorPoint 属性值被设置为(0.0,0.0),位于图层的左下角位置。


图层的 frame 值同样为(40.0,60.0,120.0,80.0),bounds 的值不变,但是图层的 position 值已经改变为(40.0,60.0)。


2、关于动画类

核心动画的动画类使用基本的动画和关键帧动画把图层的内容和选取的属性动画的显示出来。所有核心动画的动画类都是从 CAAnimation 类继承而来。
CAAnimation 实现了 CAMediaTiming 协议,提供了动画的持续时间,速度,和重复计数。 CAAnimation 也实现了 CAAction 协议。该协议为图层触发一个动画动作提供了提供标准化响应。动画类同时定义了一个使用贝塞尔曲线来描述动画改变的时间函数。例如,一个 匀速时间函数(linear timing function)在动画的整个生命周期里面一直保持速度不变, 而渐缓时间函数(ease-out timing function)则在动画接近其生命周期的时候减慢速度。核心动画额外提供了一系列抽象的和细化的动画类,比如:CATransition 提供了一个图层变化的过渡效果,它能影响图层的整个内容。 动画进行的时候淡入淡出(fade)、推(push)、显露(reveal)图层的内容。这些过渡效 果可以扩展到你自己定制的 Core Image 滤镜。CAAnimationGroup 允许一系列动画效果组合在一起,并行显示动画。

2.1动画类

 CAPropertyAnimation :是一个抽象的子类,它支持动画的显示图层的关键路 径中指定的属性一般不直接使用,而是使用它的子类,CABasicAnimation,CAKeyframeAnimation. 在它的子类里修改属性来运行动画。
CABasicAnimation: 简单的为图层的属性提供修改。 很多图层的属性修改默认会执行这个动画类。比如大小,透明度,颜色等属性
 CAKeyframeAnimation: 支持关键帧动画,你可以指定的图层属性的关键路径动画,包括动画的每个阶段的价值,以及关键帧时间和计时功能的一系列值。在 动画运行是,每个值被特定的插入值替代。核心动画 和 Cocoa Animation 同时使用这些动画类。

2.2 如何使用多个动画效果叠加

在执行动画的过程中需要同时修改position,alpha, frame等属性,使用CAAnimationGroup可以将三个动画合成一起执行:

CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim,scaleAnim,opacityAnim, nil];
animGroup.duration = 1;
[view.layer addAnimation:animGroup forKey:nil];

2.3事务管理类  

   图层的动画属性的每一个修改必然是事务的一个部分。CATransaction 是核心动画里面负责协调多个动画原子更新显示操作。事务支持嵌套使用。 


2.4 Core Animation类的继承关系图 




1、UIGestureRecognizer介绍

手势识别在iOS上非常重要,手势操作移动设备的重要特征,极大的增加了移动设备使用便捷性。
iOS系统在3.2以后,为方便开发这使用一些常用的手势,提供了UIGestureRecognizer类。手势识别UIGestureRecognizer类是个抽象类,下面的子类是具体的手势,开发这可以直接使用这些手势识别。
  • Tap(点一下)
  • Pinch(二指往內或往外拨动,平时经常用到的缩放)
  • Rotation(旋转)
  • Swipe(滑动,快速移动)
  • Pan (拖移,慢速移动)
  •  LongPress(长按)
UIGestureRecognizer的继承关系如下:



2、使用手势的步骤

使用手势很简单,分为两步:
  1. 创建手势实例。当创建手势时,指定一个回调方法,当手势开始,改变、或结束时,回调方法被调用。
  2. 添加到需要识别的View中。每个手势只对应一个View,当屏幕触摸在View的边界内时,如果手势和预定的一样,那就会回调方法。
ps:一个手势只能对应一个View,但是一个View可以有多个手势。
建议在真机上运行这些手势,模拟器操作不太方便,可能导致你认为手势失效。

3、Pan 拖动手势:

  UIImageView *snakeImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"snake.png"]];
    snakeImageView.frame = CGRectMake(50, 50, 100, 160);
    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
                                                    initWithTarget:self
                                                    action:@selector(handlePan:)];   
    [snakeImageView addGestureRecognizer:panGestureRecognizer];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    [self.view addSubview:snakeImageView];

新建一个ImageView,然后添加手势
回调方法:
- (void) handlePan:(UIPanGestureRecognizer*) recognizer
{
    CGPoint translation = [recognizer translationInView:self.view];
    recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
                                   recognizer.view.center.y + translation.y);
    [recognizer setTranslation:CGPointZero inView:self.view];
   
}

4、Pinch缩放手势

UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
                                                        initWithTarget:self
                                                        action:@selector(handlePinch:)];<p class="p1">[<span class="s1">snakeImageView</span> <span class="s2">addGestureRecognizer</span>:pinchGestureRecognizer];</p>

- (void) handlePinch:(UIPinchGestureRecognizer*) recognizer
{
    recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale);
    recognizer.scale = 1;
}


5、Rotation旋转手势

    UIRotationGestureRecognizer *rotateRecognizer = [[UIRotationGestureRecognizer alloc]
                                                     initWithTarget:self
                                                     action:@selector(handleRotate:)];
    [snakeImageView addGestureRecognizer:rotateRecognizer];

- (void) handleRotate:(UIRotationGestureRecognizer*) recognizer
{
    recognizer.view.transform = CGAffineTransformRotate(recognizer.view.transform, recognizer.rotation);
    recognizer.rotation = 0;
}




添加了这几个手势后,运行看效果,程序中的imageView放了一个
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \\
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~  
                        ~--______-~                ~-___-~
的图片,在模拟器上拖动是没问题的。缩放和旋转有点问题,估计是因为在模拟器上的模拟的两个接触点距离在imageView的边界外了,所以操作无效果。
建议在真机上运行这个手势。
在模拟器上缩放和选择的操作技巧:
可以把imageView的frame值设置大一点,按住alt键,按下触摸板(不按下不行),这样就可以旋转和缩放了。

6、添加第二个ImagView并添加手势

记住:一个手势只能添加到一个View,两个View当然要有两个手势的实例了
- (void)viewDidLoad
{
    [super viewDidLoad];

    UIImageView *snakeImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"snake.png"]];
    UIImageView *dragonImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dragon.png"]];
    snakeImageView.frame = CGRectMake(120, 120, 100, 160);
    dragonImageView.frame = CGRectMake(50, 50, 100, 160);
    [self.view addSubview:snakeImageView];
    [self.view addSubview:dragonImageView];
   
    for (UIView *view in self.view.subviews) {
        UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
                                                        initWithTarget:self
                                                        action:@selector(handlePan:)];
       
        UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
                                                            initWithTarget:self
                                                            action:@selector(handlePinch:)];
       
        UIRotationGestureRecognizer *rotateRecognizer = [[UIRotationGestureRecognizer alloc]
                                                         initWithTarget:self
                                                         action:@selector(handleRotate:)];
       
        [view addGestureRecognizer:panGestureRecognizer];
        [view addGestureRecognizer:pinchGestureRecognizer];
        [view addGestureRecognizer:rotateRecognizer];
        [view setUserInteractionEnabled:YES];
    }
    [self.view setBackgroundColor:[UIColor whiteColor]];    
}

多添加了一条龙的view,两个view都能接收上面的三种手势。运行效果如下:


7、拖动(pan手势)速度(以较快的速度拖放后view有滑行的效果)

如何实现呢?
  1. 监视手势是否结束
  2. 监视触摸的速度
- (void) handlePan:(UIPanGestureRecognizer*) recognizer
{
    CGPoint translation = [recognizer translationInView:self.view];
    recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
                                       recognizer.view.center.y + translation.y);
    [recognizer setTranslation:CGPointZero inView:self.view];
   
    if (recognizer.state == UIGestureRecognizerStateEnded) {
       
        CGPoint velocity = [recognizer velocityInView:self.view];
        CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
        CGFloat slideMult = magnitude / 200;
        NSLog(@"magnitude: %f, slideMult: %f", magnitude, slideMult);
       
        float slideFactor = 0.1 * slideMult; // Increase for more of a slide
        CGPoint finalPoint = CGPointMake(recognizer.view.center.x + (velocity.x * slideFactor),
                                         recognizer.view.center.y + (velocity.y * slideFactor));
        finalPoint.x = MIN(MAX(finalPoint.x, 0), self.view.bounds.size.width);
        finalPoint.y = MIN(MAX(finalPoint.y, 0), self.view.bounds.size.height);
       
        [UIView animateWithDuration:slideFactor*2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            recognizer.view.center = finalPoint;
        } completion:nil];
       
    }
  
代码实现解析:
  1. 计算速度向量的长度(估计大部分都忘了)这些知识了。
  2. 如果速度向量小于200,那就会得到一个小于的小数,那么滑行会很短
  3. 基于速度和速度因素计算一个终点
  4. 确保终点不会跑出父View的边界
  5. 使用UIView动画使view滑动到终点
运行后,快速拖动图像view 放开会看到view还会在原来的方向滑行一段路。

8、同时触发两个view的手势

手势之间是互斥的,如果你想同时触发蛇和龙的view,那么需要实现协议
UIGestureRecognizerDelegate,

@interface ViewController : UIViewController<UIGestureRecognizerDelegate>
@end
 
并在协议这个方法里返回YES。
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}



把self作为代理设置给手势:
        panGestureRecognizer.delegate = self;
        pinchGestureRecognizer.delegate = self;
        rotateRecognizer.delegate = self;

这样可以同时拖动或旋转缩放两个view了。

9、tap点击手势

这里为了方便看到tap的效果,当点击一下屏幕时,播放一个声音。

为了播放声音,我们加入AVFoundation.framework这个框架。
- (AVAudioPlayer *)loadWav:(NSString *)filename {
    NSURL * url = [[NSBundle mainBundle] URLForResource:filename withExtension:@"wav"];
    NSError * error;
    AVAudioPlayer * player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
    if (!player) {
        NSLog(@"Error loading %@: %@", url, error.localizedDescription);
    } else {
        [player prepareToPlay];
    }
    return player;
}

我会在最后例子代码给出完整代码,添加手势的步骤和前面一样的。
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface ViewController : UIViewController<UIGestureRecognizerDelegate>
@property (strong) AVAudioPlayer * chompPlayer;
@property (strong) AVAudioPlayer * hehePlayer;

@end

- (void)handleTap:(UITapGestureRecognizer *)recognizer {
    [self.chompPlayer play];
}

运行,点一下某个图,就会播放一个咬东西的声音。

不过这个点击播放声音有点缺陷,就是在慢慢拖动的时候也会播放。这使得两个手势重合了。怎么解决呢?使用手势的:requireGestureRecognizerToFail方法。

10、手势的依赖性

在viewDidLoad的循环里添加这段代码:
        [tapRecognizer requireGestureRecognizerToFail:panGestureRecognizer];
意思就是,当如果pan手势失败,就是没发生拖动,才会出发tap手势。这样如果你有轻微的拖动,那就是pan手势发生了。tap的声音就不会发出来了。


11、自定义手势

自定义手势继承:UIGestureRecognizer,实现下面的方法:

– touchesBegan:withEvent:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
- touchesCancelled:withEvent:

新建一个类,继承UIGestureRecognizer,代码如下:

.h文件
#import <UIKit/UIKit.h>
typedef enum {
    DirectionUnknown = 0,
    DirectionLeft,
    DirectionRight
} Direction;

@interface HappyGestureRecognizer : UIGestureRecognizer
@property (assign) int tickleCount;
@property (assign) CGPoint curTickleStart;
@property (assign) Direction lastDirection;

@end

.m文件
#import "HappyGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#define REQUIRED_TICKLES        2
#define MOVE_AMT_PER_TICKLE     25

@implementation HappyGestureRecognizer

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch * touch = [touches anyObject];
    self.curTickleStart = [touch locationInView:self.view];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
   
    // Make sure we've moved a minimum amount since curTickleStart
    UITouch * touch = [touches anyObject];
    CGPoint ticklePoint = [touch locationInView:self.view];
    CGFloat moveAmt = ticklePoint.x - self.curTickleStart.x;
    Direction curDirection;
    if (moveAmt < 0) {
        curDirection = DirectionLeft;
    } else {
        curDirection = DirectionRight;
    }
    if (ABS(moveAmt) < MOVE_AMT_PER_TICKLE) return;
   
    // 确认方向改变了
    if (self.lastDirection == DirectionUnknown ||
        (self.lastDirection == DirectionLeft && curDirection == DirectionRight) ||
        (self.lastDirection == DirectionRight && curDirection == DirectionLeft)) {
       
        // 挠痒次数
        self.tickleCount++;
        self.curTickleStart = ticklePoint;
        self.lastDirection = curDirection;
       
        // 一旦挠痒次数超过指定数,设置手势为结束状态
        // 这样回调函数会被调用。
        if (self.state == UIGestureRecognizerStatePossible && self.tickleCount > REQUIRED_TICKLES) {
            [self setState:UIGestureRecognizerStateEnded];
        }
    }
   
}

- (void)reset {
    self.tickleCount = 0;
    self.curTickleStart = CGPointZero;
    self.lastDirection = DirectionUnknown;
    if (self.state == UIGestureRecognizerStatePossible) {
        [self setState:UIGestureRecognizerStateFailed];
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self reset];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self reset];
}

@end
调用自定义手势和上面一样,回到这样写:
- (void)handleHappy:(HappyGestureRecognizer *)recognizer{
    [self.hehePlayer play];
}
手势成功后播放呵呵笑的声音。
在真机上运行,按住某个view,快速左右拖动,就会发出笑的声音了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值