注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

phperwuhan的博客

记载一个phper的历程!phperwuhan.blog.163.com

 
 
 

日志

 
 

[重要]Objective-C学习笔记  

2011-08-19 17:49:03|  分类: ios |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

来源:http://tiny4cocoa.com/doc/xcode-doc/ios-overview

从哪里开始

使用Xcode提供的内建项目模板,创建运行在iOS上的简单的应用程序是相对简单的,但是如果你想创建一个有点用处,看起来很不错的程序,还是需要你花点时间仔细阅读文档。至少,你需要花点时间通读iPhone开发者中心的其他文章,他们介绍了创建程序需要的工具和流程。

如果你完全不熟悉iOS和Mac OS X,那么对于在这个平台上编写代码来说,很重要的一点是,花点时间理解iOS开发中的基本设计模式和约定。当程序利用了框架提供的所有能力带来的好处的时候,程序就会在iOS下运行的最好。如果你花时间尝试围绕基本的框架行为,你可能会发现创建你自己的程序比简单的学习基本的设计模式困难的多。每个iPhone开发者都需要了解的基本信息详见Cocoa基础指南。该文档包含了Objective-C语言的基础,以及UIKit或者其他系统框架使用的编程约定和设计模式。

一旦你理解了创建iPhone应用程序的基本约定,你可以参考iOS编程指南获取更详尽的关于开发流程的信息。本书提供了编写iPhone应用程序的核心概念和基于任务组织的信息,包括如何显示用户介面,处理事件,和从iOS核心特性获得好处的例子。本书还描述了完整的开发流程,并介绍了如何利用Xcode构建和运行应用程序。

除了iOS编程指南,你还应该浏览可用的例子代码。iOS提供了大量的具有全部特性的例子,用来展现如何使用现有的技术创建真实世界的应用程序。你可以使用这些例子作为你自己的应用程序的起点,或者你可以观察它们学习如何使用这些特性。

如果你从来没有使用任何编程语言编过程序,那么你至少需要在开始之前,对相关概念进行一些基础的了解。对象的使用和对象对象架构是iPhone程序设计的基础,理解他们如何交互对创建你的程序非常重要。想了解面向对象概念的,请参看使用Objective-C进行面向对象编程。此外,参看Cocoa基础指南可以获得Cocoa中的面向对象设计模式的信息。

想了解更多Objective-C语言和语法的细节介绍,请参看Objective-C 2.0编程语言。

Objective-C: C的超集

Objective-C是ANSI版本C编程语言的超集,支持C的基本语法。在C代码中,你定义头文件和源代码文件,从代码实现细节分离公共声明。Objective-C头文件使用的文件名列在表1中。

表1 Objective-C代码的文件扩展名

扩展名 内容类型
.h 头文件。头文件包含类,类型,函数和常数的声明。
.m 源代码文件。这是典型的源代码文件扩展名,可以包含Objective-C和C代码。
.mm 源代码文件。带有这种扩展名的源代码文件,除了可以包含Objective-C和C代码以外还可以包含C++代码。仅在你的Objective-C代码中确实需要使用C++类或者特性的时候才用这种扩展名。


当你需要在源代码中包含头文件的时候,你可以使用标准的#include编译选项,但是Objective-C提供了更好的方法。#import选项和#include选项完全相同,只是它可以确保相同的文件只会被包含一次。Objective-C的例子和文档都倾向于使用#import,你的代码也应该是这样的。

字符串

作为C语言的超集,Objective-C支持C语言字符串方面的约定。也就是说,单个字符被单引号包括,字符串被双引号包括。然而,大多数Objective-C通常不使用C语言风格的字符串。反之,大多数框架把字符串传递给NSString对象。NSString类提供了字符串的类包装,包含了所有你期望的优点,包括对保存任意长度字符串的内建内存管理机制,支持Unicode,printf风格的格式化工具,等等。因为这种字符串使用的非常频繁,Objective-C提供了一个助记符可以方便地从常量值创建NSString对象。要使用这个助记符,你需要做的全部事情,是在普通的双引号字符串前放置一个@符号,如下面的例子所示:

NSString*  myString = @"My String\n";
NSString*  anotherString = [NSString stringWithFormat:@"%d %s", 1, @"String"];

// 从一个C语言字符串创建Objective-C字符串
NSString*  fromCString = [NSString stringWithCString:"A C string" encoding:NSASCIIStringEncoding];

如同所有其他的面向对象语言,类是Objective-C用来封装数据,以及操作数据的行为的基础结构。对象就是类的运行期间实例,它包含了类声明的实例变量自己的内存拷贝,以及类成员的指针。Objective-C的类规格说明包含了两个部分:接口和实现。接口部分包含了类声明和实例变量的定义,以及类相关的方法。实现部分包含了类方法的实际代码。图1展现了声明一个叫做MyClass的类的语法,这个类继承自NSObject基础类。类声明总是由@interface编译选项开始,由@end编译选项结束。类名之后的(用冒号分隔的)是父类的名字。类的实例(或者成员)变量声明在被大括号包含的代码块中。实例变量块后面就是类声明的方法的列表。每个实例变量和方法声明都以分号结尾。

图1 类声明

列表1展现了前面例子中MyClass类的实现。类同与类声明,类实现的位置也由两个编译选项确定,@implementation和@end。这些选项给编译器提供了要将方法和对应类联系起来,所需的范围信息。因此方法的定义和接口中对应的声明是匹配的,只是多了个代码块而已。

列表1 类实现

@implementation MyClass
   
- (id)initWithString:(NSString *) aName
{
    if (self = [super init]) {
        count count = 0;
        data = nil;
        name = [aName copy];
        return self;
    }
}
 
+ (MyClass *)createMyClassWithString: (NSString *) aName
{
    return [[[self alloc] initWithString:aName] autorelease];
}
@end

注意: 虽然前面的类只声明了方法,但是类可以声明属性。想了解更多关于属性的信息,参看“属性”。

 

当用变量保存对象的时候,始终应该使用指针类型。Objective-C对变量包含的对象支持强弱两种类型。强类型指针的变量类型声明包含了类名。弱类型指针使用id作为对象的类型。弱类型指针常用于类的集合,在集合中对象精确的类型可以是未知的。如果你用过强类型语言,你也许觉得使用弱类型变量可能会带来问题,但是他们实际上给了Objective-C程序巨大的灵活性,而且使它更强大。

