在过去的时间里,我一直在考虑的事情是,我该写一篇什么样的文章呢?之前的两篇文章都是先有问题,然后我才有目的的解决问题,现在我的困扰是,我不知道该写什么了呵呵。因为其实,大多数的问题,只要在网上搜索一下(google远比baidu要强得多),基本上都能找到解决的办法,已经有了许多相关方面的教程或参考资料了,我并不是一个喜欢重复做别人已经做得很好的工作的人,所以我现在需要你的帮助,如果你有好的关于写什么方面的文章的建议,请留言告诉我(声明:应用我并不在行!),如果我能实现的话,一定会写出来分享给大家,如果写不出来,大家一起讨论下解决也是很好的!!谢谢!!我甚至翻译了raywenderlich的“怎么在ios5上做一个简单的iphone应用程序系列”的第一部分,但是当我想要把他发布的时候我放弃了,这个不是我擅长的,子龙山人博客翻译团队做这个更专业,我不应该把这种翻译的文章放在我自己的博客上,所以我想我还是把raywenderlich的这个“怎么在ios5上做一个简单的iphone应用程序系列”的3部分都翻译完后在送给山人比较好(当然得在人家同意的前提下哈哈)。
最终,我觉得把一些经典的源码分析一下也许会是一个好主意,所以,今天我要写的是苹果官方的源码witap例子的分析。所以,首先你需要下载这个官方的源码。
前提
我们文章的标题已经揭示了这个witap例子的内容是局域网联机的,这个对联机游戏来说真的很有用,联机的话你需要真机设备才能体验到,正常情况下你需要两个真机,不过其实一个真机加一个模拟器也是可以的,我在学习这个例子的时候就是一个真机加模拟器的组合呵呵.(人穷没办法呀☹)
首先我们先在模拟器上运行一下这个例子好让我们对这个程序先有个直观的感受。
没有太多的东西,一个状态栏,下面是一个UIView,这个UIView里有一些元素(每个元素,在后面的具体代码里我们会一一指出),总之,现在界面上显示的是3条信息,一条是提示我们等待另一个玩家加入,下面一条是设备的名字(这里我是在模拟器上运行的,所以显示的是我的计算机的名字),最后一条是提示我们或者要加入另一个游戏。我们随便在屏幕上点点看,没有任何反应。我们还是乖乖的听话,让另一个玩家加入我们吧,不然真的我们什么都做不了,首先确保你的两台设备(我是我的电脑和touch)都在同一个网络内,然后打开真机上的这个程序。
look,我们的模拟器发现了我的touch,哈哈,同样的,在真机的tableView里,你也会看到你的模拟器的名字。现在两台机器已经发现彼此了,然我们点一下tableView里的名字试试看吧,有反应了,我们进入了苹果给我们带来的小游戏:
点击弹出通知的continue来继续游戏,在游戏里随便点点,我们点击屏幕上的任意一个色块,我们的另一台设备上的同一色块出现被同步的点击的效果,其实还挺好玩的呵呵。
开始
好了,我们现在对这个例子有了直观的认识了,让我们来开始一步步的分析它吧,对于代码里涉及到的知识点我们会进行不限于代码范围的讲述。
首先,让我们来从main.m文件开始吧。在other Source文件夹里,我们选中main.m来看一下它的代码。
#import <UIKit/UIKit.h> int main(int argc, char *argv[]) { NSAutoreleasePool *pool = [NSAutoreleasePool new]; UIApplicationMain(argc, argv, nil, @"AppController"); [pool release]; return 0; }
第一行是导入UIKit框架,这个不用说了。
下面是main函数,和c程序一样,这个main函数也是我们的程序的入口。其实我们的程序的起点是start函数,在start函数里调用了main函数,然后在main函数里边,构建了一个自动释放池(这个witap的例子最新的版本是1.8,这个版本并没有针对ios5更新,所以这里的代码还是用的自动释放池,在ios5之后由于引入了ARC,所以ios5的main函数和之前的版本的main函数是有变化的,不再使用自动释放池了)。
在这个main函数里边最重要的一句代码就是:UIApplicationMain(argc, argv, nil, @"AppController"),这句是开始我们程序的关键,前两个参数就是main自己的参数,一个代表命令行参数的个数,一个是指向所有命令行参数的指针。第三个参数是代表我们的程序的主类,如果为nil则代表主类是UIApplication类,如果程序中使用自定义的UIApplication类的子类作为主类,你需要自己在这里指定,不过不推荐这样做!!。第四个参数是我们的代理类,如果为nil的话,则程序假设程序的代理来自Main nib文件(ios5之前,ios5改成用委托类的类名生成的字符串来指定了)。
那么UIApplicationMain这个函数又做了什么呢?在这个函数里边,我们根据我们的参数“主类名”,这里是UIApplication类,来实例化一个主类。然后对这个实例会设置他的委托为我们在第四个参数里指定的类,并调用_run方法,_run方法又会调用CFRunLoopRunInMode(⋯⋯),CFRunLoopRunInMode方法又会根据它的参数来以相应的模式运行RunLoop(RunLoop的概念很重要,我们后边会说明),这样注册到这个模式下的事件在发生时,我们相应的事件处理方法就会收到消息并处理。(这个需要结合下一段来理解)
RunLoop是一个运行回路,每个线程都有一个自己的RunLoop,我们平时并不用管理它是因为我们的主线程中的RunLoop默认情况下就启动了,UIApplication类帮我们做的。RunLoop做的具体的工作是监测输入源,如果有输入源事件的话,RunLoop分发这个事件给事件的处理方法来处理事件,如果没有输入源事件的话,RunLoop就让我们的线程休眠,什么都不做来节省资源消耗。那么什么是输入源呢?输入源包括:用户设备输入、网络连接、周期或延迟事件、异步回调。RunLoop能监测3中类型的对象:sources (CFRunLoopSource Reference), timers (CFRunLoopTimer Reference), and observers (CFRunLoopObserver Reference),要让RunLoop监测这些对象,需要先把他们加入到RunLoop中,针对这三种对象,有3个不同的方法用来加入到RunLoop中:CFRunLoopAddSource
, CFRunLoopAddTimer
, CFRunLoopAddObserver,这样加入到RunLoop后,RunLoop才会监测这些输入源,如果不想继续监测这个输入源的话,可以用CFRunLoopRemoveSource方法从RunLoop中移出输入源。还有一点要强调的是,在把这些输入源加入到RunLoop时,我们必须要把这些输入源关联到一个或多个RunLoop模式,模式决定了在RunLoop的一次迭代中,什么事件需要处理,因为RunLoop在运行时就指定了以什么模式运行,所以,RunLoop只处理那些关联到它当前运行模式的输入源。通常情况下我们如果添加输入源到RunLoop的话,我们会把它和默认模式
kCFRunLoopDefaultMode相关联,在应用程序或线程闲置的时候就会处理这些事件。事实上你可以定义自己的模式,通过自己的模式你可以对需要处理的事件进行限制(具体请参考官方文档)。
最后对于一个程序来说,简单地来总结一下,RunLoop做的工作是什么。
我们的程序有一个UIApplication的实例变量app,这个app实例变量为我们在我们的主线程里(以某种模式,我个人猜测会是默认模式吧)运行了一个RunLoop,并且它把一些输入源加入到了这个RunLoop中(比如触摸事件、网络连接⋯⋯等等),并与当前运行的这个模式关联,这样有相应事件发生的时候,RunLoop就会监测到这些事件了,当RunLoop监测到这些事件后,就通过委托(前面设定了为AppController)层层分发,直到分发给我们的相应事件的处理方法进行处理,(比如触摸事件的处理方法是touchBegan、touchMove、touchEnd等),这样我们就能正确的处理这些事件了。这也正是我们能处理触摸事件的原因,UIApplication类,已经把相应的输入源加入到我们主线程的RunLoop中了。
下面我们来看看applicationDidFinishLaunching:方法,它是告诉我们UIApplication已经准备好了,可以运行了,是我们的程序可见代码部分,在main函数之后运行的第一个方法,打开AppController.m文件:
- (void) applicationDidFinishLaunching:(UIApplication *)application { CGRect rect; //1 UIView* view; NSUInteger x, y; //Create a full-screen window //2 _window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; [_window setBackgroundColor:[UIColor darkGrayColor]]; //Create the tap views and add them to the view controller's view rect = [[UIScreen mainScreen] applicationFrame]; //3 for(y = 0; y < kNumPads; ++y) { //4 for(x = 0; x < kNumPads; ++x) { view = [[TapView alloc] initWithFrame:CGRectMake(rect.origin.x + x * rect.size.width / (float)kNumPads, rect.origin.y + y * rect.size.height / (float)kNumPads, rect.size.width / (float)kNumPads, rect.size.height / (float)kNumPads)]; //5 [view setMultipleTouchEnabled:NO]; //6 [view setBackgroundColor:[UIColor colorWithHue:((y * kNumPads + x) / (float)(kNumPads * kNumPads)) saturation:0.75 brightness:0.75 alpha:1.0]]; [view setTag:(y * kNumPads + x + 1)]; //7 [_window addSubview:view]; //8 [view release]; //9 } } //Show the window [_window makeKeyAndVisible]; //10 //Create and advertise a new game and discover other availble games [self setup]; //11 }
注释1,就是声明一些变量,一个rect,一个view,两个整数。
注释2,实例化一个UIWindow变量,这是我们的主窗口,并把它的大小区域设为全屏大小,[[UIScreen mainScreen] bounds]]得到的就是全屏的区域大小,然后设置它的
颜色为灰色。
注释3,把我们前面申请的rect变量,设为整个屏幕除了状态栏的大小区域,[[UIScreen mainScreen] applicationFrame]得到的就是除了状态栏的屏幕大小区域。
注释4,这个for循环是添加游戏中的色块儿的(就是上面游戏运行图中的9个色块),在AppController.m文件的最上边,我们看到我们用宏定义了KNumPads为3,所以这里
是外循环和内循环都是3次,共9次。
注释5,这是实例化我们的色块儿,并分配给我们前面申请的view变量。我们的色块是单独的类TapView的实例,它是继承自UIView的,我们先不管它的实现,就把它当一个
UIView来对待就好了,后面我们会详细介绍它的内容。在实例化这些色块的时候我们通过简单的计算来给这9个色块划分不同的区域和位置,使这9个色块均匀的分布在
我们的屏幕上,当然,是去除了状态栏之后的区域。
注释6,设置我们的色块的多点触摸为否,这样我们的色块是不会相应多点触摸了。
注释7,给我们的色块设置不同的tag,这里是编号1到9。
注释8,把我们的色块加入主窗口的子集,这样当我们的主窗口显示的时候,我们的色块作为子视图,也就会显示了。
注释9,以为我们把色块加入window的时候,他帮我们retain了,所以这里我们要release。
注释10,显示我们的window。
注释11,这是调用我们这个类的setup方法,这个方法后面会详述,现在先不管了。
我们接着来看看,这个setup方法是什么:
- (void) setup { [_server release]; //1 _server = nil; [_inStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; //2 [_inStream release]; _inStream = nil; _inReady = NO; [_outStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; //3 [_outStream release]; _outStream = nil; _outReady = NO; _server = [TCPServer new];//4 [_server setDelegate:self]; NSError *error = nil; if(_server == nil || ![_server start:&error]) { //5 if (error == nil) { NSLog(@"Failed creating server: Server instance is nil"); } else { NSLog(@"Failed creating server: %@", error); } [self _showAlert:@"Failed creating server"]; //6 return; } //Start advertising to clients, passing nil for the name to tell Bonjour to pick use default name if(![_server enableBonjourWithDomain:@"local" applicationProtocol:[TCPServer bonjourTypeFromIdentifier:kGameIdentifier] name:nil]) { //7 [self _showAlert:@"Failed advertising server"]; return; } [self presentPicker:nil]; //8 }
注释1、2和3,都是一些清理方法,用来确保当我们操作的这些变量之前用内容的话,先把它们清空,再重新进行操作。(当程序里第二次调用这个方法时,就显出了这几句的效果)
,这里先是对一个TCPServer类(后面会讲)实例进行置空操作;然后把一对输入输出流对象,从当前RunLoop中移出,这样我们的RunLoop就不再监测这两个输入
输出流事件了,从RunLoop中移除后,也对它们进行置空操作;最后又把两个用来标示输入输出流是否准备好的Bool变量设为假,标示我们没有准备好。
标示4,重新初始化这个TCPServer类变量,并把它的委托设为这个AppController。(这里之所以能把TCPServer的委托delegate设为AppController是因为,这个delegate是要求符合TCPServerDelegate 协议的,而我们的AppController在interface部分已经声明了它是符合这个TCPServerDelegate 协议的)
注释5,判断这个TCPServer类实例变量_server是否为空,并对它调用start:方法,并判断start:方法的返回值。如果有问题根据判断条件输出相应的错误信息。
注释6,当有错的时候弹出警告窗口来说明失败情况。
注释7,对这个_server变量调用一个方法,这个方法是用来发布我们的服务的。(这个例子用Bonjour实现联机,而boujour实现是通过NSNetService来发布服务,用
NSNetServiceBrowser来搜索服务来实现的,这也是一个重要的知识点,后面会讲)
注释8,这个是显示我们的这篇文章中第一个图中的界面的一个方法。
这个例子真是千头万绪呀,我希望一部分一部分的拆开来分析,可是它的每一部分总是和其他内容相关联,摘不出来呀,郁闷!好了,我们继续吧,如果要弄明白这个setup方法到底做了什么,我们必需得先分析这个TCPServer类才行,然后还要细分这个注释8中的方法才能真正了解这个setup方法做了哪些工作。
难啃的骨头
我们来见识一下这个TCPServer的真面目吧,打开TCPServer.h文件:
@class TCPServer; NSString * const TCPServerErrorDomain; typedef enum { kTCPServerCouldNotBindToIPv4Address = 1, kTCPServerCouldNotBindToIPv6Address = 2, kTCPServerNoSocketsAvailable = 3, } TCPServerErrorCode;
在文件的最上面部分,我们看到,我们先用@class来修饰我们的TCPServer,这是告诉编译器,这个TCPServer是一个类,这样我们就可以在还没有声明这个类的时候在方法里先用,在以后在实现它的定义。(这样也就是为什么我们后面的协议方法中用到了TCPServer类,但这个类的声明却在协议之后,而我们在编译是不报错的原因)
我们又声明了一个字符串常量,它的定义是在TCPServer.m文件里的,在这里只是声明。
定义了TCPServer的错误代码的枚举值,用来表示不同的错误情况。
接着,我们定义了一个协议(协议是objective-c中,不同的类之间沟通的好方法,个人觉得和symbian里的M类基本上一样):
@protocol TCPServerDelegate <NSObject> @optional - (void) serverDidEnableBonjour:(TCPServer*)server withName:(NSString*)name; - (void) server:(TCPServer*)server didNotEnableBonjour:(NSDictionary *)errorDict; - (void) didAcceptConnectionForServer:(TCPServer*)server inputStream:(NSInputStream *)istr outputStream:(NSOutputStream *)ostr; @end
协议的名字叫:TCPServerDelegate,这个协议有3个可选的方法,这三个方法第一个是TCPServer用boujour发布服务成功之后我们用来处理一些东西的方法,第二个是失败的时候我们用来处理一些东西的方法,第三个是当TCPServer接受了其他设备的连接请求之后,我们用来处理东西的方法。
下面让我们看看这个TCPServer类的声明:
@interface TCPServer : NSObject <NSNetServiceDelegate> { @private id _delegate; uint16_t _port; uint32_t protocolFamily; CFSocketRef witap_socket; NSNetService* _netService; } - (BOOL)start:(NSError **)error; - (BOOL)stop; - (BOOL) enableBonjourWithDomain:(NSString*)domain applicationProtocol:(NSString*)protocol name:(NSString*)name; - (void) disableBonjour; @property(assign) id<TCPServerDelegate> delegate; + (NSString*) bonjourTypeFromIdentifier:(NSString*)identifier;
这个TCPServer类,被声明继承自NSObject类,并且它遵守NSNetServiceDelegate协议,这个协议是我们的NSNetService类的一些回调方法,就是说如果我们的NSNetService服务发布成功或者失败的话,会调用这个协议里的相应方法来进行处理。事实上这个协议的所有方法都是可选的,如果你不实现他们也不会出错,不过那样的话,我们就不能在服务发布状态改变是做相应的处理了。
在这个interface里,声明了5个私有变量,一个id类的_delegate,它用来跟踪我们这个TCPServer类的委托,一个uint16_t类型的_port变量,存储我们发布服务时绑定的Socket的端口号,一个uint32_t类型的protocolFamily,来存储我们的socket的协议族,一个CFSocketRef类的 witap_socket,就是我们的等待其他设备连接的socket,一个NSNetService类的_netService,就是我们用来发布服务的NSNetService。
start:方法,我们在这个方法里创建并配置我们用来监听网络连接的socket,并创建RunLoop输入源,加入到当前RunLoop中,这样只要有我们的这个socket有连接事件,我们就能得到通知并触发相应的回调。
stop方法,明显不过了,它是停止我们的网络连接服务的,让我们取消对网络连接事件的监听,并释放这个监听的socket。
disableBonjour方法,停止我们的当前的已经发布的服务。
enableBonjourWithDomain:applicationProtocol:name:方法是事实上进行NSNetService服务发布的方法。
接着是一个声明,声明了一个id<TCPServerDelegate>的属性delegate,这是一个满足TCPServerDelegate协议的属性。
最后是一个bonjourTypeFromIdentifier:方法,这是个辅助方法,它用来返回我们要发布的服务的协议的(这个协议不是委托类的协议,它只是一个代表唯一标识的字符串),并且这个字符串是用要求的,它不能超过14个字符,并且只能包含小写字母、数字和连接符,开关和结尾不能是连接符。
下面,我们该看看这个类的具体实现了,打开TCPServer.m文件:
#import "TCPServer.h" NSString * const TCPServerErrorDomain = @"TCPServerErrorDomain"; @interface TCPServer () @property(nonatomic,retain) NSNetService* netService; @property(assign) uint16_t port; @end
首先是包含TCPServer.h文件,然后是我们在.h文件中声明的那个常量字符串的定义。
然后下面……(其实我不太明白苹果这个地方的用法,正常情况下我会觉得这是一个分类,但是如果是分类的话,是不可以添加实例变量的,这个地方是添加了两个属性,这个什么情况??就算可以在分类里添加属性,那为什么要这么做呢?为什么不在正常的原始类里添加呢?是不是因为这样的话这个属性是在TCPServer.m文件里的,那么在这个分类里声明的属性就对外不可见不可用了呢?那又为什么分类没有自己的implementation呢?它怎么和原始类共用一个呢?如果你知道这个原因,请告诉我,不胜感激!!!)
再接着是这个TCPServer类的实现部分:
@implementation TCPServer @synthesize delegate=_delegate, netService=_netService, port=_port; - (id)init { return self; } - (void)dealloc { [self stop]; [super dealloc]; }
这里合成了三个属性的set和get方法;然后是初始化方法,只是简单地返回自己;然后是dealloc方法,这会在我们这个类销毁时调用,这个方法里是先调用stop方法停掉网络连接服务,然后调用父类的dealloc方法。
再接着看这个handleNewConnectionFromAddress方法:
- (void)handleNewConnectionFromAddress:(NSData *)addr inputStream:(NSInputStream *)istr outputStream:(NSOutputStream *)ostr { // if the delegate implements the delegate method, call it if (self.delegate && [self.delegate respondsToSelector:@selector(didAcceptConnectionForServer:inputStream:outputStream:)]) { [self.delegate didAcceptConnectionForServer:self inputStream:istr outputStream:ostr]; } }
在这个方法里,我们先判断self的委托是否为空(我们在AppController.m的setup方法里的注释4中,把委托设为了AppController),并判断这个委托响不响应didAcceptConnectionForServer:inputStream:outputStream:方法,如果响应,就对self的委托调用这个方法来处理一些事情。
现在轮到一个重量级的方法了,TCPServerAcceptCallBack:
static void TCPServerAcceptCallBack(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) { TCPServer *server = (TCPServer *)info; NSLog(@"the server in call back is: %@",server.description); if (kCFSocketAcceptCallBack == type) { // for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data; uint8_t name[SOCK_MAXADDRLEN]; socklen_t namelen = sizeof(name); NSData *peer = nil; if (0 == getpeername(nativeSocketHandle, (struct sockaddr *)name, &namelen)) { peer = [NSData dataWithBytes:name length:namelen]; } CFReadStreamRef readStream = NULL; CFWriteStreamRef writeStream = NULL; CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &readStream, &writeStream); if (readStream && writeStream) { CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); [server handleNewConnectionFromAddress:peer inputStream:(NSInputStream *)readStream outputStream:(NSOutputStream *)writeStream]; } else { // on any failure, need to destroy the CFSocketNativeHandle // since we are not going to use it any more close(nativeSocketHandle); } if (readStream) CFRelease(readStream); if (writeStream) CFRelease(writeStream); } }
这是一个回调方法,在我们的监听网络连接的socket,接收到连接事件后,这个回调方法就被调用。需要说明的是,这个回调方法的格式是固定的。原因是这样的,这个回调方法是要在创建socket的时候传递给socketCreat方法的callout参数的,这个callout是一个函数指针,这个函数指针是CFSocketCallBack 类型的,所以我们的这个回调方法也应该是这个类型的。我们来看一下这个类型:
typedef void (*CFSocketCallBack) ( CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info );
这个类型就是我们的函数指针的定义,它的返回值是void的,也就是没有返回值;再来看看它的参数:
很明显,第一个参数是触发了这个回调的socket本身,第二个是触发这个回调的事件类型,第三个代表请求连接的远端设备的地址,第四个参数有点神奇,它根据回调事件的不同,它代表的东西也不同,如果这个是连接失败回调事件,那它就代表一个错误代码的指针,如果是连接成功的回调事件,它就是一个Socket指针,如果是数据回调事件,这就是包含这些数据的指针,其它情况下它是NULL的,最后一个参数是我们创建socket的时候用的那个CFSocketContext结构的info成员(这个CFSocketContext机构我们会在后面的start:方法里再讲)。
明白这个函数指针的类型再对照着我们的回调函数看是不是结构完全一样,呵呵。
这个例子要写的东西远比我想的要多,太长了,所以,这篇就先到这儿吧,后面的内容会再发后续章节。
(能力有限,文中可能会有不对的地方,希望大家指教。谢谢!!)
评论
这个用法叫category,是objective-c语言特性的一种,可以在类外“植入”方法到相应的类里。这里这样写就是如你所说,为了实现私有方法,下面是有实现的,因为它定义在上面所以下面的代码都受用。
你好,非常感谢你的解释!!
1. 我之前对分类里加属性这个情况表示怀疑,现在我想可能是这样的,我的潜意识里还是把属性当做变量来看了(虽然我知道它是相当于声明set和get方法的),这是不对的,还是应该把属性直接当成set和get方法看,这样就可以认为这是在分类里添加了两个方法而不是变量。
2. 还有就是我对interface和impletementation缺乏认识,因为正常的情况下,我都看到的是interface和impletementation是成对出现的,所以我惯性的认为它们是应该成对出现,这里我之所以表示怀疑是因为它有两个interface而只有一个impletementation,你说“它定义在上面所以下面的代码都受用”这句话我不太理解,能更详细的解释一下吗?
我想这里能出现多个interface对一个impletementation是还是因为这是分类的原因吧,分类就是为原始类添加方法的,可能对objective-c来说分类的方法和原始类的方法没有任何区别吧,是不是原始类的方法和分类的方法在系统里就没有用来区分的标识,在编译的时候直接把分类的方法和原始类的方法一起编译,它们不分彼此,所以才可以只用一个impletementation的吧?因为impletementation和end只是告诉编译器这个类的实现从哪里开始到哪里结束而已,所以如果只用一个的话我不认为系统里能区分开原始类和分类。(这都是我个人理解,我没有找到相关方面的资料)
是的,分类的方法和头文件定义的方法是同等对待的。
这个意思是编译器是顺次编译的,下面的方法引用上面的方法是可以,而上面的方法引用下面的方法是会被提示warning的。所以有时的category只是为了单纯的去掉烦眼的warning而已。
嗯,我明白你说的了,谢谢!