卢克 2012-05-16T08:21:17-07:00 kejinlu@gmail.com Container View Controller 2012-05-16T00:00:00-07:00 卢克 http://geeklu.com/2012/05/custom-container-view-controller

一.UIViewController

做iOS开发的经常会和UIViewController打交道,从类名可知UIViewController属于MVC模型中的C(Controller),说的更具体点它是一个视图控制器,管理着一个视图(view)。

UIViewController的view是lazy loading的,当你访问其view属性的时候,view会从xib文件载入或者通过代码创建(覆盖loadView方法,自定义其view hierarchy),并返回,如果要判断一个View Controller的view是否已经被加载需要通过其提供的isViewLoaded方法来判断。
view加载后viewDidLoad会被调用,这里可以进行一些数据的请求或加载,用来更新你的界面。
当view将被加入view hierarchy中的时候viewWillAppear会被调用,view完成加入的时候viewDidAppear会被调用,同样当view将要从view hierarchy中移除的时候viewWillDisappear会被调用,完成移除的时候viewDidDisappear会被调用。
当内存紧张的时候,所有的UIViewController对象的didReceiveMemoryWarning会被调用,其默认实现是 如果当前viewController的view的superview是nil的话,则将view释放且viewDidUnload会被调用,viewDidUnload中你可以进行后继的内存清理工作(主要是界面元素的释放,当再次加载的时候需要重建)。

如果想要展示一个View Controller,一般有如下一种途径

  1. 设置成Window的rootViewController(iOS 4.0之前UIWindow并没有rootViewController属性,只能通过addSubview的方式添加一个View Controller的view)
  2. 使用某个已经存在的Container来展示,比如使用UINavigationController来展示某个View Controller [navigationController pushViewController:vc animated:YES];
  3. 以模态界面的方式展现出来 presentModalViewController
  4. 以addSubview的方式将使其view作为另一个View Controller的view的subView

直接使用4种方法是比较危险的,上一级 View Controller并不能对当前View Controller的 生命周期相关的函数进行调用,以及旋转事件的传递等。



二.Hierarchy

我们知道一个View可以将另一个View添加为子View(subview),构成一个View Hierarchy.当某一个View添加到window的View Hierarchy中时,将被“显示”。每一个View Controller管理着的其实就是一个View Hierarchy.而View Controller本身可以有Child View Controller,所以也存在一个 View Controller Hierarchy的概念,当View Controller收到上层传来的诸如旋转,显示事件的时候,需要传递给它的Child View Controller. 一般情况下,View Hierarchy 和 View Controller Hierarchy需要保持一致性,比如一个View Controller的view的superView是由其parent view controller管理着 Hierarchy



三.Container

一个iOS的app很少只由一个ViewController组成,除非这个app极其简单。 当有多个View Controller的时候,我们就需要对这些View Controller进行管理。 那些负责一个或者多个View Controller的展示并对其视图生命周期进行管理的对象,称之为容器,大部分容器本身也是一个View Controller,这样的容器可以称之为Container View Controller,也有极少数容器不是View Controller,比如UIPopoverController,其继承于NSObject。

我们常用的容器有 UINavigationController,UITabbarController等,一般容器有一些共同的特征:

  1. 提供对Child View Controller进行管理的接口,比如添加Child View Controller,切换Child View Controller的显示,移除Child View Controller 等
  2. 容器“拥有”所有的Child View Controller
  3. 容器需要负责 Child View Controller的appearance callback的调用(viewWillAppear,viewDidAppear,viewWillDisaapper,viewDidDisappear),以及旋转事件的传递
  4. 保证view hierarchy 和 view controller hierarchy 层级关系一致,通过parent view controller将child view controller和容器进行关联

从上面可以看出来,实现一个Container View Controller并不是一个简单的事情,好在iPhone的界面大小有限,一般情况下一个View Controller的view都是充满界面或者系统自带容器的,我们无需自己创建额外的容器,但是在iPad中情况就不同了。



四.Custom Container View Controller

在iOS 5之前框架并不支持自定义 Container View Controller, iOS 5开始开放了一些新的接口来支持支持自定义容器

addChildViewController:
removeFromParentViewController
transitionFromViewController:toViewController:duration:options:animations:completion:
willMoveToParentViewController:
didMoveToParentViewController:

其中前两个接口比较重要,可以直接改变View Controller 的 Hierarchy。

有点意外的是,在不做任何额外设置的情况下进行如下操作

[viewController.view addSubview:otherViewController.view]

iOS 5中otherViewController是可以立刻收到viewWillAppear和viewDidAppear的调用。

至于旋转事件的传递以及其他时机viewWillAppear viewDidAppear的调用是需要建立在 [viewController addChildViewController:otherViewController]基础上的。

当我们需要在iOS 4上实现自定义容器,或者有时候我们不想让viewWillAppear这类方法被自动调用,而是想自己来控制,这个时候我们就得需要手动来调用这些方法,而不是由框架去自动调用。 iOS 5中可以很方便的禁用掉自动调用的特性,覆盖automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers返回NO

但是单单覆盖这个方法在iOS5下还是有问题的,当执行下面的语句的时候

[viewController.view addSubview:otherViewController.view]

otherViewController还是是可以立刻收到viewWillAppear和viewDidAppear的调用。
解决这一问题的方法就是在iOS5的时候调用[viewController.view addSubview:otherViewController.view]之前 进行如下操作

[viewController addChildViewController:otherViewController]

总的来说实现兼容iOS 4和iOS 5的容器有不少问题和注意点的

  1. view加入view层级前后分别调用viewWillAppear和viewDidAppear;容器的viewWillAppear,viewDidAppear,viewWillDisappear,viewDidDisappear中需要对当前显示的Child View Controller调用相同的方法,容器需要保证Child View Controller的viewWillAppear调用之前Child View Controller的view已经load了.还有一点就是保证容器的View不会出现bounds为CGRectZero的情况,因为如果此View包含多个subview,其bounds改变的时候subview会根据自己的autoresizingMask改变frame,但是当bounds变为0再变为非0的时候,subview的frame就有可能不是你想要的了(比如某个subview的autoresizingMask为UIViewAutoresizingFlexibleBottomMargin)
  2. 容器的shouldAutorotateToInterfaceOrientation中需要检测每一个Child View Controller的shouldAutorotateToInterfaceOrientation如果一个不支持,则看做不支持
  3. 容器的willRotateToInterfaceOrientation,didRotateFromInterfaceOrientation,willAnimateRotationToInterfaceOrientation方法中需要将这些事件传递给所有的Child View Controller
  4. 由于UIViewController的parentViewController属性为只读,且iOS4中没有提供容器支持的接口(iOS 5中容器支持的接口会间接的维护这个属性),所以为了使得childViewController和容器得以关联,我们可以顶一个View Controller的基类,添加一个比如叫做superController的属性用来指定对应的parentViewController
  5. 由于UIViewController的interfaceOrientation为只读属性,且iOS5中没有提供容器接口,所以UIViewController的这个interfaceOrientation变的不可信,为了取得当前UIViewController的orientation我们可以用UIWindow下的rootViewController的interfaceOrientation的值
  6. 容器的viewDidUnload方法中需要对view未释放的childViewController的view进行释放,且调用其viewDidUnload方法

苹果对UIViewController以及其使用有着非常详细的文档 UIViewController Reference , ViewController Programming Guide

]]>
使用Jekyll和Github写博客 2012-04-15T00:00:00-07:00 卢克 http://geeklu.com/2012/04/blog-with-jekyll-and-github 周末将博客搬到Github上了,使用了Jekyll以及Github Page功能,Github Page直接支持Jekyll。 Jekyll是一个博客生成引擎,可以将markdown写成的博客生成静态的HTML博客。 当然Jekyll提供了一些的辅助工具,支持模板,可以辅助你生成分类和标签页。

Github Page是Github的一个非常好的服务,免费,可以绑定域名。