下面的例子里,展示了MyClass类的强类型和弱类型声明变量:

MyClass*  myObject1;    // Strong typing
id        myObject2;    // Weak typing

方法

Objective-C中的类可以声明两种类型的方法:实例方法和类方法。实例方法就是一个方法,它在类的一个具体实例的范围内执行。也就是说,在你调用一个实例方法前,你必须首先创建类的一个实例。而类方法,比较起来,也就是说,不需要你创建一个实例。

方法声明包括方法类型标识符,返回值类型,一个或多个方法标识关键字,参数类型和名信息。图2展示insertObject:atIndex:实例方法的声明。声明由一个减号(-)开始,这表明这是一个实例方法。方法实际的名字(insertObject:atIndex:)是所有方法标识关键的级联,包含了冒号。冒号表明了参数的出现。如果方法没有参数,你可以省略第一个(也是唯一的)方法标识关键字后面的冒号。本例中,这个方法有两个参数。

图2 方法声明语法

当你想调用一个方法,你传递消息到对应的对象。这里消息就是方法标识符,以及传递给方法的参数信息。发送给对象的所有消息都会动态分发,这样有利于实现Objective-C类的多态行为。也就是说,如果子类定义了跟父类的具有相同标识符的方法,那么子类首先收到消息,然后可以有选择的把消息转发(也可以不转发)给他的父类。

消息被中括号( [ 和 ] )包括。中括号中间,接收消息的对象在左边,消息(包括消息需要的任何参数)在右边。例如,给myArray变量传递消息insertObject:atIndex:消息,你需要使用如下的语法:

[myArray insertObject:anObj atIndex:0];

为了避免声明过多的本地变量保存临时结果,Objective-C允许你使用嵌套消息。每个嵌套消息的返回值可以作为其他消息的参数或者目标。例如,你可以用任何获取这种值的消息来代替前面例子里面的任何变量。所以,如果你有另外一个对象叫做myAppObject拥有方法,可以访问数组对象,以及插入对象到一个数组,你可以把前面的例子写成如下的样子:

[[myAppObject getArray] insertObject:[myAppObject getObjectToInsert] atIndex:0];

虽然前面的例子都是传递消息给某个类的实例,但是你也可以传递消息给类本身。当给类发消息,你指定的方法必须被定义为类方法,而不是实例方法。你可以认为类方法跟C++类里面的静态成员有点像(但是不是完全相同的)。

 

类方法的典型用途是用做创建新的类实例的工厂方法或者是访问类相关的共享信息的途径。类方法声明的语法跟实例方法的几乎完全一样,只有一点小差别。与实例方法使用减号作为方法类型标识符不同,类方法使用加号( + )

下面的例子演示了一个类方法如何作为类的工厂方法。在这里,arrayWithCapacity是NSMutableArray类的类方法,为类的新实例分配内容并初始化,然后返回给你。

NSMutableArray*   myArray = nil;    // nil is essentially the same as NULL
// Create a new array and assign it to the myArray variable.
myArray = [NSMutableArray arrayWithCapacity:0];
 

属性

属性是用来代替声明存取方法的便捷方式。属性不会在你的类声明中创建一个新的实例变量。他们仅仅是定义方法访问已有的实例变量的速记方式而已暴露实例变量的类,可以使用属性记号代替getter和setter语法。类还可以使用属性暴露一些“虚拟”的实例变量,他们是部分数据动态计算的结果,而不是确实保存在实例变量内的。

实际上可以说,属性节约了你必须要些的大量多余的代码。因为大多数存取方法都是用类似的方式实现的,属性避免了为类暴露的每个实例变量提供不同的getter和setter的需求。取而代之的是,你用属性声明指定你希望的行为,然后在编译期间合成基于声明的实际的getter和setter方法。

属性声明应该放在类接口的方法声明那里。基本的定义使用@property编译选项,紧跟着类型信息和属性的名字。你还可以用定制选项对属性进行配置,这决定了存取方法的行为。下面的例子展示了一些简单的属性声明:

@property BOOL flag;
@property (copy) NSString* nameObject;  // Copy the object during assignment.
@property (readonly) UIView* rootView;  // Create only a getter method.

使用属性另外的好处就是你可以在代码中访问他们的时候,使用点语法,如下面的例子所示:

myObject.flag = YES;
CGRect   viewFrame = myObject.rootView.frame;

虽然前面例子里面的对象和属性名是故意这么取的,他们还是展现了属性的灵活性。点语法实际上隐藏了对应的方法调用。每个可读的属性由一个与属性同名的方法支持。每个可写属性由一个叫做“set属性名”的额外方法来支持,属性名的第一个字母要大写。(这些方法是属性的实际实现方式,也是你可以声明一个没有任何实例变量支持的属性声明的原因。)如果用方法来代替前面代码中的属性,你就要下下面的代码

[myObject setFlag:YES];
CGRect   viewFrame = [[myObject rootView] frame];

要了解更多关于如何声明属性的信息,参看Objective-C 2.0编程语言的属性一章。

协议和代理

协议声明了可以被任何类实现的方法。协议不是那些类本身。他们仅是定义一个接口,其他的对象去负责实现。你实现了协议里面的方法,就叫做符合协议。

在iPhone OS中协议常用来实现委托对象。委托对象就是一个对象以其他的对象的模式行事。了解协议,委托和对象最好的办法就是看一个例子。

UIApplication类实现了一个程序需要的行为。如果想接收程序当前状态的简单消息,并不需要强制你创建UIApplication的一个子类,反之UIApplication类通过调用委托对象的指定方法来分发这些通知消息。实现UIApplicationDelegate方法的对象都可以接受这样的通知,并进行响应的反应。

协议的声明跟类接口的声明很像,只是协议没有父类,而且他们不会定义任何实例变量。下面的例子展示了一个有一个方法的协议声明:

@protocol MyProtocol    - (void)myProtocolMethod;  @end

在大多数委托协议情况下,使用某种协议仅仅是简单的实现协议定义的方法而已。 有些协议要求你明确的表明你支持这种协议,协议可以指定必须或者可选的方法。在你深入开发的过程中,你应该花点时间学习协议以及他们的用途,请阅读Objective-C 2.0编程语言的协议章节。

iOS开发工具


要开发iOS的应用程序,你需要一台安装有Xcode工具和Mac OS X的电脑。Xcode是Apple提供的开发工具集,提供项目管理,代码编辑,创建执行程序,代码级调试,代码库管理,性能调节等等功能。这个工具集的核心就是Xcode程序,提供了基本的源代码开发环境。但是Xcode不是你使用的唯一个工具,下面的章节向你介绍创建iPhone应用程序需要的工具。

Xcode

当你在Xcode中构建应用程序,你可以选择为iPhone模拟器创建,也可以选择为iPhone设备创建。模拟器提供了测试应用程序的本地环境,可以确保你的程序行为完全符合你的需求。当你对程序的基本行为已经满意后,你可以告诉Xcode构建它,然后在连接到你的计算机上的iPhone和iPod touch上运行它。在设备上面运行程序提供了终极测试环境,而且Xcode可以把内建调试器植入运行在设备上的代码中。

 

图2 从Xcode运行项目

更多关于在iOS上构建项目的更多信息,参看iOS开发者指南中的开发环境章节。 

界面构建器(Interface Builder)

界面构建器是用来可视化装配应用程序用户界面的工具。使用界面构建器,你可以用拖拽预置组件的方式装配你的程序窗口。组件包括标准的系统控件,例如选择器,文本框,按钮,还包括用来展现程序提供的视图的定制视图。当你把组件放置在窗口上,你可以通过拖拽来移动它们,利用查看器修改他们的属性,以及在这些对象和你的代码之间建立联系。当界面达到你的要求时,你可以保存这些内容为nib文件,这是一种定制的资源文件格式。在界面构建器中创建的nib文件,包括UIKit框架在运行期间在应用程序中重建相同对象所需要的全部信息。装入nib文件会创建保存在文件中的所有对象的运行期间版本,严格按照界面构建器中的配置。程序还使用你指定的连接信息在新创建的对象和程序中已存的对象间建立联系。这些连接为你的代码提供了nib文件对象的指针,以及提供了在代码中对象和用户动作通讯需要的信息。

总的来说,界面构建器为你创建程序用户界面节约了大量的时间。界面构建器节省了创建,配置和定位构成用户界面的对象所需要的定制代码。因为它是一个可视化编辑器,你可以看到与运行期间完全相同的界面。

Instruments

为了确保你的软件有最好的用户体验,Instruments环境可以帮你分析你的iPhone应用程序在模拟器或者设备上面运行的性能表现。Instruments可以从运行的应用程序中获取数据,并可以把数据展现为叫做时间线的图形。你可以获取关于程序的内存用量,磁盘活动,网络活动以及图形性能等数据。时间线视图可以一个接一个的显示所有这些不同类型的信息,让你可以把程序的所有行为联系起来(分析),而不仅仅是(一次仅观察)某个领域内的行为。需要了解更多信息,你可以查看Instruments获取的细节例子。

图3 使用Instruments调节你的应用程序

除了时间线视图,Instruments还提供了其他的工具帮助分析基于时间的程序的行为。例如,Instruments窗口可以让你保存多次运行的数据,这样你就可以发现程序的性能是得到了提升,还是需要继续努力。你可以保存这些运行的数据在Instruments文档内,并在任何时间打开他们。

如何在iPhone程序上使用Instruments的更详尽的信息,请参考iOS编程指南的开发环境章节。为了获得如何使用Instruments的通用信息,请参考Instruments用户指南。

创建一个iPhone应用程序


在表面看,创建一个iPhone应用程序和创建一个Mac OS X应用程序很像。使用相同的工具和许多相同的基础库尽管很像,还是有些显著的不同。iPhone不是桌面计算机,有不同的预期,需要完全不同设计思路。需要从iPhone OS的能力获得优势,还要避开那些对移动环境无关和不切实际的特性。iPhone和iPod touch的小尺寸屏幕意味着,你的应用程序用户界面应该合理组织,并总是关注于用户最需要的信息。

iPhone OS运行用户跟iPhone和iPod touch的设备,用桌面程序无法实现的交互方式来交互。多点触摸(Multi-Touch)是一种革命性的接收事件的新方法,报告每一个独立的手指对屏幕的触摸,并使处理多手指手势和其他的复杂输入变得非常简单。此外,内建的硬件特性,例如加速度传感器,虽然也在一些桌面系统使用,但是在iPhone OS中使用的更佳广泛,可以跟踪屏幕的当前方向并相应的调整你的内容。了解如何在你的程序中使用这些特性,有助于你聚焦适于用户的设计方式。

了解iPhone程序设计的最好办法就是看例子。本文针对例程MoveMe进行介绍。下面的例子展示iPhone程序的典型性为,包括:

  • 初始化程序
  • 显示窗口
  • 描绘定制内容
  • 处理触摸事件
  • 进行动画

图 1 展现了应用程序的接口。触摸Welcome按钮会引发一个动画,按钮会跳动并把自己的中心移动到你的指下。手指在屏幕上移动,按钮就会跟着你的手指移动。把你的手指从屏幕上拿开,引发另一个动画,按钮跳回它的原始位置。在按钮以外的任何位置双击屏幕会改变按钮欢迎词的语言。

图1 MoveMe程序的窗口

阅读本文的其他部分前,你应该下载例子(MoveMe),这样你可以直接参照源代码学习。你应该已经阅读了iPhone开发者中心下面的指南文章,对iPhone OS和开发需要的工具和语言有了基本的了解:

  • iPhone OS概述
  • iPhone开发工具

如果你对Objective-C编程语言不熟悉,你应该阅读学习Objective-C:入门手册让自己对Objective-C的基本语法有所了解。

查看MoveMe例子工程

下载MoveMe例程,你可以得到构建和运行程序需要的源代码和支持文件。你应该使用Xcode应用程序(位于 /Developer/Applications)管理iPhone OS项目。每个Xcode项目窗口包含一个工作空间用来组织代码和资源文件,编译和组装程序的构建规则,编辑和调试代码的工具。

图 2现实MoveMe程序的Xcode项目窗口。要打开这个项目,首先把它复制到本地硬盘,然后双击MoveMe.xcodeproj文件即可。(你可以在可以在Xcode选择菜单File > Open然后选择文件。)项目包含了多个Objective-C源代码文件(扩展名为.m),一些图像文件和其他资源,以及构建程序包的预定义目标。