这样彻底摆脱了对Wordpress,摆脱了对PHP环境的依赖,博客使用纯文本的形式也方便备份。

]]>
Mac&iOS之多线程 2012-02-12T00:00:00-08:00 卢克 http://geeklu.com/2012/02/thread
  • 一.线程概述
  • 二.创建线程
    • 1.使用NSThread
    • 2.使用NSObject
    • 3.POSIX Thread
  • 三.多线程进阶
    • 1.NSOperation & NSOperationQueue
    • 2.GCD
  • 四.进程间通信
    • 1.和主线程的通信
    • 2.任意线程间的通信
  • 五.RunLoop

  • 一.线程概述

    有些程序是一条直线,起点到终点;有些程序是一个圆,不断循环,直到将它切断。直线的如简单的Hello World,运行打印完,它的生命周期便结束了,像昙花一现那样;圆如操作系统,一直运行直到你关机。
    一个运行着的程序就是一个进程或者叫做一个任务,一个进程至少包含一个线程,线程就是程序的执行流。Mac和iOS中的程序启动,创建好一个进程的同时,一个线程便开始运行,这个线程叫主线程。主线程在程序中的地位和其他线程不同,它是其他线程最终的父线程,且所有界面的显示操作即AppKit或UIKit的操作必须在主线程进行。
    系统中的每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则共用进程的内存空间。每创建一个新的线程,都需要一些内存(如每个线程有自己的Stack空间)和消耗一定的CPU时间。另外当多个线程对同一个资源出现争夺的时候需要注意线程安全问题。

    二.创建线程

    创建一个新的线程就是给进程增加了一个执行流,执行流总得有要执行的代码吧,所以新建一个线程需要提供一个函数或者方法作为线程的入口。

    1.使用NSThread

    NSThread提供了创建线程的途径,还可以提供了检测当前线程是否是主线程的方法。 使用NSThread创建一个新的线程有两种方式:

    • 1.创建一个NSThread的对象,调用其start方法。对于这种方式的NSThread对象的创建,可以使用一个目标对象的方法初始化一个NSThread对象,或者创建一个继承NSThread类的子类,实现其main方法,然后在直接创建这个子类的对象。
    • 2.使用 detachNewThreadSelector:toTarget:withObject:这个类方法创建一个线程,这个比较直接了,直接使用目标对象的方法作为线程启动入口。

    2.使用NSObject

    其实NSObject直接就加入了多线程的支持,允许对象的某个方法在后台运行。如:

    [myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
    

    3.POSIX Thread

    由于Mac和iOS都是基于Darwin系统,Darwin系统的XUN内核,是基于Mach和BSD的,继承了BSD的POSIX接口,所以可以直接使用POSIX线程的相关接口来使用线程。

    创建线程的接口为 pthread_create,当然在创建之前可以通过相关函数设置好线程的属性。以下为POSIX线程使用简单的例子。

    //
    //  main.c
    //  pthread
    //
    //  Created by Lu Kejin on 1/27/12.
    //  Copyright (c) 2012 Taobao.com. All rights reserved.
    //
    
    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void *pthreadRoutine(void *);
    
    
    int main (int argc, const char * argv[])
    {
        pthread_attr_t  attr;
        pthread_t       pthreadID;
        int             returnVal;
        
        returnVal = pthread_attr_init(&attr);
        returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        int threadError = pthread_create(&pthreadID, &attr, &pthreadRoutine, NULL);
        returnVal = pthread_attr_destroy(&attr);
        
        if (threadError != 0)
        {
            // Report an error.
        }
        
        sleep(10);
        
        return 0;
    }
    
    
    void *pthreadRoutine(void *data){
        int count = 0;
        while (1) {
            printf("count = %d\n",count++);
            sleep(1);
            
        }
        return NULL;
    }
    

    三.多线程进阶

    NSOperation&NSOperationQueue

    很多时候我们使用多线程,需要控制线程的并发数,毕竟线程也是消耗系统资源的,当程序中同时运行的线程过多时,系统必然变慢。 所以很多时候我们会控制同时运行线程的数目。

    NSOperation可以封装我们的操作,然后将创建好的NSOperation对象放到NSOperationQueue中,OperationQueue便开始启动新的线程去执行队列中的操作,OperationQueue的并发度是可以通过如下方式进行设置:

    - (void)setMaxConcurrentOperationCount:(NSInteger)count
    

    GCD

    GCD是Grand Central Dispatch的缩写,是一系列的BSD层面的接口,在Mac 10.6 和iOS4.0以后才引入的,且现在NSOperation和NSOperationQueue的多线程的实现就是基于GCD的。目前这个特性也被移植到FreeBSD上了,可以查看libdispatch这个开源项目。

    比如一个在UIImageView中显示一个比较大的图片

    dispatch_queue_t imageDownloadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
    dispatch_async(imageDownloadQueue, ^{
            NSURL *imageURL = [NSURL URLWithString:@"http://test.com/test.png"];
           NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
            UIImage *image = [UIImage imageWithData:imageData];
           dispatch_async(dispatch_get_main_queue(), ^{
                [imageView setImage:image];//UIKit必须在主线程执行
            });
        });
    

    当然,GCD除了处理多线程外还有很多非常好的功能,其建立在强大的kqueue之上,效率也能够得到保障。

    四.线程间通信

    线程间通信和进程间通信从本质上讲是相似的。线程间通信就是在进程内的两个执行流之间进行数据的传递,就像两条并行的河流之间挖出了一道单向流动长沟,使得一条河流中的水可以流入另一条河流,物质得到了传递。

    1.performSelect On The Thread

    框架为我们提供了强制在某个线程中执行方法的途径,如果两个非主线程的线程需要相互间通信,可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在出线程执行的方法。

    @interface NSObject (NSThreadPerformAdditions)
    
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    // equivalent to the first method with kCFRunLoopCommonModes
    
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
    // equivalent to the first method with kCFRunLoopCommonModes
        ...
    @end
    

    2.Mach Port
    在苹果的Thread Programming Guide的Run Pool一节的Configuring a Port-Based Input Source 这一段中就有使用Mach Port进行线程间通信的例子。 其实质就是父线程创建一个NSMachPort对象,在创建子线程的时候以参数的方式将其传递给子线程,这样子线程中就可以向这个传过来的NSMachPort对象发送消息,如果想让父线程也可以向子线程发消息的话,那么子线程可以先向父线程发个特殊的消息,传过来的是自己创建的另一个NSMachPort对象,这样父线程便持有了子线程创建的port对象了,可以向这个子线程的port对象发送消息了。

    当然各自的port对象需要设置delegate以及schdule到自己所在线程的RunLoop中,这样来了消息之后,处理port消息的delegate方法会被调用,你就可以自己处理消息了。

    五.RunLoop

    RunLoop从字面上看是运行循环的意思,这一点也不错,它确实就是一个循环的概念,或者准确的说是线程中的循环。 本文一开始就提到有些程序是一个圈,这个圈本质上就是这里的所谓的RunLoop,就是一个循环,只是这个循环里加入很多特性。
    首先循环体的开始需要检测是否有需要处理的事件,如果有则去处理,如果没有则进入睡眠以节省CPU时间。 所以重点便是这个需要处理的事件,在RunLoop中,需要处理的事件分两类,一种是输入源,一种是定时器,定时器好理解就是那些需要定时执行的操作,输入源分三类:performSelector源,基于端口(Mach port)的源,以及自定义的源。编程的时候可以添加自己的源。RunLoop还有一个观察者Observer的概念,可以往RunLoop中加入自己的观察者以便监控着RunLoop的运行过程,CFRunLoop.h中定义了所有观察者的类型:

    enum CFRunLoopActivity {
       kCFRunLoopEntry = (1 << 0),
       kCFRunLoopBeforeTimers = (1 << 1),
       kCFRunLoopBeforeSources = (1 << 2),
       kCFRunLoopBeforeWaiting = (1 << 5),
       kCFRunLoopAfterWaiting = (1 << 6),
       kCFRunLoopExit = (1 << 7),
       kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    typedef enum CFRunLoopActivity CFRunLoopActivity;
    

    如果你使用过select系统调用写过程序你便可以快速的理解runloop事件源的概念,本质上讲事件源的机制和select一样是一种多路复用IO的实现,在一个线程中我们需要做的事情并不单一,如需要处理定时钟事件,需要处理用户的触控事件,需要接受网络远端发过来的数据,将这些需要做的事情统统注册到事件源中,每一次循环的开始便去检查这些事件源是否有需要处理的数据,有的话则去处理。 拿具体的应用举个例子,NSURLConnection网络数据请求,默认是异步的方式,其实现原理就是创建之后将其作为事件源加入到当前的RunLoop,而等待网络响应以及网络数据接受的过程则在一个新创建的独立的线程中完成,当这个线程处理到某个阶段的时候比如得到对方的响应或者接受完了网络数据之后便通知之前的线程去执行其相关的delegate方法。所以在Cocoa中经常看到scheduleInRunLoop:forMode:这样的方法,这个便是将其加入到事件源中,当检测到某个事件发生的时候,相关的delegate方法便被调用。对于CoreFoundation这一层而言,通常的模式是创建输入源,然后将输入源通过CFRunLoopAddSource函数加入到RunLoop中,相关事件发生后,相关的回调函数会被调用。如CFSocket的使用。 另外RunLoop中还有一个运行模式的概念,每一个运行循环必然运行在某个模式下,而模式的存在是为了过滤事件源和观察者的,只有那些和当前RunLoop运行模式一致的事件源和观察者才会被激活。

    每一个线程都有其对应的RunLoop,但是默认非主线程的RunLoop是没有运行的,需要为RunLoop添加至少一个事件源,然后去run它。一般情况下我们是没有必要去启用线程的RunLoop的,除非你在一个单独的线程中需要长久的检测某个事件。

    ]]>
    Bonjour 2012-02-03T00:00:00-08:00 卢克 http://geeklu.com/2012/02/bonjour 一.Bonjour介绍

    一般在进行Socket编程或者网络访问的时候,首先需要确认对方网络服务已经开启,且需要知道对方的域名或地址以及端口,然后才可以进行进一步操作。在互联网上好点,网络服务方一般常年开启,且一般IP地址是固定的,另由于DNS服务的存在,只要记住对方的域名便可以。但是在局域网,设备不一定连在上面,即使连上了,服务也不一定开了,每当设备连接到局域网的时候,IP地址一般都是动态分配的,所以情况变的复杂。Bonjour的存在便是苹果为了解决局域网设备间连接麻烦的问题。
    直白的说Bonjour就是是一种协议,使得局域网中的计算机可以方便的发布服务,发现服务和连接服务,达到零配置(Zeroconf)的目的。 Zeroconf Working Group指出要实现零配置网络服务的3个要求:

    • IP地址
    • 名字IP地址 的转换(即使没有DNS服务器的情况下)
    • 发现网络中的服务

    对于第一个要求相关系统和设备可以直接支持的,如动态IP地址分配。
    第二个要求则可以通过多播(UDP协议向局域网内一组机器发送数据)的方式发送 类似DNS查询的请求,开启着的网络服务收到之后便作出回应,告知自己的名字。
    第三个要求则通过DNS-SD来实现

    Bonjour一般的工作模式便是:在同一个局域网中,一方开启服务,通过Bonjour接口将这个服务发布,服务搜索方在服务列表中便可以看到对应的设备的名字,选择设备便可以进行连接了。整个过程无需事先知道服务发布方的IP地址和端口号。
    我们常用的软件如iTunes的共享,keynote的remote控制或者支持Bonjour协议的打印机都可以看到Bonjour的影子。

    二.Bonjour的实现及使用

    从上面的描述可以看出,Bonjour的用途便是在局域网内发布服务和搜索服务。 下面从实现层面讲解Bonjour。

    • Multicast DNS Responder(mDNSResponder,开源项目)
    • Low-Level Socket Based API(dns_sd.h, The DNS Service Discovery API)
    • CoreFoundation(CFNetService/CFNetServiceBroswer)
    • Foundation(NSNetService/NSNetServiceBroswer)

    一般情况下我们使用Foundation这一层接口就可以了,也是最方便的。 当然服务方在发布服务之前你得先启好网络服务,比如listening socket创建好,且开始侦听某个端口.

    1.发布服务

    netService = [[[NSNetService alloc] initWithDomain:@""
                                                  type:@"_test._tcp"
                                                  name:@""
                                                  port:port] autorelease];
    if(netService != nil) {
        [netService scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                   forMode:NSRunLoopCommonModes];
        netService.delegate = self;
        [netService publish];
    }
    

    2.浏览服务

    • 创建Service Broswer, 需要指定service type和domain,得和发布服务时候的type对应。还得设置delegate,然后实现其delegate方法,以便发现了服务之后进行处理以及对发现的服务进行获取IP地址和端口的结果进行处理。
    testServiceBrowser = [[NSNetServiceBrowser alloc] init];
    testServiceBrowser.delegate = self;
    [testServiceBrowser searchForServicesOfType:@"_test._tcp" inDomain:@""];
    
    • 实现Service Broswer 的delegate方法,处理服务增加或减少的事件
    //pragma mark NetServiceBroswer Delegate
    - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser
               didFindService:(NSNetService*)service
                    moreComing:(BOOL)moreComing {
       [netServiceArray addObject:service];
       if (!moreComing) {
           [serviceTableView reloadData];
       }
    }
    
    - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser
            didRemoveService:(NSNetService*)service
                   moreComing:(BOOL)moreComing {
        [netServiceArray removeObject:service];
        if (!moreComing) {
            [serviceTableView reloadData];
        }
    }
    
    • 连接服务

    上面发现的Net Service是不带IP地址和端口信息的。
    从服务列表中选择一个已经发现的服务,进行Resolve,便可以获取服务的详细信息了。

    - (IBAction)connect:(id)sender{
        NSUInteger selectedRow = [serviceTableView selectedRow];
        NSNetService *selectedServiece = [netServiceArray objectAtIndex:selectedRow];
        selectedServiece.delegate = self;
        [selectedServiece resolveWithTimeout:5.0];
    }
    

    Resolve成功

    //NSNetService Delegate
    - (void)netServiceDidResolveAddress:(NSNetService *)sender{
         NSLog(@"service ip:%@ port:%d",sender.address,sender.port);
        if ([sender getInputStream:&inputStream outputStream:&outputStream]) {
            [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                             forMode:NSDefaultRunLoopMode];
            [outputStream open];
            //发送数据
            NSData *helloData = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
            [outputStream write:[helloData bytes] maxLength:[helloData length]];
        }
    }
    

    上面的代码充分利用了输入输出流进行通信。如果你自己的使用socket的连接也是可以的,因为这个时候已经可以获取了对方的IP地址和端口了。

    ]]>
    Mac&iOS Socket 2012-01-31T00:00:00-08:00 卢克 http://geeklu.com/2012/01/macios-socket 大纲
    • 一.Socket简介
    • 二.BSD Socket编程准备
      • 1.地址
      • 2.端口
      • 3.网络字节序
      • 4.半相关与全相关
      • 5.网络编程模型
    • 三.socket接口编程示例
    • 四.使用select
    • 五.使用kqueue
    • 六.使用流

    注:文档中设计涉及的代码也都在本人github目录下,分别为socketServer和socketClient.对应着各个分支。 分支


    一.Socket简介

    在UNIX系统中,万物皆文件(Everything is a file)。所有的IO操作都可以看作对文件的IO操作,都遵循着这样的操作模式:打开 -> 读/写 -> 关闭,打开操作(如open函数)获取“文件”使用权,返回文件描述符,后继的操作都通过这个文件描述符来进行。很多系统调用都依赖于文件描述符,它是一个无符号整数,每一个用户进程都对应着一个文件描述符表,通过文件描述符就可以找到对应文件的信息。 在类UNIX平台上,对于控制台的标准输入输出以及标准错误输出都有对应的文件描述符,分别为0,1,2。它们定义在 unistd.h

    #define     STDIN_FILENO   0   /* standard input file descriptor */
    #define    STDOUT_FILENO   1   /* standard output file descriptor */
    #define    STDERR_FILENO   2   /* standard error file descriptor */   
    

    在Mac系统中,可以通过Activity Monitor来查看某个进程打开的文件和端口。 已打开文件

    UNIX内核加入TCP/IP协议的时候,便在系统中引入了一种新的IO操作,只不过由于网络连接的不可靠性,所以网络IO比本地设备的IO复杂很多。这一系列的接口叫做BSD Socket API,当初由伯克利大学研发,最终成为网络开发接口的标准。 网络通信从本质上讲也是进程间通信,只是这两个进程一般在网络中不同计算机上。当然Socket API其实也提供了专门用于本地IPC的使用方式:UNIX Domain Socket,这个这里就不细说了。本文所讲的Socket如无例外,均是说的Internet Socket。

    在本地的进程中,每一个进程都可以通过PID来标识,对于网络上的一个计算机中的进程如何标识呢?网络中的计算机可以通过一个IP地址进行标识,一个计算机中的某个进程则可以通过一个无符号整数(端口号)来标识,所以一个网络中的进程可以通过IP地址+端口号的方式进行标识。

    二.BSD Socket编程准备

    1.地址

    在程序中,我们如何保存一个地址呢?在 <sys/socket.h>中的sockaddr便是描述socket地址的结构体类型.

    /*
    * [XSI] Structure used by kernel to store most addresses.
    */
    struct sockaddr {
        __uint8_t    sa_len;       /* total length */
        sa_family_t  sa_family;    /* [XSI] address family */
        char        sa_data[14];   /* [XSI] addr value (actually larger) */
    };
    

    为了方便设置用语网络通信的socket地址,引入了sockaddr_in结构体(对于UNIX Domain Socket则对应sockaddr_un)

    /*
     * Socket address, internet style.
     */
    struct sockaddr_in {
        __uint8_t    sin_len;
        sa_family_t  sin_family;
        in_port_t    sin_port;//得是网络字节序
        struct   in_addr sin_addr;//in_addr存在的原因则是历史原因,其实质是代表一个IP地址的32位整数
        char        sin_zero[8];//bzero之,纯粹是为了兼容sockaddr
    };
    

    在实际编程的时候,经常需要将sockaddr_in强制转换成sockaddr类型。

    2.端口

    说到端口我们经常会联想到硬件,在网络编程中的端口其实是一个标识而已,或者说是系统的资源而已。系统提供了端口分配和管理的机制。

    3.网络字节序

    谈网络字节序(Endianness)之前我们先说说什么是字节序。字节序又叫端序,就是指计算机中存放 多字节数据的字节的顺序。典型的就是数据存放在内存中或者网络传输时的字节的顺序。常用的字节序有大端序(big-endian),小端序(litle-endian,另还有不常见的混合序middle-endian)。不同的CPU可能会使用不同的字节序,如X86,PDP-11等处理器为小端序,Motorola 6800,PowerPC 970等使用的是大端序。小端序是指低字节位存放在内存地址的低端,高端序是指高位字节存放在内存的低端。 举个例子来说明什么是大端序和小端序: 比如一个4字节的整数 16进制形式为 0x12345678,最左边是高位。

    大端序

    低位 高位
    12345678

    小端序

    低位 高位
    78563412

    TCP/IP 各层协议将字节序使用的是大端序,我们把TCP/IP协议中使用的字节序称之为网络字节序。 编程的时候可以使用定义在sys/_endian.h中的相关的接口进行本地字节序和网络字节序的互转。

    #define ntohs(x)   __DARWIN_OSSwapInt16(x) // 16位整数 网络字节序转主机字节序
    #define htons(x)   __DARWIN_OSSwapInt16(x) // 16位整数 主机字节序转网络字节序
    
    #define ntohl(x)   __DARWIN_OSSwapInt32(x)  //32位整数 网络字节序转主机字节序
    #define htonl(x)   __DARWIN_OSSwapInt32(x) //32位整数 主机字节序转网络字节序
    

    以上声明中 n代表netwrok, h代表host ,s代表short,l代表long

    如果数据是单字节的话,则其没有字节序的说法了。

    4.半相关与全相关

    半相关(half-association)是指一个三元组 (协议,本地IP地址,本地端口),通过这个三元组就可以唯一标识一个网络中的进程,一般用于listening socket。但是实际进行通信的过程,至少需要两个进程,且它们所使用的协议必须一致,所以一个完成的网络通信至少需要一个五元组表示(协议,本地地址,本地端口,远端地址,远端端口),这样的五元组叫做全相关。

    5.网络编程模型

    网络存在的本质其实就是网络中个体之间的在某个领域的信息存在不对等性,所以一般情况下总有一些个体为另一些个体提供服务。提供服务器的我们把它叫做服务器,接受服务的叫做客户端。所以在网络编程中,也存在服务器端和客户端之分。

    服务器端客户端
    创建Socket-
    将Socket和本地的地址端口绑定-
    开始进行侦听创建一个Socket和服务器的地址并通过它们向服务器发送连接请求
    握手成功,接受请求,得到一个新的Socket,通过它可以和客户端进行通信连接成功,客户端的Socket会绑定到系统分配的一个端口上,并可以通过它和服务器端进行通信

    三.BSD Socket编程详解

    下面的例子是一个简单的一对一聊天的程序,分服务器和客户端,且发送消息和接受消息次序固定。

    Server端代码

    #include <stdio.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    
    int main (int argc, const char * argv[])
    {
        struct sockaddr_in server_addr;
        server_addr.sin_len = sizeof(struct sockaddr_in);
        server_addr.sin_family = AF_INET;//Address families AF_INET互联网地址簇
        server_addr.sin_port = htons(11332);
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        bzero(&(server_addr.sin_zero),8);
        
        //创建socket
        int server_socket = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM 有连接
        if (server_socket == -1) {
            perror("socket error");
            return 1;
        }
        
        //绑定socket:将创建的socket绑定到本地的IP地址和端口,此socket是半相关的,只是负责侦听客户端的连接请求,并不能用于和客户端通信
        int bind_result = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
        if (bind_result == -1) {
            perror("bind error");
            return 1;
        }
        
        //listen侦听 第一个参数是套接字,第二个参数为等待接受的连接的队列的大小,在connect请求过来的时候,完成三次握手后先将连接放到这个队列中,直到被accept处理。如果这个队列满了,且有新的连接的时候,对方可能会收到出错信息。
        if (listen(server_socket, 5) == -1) {
            perror("listen error");
            return 1;
        }
    
        struct sockaddr_in client_address;
        socklen_t address_len;
        int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &address_len);
        //返回的client_socket为一个全相关的socket,其中包含client的地址和端口信息,通过client_socket可以和客户端进行通信。
        if (client_socket == -1) {
            perror("accept error");
            return -1;
        }
        
        char recv_msg[1024];
        char reply_msg[1024];
        
        while (1) {
            bzero(recv_msg, 1024);
            bzero(reply_msg, 1024);
            
            printf("reply:");
            scanf("%s",reply_msg);
            send(client_socket, reply_msg, 1024, 0);
            
            long byte_num = recv(client_socket,recv_msg,1024,0);
            recv_msg[byte_num] = '\0';
            printf("client said:%s\n",recv_msg);
    
        }
        
        return 0;
    }
    

    Client端代码

    #include <stdio.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    
    int main (int argc, const char * argv[])
    {
        struct sockaddr_in server_addr;
        server_addr.sin_len = sizeof(struct sockaddr_in);
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(11332);
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        bzero(&(server_addr.sin_zero),8);
        
        int server_socket = socket(AF_INET, SOCK_STREAM, 0);
        if (server_socket == -1) {
            perror("socket error");
            return 1;
        }
        char recv_msg[1024];
        char reply_msg[1024];
        
        if (connect(server_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in))==0)  {
        //connect 成功之后,其实系统将你创建的socket绑定到一个系统分配的端口上,且其为全相关,包含服务器端的信息,可以用来和服务器端进行通信。
            while (1) {
                bzero(recv_msg, 1024);
                bzero(reply_msg, 1024);
                long byte_num = recv(server_socket,recv_msg,1024,0);
                recv_msg[byte_num] = '\0';
                printf("server said:%s\n",recv_msg);
                
                printf("reply:");
                scanf("%s",reply_msg);
                if (send(server_socket, reply_msg, 1024, 0) == -1) {
                    perror("send error");
                }
            }
            
        }
        
        // insert code here...
        printf("Hello, World!\n");
        return 0;
    }
    

    上面的服务器端和客户端连接成功之后打开的端口的情况是怎么样的呢?

    • 服务器端 ,存在一个用于listen的半相关的socket,一个用于和客户端进行通信的全相关的socket 服务器端进程打开文件

    • 客户端 存在一个用于和服务器端进行通信的全相关的socket 客户端进程打开文件

    由于accept只运行了一次,所以服务器端一次只能和一个客户端进行通信,且使用的send和recv方法都是阻塞的,所以上面这个例子存在一个问题就是服务器端客户端连接成功之后,发送,接受,发送,接受的次序就被固定了。比如服务器端发送消息之后就等客户端发送消息了,没有接受到客户端的消息之前服务器端是没有办法发送消息的。使用select这个这个系统调用可以解决上面的问题。

    四.使用select

    select这个系统调用,是一种多路复用IO方案,可以同时对多个文件描述符进行监控,从而知道哪些文件描述符可读,可写或者出错,不过select方法是阻塞的,可以设定超时时间。 select使用的步骤如下:

    • 1.创建一个fd_set变量(fd_set实为包含了一个整数数组的结构体),用来存放所有的待检查的文件描述符
    • 2.清空fd_set变量,并将需要检查的所有文件描述符加入fd_set
    • 3.调用select。若返回-1,则说明出错;返回0,则说明超时,返回正数,则为发生状态变化的文件描述符的个数
    • 4.若select返回大于0,则依次查看哪些文件描述符变的可读,并对它们进行处理
    • 5.返回步骤2,开始新一轮的检测

    若上面的聊天程序使用select进行改写,则是下面这样的

    服务器端

    #include <stdio.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <unistd.h>
    #define BACKLOG 5 //完成三次握手但没有accept的队列的长度
    #define CONCURRENT_MAX 8 //应用层同时可以处理的连接
    #define SERVER_PORT 11332
    #define BUFFER_SIZE 1024
    #define QUIT_CMD ".quit"
    int client_fds[CONCURRENT_MAX];
    int main (int argc, const char * argv[])
    {
        char input_msg[BUFFER_SIZE];
        char recv_msg[BUFFER_SIZE];   
        //本地地址
        struct sockaddr_in server_addr;
        server_addr.sin_len = sizeof(struct sockaddr_in);
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(SERVER_PORT);
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        bzero(&(server_addr.sin_zero),8);
        //创建socket
        int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (server_sock_fd == -1) {
            perror("socket error");
            return 1;
        }
        //绑定socket
        int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
        if (bind_result == -1) {
            perror("bind error");
            return 1;
        }
        //listen
        if (listen(server_sock_fd, BACKLOG) == -1) {
            perror("listen error");
            return 1;
        }
        //fd_set
        fd_set server_fd_set;
        int max_fd = -1;
        struct timeval tv;
        tv.tv_sec = 20;
        tv.tv_usec = 0;
        while (1) {
            FD_ZERO(&server_fd_set);
            //标准输入
            FD_SET(STDIN_FILENO, &server_fd_set);
            if (max_fd < STDIN_FILENO) {
                max_fd = STDIN_FILENO;
            }
            //服务器端socket
            FD_SET(server_sock_fd, &server_fd_set);
            if (max_fd < server_sock_fd) {
                max_fd = server_sock_fd;
            }
            //客户端连接
            for (int i = 0; i < CONCURRENT_MAX; i++) {
                if (client_fds[i]!=0) {
                    FD_SET(client_fds[i], &server_fd_set);
                    
                    if (max_fd < client_fds[i]) {
                        max_fd = client_fds[i];
                    }
                }
            }
            int ret = select(max_fd+1, &server_fd_set, NULL, NULL, &tv);
            if (ret < 0) {
                perror("select 出错\n");
                continue;
            }else if(ret == 0){
                printf("select 超时\n");
                continue;
            }else{
                //ret为未状态发生变化的文件描述符的个数
                if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
                    //标准输入
                    bzero(input_msg, BUFFER_SIZE);
                    fgets(input_msg, BUFFER_SIZE, stdin);
                    //输入 ".quit" 则退出服务器
                    if (strcmp(input_msg, QUIT_CMD) == 0) {
                        exit(0);
                    }
                    for (int i=0; i<CONCURRENT_MAX; i++) {
                        if (client_fds[i]!=0) {
                            send(client_fds[i], input_msg, BUFFER_SIZE, 0);
                        }
                    }
                }
                if (FD_ISSET(server_sock_fd, &server_fd_set)) {
                    //有新的连接请求
                    struct sockaddr_in client_address;
                    socklen_t address_len;
                    int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
                    if (client_socket_fd > 0) {
                        int index = -1;
                        for (int i = 0; i < CONCURRENT_MAX; i++) {
                            if (client_fds[i] == 0) {
                                index = i;
                                client_fds[i] = client_socket_fd;
                                break;
                            }
                        }
                        if (index >= 0) {
                            printf("新客户端(%d)加入成功 %s:%d \n",index,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                        }else{
                            bzero(input_msg, BUFFER_SIZE);
                            strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n");
                            send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
                            printf("客户端连接数达到最大值,新客户端加入失败 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                        }
                    }
                }
                for (int i = 0; i <CONCURRENT_MAX; i++) {
                    if (client_fds[i]!=0) {
                        if (FD_ISSET(client_fds[i], &server_fd_set)) {
                            //处理某个客户端过来的消息
                            bzero(recv_msg, BUFFER_SIZE);
                            long byte_num = recv(client_fds[i],recv_msg,BUFFER_SIZE,0);
                            if (byte_num > 0) {
                                if (byte_num > BUFFER_SIZE) {
                                    byte_num = BUFFER_SIZE;
                                }
                                recv_msg[byte_num] = '\0';
                                printf("客户端(%d):%s\n",i,recv_msg);
                            }else if(byte_num < 0){
                                printf("从客户端(%d)接受消息出错.\n",i);
                            }else{
                                FD_CLR(client_fds[i], &server_fd_set);
                                client_fds[i] = 0;
                                printf("客户端(%d)退出了\n",i);
                            }
                        }
                    }
                }
            }
        }
        return 0;
    }
    

    客户端

    #include <stdio.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    #define BUFFER_SIZE 1024
    
    int main (int argc, const char * argv[])
    {
        struct sockaddr_in server_addr;
        server_addr.sin_len = sizeof(struct sockaddr_in);
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(11332);
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        bzero(&(server_addr.sin_zero),8);
        
        int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (server_sock_fd == -1) {
            perror("socket error");
            return 1;
        }
        char recv_msg[BUFFER_SIZE];
        char input_msg[BUFFER_SIZE];
        
        if (connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in))==0) {
            fd_set client_fd_set;
            struct timeval tv;
            tv.tv_sec = 20;
            tv.tv_usec = 0;
    
            
            while (1) {
                FD_ZERO(&client_fd_set);
                FD_SET(STDIN_FILENO, &client_fd_set);
                FD_SET(server_sock_fd, &client_fd_set);
                
                int ret = select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
                if (ret < 0 ) {
                    printf("select 出错!\n");
                    continue;
                }else if(ret ==0){
                    printf("select 超时!\n");
                    continue;
                }else{
                    if (FD_ISSET(STDIN_FILENO, &client_fd_set)) {
                        bzero(input_msg, BUFFER_SIZE);
                        fgets(input_msg, BUFFER_SIZE, stdin);
                        if (send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1) {
                            perror("发送消息出错!\n");
                        }
                    }
                    
                    if (FD_ISSET(server_sock_fd, &client_fd_set)) {
                        bzero(recv_msg, BUFFER_SIZE);
                        long byte_num = recv(server_sock_fd,recv_msg,BUFFER_SIZE,0);
                        if (byte_num > 0) {
                            if (byte_num > BUFFER_SIZE) {
                                byte_num = BUFFER_SIZE;
                            }
                            recv_msg[byte_num] = '\0';
                            printf("服务器:%s\n",recv_msg);
                        }else if(byte_num < 0){
                            printf("接受消息出错!\n");
                        }else{
                            printf("服务器端退出!\n");
                            exit(0);
                        }
    
                    }
                }
            }
            
        }
        
        return 0;
    }
    

    当然select也有其局限性。当fd_set中的文件描述符较少,或者大都数文件描述符都比较活跃的时候,select的效率还是不错的。Mac系统中已经定义了fd_set 最大可以容纳的文件描述符的个数为1024

       //sys/_structs.h
       #define __DARWIN_FD_SETSIZE 1024
       /////////////////////////////////////////////
       //Kernel.framework sys/select.h
       #define FD_SETSIZE  __DARWIN_FD_SETSIZE
    

    每一次select 调用的时候,都涉及到user space和kernel space的内存拷贝,且会对fd_set中的所有文件描述符进行遍历,如果所有的文件描述符均不满足,且没有超时,则当前进程便开始睡眠,直到超时或者有文件描述符状态发生变化。当文件描述符数量较大的时候,将耗费大量的CPU时间。所以后来有新的方案出现了,如windows2000引入的IOCP,Linux Kernel 2.6中成熟的epoll,FreeBSD4.x引入的kqueue。

    五.使用kqueue

    Mac是基于BSD的内核,所使用的是kqueue(kernel event notification mechanism,详细内容可以Mac中 man 2 kqueue),kqueue比select先进的地方就在于使用事件触发的机制,且其调用无需每次对所有的文件描述符进行遍历,返回的时候只返回需要处理的事件,而不像select中需要自己去一个个通过FD_ISSET检查。
    kqueue默认的触发方式是level 水平触发,可以通过设置event的flag为EV_CLEAR 使得这个事件变为边沿触发,可能epoll的触发方式无法细化到单个event,需要查证。

    kqueue中涉及两个系统调用,kqueue()和kevent()

    • kqueue() 创建kernel级别的事件队列,并返回队列的文件描述符
    • kevent() 往事件队列中加入订阅事件,或者返回相关的事件数组

    kqueue使用的流程一般如下:

    • 创建kqueue
    • 创建struct kevent变量(注意这里的kevent是结构体类型名),可以通过EV_SET这个宏提供的快捷方式进行创建
    • 通过kevent系统调用将创建好的kevent结构体变量加入到kqueue队列中,完成对指定文件描述符的事件的订阅
    • 通过kevent系统调用获取满足条件的事件队列,并对每一个事件进行处理
    #include <stdio.h>
    #include <stdlib.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/event.h>
    #include <sys/types.h>
    #include <sys/time.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <unistd.h>
    #define BACKLOG 5 //完成三次握手但没有accept的队列的长度
    #define CONCURRENT_MAX 8 //应用层同时可以处理的连接
    #define SERVER_PORT 11332
    #define BUFFER_SIZE 1024
    #define QUIT_CMD ".quit"
    int client_fds[CONCURRENT_MAX];
    struct kevent events[10];//CONCURRENT_MAX + 2
    int main (int argc, const char * argv[])
    {
        char input_msg[BUFFER_SIZE];
        char recv_msg[BUFFER_SIZE];
        //本地地址
        struct sockaddr_in server_addr;
        server_addr.sin_len = sizeof(struct sockaddr_in);
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(SERVER_PORT);
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        bzero(&(server_addr.sin_zero),8);
        //创建socket
        int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (server_sock_fd == -1) {
            perror("socket error");
            return 1;
        }
        //绑定socket
        int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
        if (bind_result == -1) {
            perror("bind error");
            return 1;
        }
        //listen
        if (listen(server_sock_fd, BACKLOG) == -1) {
            perror("listen error");
            return 1;
        }
        struct timespec timeout = {10,0};
        //kqueue
        int kq = kqueue();
        if (kq == -1) {
            perror("创建kqueue出错!\n");
            exit(1);
        }
        struct kevent event_change;
        EV_SET(&event_change, STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, NULL);
        kevent(kq, &event_change, 1, NULL, 0, NULL);
        EV_SET(&event_change, server_sock_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
        kevent(kq, &event_change, 1, NULL, 0, NULL);
        while (1) {
            int ret = kevent(kq, NULL, 0, events, 10, &timeout);
            if (ret < 0) {
                printf("kevent 出错!\n");
                continue;
            }else if(ret == 0){
                printf("kenvent 超时!\n");
                continue;
            }else{
                //ret > 0 返回事件放在events中
                for (int i = 0; i < ret; i++) {
                    struct kevent current_event = events[i];
                    //kevent中的ident就是文件描述符
                    if (current_event.ident == STDIN_FILENO) {
                        //标准输入
                        bzero(input_msg, BUFFER_SIZE);
                        fgets(input_msg, BUFFER_SIZE, stdin);
                        //输入 ".quit" 则退出服务器
                        if (strcmp(input_msg, QUIT_CMD) == 0) {
                            exit(0);
                        }
                        for (int i=0; i<CONCURRENT_MAX; i++) {
                            if (client_fds[i]!=0) {
                                send(client_fds[i], input_msg, BUFFER_SIZE, 0);
                            }
                        }
                    }else if(current_event.ident == server_sock_fd){
                        //有新的连接请求
                        struct sockaddr_in client_address;
                        socklen_t address_len;
                        int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
                        if (client_socket_fd > 0) {
                            int index = -1;
                            for (int i = 0; i < CONCURRENT_MAX; i++) {
                                if (client_fds[i] == 0) {
                                    index = i;
                                    client_fds[i] = client_socket_fd;
                                    break;
                                }
                            }
                            if (index >= 0) {
                                EV_SET(&event_change, client_socket_fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
                                kevent(kq, &event_change, 1, NULL, 0, NULL);
                                printf("新客户端(fd = %d)加入成功 %s:%d \n",client_socket_fd,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                            }else{
                                bzero(input_msg, BUFFER_SIZE);
                                strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n");
                                send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
                                printf("客户端连接数达到最大值,新客户端加入失败 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
                            }
                        }
                    }else{
                        //处理某个客户端过来的消息
                        bzero(recv_msg, BUFFER_SIZE);
                        long byte_num = recv((int)current_event.ident,recv_msg,BUFFER_SIZE,0);
                        if (byte_num > 0) {
                            if (byte_num > BUFFER_SIZE) {
                                byte_num = BUFFER_SIZE;
                            }
                            recv_msg[byte_num] = '\0';
                            printf("客户端(fd = %d):%s\n",(int)current_event.ident,recv_msg);
                        }else if(byte_num < 0){
                            printf("从客户端(fd = %d)接受消息出错.\n",(int)current_event.ident);
                        }else{
                            EV_SET(&event_change, current_event.ident, EVFILT_READ, EV_DELETE, 0, 0, NULL);
                            kevent(kq, &event_change, 1, NULL, 0, NULL);
                            close((int)current_event.ident);
                            for (int i = 0; i < CONCURRENT_MAX; i++) {
                                if (client_fds[i] == (int)current_event.ident) {
                                    client_fds[i] = 0;
                                    break;
                                }
                            }
                            printf("客户端(fd = %d)退出了\n",(int)current_event.ident);
                        }
                    }
                }
            }
        }
        return 0;
    }
    

    其实kqueue的应用场景非常的广阔,可以监控文件系统中文件的变化(对文件变化的事件可以粒度非常的细,具体可以查看kqueue的手册),监控系统进程的生命周期。GCD的事件处理便是建立在kqueue之上的。

    六.使用Streams

    使用Objective-C的一大优点便是面向对象编程,使得逻辑抽象得更加优美,更加符合人类思维。 一开始说过,无论是对于文件的操作或者对于网络的操作,本质上都是IO操作,无非写数据和读数据,可以对这种输入输出进行抽象,抽象成输入流和输出流, 从输入流中读取数据,往输出流中写数据。 Cocoa中的NSInputStream和NSOutputStream便是输入流和输出流的抽象,它们的实现分别基于CoreFoundation中的CFReadStream和CFWriteStream。 输入输出流对runloop有很好的支持。 NSInputStream和CFReadStream以及NSOutputStream和CFWriteStream之间可以通过 "toll-free bridging"实现无缝的类型转换。 CoreFoundation中的CFStream提供了输入输出流和CFSocket绑定的函数。 这样便可以通过输入输出流和远端进行通信了。

    首先通过XCode创建一个Foundation(C的也行,但是你得将main.c 改成main.m)的命令行项目. 创建一个ChatServer的类,包含一个run的方法。在Cocoa的程序中有一点是和C语言不同的,你无需自己去写一个死循环充当runloop,框架本身就对runloop进行了支持,需要做的就是将事件源加入到当前线程的runloop中,然后启动runloop。 所以在run方法中,创建好用于侦听连接请求的socket,socket有对应的处理连接accept的回调函数,以及把它封装成runloop的输入源,加入到当前runloop。 我们还得从标准输入获取需要发送消息,所以使用了CFFileDescriptor,它是文件描述符的objc的封装,加入了runloop的支持,通过它可以将标准输入以输入源的方法加入到当前runloop,当标准输入缓冲区有数据可读的时候,设置好的回调函数便会被调用。 最后启动runloop。

    ChatServer中的run方法

    - (BOOL)run:(NSError **)error{
        BOOL successful = YES;
        CFSocketContext socketCtxt = {0, self, NULL, NULL, NULL};
        _socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, 
                                 IPPROTO_TCP, 
                                 kCFSocketAcceptCallBack,
                                 (CFSocketCallBack)&SocketConnectionAcceptedCallBack,
                                 &socketCtxt);
        if (NULL == _socket) {
            if (nil != error) {
                *error = [[NSError alloc] 
                          initWithDomain:ServerErrorDomain
                          code:kServerNoSocketsAvailable
                          userInfo:nil];
            }
            successful = NO;
        }
        if(YES == successful) {
            // enable address reuse
            int yes = 1;
            setsockopt(CFSocketGetNative(_socket), 
                       SOL_SOCKET, SO_REUSEADDR,
                       (void *)&yes, sizeof(yes));
            uint8_t packetSize = 128;
            setsockopt(CFSocketGetNative(_socket),
                       SOL_SOCKET, SO_SNDBUF,
                       (void *)&packetSize, sizeof(packetSize));
            setsockopt(CFSocketGetNative(_socket),
                       SOL_SOCKET, SO_RCVBUF,
                       (void *)&packetSize, sizeof(packetSize));
            struct sockaddr_in addr4;
            memset(&addr4, 0, sizeof(addr4));
            addr4.sin_len = sizeof(addr4);
            addr4.sin_family = AF_INET;
            addr4.sin_port = htons(CHAT_SERVER_PORT); 
            addr4.sin_addr.s_addr = htonl(INADDR_ANY);
            NSData *address4 = [NSData dataWithBytes:&addr4 length:sizeof(addr4)];
            if (kCFSocketSuccess != CFSocketSetAddress(_socket, (CFDataRef)address4)) {
                if (error) *error = [[NSError alloc] 
                                     initWithDomain:ServerErrorDomain
                                     code:kServerCouldNotBindToIPv4Address
                                     userInfo:nil];
                if (_socket) CFRelease(_socket);
                _socket = NULL;
                successful = NO;
            } else {
                // now that the binding was successful, we get the port number 
                NSData *addr = [(NSData *)CFSocketCopyAddress(_socket) autorelease];
                memcpy(&addr4, [addr bytes], [addr length]);
                self.port = ntohs(addr4.sin_port);
                // 将socket 输入源加入到当前的runloop
                CFRunLoopRef cfrl = CFRunLoopGetCurrent();
                CFRunLoopSourceRef source4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
                CFRunLoopAddSource(cfrl, source4, kCFRunLoopDefaultMode);
                CFRelease(source4);             
                //标准输入,当在命令行中输入时,回调函数便会被调用
                CFFileDescriptorContext context = {0,self,NULL,NULL,NULL};
                CFFileDescriptorRef stdinFDRef = CFFileDescriptorCreate(kCFAllocatorDefault, STDIN_FILENO, true, FileDescriptorCallBack, &context);
                CFFileDescriptorEnableCallBacks(stdinFDRef,kCFFileDescriptorReadCallBack);
                CFRunLoopSourceRef stdinSource = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, stdinFDRef, 0);
                CFRunLoopAddSource(cfrl, stdinSource, kCFRunLoopDefaultMode);
                CFRelease(stdinSource);
                CFRelease(stdinFDRef); 
                CFRunLoopRun();
            }
        }
        return successful;
    }
    

    当有客户端连接请求过来时, SocketConnectionAcceptedCallBack这个回调函数会被调用,根据新的全相关的socket,生成输入输出流,并设置输入输出流的delegate方法,将其添加到当前的runloop,这样流中有数据过来的时候,delegate方法会被调用。

    SocketConnectionAcceptedCallBack函数

    static void SocketConnectionAcceptedCallBack(CFSocketRef socket, 
                                                 CFSocketCallBackType type, 
                                                 CFDataRef address, 
                                                 const void *data, void *info) {
        ChatServer *theChatServer = (ChatServer *)info;
        if (kCFSocketAcceptCallBack == type) { 
            // 摘自kCFSocketAcceptCallBack的文档,New connections will be automatically accepted and the callback is called with the data argument being a pointer to a CFSocketNativeHandle of the child socket. This callback is usable only with listening sockets.
            CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;
            // create the read and write streams for the connection to the other process
            CFReadStreamRef readStream = NULL;
            CFWriteStreamRef writeStream = NULL;
            CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle,
                                         &readStream, &writeStream);
            if(NULL != readStream && NULL != writeStream) {
                CFReadStreamSetProperty(readStream, 
                                        kCFStreamPropertyShouldCloseNativeSocket,
                                        kCFBooleanTrue);
                CFWriteStreamSetProperty(writeStream, 
                                         kCFStreamPropertyShouldCloseNativeSocket,
                                         kCFBooleanTrue);
                NSInputStream *inputStream = (NSInputStream *)readStream;//toll-free bridging
                NSOutputStream *outputStream = (NSOutputStream *)writeStream;//toll-free bridging
                inputStream.delegate = theChatServer;
                [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
                [inputStream open];
                outputStream.delegate = theChatServer;
                [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
                [outputStream open];
                Client *aClient = [[Client alloc] init];
                aClient.inputStream = inputStream;
                aClient.outputStream = outputStream;
                aClient.sock_fd = nativeSocketHandle;
                [theChatServer.clients setValue:aClient  
                                         forKey:[NSString stringWithFormat:@"%d",inputStream]];
                NSLog(@"有新客户端(sock_fd=%d)加入",nativeSocketHandle);
            } else {
                close(nativeSocketHandle);
            }
            if (readStream) CFRelease(readStream);
            if (writeStream) CFRelease(writeStream);
        }
    }
    

    当客户端有数据传过来时,相应的NSInputStream的delegate方法被调用

     
    - (void) stream:(NSStream*)stream handleEvent:(NSStreamEvent)eventCode {
        switch (eventCode) {
            case NSStreamEventOpenCompleted: {
                break;
            }
            case NSStreamEventHasBytesAvailable: {
                Client *client = [self.clients objectForKey:[NSString stringWithFormat:@"%d",stream]];
                NSMutableData *data = [NSMutableData data];
                uint8_t *buf = calloc(128, sizeof(uint8_t));
                NSUInteger len = 0;
                while([(NSInputStream*)stream hasBytesAvailable]) {
                    len = [(NSInputStream*)stream read:buf maxLength:128];
                    if(len > 0) {
                        [data appendBytes:buf length:len];
                    }
                }
                free(buf);
                if ([data length] == 0) {
                    //客户端退出
                    NSLog(@"客户端(sock_fd=%d)退出",client.sock_fd);
                    [self.clients removeObjectForKey:[NSString stringWithFormat:@"%d",stream]];
                    close(client.sock_fd);
                }else{
                    NSLog(@"收到客户端(sock_fd=%d)消息:%@",client.sock_fd,[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
                }
                break;
            }
            case NSStreamEventHasSpaceAvailable: {
                break;
            }
            case NSStreamEventEndEncountered: {
                break;
            }
            case NSStreamEventErrorOccurred: {
                break;
            }
            default:
                break;
        }
    }
    

    当在debug窗口中输入内容并回车时,标准输入缓冲区中便有数据了,这个时候回调函数FileDescriptorCallBack将被调用,处理标准输入。

    static void FileDescriptorCallBack(CFFileDescriptorRef f,
                                       CFOptionFlags callBackTypes,
                                       void *info){
        int fd = CFFileDescriptorGetNativeDescriptor(f);
        ChatServer *theChatServer = (ChatServer *)info;
        if (fd == STDIN_FILENO) {
            NSData *inputData = [[NSFileHandle fileHandleWithStandardInput] availableData];
            NSString *inputString = [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] autorelease];
            NSLog(@"准备发送消息:%@",inputString);
            for (Client *client in [theChatServer.clients allValues]) {
                [client.outputStream write:[inputData bytes] maxLength:[inputData length]];
            }
            //处理完数据之后必须重新Enable 回调函数
            CFFileDescriptorEnableCallBacks(f,kCFFileDescriptorReadCallBack);
        }
    }
    
    ]]>
    iOS持久化 2012-01-19T00:00:00-08:00 卢克 http://geeklu.com/2012/01/ios-persistence
  • 文件系统
  • 归档和序列化
  • 数据库

  • 1.文件系统

    不管是Mac OS X 还是iOS的文件系统都是建立在UNIX文件系统基础之上的。

    1.1 沙盒模型

    在iOS中,一个App的读写权限只局限于自己的沙盒目录中。

    沙盒模型到底有哪些好处呢?
    安全:别的App无法修改你的程序或数据
    保护隐私:别的App无法读取你的程序和数据
    方便删除:因为一个App所有产生的内容都在自己的沙盒中,所以删除App只需要将沙盒删除就可以彻底删除程序了

    iOS App沙盒中的目录

    • App Bundle ,如xxx.app 其实是一个目录,里面有app本身的二进制数据以及资源文件
    • Documents, 存放程序产生的文档数据
    • Library , 下面默认包含下面两个目录 Caches Preferences
    • tmp, 临时文件目录

    如果我们想在程序中获取上面某个目录的路径,应该如何实现呢? 下面就讲讲路径的获取, 通过NSPathUtilities.h中的NSSearchPathForDirectoriesInDomains函数,我们便可以获取我们想要的路径。 此函数具体声明如下:

    NSArray *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
    directory 目录类型 比如Documents目录 就是NSDocumentDirectory
    domainMask 在iOS的程序中这个取NSUserDomainMask
    expandTilde YES,表示将~展开成完整路径

    注意函数返回的类型为数组,在iOS中一般这个数组中只包含一个元素,所以直接取lastObject即可。

    1.2 NSFileManager

    NSFileManager提供一个类方法获得一个单例。

    /* Returns the default singleton instance.*/
    + (NSFileManager *)defaultManager;
    

    下面罗列了NSFileManager的常用方法

    • 新建目录
    - (BOOL)createDirectoryAtPath:(NSString *)path withIntermediateDirectories:(BOOL)createIntermediates attributes:(NSDictionary *)attributes error:(NSError **)error;
    

    createIntermediates这个参数一般为YES,表示如果目录路径中间的某个目录不存在则创建之,如果是NO的话,则要保证所创建目录的父目录都必须已经存在

    • 获取目录下的所有文件
    - (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
    

    如果目录为空,则返回空数组

    • 其他的一些方法
    - (BOOL)copyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
    - (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
    - (BOOL)linkItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
    - (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;
    

    更多的可以查看文档 NSFileManager Class Reference

    在实际项目中,我们一般会写一个工具类来负责项目中所有的路径操作。

    2. 归档(Archives) 和 序列化(Serializations)

    我们经常听到“序列化”,“反序列化”这样的字眼,其实“序列化”的意思就是将对象转换成字节流以便保存或传输,“反序列化”便是一个相反的过程,从字节流转到对象。

    在这节中涉及到一种文件类型plist,plist就是Property List 的缩写,即所谓的属性列表,属性列表有两种数据格式,一种是XML的,方便阅读和编辑;另一种是二进制的,节省存储空间,以及提高效率。

    在Objective-C中这个对象和字节流的互转分成两类:

    • 归档 普通自定义对象和字节流之间的转换
    • 序列化 某些特定类型(NSDictionary, NSArray, NSString, NSDate, NSNumber,NSData)的数据和字节流之间(通常将其保存为plist文件)的转换

    不过本质上讲上述两种都是对象图(Object Graph)和字节流之间的转换. Apple关于序列化和归档的编程指南: Archives and Serializations Programming Guide

    2.1 归档

    如果我们需要将自定义的一个对象保存到文件,应该如何做呢?
    这里引入两个东西:一个是NSCoding协议 ;另一个是NSKeyedArchiver,NSKeyedArchiver其实继承于NSCoder,可以以键值对的方式将对象的属性进行序列化和反序列化。
    具体的过程可以这样描述 通过NSKeyedArchiver 可以将实现了NSCoding协议的对象 和 字节流 相互转换

    像一些框架中的数据类型如NSDictionary,NSArray,NSString... 都已经实现了NSCoding协议,所以可以直接对他们进行归档操作。

    这里来一个比较完整的例子,一个Address类,一个User类,User类下有个Address类型的属性。

    Address类

    @interface Address : NSObject<NSCoding>{
        NSString *country;
        NSString *city;
    }
    @property(nonatomic,copy) NSString *country;
    @property(nonatomic,copy) NSString *city;
    @end
    //////////////////////////////////////////////////////
    #import "Address.h"
    
    @implementation Address
    @synthesize country;
    @synthesize city;
    
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        [aCoder encodeObject:country forKey:@"country"];
        [aCoder encodeObject:city forKey:@"city"];
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder{
        if (self = [super init]) {
            [self setCountry:[aDecoder decodeObjectForKey:@"country"]];
            [self setCity:[aDecoder decodeObjectForKey:@"city"]];
        }
        return self;
    }
    
    @end
    

    User类

    #import <Foundation/Foundation.h>
    #import "Address.h"
    @interface User : NSObject<NSCoding>{
        NSString *_name;
        NSString *_password;
    
        Address *_address;
    }
    @property(nonatomic,copy) NSString *name;
    @property(nonatomic,copy) NSString *password;
    @property(nonatomic,retain) Address *address;
    
    @end
    /////////////////////////////////////////////////////////
    #import "User.h"
    
    @implementation User
    @synthesize name = _name;
    @synthesize password = _password;
    @synthesize address = _address;
    
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        [aCoder encodeObject:_name forKey:@"name"];
        [aCoder encodeObject:_password forKey:@"password"];
        [aCoder encodeObject:_address forKey:@"address"];
    }
    - (id)initWithCoder:(NSCoder *)aDecoder{
        if (self = [super init]) {
            [self setName:[aDecoder decodeObjectForKey:@"name"]];
            [self setPassword:[aDecoder decodeObjectForKey:@"password"]];
            [self setAddress:[aDecoder decodeObjectForKey:@"address"]];
        }
        return self;
    }
    @end
    

    使用示例

    Address *myAddress = [[[Address alloc] init] autorelease];
    myAddress.country = @"中国";
    myAddress.city = @"杭州";
     
    User *user = [[[User alloc] init] autorelease];
    user.name = @"卢克";
    user.password = @"lukejin";
    user.address = myAddress;
    
    [NSKeyedArchiver archiveRootObject:user toFile:@"/Users/Luke/Desktop/user"];
    
    id object = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/Luke/Desktop/user"];
    NSLog(@"Object Class : %@",[object class]);
    

    通过查看文件内容可以发现,保存的是plist的二进制数据格式。 转成XML可以看到如下内容:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>$archiver</key>
        <string>NSKeyedArchiver</string>
        <key>$objects</key>
        <array>
            <string>$null</string>
            <dict>
                <key>$class</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>8</integer>
                </dict>
                <key>address</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>4</integer>
                </dict>
                <key>name</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>2</integer>
                </dict>
                <key>password</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>3</integer>
                </dict>
            </dict>
            <string>卢克</string>
            <string>lukejin</string>
            <dict>
                <key>$class</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>7</integer>
                </dict>
                <key>city</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>6</integer>
                </dict>
                <key>country</key>
                <dict>
                    <key>CF$UID</key>
                    <integer>5</integer>
                </dict>
            </dict>
            <string>中国</string>
            <string>杭州</string>
            <dict>
                <key>$classes</key>
                <array>
                    <string>Address</string>
                    <string>NSObject</string>
                </array>
                <key>$classname</key>
                <string>Address</string>
            </dict>
            <dict>
                <key>$classes</key>
                <array>
                    <string>User</string>
                    <string>NSObject</string>
                </array>
                <key>$classname</key>
                <string>User</string>
            </dict>
        </array>
        <key>$top</key>
        <dict>
            <key>root</key>
            <dict>
                <key>CF$UID</key>
                <integer>1</integer>
            </dict>
        </dict>
        <key>$version</key>
        <integer>100000</integer>
    </dict>
    </plist>
    

    2.2 序列化

    在实际的项目中,我们一般是将NSDictionary或NSArray的对象保存到文件或者从文件读取成对象。 当然这种只是适用于数据量不是很大的应用场景。 NSDictionary和NSArray 都有一个写入文件的方法

     
    - (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
    

    NSDictionary和NSArray会直接写成plist文件。

    2.2.1 序列化的方式

    序列化可以通过两种途径来进行

    使用数据对象自带的方法

    写文件

     
    NSMutableDictionary *dataDictionary = [[[NSMutableDictionary alloc] init] autorelease];
     [dataDictionary setValue:[NSNumber numberWithInt:222] forKey:@"intNumber"];
     [dataDictionary setValue:[NSArray arrayWithObjects:@"1",@"2", nil] forKey:@"testArray"];
     [dataDictionary writeToFile:@"/Users/Luke/Desktop/test.plist" atomically:YES];
    

    写完的文件内容如下:

      
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>intNumber</key>
        <integer>222</integer>
        <key>testArray</key>
        <array>
            <string>1</string>
            <string>2</string>
        </array>
    </dict>
    </plist>
    

    从文件读取

        
    NSDictionary *dictionaryFromFile = [NSDictionary dictionaryWithContentsOfFile:@"/Users/Luke/Desktop/test.plist"];
    
    使用NSPropertyListSerialization类

    通过NSPropertyListSerialization类可以将数据对象直接转成NSData或者直接写到文件或者流中去.

        
    NSMutableDictionary *dataDictionary = [[[NSMutableDictionary alloc] init] autorelease];
    [dataDictionary setValue:[NSNumber numberWithInt:222] forKey:@"intNumber"];
    [dataDictionary setValue:[NSArray arrayWithObjects:@"1",@"2", nil] forKey:@"testArray"];
    
    NSString *error;
    NSData *xmlData = [NSPropertyListSerialization dataFromPropertyList:dataDictionary
                                                               format:NSPropertyListXMLFormat_v1_0
                                                     errorDescription:&error];
    if(xmlData) {
        NSLog(@"No error creating XML data.");
        [xmlData writeToFile:@"/Users/Luke/Desktop/test2.plist" atomically:YES];
    }
    else {
        if (error) {
            NSLog(@"error:%@", error);
            [error release];
        }
    }
    

    读取

        
        NSDictionary *dictionaryFromFile = (NSDictionary *)[NSPropertyListSerialization 
                                                           propertyListWithData:[NSData dataWithContentsOfFile:@"/Users/Luke/Desktop/test2.plist"] 
                                                            options:0
                                                            format:NULL
                                                            error:&error];
    

    2.2.2 User Defaults

    User Defaults 顾名思义就是一个用户为系统以及程序设置的默认值。每个用户都有自己的一套数据,用户和用户之间没法共享的。

    我们都知道每一个程序都会保存一些设置数据,比如记住上次窗口的位置和大小,记住是否弹出某些提示信息等。苹果提供了一个统一的解决方案,就是每一个app都有一个plist文件专门用以保存偏好设置数据。 plist文件名默认是程序Bundle identifier,扩展名为plist.

    除了程序自己的设置外,系统还有一些全局的或者其它的一些设置,也属于User Defaults的范畴,User Defaults的持久化数据都保存在 ~/Library/Preferences 目录中.

    这里有一点简要的说一下,User Defaults 中存放的key value分放在多个Domain中,取的时候按一定的次序取查找,次序如下:

    • The Argument Domain 程序启动的时候以参数的方式传入的
    • The Application Domain 通过NSUserDefaults往里面写数据的时候默认就是写到这个Domain的,通过Bundle identifier来标识
    • The Global Domain 用户的全局的设置(系统的偏好设置)会放在这个Domain下,比如用户的语言设置,滚动条的设置等,里面的设置会对所有的程序起作用。
    • The Languages Domains
    • The Registration Domain 这个domain里面的key value是提供默认值的,一般会在程序启动的设置进行设置,他们都不会被持久化到文件的。当某个key对应的值在上面的那些domain中都不存在的时候,就到这里找。

    Mac系统还为user defaults提供了很好的命令行工具,defaults 你可以通过下面的方式查看具体使用方式

    man defaults
    

    可以通过defaults domains查看当前用户的所有的domain,通过 defaults read NSGlobalDomain 读取 The Global Domain 中的所有值。

    NSUserDefaults 类来读写Preferences设置,而无需考虑文件位置等细节问题。

    NSUserDefaults 用起来和 NSDictionary 很相似,多了一个Domain的概念在里面。 NSUserDefaults 一样提供了一个获取单例的方法.

    + (NSUserDefaults *)standardUserDefaults
    

    NSUserDefaults提供了一系列的接口来根据key获取对应的value,搜索的次序按照上面提及到的次序在各个Domain中进行查找。还提供了一系列的 Setting Default Values的方法,这些设置的值都是在 The Application Domain 下的.当然也提供了修改其他Domain下的值的方法,只是需要整体的设置。

    3.数据库

    Mac上自带安装了SQLite3 ,如果你之前接触过关系型数据库,你可以通过命令行来对SQLite进行初步的认识

        
    $ sqlite3 test.db
    SQLite version 3.7.5
    Enter ".help" for instructions
    Enter SQL statements terminated with a ";"
    sqlite>create table if not exists names(id integer primary key asc, name text); 
    sqlite> insert into names(name) values('Luke');
    sqlite> select * from names;
    1|Luke
    sqlite> 
    

    那如果在代码中使用SQLite呢?

    • 添加sqlite的动态链接库 libsqlite3.0.dylib
    • 引入头文件 #import "sqlite3.h"

    这样之后你便可以通过C的接口来操作数据库了

        
    sqlite3 *database;//sqlite3的类型其实只是一个结构体struct
    NSArray *documentsPaths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory 
                                                             , NSUserDomainMask 
                                                             , YES); 
    NSString *databaseFilePath=[[documentsPaths objectAtIndex:0] stringByAppendingPathComponent:@"luke.db"];
    
    //打开数据库
    if (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) { 
        NSLog(@"open sqlite db ok."); 
        char *errorMsg; 
        const char *createSql="create table if not exists names (id integer primary key asc,name text)";
        //创建表
        if (sqlite3_exec(database, createSql, NULL, NULL, &errorMsg)==SQLITE_OK) { 
            NSLog(@"create ok."); 
        }else {
            NSLog(@"error: %s",errorMsg); 
            sqlite3_free(errorMsg);
        }
        
        
        //插入数据
        const char *insertSql="insert into names (name) values(\"Luke\")"; 
        if (sqlite3_exec(database, insertSql, NULL, NULL, &errorMsg) == SQLITE_OK) { 
            NSLog(@"insert ok."); 
        }else {
            NSLog(@"error: %s",errorMsg); 
            sqlite3_free(errorMsg); 
        }
        
        
        const char *selectSql="select id,name from names"; 
        sqlite3_stmt *statement; 
        if (sqlite3_prepare_v2(database, selectSql, -1, &statement, nil) == SQLITE_OK) { 
            NSLog(@"select ok.");
        }
        
        while (sqlite3_step(statement)==SQLITE_ROW) { 
            int _id=sqlite3_column_int(statement, 0); 
            char *name=(char *)sqlite3_column_text(statement, 1); 
            NSString *nameString = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
            NSLog(@"row>>id %i, name %@",_id,nameString); 
        }
        
        sqlite3_finalize(statement);
        
    }
    
    sqlite3_close(database);
    

    你会发现这完全是C语言编程,和Objective-C的代码混在一起格格不入,也很不方便,所以便有人开发了开源的sqlite c接口的wrapper

    具体的使用方法,各自的文档都写的比较清楚。 FMDB不支持多线程同时使用同一个数据库连接进行操作,否则会有线程安全问题,有可能导致数据库文件损坏。EGODatabase则引入了多线程的支持,部分代码借鉴了FMDB,两者在使用上非常的相似。另EGODatabase提供了异步数据库操作的支持,将数据库操作封装成数据库请求(其继承于NSOperation),数据库请求创建好了,丢到一个OperationQueue中被异步的进行执行,当请求数据完成之后 ,相应的delegate方法会被调用,然后你可以在主线程更新显示了.

    ]]>
    Blocks编程 2012-01-19T00:00:00-08:00 卢克 http://geeklu.com/2012/01/block
  • 介绍
  • 声明创建和调用
  • Block和变量
  • Block实际应用

  • 1.介绍

    Block是一个C Level的语法以及运行时的一个特性,非常像标准C中的函数(函数指针),但是其运行需要编译器和运行时支持,目前LLVM+Clang可以很好的支持Block(苹果修改过的GCC也可以)。Block和函数不同的是其语义 闭包 特性,以及可以有匿名block的存在。 你可以在LLVM的官方网站查看Block语言规范.
    你可以通过^ 运算符来声明一个block变量,或用来表明block定义的开始,而block的代码块则是包含在一对花括号{}内的.

    int multiplier = 2;   
    int (^myBlock)(int) = ^(int num){   
        return num * multiplier;   
    };   
    printf("%d",myBlock(4));   
    

    上面代码中的myBlock就是Block的变量名,由myBlock变量的声明可以看出,它返回值为int类型,且存在一个int型的参数。 等于号后面就是Block的定义并将其赋值给myBlock . Block的调用就和C函数的使用类似.

    2. 声明创建和调用

    声明Block变量
    Block变量保存着指向Block的指针,声明一个Block变量就和声明一个函数指针变量类似,只是将*改成了. 其他的就和C的类型系统保持一致了。

    void (^blockReturningVoidWithVoidArgument)(void);   
    int (^blockReturningIntWithIntAndCharArguments)(int, char);   
    void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
    

    另 Block还支持可变参数variadic (...) ,没有参数的话,变量列表的地方必须写上void关键字.

    声明Block类型 你可以通过typedef声明Block的类型,这样多个地方需要使用同种类型的Block的时候会比较方便,

    typedef float (^MyBlockType)(float, float);   
    MyBlockType myFirstBlock = // ... ;   
    MyBlockType mySecondBlock = // ... ;
    

    Block创建 声明了一个Block变量之后,可以为这个变量赋值

    blockReturningVoidWithVoidArgument = ^{   
          printf("%s","Block Returing Void With Void Argument.");   
    }
    

    当然你可以在声明变量的同时赋值

    void (^blockReturningVoidWithVoidArgument)(void) = ^{   
          printf("%s","Block Returing Void With Void Argument.");   
    }
    

    Block调用 Block的调用和函数的调用是非常相似的,如上面定义的blockReturningVoidWithVoidArgument,调用的时候则直接 blockReturningVoidWithVoidArgument();便可.

    匿名Block 当一个Block作为函数参数的时候,一般实参都是以匿名Block的方式传过去的。

    void callVoidVoid(void (^closure)(void)) {   
       closure();   
    }   
    int main(int argc, char *argv[]) {   
        __block int i = 10;       
        callVoidVoid(^{ ++i; });   
       if (i != 11) {   
           printf("*** %s didn't update i\n", argv[0]);   
           return 1;   
       }   
       printf("%s: success\n", argv[0]);   
       return 0;   
    }
    

    当然你也可以直接调用匿名Block,如

    ^{ ++i; }();
    

    3.Block和变量

    一个Block的内部是可以引用自身作用域外的变量的,包括static变量,extern变量或自由变量(定义一个变量的时候,如果不加存储修饰符,默认情况下就是自由变量auto,auto变量保存在stack中的.除了auto之外还存在register,static等存储修饰符) ,对于自由变量,在Block中是只读的。在引入block的同时,还引入了一种特殊的变量存储修饰符__block,通过它的变量叫做block变量,block变量在block内部可以进行写操作的。这些变量中,自由变量是最特殊的,在Block声明的时候,自由变量在Block内部只读且其值被固定住(自由变量被拷贝了一份,且限定为const),即使在block调用前改变了这个自由变量的值,block调用的时候,看到的却还是block声明的时候的那个值。

    代码示例 3.1

    __block int blockValue = 0;
    int autoValue = 0;
    
    void(^printValue)(void) = ^{
        printf("blockValue = %d\n",blockValue);
        printf("autoValue = %d\n",autoValue);
    
    };
    blockValue ++;
    autoValue ++;
    printValue();
    

    3.1中的代码,输出的值为

    blockValue = 1
    autoValue = 0
    

    可以看到自由变量尽管自增了,但是在调用printValue这个Block的时候,看到的还是其定义的时候看到的那个autoValue的值,autoValue的值在Block的内部示无法修改的,要不然编译器会报错:

    Semantic Issue: Variable is not assignable (missing __block type specifier)
    

    Block定义时内存是分配在stack上的,当其作用域结束,就会被自动释放,所以你不需要去担心它的内存情况,我们可以对一个Block进行Block_copy()操作,Block_copy()之后,Block会被拷贝到heap中的内存中,且其所有的引用到的自由变量也将会被拷贝,当然你得记得通过Block_release()释放heap的内存空间哦。 在objc中Block是和对象一样被看作一等公民的(其实这是objc的Block扩展的功劳),你可以像使用对象那样对Block进行retain(retain只对heap中的Block起作用),copy以及release操作.

    在Block内部如果引用到对象或者对象的成员变量,那么当Block被拷贝Block_copy()之后,这个对象的引用计数会增加。

    代码示例 3.2

    NSObject *testObject = [[NSObject alloc] init];
    
    NSLog(@"%lu",[testObject retainCount]); //1
    NSLog(@"%lu",[self retainCount]); //1
    
    void(^testBlock)(void) = ^{
           NSLog(@"The Test String : %@", testObject);
       NSLog(@"The Window Object : %@", _window);
    };
    
    NSLog(@"%lu",[testObject retainCount]); //1
    NSLog(@"%lu",[self retainCount]); //1
      
    void(^testBlock2)(void) = Block_copy(testBlock); //testBlock会被拷贝到heap中,所以用完了要自己调用Block_release进行释放
    NSLog(@"%lu",[testObject retainCount]); //2
    NSLog(@"%lu",[self retainCount]); //2
    
    testBlock2();
    
    Block_release(testBlock2);
    
    NSLog(@"%lu",[testObject retainCount]); //1
    NSLog(@"%lu",[self retainCount]); //1
    
    [testObject release];
    

    Block的闭包特性使得Block可以脱离其定义的作用域进行运行,所以你可以在一个函数中返回一个Block,在别的线程或者当前线程的RunLoop中进行运行,而不用担心那些引用到的外部变量是否被释放掉了。

    4.Block实际应用

    那么我们一般什么时候会用到Block呢? Blocks通常是一小段自包含的代码片段.所以它经常被用于多线程运行的代码单元(如GCD),或用于处理聚合类元素单元,或者作为某个函数调用完成后的回调函数.

    Block用作回调函数比传统的回调函数有以下的优越性:

    • 在函数调用的时候,将Block作为一个参数传给函数
    • 允许访问本地变量,这样可以避免通过结构体将本地变量封装后传递给回调函数

    应用1: Animations & Completion Handler

    [UIView animateWithDuration:2 
                   animations:^{
                        self.view.backgroundColor = [UIColor redColor];
                    }  
                    completion:^(BOOL finished){
                        if (finished){
                            self.view.backgroundColor = [UIColor blueColor];
                        } 
                    }];
    

    应用2: Enumeration
    对数据集合类中的每一个元素进行遍历,每次传入一个对象,进行处理

    NSArray *cards = [NSArray arrayWithObjects:@"Jack", @"Queen", @"King", @"Ace", nil];
    [cards enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
         NSLog(@"%@ card at index %d", object, index);
     }];
    

    应用3: Notification Handler

        
    [[NSNotificationCenter defaultCenter] 
     addObserverForName:@"TestNotification" 
     object:nil 
     queue:aNSOperationQueue 
     usingBlock:^(NSNotification *notification){
         NSLog(@"Notification: %@",notification);
     }];
    

    应用4: GCD

        
    dispatch_queue_t imageDownloadQueue = dispatch_queue_create("Image Download Queue", NULL);
    dispatch_async(imageDownloadQueue, ^{
        NSURL *imageURL = [NSURL URLWithString:@"http://xxx.xx.com/a.png"];
        NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
        UIImage *image = [UIImage imageWithData:imageData];
        dispatch_async(dispatch_get_main_queue(), ^{
            [imageView setImage:image];
        });
    });
    
    ]]>
    iPhone中Png图片格式的研究 2011-10-25T00:00:00-07:00 卢克 http://geeklu.com/2011/10/iphone-cgbi-png-format 有时候我们看到一个App,想看看他的一些界面是如何实现的,这个时候需要查看一下它的图片资源,不过iOS的png图片编译后一般的图片阅读器都是没法查看的,本文将告诉的原因和转换出原图的方法(得安装XCode)。

    ipa 解压,将png相关文件夹拷贝出来,在命令行下使用/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush -revert-iphone-optimizations xxx.png yyy.png

    我们都知道一个编译好的iPhone app 其中的png图片一般普通的图片阅读器是无法直接读取的,这是因为XCode在编译的过程中,将图片进行了优化,实际上它已经不是一个png图片了。 这边有一些apple iPhone png自己格式的一些说明 http://iphonedevwiki.net/index.php/CgBI_file_format

    在Png数据中,我们最关心的莫过于png的数据块,其中包含了png每一个像素的信息,当然了为了减少存储空间,这些像素信息都是压缩保存的。而且是使用zlib进行压缩的,压缩后 包含zlib header 信息,还有由于解压验证的crc信息。 而iPhone的CgBI格式的png则将原始的png图片作如下变化:

    • 增加一个新的关键块 CgBI Chunk 四个字节
    • zlib的header和CRC信息全部从IDAT中移除
    • 红蓝交换,每一个像素(RGBA)中的R和B进行调换变成BGRA ,解压后每一个像素有四个字节组成,也就是将每一个像素的 第一个字节和第三个字节调换
    • 透明像素处理 Premultiplied Alpha,这个的意思是为了图像加载变得更快,预先将Alpha的信息乘到像素的颜色信息中去,这样后期计算的时候就可以减少CPU或者GPU计算了

    把一个正常的PNG图片优化成iPhone 的png图片格式可以使用XCode自带的工具 /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush -iphone 还有一个第三方的开源工具也可以 https://github.com/DHowett/pincrush

    如果你想把一个经过优化后的图片还原成普通图片阅读器可以查看的png图片,就是对上面的过程进行反向处理。 现在可以找到的第三方的转换的一般有如下几个 ipin.py(Python版本) http://www.axelbrz.com.ar/?mod=iphone-png-images-normalizer iPhonePNG(C版本) http://www.newsfirerss.com/blog/?p=176

    经过本人测试,上面的这些转换工具都没有对图片alpha相关信息的做任何处理,也可能是别的原因,有一些图片转换后的结果和原始图片还是有出入的。

    编译后如果使用第三方的python或者C版本的代码来转换,转换后的图片都是这样的,感觉边角的像素有点问题,不过大部分情况下 ,图片都是ok的

    我尝试通过修改第三方的代码,想将Premultiplied Alpha 还原过去,但是还是存在各种问题,最终没有结果。
    只能最终采用XCode自带的工具进行转换 `/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush -revert-iphone-optimizations 1.png 2.png`
    这个pngcrush是apple改自开源的pngcrush 只可惜苹果修改后的版本却没有开源出来。

    为了避免每次都需要在命令行中进行操作,你可以通过automator新建一个shell的service

    for path in "$@"
    do
    mv "$path" "$path".tmp
    /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush -revert-iphone-optimizations "$path".tmp "$path"
    rm "$path".tmp
    done
    

    当然你可以修改脚本,并可以作用于文件和文件夹,对目标进行判断,文件夹则递归文件夹中的png文件进行逐个处理。

    ]]>
    SWATCH 计时器的使用 2011-10-17T00:00:00-07:00 卢克 http://geeklu.com/2011/10/swatch-timer 一般Swatch手表的计时都是采用所谓的三针计时方式:一个是秒针,一个是分针,另外一个是1/10秒针,当开始计时时,秒针开始计时,走到一圈时即是一分,分针就计为1分,当终止计时时,1/10秒针计算的是1/10秒的时间。所以三针计时方式可以精确到1/10秒。

    今天刚拿到上图中的这块表,完全搞不定如何计时。最大的误解就在于那个最长的秒针。尼玛这个最长的秒针不是手表本身的秒针,而是计时的秒针的啊,这完全违反一般的认识啊!!!! 而下方的小圆盘中的不停走动的秒针才是表的正真的秒针啊!!!

    左上方是用于计分钟的,一格是一分,右上方是记1/10秒的,一格是1/10秒。而中间的这个最大的秒针是计时用的秒针。 表的右侧有三个功能按钮,在计时归零的状态下,按一下最上方的按钮,最大的秒针开始走动,代表开始计时,再按一下暂停计时,这个时候,1/10秒的地方就会显示对应的10分之几秒,然后再按上方的按钮则继续计时,再按暂停计时。在暂停的时候按下下方的按钮,则进行计时归零。如果归零有偏位,则可以通过拔开中间的按钮一档,然后按上方或者下方的按钮进行秒针和1/10秒的归零位置调整。

    说实话,swatch计时功能使用最大的障碍就是使用最大的秒针来作为计时用的秒针(而一般人的认识中这个大指针是表的秒针)。

    ]]>
    NSCell copyWithZone 2011-08-09T00:00:00-07:00 卢克 http://geeklu.com/2011/08/nscell-copywithzone 有时候,我们的TableView需要显示的内容是自定义样式的。
    这个时候你需要创建一个自定义的Cell来渲染你自己想要的内容。
    所以你继承了NSCell,并拥有一个数据对象,用来作为渲染的数据来源。

    由于NSTableView需要使用到NSCell的copyWithZone方法在适当的时候复制NSCell,而NSCell默认的copyWithZone方法的实现方式是使用NSCopyObject创建了原始cell对象的一份浅拷贝,所以在复制的时候只会简单的复制指针的值,而不会去深拷贝或者去retain,那么这样程序运行的时候就会出错了。原因就是Cell所引用的某个对象被错误的完全释放了(复制的时候没有retain,但是在释放cell的时候,将这个对象release了一把)。所以在实现copyWithZone的时候可以采用如下方法。

    - (id)copyWithZone:(NSZone *)zone
    {
        id cellCopy = [super copyWithZone:zone];
        /* Assume that other initialization takes place here. */
     
        cellCopy->titleCell = nil;
        [cellCopy setTitleCell:[self titleCell]];
     
        return cellCopy;
    }
    

    参考文档: 1.内存管理编程指南:实现对象复制 2.NSTableView copies cells – bug or feature?

    ]]>