图2 MoveMe项目窗口


在iPhone OS,Xcode项目的最终目标是程序包,一个特性类型的目录,放置程序的二进制执行文件以及资源文件。iPhone OS包是一个相对平面的目录结构,包含的大多数文件都在包目录的顶层。然而,包文件也可以包含子目录去存储字符串的本地化版本以及其他的语言相关的资源文件。本文不需要你了解程序的确切结构,但是你感兴趣的话可以在iPhone OS编程指南程序包章节找到这些信息。

构建MoveMe程序

要构建MoveMe程序并在模拟器里运行它,要找如下的步骤去做:

1. 在Xcode中打开MoveMe.xcodeproj文件
2. 在项目工具栏,确保Active SDK菜单中选中了simulator选项。(如果Active SDK菜单在工具栏中没有出现,选择菜单Project > Set Active SDK > Simulator。)
3. 从菜单选择Build > Build and Go (Run),或者简单的点击工具栏上的Build and Go按钮。

当程序构建完成,Xcode把它装入到模拟器中然后开始运行它。使用鼠标,你可以点击Welcome按钮,并把它拖到屏幕的任意位置,查看程序的行为(是否符合需求)。如果你把一个设备设置为开发所用,你可以构建程序并让它在设备上运行。了解如何配置设备为开发所用以及如何装入程序的信息,可以参看iPhone OS编程指南中的开发环境章节。

简述内存管理

iPhone OS主要是一个面向对象系统,所以你分配的大多数内存都是以Objective-C对象的形式存在的。iPhone OS中的对象使用引用计数机制来确定是否可以安全的释放对象占用的内存。当你创建一个对象,它的引用计数从1开始。客户获得对象后可以选择保留它,这样就要把他的引用技术加1。如果客户保留一个对象,那么它也必须在不再需要的时候释放这个对象。释放一个对象就会让它的引用技术减1。当一个对象的引用计数等于0,系统就会自动的回收对象占用的内存。

注意:iPhone OS并不支持Mac OS X v10.5以及更新版本中的垃圾回收型内存管理机制。如果你需要分配普通的内存块,没有跟一个对象想关联的内存块,你可以使用标准的malloc库调用。任何使用malloc函数分配的内存,你都有责任在用完后,调用free函数释放。系统不会帮助你释放基于malloc的内存块。

无论你想如何分配内存,在iPhone OS中管理全部内存使用都比在Mac OS X中更加的重要。虽然iPhone OS有一个虚拟内存系统,但是他不使用交换文件。这就是说如果需要的话,代码页可以被覆盖,但是同时应用程序的数据必须适应内存的尺寸。系统监控着空闲内存的大小,并试图给程序需要的全部内存。但是当内存使用情况太危急的时候,系统可能会终止你的应用程序。但是这个选项仅仅系统是为了确保系统有足够的内存执行最重要的操作,例如接电话,的最后一招。

更多关于在iPhone OS下分配内存的信息,参看Cocoa基础指南。更多关于如何改善程序内存使用的信息,参看iPhone OS编程指南的管理你的内存使用章节。

初始化MoveMe程序

就像每个基于C的程序一样,每个iPhone程序的初始入口点也是main函数。好消息是,当你使用Xcode的iPhone模板创建新项目的时候,你不需要自己写这个函数。项目模板包含了一个带有启动一个程序所有需要代码的这个函数的版本。

列表1展示了MoveMe程序的main函数。这个main函数位于项目中的main.m文件内。你创建的所有程序都会有一个跟这个一样的main函数。这个函数执行两个关键任务。首先,它为程序创建一个最高级的自动释放的内存池,这个内存池可以回收使用autorelease函数释放的 Objective-C对象占用的内存。其次,它调用UIApplicationMain函数创建MoveMe程序的关键对象,初始化这些对象,开始事件处理循环。直到程序退出这个函数才会返回。

列表1 使用提供的main函数

int main(int argc, char *argv[])  {      NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];      int retVal = UIApplicationMain(argc, argv, nil, nil);      [pool release];      return retVal;  }

定义程序的委托对象

你的项目中最重要的架构细节之一就是定义一个程序委托对象,从你提供的类实例化。MoveMe项目中的程序委托类的接口声明在 MoveMeAppDelegate.h文件内,实现定义在MoveMeAppDelegate.m文件内。一旦你把这些文件加入到项目中,你可以使用界面构建器指明这个类作为程序的委托对象。界面构建器是一个可视化工具,用来创建和管理窗口的视图,设置视图之间的层级,配置每个视图的选项,在视图和程序的其他对象间建立联系。因为它是一个可视化工具,你可以通过在窗口上拖动组件完成这些任务。操作的结果是你的界面的交互版本,你可以快速修改,随时看到改变。界面构建器把你的用户界面保存为一个nib文件,这是你的程序对象图形的存档。

要启动界面构建器,并查看程序委托对象的定义,双击Xcode项目窗口中的文件组面板中的MainWindow.xib文件。MainWindow.xib 是一个nib文件,包含了程序的窗口,定义了程序中几个重要对象之间的关系,包括程序的委托对象。要了解程序委托关系是如何建立的,点击nib文件文档窗口(标题为MainWindow.xib)中的File's Owner图标,显示查看器窗口(选择菜单Tools > Inspector),然后点击查看器窗口的连接页。

如图3所示,查看器现实File's Owner对象(表示nib文件中的应用程序)有一个委托出口连接到MoveMeAppDelegate对象。

图3 程序委托

程序委托对象与标准的UIApplication对象串联工作,对程序状态改变做出反应。程序对象做大多数的繁重工作,但是委托也负责几项关键的行为,如下:

  • 设定程序窗口,初始化用户界面
  • 执行定制数据引擎所需的格外初始化任务
  • 打开与程序定制的URL模式相关联的内容
  • 对设备方向的变化做出响应
  • 处理内存不足的警告
  • 处理系统要求程序退出的请求

启动期间,对委托对象最紧急的任务就是设定和展现程序窗口,详情在“创建程序窗口”中描述。委托对象还应该为了程序可以立即使用而执行需要的任务,例如从之前的一个状态恢复程序,或者创建需要的对象。当程序退出时,委托对象需要执行有序的程序关闭操作,并保存下次启动循环所需要的信息。

想了解iPhone程序的基础架构和生命周期循环,参看iPhone OS编程指南的核心程序架构章节。

创建程序窗口

每个程序都需要创建窗口覆盖整个屏幕然后在窗口内放置内容。iPhone OS的图形程序不会和其他的程序一起运行。实际上除了核心和一些底层系统守护进程,程序启动后就独占系统了。甚至,你的程序可能最多需要一个窗口,一个 UIWindow类的实例。你希望改变用户界面的时候,你只需要改变窗口上显示的视图即可。

窗口提供了用户界面的描绘空间,但是视图对象提供实际的内容。视图对象是UIView类的实例,描绘内容,并响应对内容的交互行为。iPhone OS定义了标准视图去表现表格,按钮,文本框以及其他类型的交互控件。你可以把这些视图都加入到窗口,或者你可以用继承UIView定义一个定制视图,实现一些定制描绘和事件响应。MoveMe程序定义了两个视图,由MoveMeView和PlacardView两个类实现,显示用户界面,处理用户交互事件。

程序启动期间,目标是创建程序窗口并尽快的显示一些初始内容。窗口从MainWindow.xib文件中展开。当程序达到启动完成状态,准备就绪可以处理事件的时候,UIApplication对象向委托对象发出applicationDidFinishLaunching:消息。这个消息暗示委托对象把内容放入窗口,并执行其他程序所需的初始化操作。

在MoveMe程序中,委托在applicationDidFinishLaunching:消息中做如下工作:

  1. 创建一个视图控制器对象,管理窗口中的内容视图。
  2. 使用MoveMeView类的实例初始化试图控制器,这个实例保存在MoveMeView.xib文件内,作为背景视图并填满整个窗口边框。
  3. 把视图控制器作为子视图加入窗口。
  4. 显示窗口。

列表2展示了MoveMe程序的applicationDidFinishLaunching:方法,该方法定义在程序委托对象的实现文件MoveMeAppDelegate.m中。这个方法创建了窗口的主内容视图,并使窗口可见。展现窗口让系统了解到,程序已经准备就绪可以处理事件了。

列表2 创建内容视图

- (void)applicationDidFinishLaunching:(UIApplication *)application  {      // Set up the view controller      UIViewController *aViewController = [[UIViewController alloc]      initWithNibName:@"MoveMeView" bundle:[NSBundle mainBundle]];      self.viewController = aViewController;      [aViewController release];        // Add the view controller's view as a subview of the window      UIView *controllersView = [viewController view];      [window addSubview:controllersView];      [window makeKeyAndVisible];  }  

注意:你可以利用applicationDidFinishLaunching:方法执行设定程序用户界面以外的其他任务。许多程序利用它初始化需要的数据结构,读取用户参数,或者返回到上次程序退出时的状态。

虽然前面的代码创建了窗口的背景视图,并显示了窗口,但是在前面的代码中,你无法找到创建PlacardView类显示Welcome按钮的代码。这个行为由MoveMeView类的 setUpPlacardView方法来处理,这个方法在MoveMeView对象从nib文件中展开的时候会被调用。setUpPlacardView 方法在列表3中展示。这个视图的初始化部分包括了PlacardView对象的创建。因为MoveMeView类提供了整个程序的背景,所以它把 PlacardView对象加为子视图。两个视图见的关系不仅令 Welcome按钮显示在程序的背景之上,而且让MoveMeView类可以处理针对按钮的事件。

列表3 创建placard视图

- (void)setUpPlacardView  {        // Create the placard view -- it calculates its own frame based on its image      PlacardView *aPlacardView = [[PlacardView alloc] init];      self.placardView = aPlacardView;      [aPlacardView release];      placardView.center = self.center;      [self addSubview:placardView];  }

想了解关于创建窗口和视图的更详细信息,参看iPhone OS编程指南的窗口和视图章节。

描绘Welcome按钮

UIKit提供的标准视图无需修改就可以描绘很多类型的简单内容。例如,你可以用UIImageView类显示图像,用UILabel类显示文本信息。 MoveMe程序的MoveMeView类也得益于UIView对象的基本属性,具体是backgroundColor属性,用颜色填满视图。这个属性可以在视图对象的初始化方法中被设置。这里,这个属性被设置于MoveMeView.xib文件中,使用界面构建器的查看器窗口的属性页的颜色选择器选择颜色。当你需要动态描绘内容,你必须使用在UIKit里的一些更高级的描绘特性,或者你应该使用Quartz或者OpenGL ES。

MoveMe 程序的PlacardView类描绘Welcome按钮并管理按钮在屏幕上面的位置。虽然PlacardView类可以用嵌入UIImageView和 UILabel对象的方法描绘内容,但是这里明确的描绘内容,来显示全部的流程。所以,这个类实现了drawRect:方法,该方法是用来定制描绘的位置。

drawRect:方法被调用时,描绘环境已经配置就绪了。需要做的只是指定描绘命令描绘定制内容。在PalcardView类中,内容包括一个背景图像(保存在Placard.png资源文件内)和一个定制字符串,文本可以动态改变。要描绘内容,该类需要执行下面的步骤:

  1. 在视图的原点描绘背景图片。(因为视图大小已经调整到符合图片尺寸,这个步骤提供了完整的按钮北京。)
  2. 计算welcome字符串的位置,令其可以显示在按钮中间。(因为字符串的尺寸可以改变,所以位置每次都需要根据当前的字符串尺寸计算。)
  3. 设定描绘颜色为黑色。
  4. 用黑色描绘字符串,位置略有偏移。
  5. 设定描绘颜色为白色。
  6. 在正确的位置用白色描绘字符串。

列表4展示 PlacardView类的drawRect方法。成员变量placardImage包含一个带有按钮背景图片的UIImage对象,成员变量 currentDisplayString是一个NSString对象包含了welcome字符串。描绘图像后,这个方法计算字符串相对视图的位置。字符串的尺寸是已知的,在字符串装入的时候计算好并保存在成员变量textSize中。字符串会被描绘两次,一次用黑色一次用白色,使用NSString的drawAtPoint:forWidth:withFont:fontSize:lineBreakMode:baselineAdjustment:方法。

列表4 描绘Welcome按钮

- (void)drawRect:(CGRect)rect  {      // Draw the placard at 0, 0      [placardImage drawAtPoint:(CGPointMake(0.0, 0.0))];          /*      Draw the current display string.      This could be done using a UILabel, but this serves to illustrate      the UIKit extensions to NSString. The text is drawn center of the      view twice - first slightly offset in black, then in white -- to give      an embossed appearance. The size of the font and text are calculated      in setupNextDisplayString.      */          // Find point at which to draw the string so it will be in the center of the view      CGFloat x = self.bounds.size.width/2 - textSize.width/2;      CGFloat y = self.bounds.size.height/2 - textSize.height/2;      CGPoint point;          // Get the font of the appropriate size      UIFont *font = [UIFont systemFontOfSize:fontSize];          [[UIColor blackColor] set];      point = CGPointMake(x, y + 0.5);      [currentDisplayString drawAtPoint:point      forWidth:(self.bounds.size.width-STRING_INDENT)      withFont:font      fontSize:fontSize      lineBreakMode:UILineBreakModeMiddleTruncation      baselineAdjustment:UIBaselineAdjustmentAlignBaselines];          [[UIColor whiteColor] set];      point = CGPointMake(x, y);          [currentDisplayString drawAtPoint:point      forWidth:(self.bounds.size.width-STRING_INDENT)      withFont:font      fontSize:fontSize      lineBreakMode:UILineBreakModeMiddleTruncation      baselineAdjustment:UIBaselineAdjustmentAlignBaselines];  }

当你需要描绘比图像和字符串更复杂的内容的时候,你可以选择Quartz或者OpenGL ES。Quartz和UIKit协作,处理描绘矢量路径,图像,斜线,PDF和其他你想动态创建的复杂内容。因为Quartz和UIKit基于相同的描绘环境,你可以直接在视图的drawRect:方法中调用Quartz函数,甚至可以在UIKit类中混用Quartz。

OpenGL ES是Quartz和UIKit之外的选择,可以让你用类似Mac OS X中OpenGL技术的函数来渲染2D和3D内容。跟Quartz和UIKit不同的是,你不能用视图的drawRect:方法来进行描绘。仍旧是使用视图,但是主要是用视图对象作为OpenGL ES代码的描绘空间。按照什么频率更新描绘空间,使用什么对象,取决于你自己的选择。

对于每种描绘技术和如何使用他们的细节信息,参看iPhone OS编程指南的图形和描绘。

处理触摸事件

iPhone OS的多点触摸接口令你的程序可以识别和响应多个手指触摸设备产生的不同事件。响应多手指触摸的能力带来了强大的能力,但是也带来跟传统的基于鼠标的事件处理系统操作的明显不同。每次手指触摸设备的表面,触摸传感器产生一个触摸事件。每次手指移动,额外的触摸事件产生表明手指的新位置。一旦手指离开设备表面,系统分发另外一个触摸事件表明这一点。

因为同时可能有多个手指触摸设备,这就产生了利用这些事件来识别复杂用户手势的可能性。系统对识别一些常用手势(例如,双击)提供了帮助,但是你可以检测到更复杂的手势。当事件系统产生了一个新的触摸事件,它包含了每个手指当前的状态,包括触摸或者刚离开设备的表面。因为每个事件对象包括所有的活动触摸事件,你可以用新的事件监控每个手指的动作。你可以在事件行为之间跟踪每个手指的移动来检测手势,你可以用来跟程序中的内容交互。例如,如果事件表明用户正在实施手指分开和手指并拢手势,而且下面的视图支持放大,所以你可以用这些时间改变当前的缩放级别。

图4 用触摸事件检测手势

系统把事件分发给程序的响应者对象,它是UIResponder类的实例。在iPhone程序中,程序的视图往往是你的定制响应者对象。MoveMe程序实现两个视图类,但是实际上只有MoveMeView类响应事件消息。该类通过覆盖 UIResponder的下列方法,检测在Welcome按钮内外的轻点。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;  - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;  - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

为了简化事件处理行为,MoveMe程序仅跟踪接触设备表面的第一次手指的事件。这可以利用UIView类对此的支持,此类在默认情况下禁用多点触摸事件。对于不需要跟踪多个手指事件的程序,这个特性很方便。当多点触摸事件被禁用的时候,系统仅分发跟第一个接触设备的手指的消息。与其他的触摸相关的事件不会被分发给视图。如果希望得到其他触摸的信息,你可以利用UIView类的 setMultipleTouchEnabled:方法,打开多点触摸支持。

作为事件处理部分,MoveMeView类执行下面的操作:

  1. 当触摸第一次发生,检查事件发生的位置。
    • 双击Welcome按钮外的位置,更新按钮显示的字符串。
    • 单击按钮,把按钮的中心移到手指之下,并引发一个动画放大按钮。
    • 其他的触摸都忽略。
  2. 如果手指在按钮之内移动,按钮的位置就会更新以匹配手指。
  3. 如果手指在按钮内,然后离开设备的表面,按钮以动画的形式回到他的原始位置。

列表5展示MoveMeView类的 touchesBegan:withEvent:方法。手指首次触摸设备的时候系统调用这个方法。方法获得所有的触摸,然后抽出唯一的触摸以及被触摸的物体。UITouch对象中的信息可以说明触摸发生在哪个视图(MoveMeView或者PlacardView)上,以及与之相关的触碰数量。如果触摸是按钮外的双击,touchesBegan:withEvent:方法调用setupNextDisplayString方法改变按钮的欢迎字符串。如果事件发生在Welcome按钮内,使用animateFirstTouchAtPoint:方法放大按钮,并把按钮中心移动到触摸发生的位置上。其他触摸相关的事件都被忽略了。

列表5 处理初步的触摸事件

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event  {      // We only support single touches, so anyObject      // retrieves just that touch from touches      UITouch *touch = [touches anyObject];          // Only move the placard view if the touch was in the placard view      if ([touch view] != placardView)      {      // In case of a double tap outside the placard view,      // update the placard's display string      if ([touch tapCount] == 2)      {      [placardView setupNextDisplayString];      }      return;      }      // Animate the first touch      CGPoint touchPoint = [touch locationInView:self];      [self animateFirstTouchAtPoint:touchPoint];  }

列表6展示MoveMeView类的touchesMoved:withEvent:方法。系统在响应手指已经触摸设备,而且从原始位置移开的时候调用这个方法。MoveMe程序仅跟踪发生在Welcome按钮上的移动。这个方法检查触摸事件的位置,并用之调整Placard对象的中点。视图的系统会引发在新位置的自动重绘。

列表6 响应触摸的移动

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event  {      UITouch *touch = [touches anyObject];          // If the touch was in the placardView, move the placardView      // to its location      if ([touch view] == placardView)      {          CGPoint location = [touch locationInView:self];          placardView.center = location;          return;      }  }

当用户的手指离开屏幕,MoveMe用动画的形式把按钮移动回它的原始位置,程序窗口的中点。列表7展示了开始动画的touchesEnded:withEvent:方法。

列表7 释放Welcome按钮

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event  {      UITouch *touch = [touches anyObject];          // If the touch was in the placardView, bounce it back to the center      if ([touch view] == placardView)      {          // Disable user interaction so subsequent touches          // don't interfere with animation          self.userInteractionEnabled = NO;          [self animatePlacardViewToCenter];          return;      }  }

为了简化程序的事件处理流程,touchesEnded:withEvent:方法在按钮以动画方式返回原始位置的过程中暂时禁用了新的触摸事件。如果不这么做的话,每个事件处理方法中都需要判断按钮是不是正在动画中,如果是就要终止动画。在按钮返回屏幕中心的时候短时间禁用用户交互,简化了事件处理代码,无非额外的逻辑。当按钮达到他的原始位置,MoveMeView类的 animationDidStop:finished:方法重新启用用户交互,这样事件循环又可以继续下去。

更多关于在iPhone OS下处理事件的信息,参看iPhone OS编程指南的事件处理章节。

按钮动画移动

在iPhone程序中,动画起了非常重要的作用。动画广泛用于给用户相关信息以及立即响应。例如,当用户在程序中浏览层级数据,iPhone不是仅仅直接用其他的屏幕代替当前的屏幕,而是让屏幕以动画的方式就位。移动的方向用户走向层级的高层还是低层,并给用户可视化的信息这里有新的信息。

因为动画很重要,所以UIKit的类中已经内建了对它的支持。MoveMe程序获益于这种支持,动画改变Welcome按钮的外观。当用户第一次触摸按钮,程序引发一个动画让按钮的尺寸变大。

当用户放开这个按钮,另外的动画产生让按钮回到原始位置。创建这样动画的步骤如下:

  1. 调用你想动画的视图的beginAnimations:context:方法。
  2. 设置动画的属性。
  3. 调用commitAnimations方法开始动画。

列表8展示Welcome按钮第一次被触摸的时候令它动画的代码。这个方法设置了动画的持续时间,并对按钮实施放大操作。当动画结束,动画的机制调用委托的 growAnimationDidStop:finished:context:方法,通过令按钮轻微晃动的方式结束动画,并把placard视图移动到触点。

列表8 让Welcome产生动画

- (void)animateFirstTouchAtPoint:(CGPoint)touchPoint  {      #define GROW_ANIMATION_DURATION_SECONDS 0.15            NSValue *touchPointValue = [[NSValue valueWithCGPoint:touchPoint] retain];      [UIView beginAnimations:nil context:touchPointValue];      [UIView setAnimationDuration:GROW_ANIMATION_DURATION_SECONDS];      [UIView setAnimationDelegate:self];      [UIView setAnimationDidStopSelector: @selector(growAnimationDidStop:finished:context:)];      CGAffineTransform transform = CGAffineTransformMakeScale(1.2, 1.2);      placardView.transform = transform;      [UIView commitAnimations];      }          - (void)growAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context      {      #define MOVE_ANIMATION_DURATION_SECONDS 0.15          [UIView beginAnimations:nil context:NULL];      [UIView setAnimationDuration:MOVE_ANIMATION_DURATION_SECONDS];      placardView.transform = CGAffineTransformMakeScale(1.1, 1.1);          // Move the placard view under the touch      NSValue *touchPointValue = (NSValue *)context;      placardView.center = [touchPointValue CGPointValue];      [touchPointValue release];      [UIView commitAnimations];  }

更多关于使用基于视图的内建动画的信息,参看iPhone OS编程指南的动画视图章节。更多关系核心动画的信息,参看iPhone OS编程指南的应用核心动画效果章节。

完成程序

前面的章节,你已经看到MoveMe程序如何初始化,展现用户界面和响应事件。除了这些概念以外,在构建程序并把它装载到设备之前,你还需要了解一些小细节。最后需要考虑的是你的程序的信息属性列表(info.plist)。这是程序用来跟系统通信的一个XML文件。Xcode 为你的程序创建一个默认版本,并把程序的初始化配置信息放在其中。你可以拓展这个信息,为系统提供所需的关于程序的额外细节信息。例如,你使用这个文件通信,关于程序的版本,支持的定制URL模式,启动镜像,默认的视觉状态和系统状态栏的风格。

列表9展示了MoveMe程序的info.plist文件的内容。该文件指明可执行文件的名字,显示在用户主屏幕的图像文件,以及在系统中唯一确定程序的字符串。因为MoveMe程序是一个全屏程序,也就是说,他不显示状态栏,所以它包含了键UIStatusBarHidden并把值设为true。把这个键设置为true令系统得知在启动或者程序运行期间,不要显示程序状态栏。虽然MoveMe程序也可以用编程的手段配置相同的行为,但是那样的话行为只能在程序已经启动后起作用,看起来有点莫名其妙的。

列表9 Info.plist文件的内容

  CFBundleDevelopmentRegion    en    CFBundleDisplayName    ${PRODUCT_NAME}    CFBundleExecutable    ${EXECUTABLE_NAME}    CFBundleIconFile    Icon.png    CFBundleIdentifier    com.yourcompany.${PRODUCT_NAME:identifier}    CFBundleInfoDictionaryVersion    6.0    CFBundleName    ${PRODUCT_NAME}    CFBundlePackageType    APPL    CFBundleSignature    ????    CFBundleVersion    1.0    UIStatusBarHidden        NSMainNibFile    MainWindow    

注意:你可以使用文本编辑器编辑info.plist文件的内容,显示为Info.plist文件内容对应的XML内容,或者使用属性列表编辑器,显示为键值对的表格。Xcode也提供了信息窗口访问程序目标的某些属性。要展现这个窗口,选择程序目标然后选择菜单File > Get Info。属性表包含一些info.plist文件中的属性。

想了解更多关于程序的info.plist文件的信息,参看iPhone OS编程指南的属性列表章节。

现在你已经了解了创建自己的iPhone程序需要的全部基础信息。下面的步骤是学习更多的iPhone OS特性。程序应该利用iPhone OS内建的特性创建直观而且令人愉悦的用户体验。这些特性有些在“深入探讨你的程序”,但是关于完全的特性列表以及如何使用它们的信息,参看iPhone OS编程指南。

深入谈讨你的程序

这里有一些用户可以获得的iPhone和iPod touch的特性。有些特性跟硬件相关,例如根据设备的方向自动调整视图。有些是跟软件相关的,例如所有的内建iPhone应用程序都使用同一个联系人列表。因为下面描述的这些特性跟基本的用户体验有关,所以你应该在原始设计就考虑这些特性是否适用于你的程序。

用加速度传感器跟踪方向和移动

iPhone和iPod touch的加速度传感器可以为系统和你的程序提供输入。一个加速度传感器测量在一个方向上速度的变化。iPhone和iPod touch都有三个加速度传感器用来测量3D空间的每个主要轴向上的变化,允许你检测任何方向的移动。

图5 加速度传感器轴向

也许你不认为测量加速度变化有啥用处,但是实际上这个信息可以有很大的用处。重力的作用总是把物体拉向地面。这个力的作用造成即使设备是静止不动的,仍旧可以测量到朝向地面的加速度。通过跟踪加速度传感器记录的加速度,以及加速度的大小,你可以精确的检测到设备在3D空间内的方向。你可以把方向作为程序的输入。

系统使用加速度传感器监控设备当前的方向,并在方向改变的时候通知程序。如果程序的用户界面支持横竖两种模式,那么你应该把视图控制器加入到你的设计中。UIViewController类提供了,旋转用户界面以及根据方向改变自动调整视图位置的机制。

如果你想直接访问原始的加速度传感器数据,你可以使用UIKit内的共享对象UIAccelerometer。加速度传感器在一个可以配置的时间间隔下定时报告当前的加速度值。你还可以使用这些数据来检测设备方向或者检测其他的瞬间移动,例如用户前后摇晃设备。你可以把这个信息作为游戏或者其他程序的输入。想要找到如何配置UIAccelerometer对象,以及接收加速度传感器事件的例子,参看iPhone OS编程指南的访问加速度传感器事件章节。

访问用户通讯录

用户的通讯录列表是所有系统程序共享的重要资源。电话,邮件和短信息程序都用它来识别用户要联系的联系人,并使基本交互变得容易,例如打电话,发邮件,或者短信息。你自己的程序也可以为类似的目的访问用户的通讯录信息,或者其他程序需要的相关信息。

图6 访问用户通讯录

iPhone OS提供对用户通讯录的直接访问,以及通过一个标准的选择接口进行的间接访问。使用直接访问,你可以直接用通讯录数据库获得联系人信息。你可以用另外的方式来展现这些联系信息,或者根据程序的需求过滤它们。如果你不需要一个定制接口,iPhone OS也提供了一系列系统接口选择和创建联系人。把这些接口加入你的程序需要一些额外的工作,但是可以令你的程序的外观看起来更像系统的一部分。

访问用户的联系信息需要地址簿和地址簿用户界面框架。更多关于这些框架,参看地址簿用户界面框架参考手册和地址簿框架参考手册。

获得用户当前的位置

运行iPhone OS的设备是为用户频繁使用而设计的。你所设计的程序需要把这一点纳入考虑之中。互联网和Web让人们可以在任何地方经营自己的事业,所以可以追踪用户当前位置就变成了必备的用户体验了。毕竟,我们不能为一个在洛杉矶口渴的人,列出纽约的咖啡店。这就是Core Location框架的用处。

Core Location框架追踪手机机站和Wi-Fi无线热点的信号,然后使用他们三角定位出用户当前的位置(此文档比较早期,现在已经包括直接获取GPS信号了)。你可以仅用该框架获取一个固定的初始位置,也可以在用户位置发生变化的时候得到通知。利用这些信息,你可以过滤要提供给用户的信息,或者做一些其他的事情。

在程序中获取位置信息数据的例子,参见iPhone OS编程指南的“获得用户当前位置”章节。

播放音频和视频

iPhone OS通过Core Audio和OpenAL框架支持你的程序中的音频特性,并使用Media Player框架提供视频回放的能力。你可以用它播放简单的声音效果,或者多个声道的音频,混音,把它们限制在某个音域内,甚至可以使用iPhone的振动功能。如果你是一个游戏开发者,而且你的代码中已经使用了OpenAL的特性,你可以直接在iPhone OS中继续使用这些代码在你的游戏中定位和回放你的音频。

Media Player框架是用来回放全屏视频文件。这个框架支持多种标准的电影文件格式,你可以控制回放的环境,包括是否显示用户控制器,以及视频内容的长宽比。游戏开发者可以利用这个框架播放过场影片或者预先渲染好的内容,而基于媒体的程序也可以用这个框架回放电影文件。

图7 播放视频

更多关于iPhone OS中的多媒体技术的信息,参见iPhone OS编程指南的“音频和视频技术”章节。

使用内置相机

iPhone的相机程序允许用户拍摄照片,然后保存在照片库中,和其他从电脑上传来的照片放在一起。虽然iPod touch没有照相机,但是它也有一个用来保存用户上传照片的照片库。iPhone OS用UIKit框架中的UIImagePickerController类来支持这些特性。

图8 iPhone的相机

UIImagePickerController类为你的程序提供了相机和照片库的访问接口。包括照相和照片程序的很多程序,使用这些标准的系统接口。当你显示一个图片选择界面,选择控制器UIImagePickerController实现所有所需的用户交互细节,然后返回一个结果图像到你的应用程序。

更多关于照片选择器接口的信息,参见iPhone OS程序开发指南中的“用照相机照相”以及“从照片库中选择照片”章节。

  评论这张
 
阅读(1089)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017