0%

SQLite 数据类型

SQLite 数据类型是一个用来指定任何对象的数据类型的属性。SQLite 中的每一列,每个变量和表达式都有相关的数据类型。

您可以在创建表的同时使用这些数据类型。SQLite 使用一个更普遍的动态类型系统。在 SQLite 中,值的数据类型与值本身是相关的,而不是与它的容器相关。

SQLite 存储类

每个存储在 SQLite 数据库中的值都具有以下存储类之一:

存储类 描述
NULL 值是一个 NULL 值。
INTEGER 值是一个带符号的整数,根据值的大小存储在 1、2、3、4、6 或 8 字节中。
REAL 值是一个浮点值,存储为 8 字节的 IEEE 浮点数字。
TEXT 值是一个文本字符串,使用数据库编码(UTF-8、UTF-16BE 或
BLOB 值是一个 blob 数据,完全根据它的输入存储。

SQLite 的存储类稍微比数据类型更普遍。INTEGER 存储类,例如,包含 6 种不同的不同长度的整数数据类型。


SQLite Affinity 类型

SQLite 支持列上的类型 affinity 概念。任何列仍然可以存储任何类型的数据,但列的首选存储类是它的 affinity。在 SQLite3 数据库中,每个表的列分配为以下类型的 affinity 之一:

Affinity 描述
TEXT 该列使用存储类 NULL、TEXT 或 BLOB 存储所有数据。
NUMERIC 该列可以包含使用所有五个存储类的值。
INTEGER 与带有 NUMERIC affinity 的列相同,在 CAST 表达式中带有异常。
REAL 与带有 NUMERIC affinity 的列相似,不同的是,它会强制把整数值转换为浮点表示。
NONE 带有 affinity NONE 的列,不会优先使用哪个存储类,也不会尝试把数据从一个存储类强制转换为另一个存储类。

SQLite Affinity 及类型名称

下表列出了当创建 SQLite3 表时可使用的各种数据类型名称,同时也显示了相应的应用 Affinity:

数据类型 Affinity
INT
INTEGERTINYINT
SMALLINT
MEDIUMINT
BIGINT
UNSIGNED BIG INT
INT2
INT8
INTEGER
CHARACTER(20)
VARCHAR(255)
VARYING CHARACTER(255)
NCHAR(55)
NATIVE CHARACTER(70)
NVARCHAR(100)
TEXT
CLOB
TEXT
BLOB
no datatype specified
NONE
REAL
DOUBLE
DOUBLE PRECISION
FLOAT
REAL
NUMERIC
DECIMAL(10,5)
BOOLEAN
DATE
DATETIME
NUMERIC

Boolean 数据类型

SQLite 没有单独的 Boolean 存储类。相反,布尔值被存储为整数 0(false)和 1(true)。


Date 与 Time 数据类型

SQLite 没有一个单独的用于存储日期和/或时间的存储类,但 SQLite 能够把日期和时间存储为 TEXT、REAL 或 INTEGER 值。

存储类 日期格式
TEXT 格式为 “YYYY-MM-DD HH:MM:SS.SSS” 的日期。
REAL 从公元前 4714 年 11 月 24 日格林尼治时间的正午开始算起的天数。
INTEGER 从 1970-01-01 00:00:00 UTC 算起的秒数。

您可以以任何上述格式来存储日期和时间,并且可以使用内置的日期和时间函数来自由转换不同格式。

SQLite 创建数据库

SQLite 的 sqlite3 命令被用来创建新的 SQLite 数据库。您不需要任何特殊的权限即可创建一个数据。


语法

sqlite3 命令的基本语法如下:

$sqlite3 DatabaseName.db

通常情况下,数据库名称在 RDBMS 内应该是唯一的。

实例

如果您想创建一个新的数据库 <testDB.db>,SQLITE3 语句如下所示:

$sqlite3 testDB.db
SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite>

上面的命令将在当前目录下创建一个文件 testDB.db。该文件将被 SQLite 引擎用作数据库。如果您已经注意到 sqlite3 命令在成功创建数据库文件之后,将提供一个 sqlite> 提示符。
一旦数据库被创建,您就可以使用 SQLite 的 .databases 命令来检查它是否在数据库列表中,如下所示:

sqlite>.databases
seq  name             file                                                      
---- ---------------- --------------------------------------------
0    main             /Users/wangruofeng/Documents/SQLitePractice/testDB.db

您可以使用 SQLite .quit 命令退出 sqlite 提示符,如下所示:

sqlite>.quit
$

.dump 命令

您可以在命令提示符中使用 SQLite .dump 点命令来导出完整的数据库在一个文本文件中,如下所示:

$sqlite3 testDB.db .dump > testDB.sql

上面的命令将转换整个 testDB.db 数据库的内容到 SQLite 的语句中,并将其转储到 ASCII 文本文件 testDB.sql 中。您可以通过简单的方式从生成的 testDB.sql 恢复,如下所示:

$sqlite3 testDB.db < testDB.sql

此时的数据库是空的,一旦数据库中有表和数据,您可以尝试上述两个程序。现在,让我们继续学习下一章。

SQLite 附加数据库

假设这样一种情况,当在同一时间有多个数据库可用,您想使用其中的任何一个。SQLite 的 ATTACH DTABASE 语句是用来选择一个特定的数据库,使用该命令后,所有的 SQLite 语句将在附加的数据库下执行。相当于给数据库取一个别名


语法

SQLite 的 ATTACH DATABASE 语句的基本语法如下:

ATTACH DATABASE 'DatabaseName' As 'Alias-Name';

如果数据库尚未被创建,上面的命令将创建一个数据库,如果数据库已存在,则把数据库文件名称与逻辑数据库 ‘Alias-Name’ 绑定在一起。


实例
如果想附加一个现有的数据库 testDB.db,则 ATTACH DATABASE 语句将如下所示:

sqlite> ATTACH DATABASE 'testDB.db' as 'TEST';

使用 SQLite .database 命令来显示附加的数据库。

sqlite> .database
seq  name             file
---  ---------------  ----------------------
0    main             /home/sqlite/testDB.db
2    test             /home/sqlite/testDB.db

数据库名称 main 和 temp 被保留用于主数据库和存储临时表及其他临时数据对象的数据库。这两个数据库名称可用于每个数据库连接,且不应该被用于附加,否则将得到一个警告消息,如下所示:

sqlite>  ATTACH DATABASE 'testDB.db' as 'TEMP';
Error: database TEMP is already in use
sqlite>  ATTACH DATABASE 'testDB.db' as 'main';
Error: database main is already in use

SQLite 分离数据库

SQLite的 DETACH DTABASE 语句是用来把命名数据库从一个数据库连接分离和游离出来,连接是之前使用 ATTACH 语句附加的。如果同一个数据库文件已经被附加上多个别名,DETACH 命令将只断开给定名称的连接,而其余的仍然有效。您无法分离 main 或 temp 数据库。

如果数据库是在内存中或者是临时数据库,则该数据库将被摧毁,且内容将会丢失。

语法

SQLite 的 DETACH DATABASE ‘Alias-Name’ 语句的基本语法如下:

DETACH DATABASE 'Alias-Name';

在这里,’Alias-Name’ 与您之前使用 ATTACH 语句附加数据库时所用到的别名相同。

实例

假设在前面的章节中您已经创建了一个数据库,并给它附加了 ‘test’ 和 ‘currentDB’,使用 .database/.databases 命令,我们可以看到:

sqlite> .databases
seq  name             file
---  ---------------  ----------------------
0    main             /home/sqlite/testDB.db
2    test             /home/sqlite/testDB.db
3    currentDB        /home/sqlite/testDB.db

现在,让我们尝试把 ‘currentDB’ 从 testDB.db 中分离出来,如下所示:

sqlite> DETACH DATABASE 'currentDB';

现在,如果检查当前附加的数据库,您会发现,testDB.db 仍与 ‘test’ 和 ‘main’ 保持连接。

sqlite> .databases
seq  name             file
---  ---------------  ----------------------
0    main             /home/sqlite/testDB.db
2    test             /home/sqlite/testDB.db

SQLite 创建表

SQLite 的 CREATE TABLE 语句用于在任何给定的数据库创建一个新表。创建基本表,涉及到命名表、定义列及每一列的数据类型。


语法

CREATE TABLE 语句的基本语法如下:

CREATE TABLE database_name.table_name(
   column1 datatype  PRIMARY KEY(one or more columns),
   column2 datatype,
   column3 datatype,
   .....
   columnN datatype,
);

CREATE TABLE 是告诉数据库系统创建一个新表的关键字。CREATE TABLE 语句后跟着表的唯一的名称或标识。您也可以选择指定带有 table_name 的 database_name

实例

下面是一个实例,它创建了一个 COMPANY 表,ID 作为主键,NOT NULL 的约束表示在表中创建纪录时这些字段不能为 NULL:

sqlite> CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL
);

让我们再创建一个表,我们将在随后章节的练习中使用:

sqlite> CREATE TABLE DEPARTMENT(
   ID INT PRIMARY KEY      NOT NULL,
   DEPT           CHAR(50) NOT NULL,
   EMP_ID         INT      NOT NULL
);

您可以使用 SQLIte 命令中的 .tables/.table 命令来验证表是否已成功创建,该命令用于列出附加数据库中的所有表。

sqlite> .tables
COMPANY          DEPARTMENT       TEST.COMPANY     TEST.DEPARTMENT

在这里,可以看到 COMPANY 表出现两次,一个是主数据库的 COMPANY 表,一个是为 testDB.db 创建的 ‘test’ 别名的 test.COMPANY 表。您可以使用 SQLite .schema 命令得到表的完整信息,如下所示:

sqlite>.schema COMPANY
CREATE TABLE COMPANY(
   ID INT PRIMARY KEY     NOT NULL,
   NAME           TEXT    NOT NULL,
   AGE            INT     NOT NULL,
   ADDRESS        CHAR(50),
   SALARY         REAL
);

SQLite 删除表

SQLite 的 DROP TABLE 语句用来删除表定义及其所有相关数据、索引、触发器、约束和该表的权限规范。

使用此命令时要特别注意,因为一旦一个表被删除,表中所有信息也将永远丢失。


语法

DROP TABLE 语句的基本语法如下。您可以选择指定带有表名的数据库名称,如下所示:

DROP TABLE database_name.table_name;

实例

让我们先确认 COMPANY 表已经存在,然后我们将其从数据库中删除。

sqlite> .tables
COMPANY       test.COMPANY

这意味着 COMPANY 表已存在数据库中,接下来让我们把它从数据库中删除,如下:

sqlite> DROP TABLE COMPANY;
sqlite>

现在,如果尝试 .TABLES 命令,那么将无法找到 COMPANY 表了:

sqlite> .tables
sqlite>

显示结果为空,意味着已经成功从数据库删除表。

资料整理于:RUNOOB.COM-SQLite教程
转载请注明出处。

简介:

SQLite 是一个软件库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。
本教程将告诉您如何使用 SQLite 编程,并让你迅速上手。

什么是 SQLite?

SQLite是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库,这意味着与其他数据库一样,您不需要在系统中配置。
就像其他数据库,SQLite 引擎不是一个独立的进程,可以按应用程序需求进行静态或动态连接。SQLite 直接访问其存储文件

为什么要用 SQLite?

  • 不需要一个单独的服务器进程或操作的系统(无服务器的)。
  • SQLite 不需要配置,这意味着不需要安装或管理。
  • 一个完整的 SQLite 数据库是存储在一个单一的跨平台的磁盘文件。
  • SQLite 是非常小的,是轻量级的,完全配置时小于 400KiB,省略可选功能配置时小于250KiB。
  • SQLite 是自给自足的,这意味着不需要任何外部的依赖。
  • SQLite 事务是完全兼容 ACID 的,允许从多个进程或线程安全访问。
  • SQLite 支持 SQL92(SQL2)标准的大多数查询语言的功能。
  • SQLite 使用 ANSI-C 编写的,并提供了简单和易于使用的 API。
  • SQLite 可在 UNIX(Linux, Mac OS-X, Android, iOS)和 Windows(Win32, WinCE, WinRT)中运行。

SQLite 命令

与关系数据库进行交互的标准 SQLite 命令类似于 SQL。命令包括 CREATE、SELECT、INSERT、UPDATE、DELETE 和 DROP。这些命令基于它们的操作性质可分为以下几种:

DDL - 数据定义语言

命令 描述
CREATE 创建一个新的表,一个表的视图,或者数据库中的其他对象。
ALTER 修改数据库中的某个已有的数据库对象,比如一个表。
DROP 删除整个表,或者表的视图,或者数据库中的其他对象。

DML - 数据操作语言

命令 描述
INSERT 创建一条记录。
UPDATE 修改记录。
DELETE 删除记录。

DQL - 数据查询语言

命令 描述
SELECT 从一个或多个表中检索某些记录。

在 Mac OS X 上安装 SQLite

最新版本的 Mac OS X 会预安装 SQLite,但是如果没有可用的安装,只需按照如下步骤进行:
请访问 SQLite 下载页面,从源代码区下载 sqlite-autoconf-*.tar.gz。
步骤如下:

$tar xvfz sqlite-autoconf-3071502.tar.gz
$cd sqlite-autoconf-3071502
$./configure --prefix=/usr/local
$make
$make install

上述步骤将在 Mac OS X 机器上安装 SQLite,您可以使用下列命令进行验证:

$sqlite3
SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

最后,在 SQLite 命令提示符下,使用 SQLite 命令做练习。

SQLite语法

大小写敏感性

有个重要的点值得注意,SQLite 是不区分大小写的,但也有一些命令是大小写敏感的,比如 GLOB 和 glob 在 SQLite 的语句中有不同的含义。

注释

SQLite 注释是附加的注释,可以在 SQLite 代码中添加注释以增加其可读性,他们可以出现在任何空白处,包括在表达式内和其他 SQL 语句的中间,但它们不能嵌套。
SQL 注释以两个连续的 “-“ 字符(ASCII 0x2d)开始,并扩展至下一个换行符(ASCII 0x0a)或直到输入结束,以先到者为准。
您也可以使用 C 风格的注释,以 “/*“ 开始,并扩展至下一个 “*/“ 字符对或直到输入结束,以先到者为准。SQLite的注释可以跨越多行。

SQLite 语句

所有的 SQLite 语句可以以任何关键字开始,如 SELECT、INSERT、UPDATE、DELETE、ALTER、DROP 等,所有的语句以分号(;)结束。

SQLite ANALYZE 语句:

ANALYZE;
or
ANALYZE database_name;
or
ANALYZE database_name.table_name;

SQLite AND/OR 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  CONDITION-1 {AND|OR} CONDITION-2;

SQLite ALTER TABLE 语句:

ALTER TABLE table_name ADD COLUMN column_def...;
SQLite ALTER TABLE 语句(Rename):
ALTER TABLE table_name RENAME TO new_table_name;

SQLite ATTACH DATABASE 语句:

ATTACH DATABASE 'DatabaseName' As 'Alias-Name';
SQLite BEGIN TRANSACTION 语句:
BEGIN;
or
BEGIN EXCLUSIVE TRANSACTION;

SQLite BETWEEN 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  column_name BETWEEN val-1 AND val-2;

SQLite COMMIT 语句:

COMMIT;

SQLite CREATE INDEX 语句:

CREATE INDEX index_name
ON table_name ( column_name COLLATE NOCASE );

SQLite CREATE UNIQUE INDEX 语句:

CREATE UNIQUE INDEX index_name
ON table_name ( column1, column2,...columnN);

SQLite CREATE TABLE 语句:

CREATE TABLE table_name(
   column1 datatype,
   column2 datatype,
   column3 datatype,
   .....
   columnN datatype,
   PRIMARY KEY( one or more columns )
);

SQLite CREATE TRIGGER 语句:

CREATE TRIGGER database_name.trigger_name
BEFORE INSERT ON table_name FOR EACH ROW
BEGIN
   stmt1;
   stmt2;
   ....
END;

SQLite CREATE VIEW 语句:

CREATE VIEW database_name.view_name  AS
SELECT statement....;

SQLite CREATE VIRTUAL TABLE 语句:

CREATE VIRTUAL TABLE database_name.table_name USING weblog( access.log );
or
CREATE VIRTUAL TABLE database_name.table_name USING fts3( );

SQLite COMMIT TRANSACTION 语句:

COMMIT;
SQLite COUNT 子句:
SELECT COUNT(column_name)
FROM   table_name
WHERE  CONDITION;

SQLite DELETE 语句:

DELETE FROM table_name
WHERE  {CONDITION};

SQLite DETACH DATABASE 语句:

DETACH DATABASE 'Alias-Name';

SQLite DISTINCT 子句:

SELECT DISTINCT column1, column2....columnN
FROM   table_name;

SQLite DROP INDEX 语句:

DROP INDEX database_name.index_name;

SQLite DROP TABLE 语句:

DROP TABLE database_name.table_name;

SQLite DROP VIEW 语句:

DROP INDEX database_name.view_name;

SQLite DROP TRIGGER 语句:

DROP INDEX database_name.trigger_name;

SQLite EXISTS 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  column_name EXISTS (SELECT * FROM   table_name );

SQLite EXPLAIN 语句:

EXPLAIN INSERT statement...;
or
EXPLAIN QUERY PLAN SELECT statement...;

SQLite GLOB 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  column_name GLOB { PATTERN };

SQLite GROUP BY 子句:

SELECT SUM(column_name)
FROM   table_name
WHERE  CONDITION
GROUP BY column_name;

SQLite HAVING 子句:

SELECT SUM(column_name)
FROM   table_name
WHERE  CONDITION
GROUP BY column_name
HAVING (arithematic function condition);

SQLite INSERT INTO 语句:

INSERT INTO table_name( column1, column2....columnN)
VALUES ( value1, value2....valueN);

SQLite IN 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  column_name IN (val-1, val-2,...val-N);

SQLite Like 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  column_name LIKE { PATTERN };

SQLite NOT IN 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  column_name NOT IN (val-1, val-2,...val-N);

SQLite ORDER BY 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  CONDITION
ORDER BY column_name {ASC|DESC};

SQLite PRAGMA 语句:

PRAGMA pragma_name;

For example:

PRAGMA page_size;
PRAGMA cache_size = 1024;
PRAGMA table_info(table_name);

SQLite RELEASE SAVEPOINT 语句:

RELEASE savepoint_name;

SQLite REINDEX 语句:

REINDEX collation_name;
REINDEX database_name.index_name;
REINDEX database_name.table_name;

SQLite ROLLBACK 语句:

ROLLBACK;
or
ROLLBACK TO SAVEPOINT savepoint_name;

SQLite SAVEPOINT 语句:

SAVEPOINT savepoint_name;
SQLite SELECT 语句:
SELECT column1, column2....columnN
FROM   table_name;

SQLite UPDATE 语句:

UPDATE table_name
SET column1 = value1, column2 = value2....columnN=valueN
[ WHERE  CONDITION ];

SQLite VACUUM 语句:

VACUUM;

SQLite WHERE 子句:

SELECT column1, column2....columnN
FROM   table_name
WHERE  CONDITION;

资料整理于:RUNOOB.COM-SQLite教程
转载请注明出处。

简介

CocoaPods是iOS最常用的第三方类库管理工具,绝大部分有名的开源类库
支持CocoaPods。

CocoaPods是用Ruby实现的,要使用它首先需要有Ruby的环境。

安装

幸亏OS X系统默认已经可以运行Ruby了,我们只需执行以下命令:

1
sudo gem install cocoapods

由于某些原因,执行时会出现下面的错误提示:

1
2
3
4
ERROR :Could not find a valid gem `cocoapods` (>= 0), here is why:
Unable to download data from https://rubygems.org/ - Errno::EPIPI:
Broken pipe - SSL_connect
(https://rubygems.org/lastest_specs.4.8.gz)

安装成功后,接着执行命令:

1
pod setup

如果Ruby环境不够新,可能需要更新以下:

1
sudo gem update --system

至此安装就完成了,我们可以尝试搜索一个第三方类库:

1
pod search AFNetworking

使用

使用CocoaPods第一步,是在当前项目下,新建一个Podfile文件:

1
touch Podfile

然后利用vim打开Podfile文件编辑,加入你想要的类库,格式如下:

1
2
3
4
5
6
platform :ios
pod 'Reachability', '3.1.0'

platform :ios, '6.0'
pod 'JSONKit', '1.4'
pod 'AFNetworking', '~> 2.3.1'

如果是拷贝别人的项目,或是一个很久没打开过的项目,可能需要先执行一下:

1
pod update

最后一步,执行命令:

1
pod install

当终端出现类似下面的提示后,就代表成功了:

1
[!] From now no use `Sample0814.xcworkspace`.

这个时候会看到项目文件夹多了一个xxx.xcworkspace,以后要通过这个文件
打开项目,老项目xxx.xcodeproj不再使用。

  1. 上面的每一步都可能出现问题,但大部分问题都是因为局域网的原因,用一个网速稳
    定的境外VPN可破
  2. 如果上面因为权限问题安装失败,必须每次都要删除
1
rm -rf /User/loginname/Library/Caches/CocoaPods/

因为这个缓冲中会存下你的github的东西,造成每次调用上次权限问题的缓存。

  1. 关于Podfile文件编辑时,第三方版本号的各种写法:
1
2
3
4
5
6
7
8
9
pod 'AFNetworking'      		 # 不显式指定依赖库版本,表示每次都获取最新版本
pod 'AFNetworking', '2.0' # 只使用2.0版本
pod 'AFNetworking', '>2.0' # 使用高于2.0的版本
pod 'AFNetworking', '>=2.0' # 使用大于或等于2.0的版本
pod 'AFNetworking', '<2.0' # 使用小于2.0的版本
pod 'AFNetworking', '<=2.0' # 使用小于或等于2.0的版本
pod 'AFNetworking', '~>0.1.2' # 使用大于等于0.1.2但小于0.2的版本,相当于>=0.1.2并且<0.2.0
pod 'AFNetworking', '~>0.1' # 使用大于等于0.1但小于1.0的版本
pod 'AFNetworking', '~>0' # 高于0的版本,写这个限制和什么都不写是一个效果,都表示使用最新版本

参考资料

GET和POST是两种最常用的与服务器进行交互的HTTP方法 .

GET

  • GET的语义是获取指定的URL资源, 将数据按照 variable = value 的形式, 添加到action所指向的URL后面, 并且两者使用 ‘ ? ‘连接, 各变量之间使用 ‘ & ‘连接 .
  • 对用户来说不安全, 因为在传输过程中, 数据被放在请求的URL中.
  • 传输的数据量小, 这主要是因为受URL长度限制.

URL长度限制

在http协议中,其实并没有对url长度作出限制,往往url的最大长度和用户浏览器和Web服务器有关,不一样的浏览器,能接受的最大长度往往是不一样的,当然,不一样的Web服务器能够处理的最大长度的URL的能力也是不一样的。

IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。

Firefox浏览器URL的长度限制为65,536个字符 ;

Apache(Server)能够接受的最大URL长度为8192个字符 ;

如果浏览器的编码为UTF8的话,一个汉字最终编码后的字符长度为9个字符。

GET请求示例

GetRequest

POST

  • POST语义是向指定URL的资源添加数据.
  • 将数据放在数据体中, 按照变量和值相对应的方式, 传递到action所指向的URL.
  • 所有数据对用户来说不可见.
  • 可以传输大量数据, 上传文件只能使用POST.

POST请求示例

PostRequest

在浏览器中判断GET&POST请求

因为POST请求会向服务器发送数据体, 因此刷新页面时会出现提示窗口. 而GET请求不会向服务器发送数据体, 因此没有提示 .

从请求本质来看, GET请求要比POST更安全, 效率也会更高 .(对服务器而言)

iOS网络发送网络请求的步骤

  1. 实例化URL( 网络资源 ) ;

  2. 根据URL建立URLRequest ( 网络请求 ) ;

    默认为GET请求; 对于POST请求, 需要创建请求的数据体 .

  3. 利用 URLConnection 发送网络请求(发送请求并获得结果) ;

NSURLConnection提供了两个静态方法可以直接以同步或异步的方式向服务器发送网络请求.

同步请求:
sendSynchronousRequest : returningResponse : error :

异步请求:
sendAsynchronousRequest : queue : completionHandler :

在网络请求过程中, 接收数据的过程实际上是通过 NSURLConnectionDataDelegate来实现的, 常用代理方法包括:

1
2
3
4
5
6
7
8
// 服务器开始返回数据
- (void)connection:didReceiveResponse:
// 收到服务器返回的数据,本方法会被调用多次
- (void)connection:didReceiveData:
// 数据接收完毕,做数据的最后处理
- (void)connectionDidFinishLoading:
// 网络连接错误
- (void)connection:didFailWithError:

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

HTTP POST使用注意事项:

  • http Body 中的NSData 编码方式要用NSASCIIStringEncoding而不是NSUTF8StringEncoding

  • 通过

    NSString *postLength = [NSString stringWithFormat:@"%d",[postData length]];


计算数据的长度

POST参数设置

1
2
3
4
5
6
//设置header Content-Length
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
//设置header contentType
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Current-Type"];
//设置body
[request setHTTPBody:postData];

备注:普通postheaderCurrent-Typeapplication/x-www-form-urlencoded

Multipart Forms POST参数设置

1
2
3
4
5
6
//设置header contentType
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request addValue:contentType forHTTPHeaderField:@"Content-Type"];

//设置body contentType
[body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];

备注:Multipart FormsheaderCurrent-Typemultipart/form-data

request body like this

    --YOUR_BOUNDARY_STRING
    Content-Disposition: form-data; name="photo"; filename="calm.jpg"
    Content-Type: image/jpeg

    YOUR_IMAGE_DATA_GOES_HERE
    --YOUR_BOUNDARY_STRING
    Content-Disposition: form-data; name="message"

    My first message
    --YOUR_BOUNDARY_STRING
    Content-Disposition: form-data; name="user"

    1
    --YOUR_BOUNDARY_STRING

I’m sending over three variables: an image named photo, a string named message, and an integer named user. It’s important to note the linebreaks and the dashes before the boundary string. These must be included in order to build a good request. Now lets write some objective-c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

NSString *boundary = @"YOUR_BOUNDARY_STRING";
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
[request addValue:contentType forHTTPHeaderField:@"Content-Type"];

NSMutableData *body = [NSMutableData data];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"photo\"; filename=\"%@.jpg\"\r\n", self.message.photoKey] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[NSData dataWithData:imageData]];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"message\"\r\n\r\n%@", self.message.message] dataUsingEncoding:NSUTF8StringEncoding]];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"user\"\r\n\r\n%d", 1] dataUsingEncoding:NSUTF8StringEncoding]];

[body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

[request setHTTPBody:body];

Now all we need to do is make a connection to the server and send the request:

[request setHTTPBody:body];
1
2
3
4
NSURLResponse *response;
NSError *error;

[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

参考资料:

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

3种工厂模式

概述:工厂模式是个系列,分为简单工厂模式, 工厂方法模式, 抽象工厂模式,这三种模式也非常常用。这些模式最最经典的就例子就是设计计算器。

  • Factory Method (工厂方法模式)
  • Abstract Factory (抽象工厂模式)
  • Simple Factory(简单工厂模式)

参考GoF《Design Patterns》一书

GOF是这样描述工厂模式的:

“Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.”

在基类中定义创建对象的一个接口,让子类决定实例化哪个类。工厂方法让一个类的实例化延迟到子类中进行。

简单工厂模式

严格的说,简单工厂模式并不是23种常用的设计模式之一,它只算工厂模式的一个特殊实现。简单工厂模式在实际中的应用相对于其他2个工厂模式用的还是相对少得多,因为它只适应很多简单的情况,最最重要的是它违背了我们在概述中说的开放-封闭原则。因为每次你要新添加一个功能,都需要在生switch-case 语句(或者if-else 语句)中去修改代码,添加分支条件

简单工厂模式角色分配:

  1. Creator(产品创建者)
    简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。

  2. Product ( 产品抽象类)
    简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。

  3. Concrete Product (具体产品)
    是简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。

简单的工厂模式UML图
简单工厂模式

考虑下面一个事例: 加入你是一个商人,你做的的是手机生意。现在你生产android 手机和iphone等,考虑到以后你可能还会生产其他手机例如ubuntu手机。假定你选择了简单工厂模式来实现。那么显然,我们需要所有产品的抽象基类(Product) 即是Phone类:

1
2
3
4
5
6
class Phone   
{
public:
virtual ~Phone(){};//在删除的时候防止内存泄露
virtual void call(string number) = 0;
};

然后我们需要具体的产品类 Concrete Product: AndroidPhone 和iOSPhone

1
2
3
4
5
6
7
8
9
10
11
class AndroidPhone : public Phone   
{
public:
void call(string number){ cout<<"AndroidPhone is calling..."<<endl;}
};

class IosPhone : public Phone
{
public:
void call(string number) { cout<<"IosPhone is calling..."<<endl;}
};

最后我们需要Creator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PhoneFactory  
{
public:
Phone* createPhone(string phoneName)
{
if(phoneName == "AndroidPhone")
{
return new AndroidPhone();
}else if(phoneName == "IosPhone")
{
return new IosPhone();
}

return NULL;
}
};

客户端这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main()  
{
PhoneFactor factory;
Phone* myAndroid = factory.createPhone("AndroidPhone");
Phone* myIPhone = factory.createPhone("iOSPhone");
if(myAndroid)
{
myAndroid->call("123");
delete myAndroid;
myAndroid = NULL;
}

if(myIPhone)
{
myIPhone->call("123");
delete myIPhone;
myIPhone = NULL;
}
}

这就是简单工厂方法,把所有的创建交给creator,creator 通过switch-case(或者if-else)语句来选择具体创建的对象。简单明了。但是就如上面所说,它最致命的问题的违背了开放-封闭原则。每次你要新添加一个功能,都要修改factor里面的createPhone代码。 但是工厂方法模式可以解决这个问题。

工厂方法模式

个人觉得工厂方法模式在工厂模式家族中是用的最多模式。上面说过了,如果简单工厂模式,要添加一个新功能,比如我现在要增加WinPhone 的生产,那么我要修改PhoneFactory中的createPhone 中的分支判断条件。这违背了开放-封闭原则,那为什么不能将创建方法放到子类中呢?
工厂方法的定义就是:定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。

工厂方法模式角色:

  1. 抽象工厂(Creator)角色:是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。
  2. 具体工厂(Concrete Creator)角色:这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建产品对象。
  3. 抽象产品(Product)角色:工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。
  4. 具体产品(Concrete Product)角色:这个角色实现了抽象产品角色所定义的接口。某具体产品有专门的具体工厂创建,它们之间往往一一对应

工厂方法模式UML图:
工厂方法模式

看定义看的晕乎乎的?那么我们来看代码:产品接口,以及其相应的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Phone   
{
public:
virtual ~Phone(){};//在删除的时候防止内存泄露
virtual void call(string number) = 0;
};

class AndroidPhone : public Phone
{
public:
void call(string number){ cout<<"AndroidPhone is calling..."<<endl;}
};

class iOSPhone : public Phone
{
public:
void call(string number) { cout<<"iOSPhone is calling..."<<endl;}
};

上面这个和简单工厂方法还是一样的。接下来不一样的来了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PhoneFactory  
{
public:
virtual ~PhoneFactory(){};
virtual Phone* createPhone() = 0;
};

class AndroidPhoneFactory : public PhoneFactory
{
public:
virtual Phone* createPhone()
{
return new AndroidPhone();
}
};

class IosPhoneFactory : public PhoneFactory
{
public:
virtual Phone* createPhone()
{
return new IosPhone();
}
};

工厂方法将PhoneFactory抽象成了基类,PhoneFactory的createPhone不在像以前那样将所有的判断塞到里面。而是改由其子类来实现创建功能,这感觉就是权力下放。
客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void main()  
{
PhoneFactory* androidCreator = new AndroidPhoneFactory();
PhoneFactory* iosCreator = new IosPhoneFactory();
Phone* myAndroid = androidCreator->createPhone();
Phone* myIPhone = iosCreator->createPhone();
if(myAndroid)
{
myAndroid->call("123");
delete myAndroid;
myAndroid = NULL;
}

if(myIPhone)
{
myIPhone->call("123");
delete myIPhone;
myIPhone = NULL;
}

delete androidCreator;
delete iosCreator;
}

在工厂方法模式中,核心工厂类不在负责产品的创建,而是将具体的创建工作交给子类去完成。也就是后所这个核心工厂仅仅只是提供创建的接口,具体实现方法交给继承它的子类去完成。当我们的系统需要增加其他新功能时,只需要继承PhoneFactory这个类,并且实现createPhone接口。 不需要对原工厂PhoneFactory进行任何修改,这样很好地符合了“开放-封闭“原则。

虽然工厂方法模式满足了”开放-封闭”原则,但是这个模式也仍然有缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,是的系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

抽象工厂模式

在工厂方法模式中,其实我们有一个潜在意识的意识。那就是我们生产的都是同一类产品,例如我们生产的都是手机!那么现在假如现在我们又要生产平板了了呢?那么就要用到抽象工厂模式。我抽象工厂模式也用的比较多在工厂模式家族中,仅次于工厂方法模式。在了解抽象工厂模式之前,还是老生常谈的理清下产品等级结构和产品簇的概念。下面的图还是老图。但是我讲讲我的理解:

抽象工厂模式
产品等级结构:产品的等级结构也就是产品的继承结构。我理解就是同一类产品,比如手机是一个系列,有android手机,ios手机,win手机,那么这个抽象类手机和他的子类就构成了一个产品等级结构。那其他的平板显然不是和手机一个系列的,一个平板,一个是手机,所以他们是不同的产品等级结构。

产品族: 在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。比如分为android产品,和ios产品。其中一个ios产品包含ios手机和ios平板。显然ios手机和ios平板不是同一个产品等级结构的,因为一个是手机,一个是平板。但他们是同一个产品簇—都是ios产品。
希望大家通过上面的例子大家明白了这两个概念。

抽象工厂模式的UML图:
抽象工厂模式

接着上面的话题,现在假如我要增加对平板的支持,那么我们肯定先添加两个产品等级结构,一个是手机,一个是平板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//产品等级结构--手机  
class Phone
{
public:
virtual ~Phone(){};//在删除的时候防止内存泄露
virtual void call(string number) = 0;
};

class AndroidPhone : public Phone
{
public:
void call(string number){ cout<<"AndroidPhone is calling..."<<endl; }
};

class IosPhone : public Phone
{
public:
void call(string number) { cout<<"IosPhone is calling..."<<endl; }
};

//产品等级结构--平板
class Pad
{
public:
virtual ~Pad(){};
virtual void playMovie() = 0;
};

class AndroidPad : public Pad
{
public:
virtual void playMovie(){ cout<<"AndriodPad is playing movie..."<<endl; }
};

class IosPad : public Pad
{
public:
virtual void playMovie(){ cout<<"IosPad is playing movie..."<<endl; }
};

然后具体的工厂我们整个工厂是生产移动设备的所以我们取名为MobileFactory,然后工厂可以生产平板和手机,故有了createPhone 和createPad两个接口。

1
2
3
4
5
6
7
class MobileFactory  
{
public:
virtual ~MobileFactory(){};
virtual Phone* createPhone() = 0;
virtual Pad* createPad() = 0;
};

接着是 android 产品簇 的工厂类,负责生产android 的手机和平板:

1
2
3
4
5
6
7
8
9
10
11
12
class AndroidFactory : public MobileFactory  
{
public:
Phone* createPhone()
{
return new AndroidPhone();
}
Pad* createPad()
{
return new AndroidPad();
}
};

接着是ios的产品簇的工厂类,负责生产ios的手机和平板:

1
2
3
4
5
6
7
8
9
10
11
12
13
class IosFactory : public MobileFactory  
{
public:
Phone* createPhone()
{
return new IosPhone();
}

Pad* createPad()
{
return new IosPad();
}
};

最后客户端这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main()  
{
MobileFactory* androidCreator = new AndroidFactory();
MobileFactory* iosCreator = new IosFactory();
Phone* myAndroidPhone = androidCreator->createPhone();
Pad* myAndroidPad = androidCreator->createPad();
Phone* myIosPhone = iosCreator->createPhone();
Pad* myIosPad = iosCreator->createPad();

myAndroidPhone->call("123");
myAndroidPad->playMovie();

myIosPhone->call("123");
myIosPad->playMovie();
//这里没有做释放和判断,请自己判断和释放
}

总结:
抽象工厂模式适用于那些有多种产品的产品簇,并且每次使用其中的某一产品簇的产品。
缺点 : 抽象工厂模式的添加新功能也非常麻烦,比工厂方法模式都还要复杂的多。
优点: 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象

主要用途

工厂方法要解决的问题是对象的创建时机,它提供了一种扩展的策略,很好地符合了开放封闭原则。工厂方法也叫做虚构造器(Virtual Constructor)。

工厂方法的类结构图

什么时候使用工厂方法

当是如下情况是,可以使用工厂方法:一个类不知道它所必须创建的对象的类时,一个类希望有它的子类决定所创建的对象时。

更多关于工厂方法的介绍,可以参考本文最后给出的参考内容。下面我们就来看看在iOS中工厂方法的一种实现方法。

iOS中工厂方法的实现实例

如下有一个类图,该图描述了下面即将实现的工厂方法(利用工厂方法,创建出不同的形状)。其中BVShapeFactory为工厂方法的基类,BVShape为形状的基类,BVClient通过BVShapeFactory,利用 BVShapeFactory的子类(BVCircleShapeFactory和BVSquareShapeFactory)分别创建出BVCircleShape和BVSquareShape。
工厂方法

github下载地址:https://github.com/BeyondVincent/ios_patterns/tree/master/FactoryMethodPattern

参考资料

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

本文目录

  • 前言
  • 1.获取当前线程
  • 2.获取主线程
  • 3.NSThread的创建
  • 4.暂停当前线程
  • 5.线程的其他操作
  • 6.优缺点

前言

每个iOS应用程序都有个专门用来更新显示UI界面、处理用户触摸事件的主线程,因此不能将其他太耗时的操作放在主线程中执行,不然会造成主线程堵塞(出现卡机现象),带来极坏的用户体验。一般的解决方案就是将那些耗时的操作放到另外一个线程中去执行,多线程编程是防止主线程堵塞,增加运行效率的最佳方法。

iOS中有3种常见的多线程编程方法

  1. NSThread
    这种方法需要管理线程的生命周期、同步、加锁问题,会导致一定的性能开销

  2. NSOperationNSOperationQueue
    是基于OC实现的。NSOperation以面向对象的方式封装了需要执行的操作,然后可以将这个操作放到一个NSOperationQueue中去异步执行。不必关心线程管理、同步等问题。

  3. Grand Centeral Dispatch
    简称GCD,iOS4才开始支持,是纯C语言的API。自iPad2开始,苹果设备开始有了双核CPU,为了充分利用这2个核,GCD提供了一些新特性来支持多核并行编程

这篇文章简单介绍NSThread这个类,一个NSThread实例就代表着一条线程

1.获取当前线程

1
NSThread *current = [NSThread currentThread];

2.获取主线程

1
2
NSThread *main = [NSThread mainThread];
NSLog(@"主线程:%@", main);

打印结果是:

2013-04-18 21:36:38.599 thread[7499:c07] 主线程:<NSThread: 0x71434e0>{name = (null), num = 1}

num相当于线程的id,主线程的num是为1的

3.NSThread的创建

a.动态方法

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

在第2行创建了一条新线程,然后在第4行调用start方法启动线程,线程启动后会调用self的run:方法,并且将@”mj”作为方法参数

1
2
3
4
// 初始化线程
NSThread *thread = [[[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"mj"] autorelease];
// 开启线程
[thread start];

假如run:方法是这样的:

1
2
3
4
- (void)run:(NSString *)string {
NSThread *current = [NSThread currentThread];
NSLog(@"执行了run:方法-参数:%@,当前线程:%@", string, current);
}

打印结果为:

2013-04-18 21:40:33.102 thread[7542:3e13] 执行了run:方法-参数:mj,当前线程:<NSThread: 0x889e8d0>{name = (null), num = 3}

可以发现,这条线程的num值为3,说明不是主线程,主线程的num为1

b.静态方法

1
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
1
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"mj"];

c.隐式创建线程

1
[self performSelectorInBackground:@selector(run:) withObject:@"mj"];

会隐式地创建一条新线程,并且在这条线程上调用self的run:方法,以@”mj”为方法参数

4.暂停当前线程

1
[NSThread sleepForTimeInterval:2];
1
2
NSDate *date = [NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]];  
[NSThread sleepUntilDate:date];

上面两种做法都是暂停当前线程2秒

5.线程的其他操作

a.在指定线程上执行操作

1
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];
  • 上面代码的意思是在thread这条线程上调用self的run方法
  • 最后的YES代表:上面的代码会阻塞,等run方法在thread线程执行完毕后,上面的代码才会通过

b.在主线程上执行操作

1
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];  

在主线程调用self的run方法

c.在当前线程执行操作

1
[self performSelector:@selector(run) withObject:nil];

在当前线程调用self的run方法

6.优缺点

  1. 优点:NSThread比其他多线程方案较轻量级,更直观地控制线程对象
  2. 缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

Post info: Updated for Xcode 7.1 and Swift 2.1 – 1 January 2016

Update note: This tutorial was updated to iOS 8, Xcode 6.1 and Swift by Richard Turton.

Original post by Tutorial Team member Soheil Azarpour.

每个人都有点击一个按钮或者进入一些文本在iOS或者Mac App上的令人沮丧的经历,就是突然-WHAM,用户交互停止了响应。

在Mac上,你的用户开始盯着一个沙漏或者一个七彩的轮子开始旋转直到它们再次恢复UI交互为止。在一个iOS app中,用户期望App立即响应它们的触摸事件,无响应的app给人的感觉是笨重和缓慢,这样通常会导致收到差评。

保持你的app的可交互的状态说起来容易做起来难,一旦你的app需要执行不仅仅是少量的任务,事情很快就变得很复杂,我们并没有多少事件在主事件循环执行繁重的工作并且同时提供一个可以响应的UI。

低级的开发者是怎样做的呢?解决方案就是把工作从主线程中移除通过并发。并发意味着你应用所有的操作同时执行在多个流(或者线程)中–这样的话用户界面就能保持响应的要执行的工作。

一种实现并发操作在iOS是通过NSOperationNSOperationQueue这两个类。在这个教程中,你将学会怎样使用它们!你将从一个没有使用并发的App开始,所以它将出现的非常迟钝和无响应。然后你将重做你的应用给它们添加并发操作并且–希望–呈现一个更加响应良好的可交互界面给用户!

我们开始吧

这份样品工程的总的目标的就是展示一个滤镜处理过的图片的表视图。图片将从网络上下载,经过一个滤镜处理后,然后在表视图中显示。

下面是这个app模型的示意图

app模型的示意图

第一个版本尝试

下载你将在这个教程中使用的第一个版本的项目

注意:所以的图片来自 stock.xchng.。一下图片在数据源故意错误命名,以便这里有些例子是图片下载失败好处理失败的情况。

构建并且运行这个工程,最终你将看到这个app运行起来显示一列照片。尝试滚动这个列表,很痛苦,不是吗?
list of photoes

传统的相册,运行缓慢

所有的操作都发生在ListViewController.swift里,并且最主要的是发生在 tableView(_:cellForRowAtIndexPath:)方法里。看一下那个方法和注释这里有两件相当集中的事情需要思考:

  1. 从网络载入图片数据。即使网络状况良好,app将仍然必须等待直到下载完成了才能继续。
  2. 使用Core Image给图片加滤镜。这个方法给图片应用一个深褐色的滤镜,假如你想了解更多关于Core Image滤镜的知识,请点击Beginning Core Image in Swift

还有,你将载入一系列图片请求从网络当它第一次被请求时:

1
lazy var photos = NSDictionary(contentsOfURL:dataSourceURL)

所有的工作发生在应用的主线程。由于主线程也负责用户交互,让它一直忙于从网络下载东西和给图片加滤镜消磨掉了响应中的app。你可以通过使用Xcode的仪表测量视图获得这样一个快速的概述。你可以通过显示调试导航(Commnad+6)接入这个仪表视图,让后选择CPU当app正在运行的时候。

gauges

Xcode的仪表视图表明,主线程的任务非常重

你会看到那些所有的长钉在Thread 1,那就是app的主线程。更多详细的信息你可以运行app的Instruments,但是那个在 whole other tutorial :].

是时候考虑怎样改善一下你的用户体验了!

任务,线程和进程

在你专研这篇教程之前,这里有一下技术上的慨念需要理一下,我将定义一些专用术语:

  • 任务 :一个简单单个的需要完成的工作
  • 线程 :操作系统提供的机制允许多个用户操作同时进行在一个应用中
  • 进程 :一个可执行的代码块,它可以由多个线程构成

注意:在iOS和OS X中,线程的功能由POSIX线程API(或者 pthreads)实现,并且它使操作系统的一部分。而这个又是相当底层的东西,你将发现它很容易犯错误;或许关于线程最糟糕的事情是那些很难被发现的错误!

Foundation框架包含了一个叫做NSThread的类,这让我们处理事情更容易,但是用NSThread管理多个线程仍是一件令人头疼的事情。NSOperationNSOperationQueue是为了最大化简化处理多线程的更高级的类。

在这个图解中,你会看到进程,线程和任务之间的关系:

the relationship between a process, threads, and tasks

进程,线程和任务

正如你所见,一个进程可以包涵多个执行的线程,每个线程能够同时执行多个任务。

在这个图集中, thread 2执行文件的读的工作,同时thread 1执行UI相关的代码。这和你应该怎样在iOS中构建你的代码(主线程执行任何和UI相关的工作,第二线程应当执行慢的或者长时间运行的耗时操作例如读取文件,接入网络等等)有点相似。

NSOperation vs. Grand Central Dispatch (GCD)

你应该听说过Grand Central Dispatch (GCD).。简单的来说,GCD由语言的特征,运行时库和系统增强组成来提供一个在iOSOS X多核硬件中支持并发系统并且综合的改良。假如你想了解更多关于GCD相关的知识,有可以阅读我们的Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial

NSOperationNSOperationQueue构建在GCD之上。普遍来说,苹果推荐使用最高级别抽象,当需要显示他们一些需要的测量工作时回到最底层。

下面是关于这两者的一些简单比较,将帮助你决定何时何地选择使用GCD或者NSOperation

  • GCD 是一个轻量级的方式来描述将要被并发执行的工作单元。你不必定制这个工作单元的时刻表;系统为你定制时刻表。在blocks中增加依赖是一件头疼的事情。取消或者暂停一个block来进行额外的工作为作为开发者的你! :]

  • NSOperation 和GCD相比增加了一些额外开支,但是你能够在各种操作之间增加依赖并且恢复,取消,暂停他们。

这个教程将使用NSOperation,因为你将处理一个列表为了好的表现并且由于它大量的消耗的资源你需要能够取消一个操作针对某个图片,假如用户已经将那张图片滚出屏幕。即使这些操作在后台线程,假如这里有一打事情在队列里等着它们去处理,这将表现得跟糟糕。

重构 App Model

是时候重构开始的非多线程的模型了!假如你仔细观察先前的模型,你会发现这里有三个可以被改进的线程受困区域。通过切割这三个区域让后把他们放在单独的线程里,主线程的压力将得到缓解并且能够保持和用户交互。

NSOperation_model_improved

改进后的 model

为了摆脱你应用的瓶颈,你需要一个指定一个线程来响应你的用户时间,一个线程专注于下载资源和图片,一个线程执行图片滤镜操作。在新的模型中,app从主线程启动让后载入一个空的表视图。同时,app启动第二个线程开始下载数据资源。

一旦数据资源下载完成,你将通知表视图重新载入。这些事情必须在主线程完成,因为它涉及到用户界面相关的操作。从这一点来说,表视图知道有多少行,并且它知道他将显示图片的URL地址,但是她不知道它是否真的有图片!假如你立即开始下载所有的图片在这个点上,这可能导致效率极其低下,因为你不需要一次性把所有图片下载完!

为了让这个变得更好我们能够做什么?

一个更好的模型就是可交互的行在屏幕范围内可见时才开始下载图片。所以你的代码开始将询问表视图有多少行可见。因此,代码应该直到这里有一个未加滤镜的图片等待处理时才开始处理图片加滤镜操作。

为了使app更加快速响应,代码将要让图片一旦下载完毕立即显示。让后才开始进行图片加滤镜操作,让后更新UI界面来显示已经经过滤镜处理后的图片。下面的图标显示了这个过程的控制流:

Control Flow

Cotroll Flow

为了获得这些对象,你需要跟踪这张图片现在是否正在被下载,一旦完成下载,假如图片的滤镜被应用上。你需要跟跟踪每个操作的状态,它是否正在下载中或者执行滤镜操作,以便你能够取消,暂停或者恢复每个操作当用户滚动的时候。

Okey! 现在你准备好开始码代码了! :]

打开你下载的工程,添加一个新的Swift File到你的工程中命名为** PhotoOperations.swift**。添加下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PendingOperations {
lazy var downloadsInProgress = [NSIndexPath:NSOperation]()
lazy var downloadQueue:NSOperationQueue = {
var queue = NSOperationQueue()
queue.name = "Download queue"
queue.maxConcurrentOperationCount = 1
return queue
}()

lazy var filtrationsInProgress = [NSIndexPath:NSOperation]()
lazy var filtrationQueue:NSOperationQueue = {
var queue = NSOperationQueue()
queue.name = "Image Filtration queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
}

这回类包含了2个字典为了跟踪激活和正在进行中的下载和滤镜操作对表中的每一行,并且两个操作队列都有各自的操作类型。

所有的值被懒加载的方式创建,意味着他们不会被初始化知道他们第一次被接入。这样改善了你app的表现性能。

创建一个NSOperationQueue是非常简单的,正如你所见,给你的队列命名是非常有用的,因为名字会在仪器或者调速器中显示。maxConcurrentOperationCount在这里由于这个教程的缘故被设置成1,是为了让你看到操作一个接一个完成。你可以离开这一部分允许队列决定他一次处理多少个操作–这样会进一步改善性能。

队列是怎样决定一次运行多少个操作的呢?这是一个非常好的问题! :] 这取决于硬件。默认,NSOperationQueue将要处理一写计算在屏幕背后,决定什么是最好的需要看代码是运行在某个具体的平台,和将载入的最大数量的线程数。

考虑到下面的例子,假设系统此时是空闲的,这里有很多资源可用,所以这个队列能够载入可能8条并发的线程。下一个时刻你运行程序,系统可能忙于其他不相关的正在抢夺资源的操作,这时队列就仅仅载入2个并发的线程。因为你已经设置了一个最大并发操作数,在这个app中一次只会进行一个操作。

注意:你可能想知道为什么你必须跟踪所有的激活的和正在进行中操作。队列有一个operations的方法,它将返回一个操作的数组,所有为什么不用它呢?在这个工程中这样做效果不是很好。你需要跟踪更表视图行数关联的操作,它可能会重复执行数组每次你需要一个的时候。把它们储存在一个字典中用index path来作为他的key方便快速和高效的查找。

是时候考虑下载和过滤操作了。添加下列代码到 PhotoOperations.swift:文件的末尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ImageDownloader: NSOperation {
//1
let photoRecord: PhotoRecord

//2
init(photoRecord: PhotoRecord) {
self.photoRecord = photoRecord
}

//3
override func main() {
//4
if self.cancelled {
return
}
//5
let imageData = NSData(contentsOfURL:self.photoRecord.url)

//6
if self.cancelled {
return
}

//7
if imageData?.length > 0 {
self.photoRecord.image = UIImage(data:imageData!)
self.photoRecord.state = .Downloaded
}
else
{
self.photoRecord.state = .Failed
self.photoRecord.image = UIImage(named: "Failed")
}
}
}

NSOperation是一个抽象类,为它的子类而设计。每个子类代表了一个特别的任务正如呈现在列表早期那样。

下面是上面代码每行注释到底发生了什么的说明:

  1. 添加一个常量引用到和操作相关的PhotoRecord对象
  2. 创建一个设计初始化方法允许photo record参数可以被传进来
  3. main是你在NSOperation子类中需要重写的方法,用来执行相关工作
  4. 在启动开始前检查是否被取消,操作应该定期检查是否已经被取消在尝试长时间或密集的工作之前
  5. 下载图片数据
  6. 再次检查是否被取消
  7. 假如这里有数据,创建一个图片对象然后把它添加到记录中,同时改变它的状态,假如这里没有数据,把这条记录标记成失败然后设置适当的图片

下一步,你将创建另一个操作来处理图片加滤镜的操作!添加下面的代码到PhotoOperations.swift文件末尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ImageFiltration: NSOperation {
let photoRecord: PhotoRecord

init(photoRecord: PhotoRecord) {
self.photoRecord = photoRecord
}

override func main () {
if self.cancelled {
return
}

if self.photoRecord.state != .Downloaded {
return
}

if let filteredImage = self.applySepiaFilter(self.photoRecord.image!) {
self.photoRecord.image = filteredImage
self.photoRecord.state = .Filtered
}
}
}

除了你对图片应用滤镜(使用一个未实现的方法,因此编译1错误)而不是下载它之外,这个看起来和下载操作非常相像。

添加遗失的图片滤镜处理方法到ImageFiltration类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(image:UIImage) -> UIImage? {
let inputImage = CIImage(data:UIImagePNGRepresentation(image))

if self.cancelled {
return nil
}
let context = CIContext(options:nil)
let filter = CIFilter(name:"CISepiaTone")
filter.setValue(inputImage, forKey: kCIInputImageKey)
filter.setValue(0.8, forKey: "inputIntensity")
let outputImage = filter.outputImage

if self.cancelled {
return nil
}

let outImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
let returnImage = UIImage(CGImage: outImage)
return returnImage
}

图片添加滤镜操作使用先前ListViewController中相同的实现。已经把它移动到这里来了以至于它它能在后台的一个单独的操作中完成。再次强调,你应该非常频繁的检查取消操作;最佳实践是在进行任何耗时操作调用之前和之后。一旦加滤镜操作完成,你应该立即设置photo record 实例的值。

很棒!现在你已经有了所有的工具和基础为了处理后台任务进程的操作。是时候回到控制器修改它以便能利用所有这些新的福利。

切换到ListViewController.swift文件,然后删除lazy var photos接口声明。添加下面声明:

1
2
var photos = [PhotoRecord]()
let pendingOperations = PendingOperations()

这些将持有一个数组的你在开始创建的PhotoDetails对象,PendingOperations对象来管理操作。

添加一个下载photos property列表新的方法到类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func fetchPhotoDetails() {
let request = NSURLRequest(URL:dataSourceURL!)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true

NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {response,data,error in
if data != nil {
let datasourceDictionary = NSPropertyListSerialization.propertyListWithData(data, options: Int(NSPropertyListMutabilityOptions.Immutable.rawValue), format: nil, error: nil) as! NSDictionary

for(key : AnyObject,value : AnyObject) in datasourceDictionary {
let name = key as? String
let url = NSURL(string:value as? String ?? "")
if name != nil && url != nil {
let photoRecord = PhotoRecord(name:name!, url:url!)
self.photos.append(photoRecord)
}
}

self.tableView.reloadData()
}

if error != nil {
let alert = UIAlertView(title:"Oops!",message:error.localizedDescription, delegate:nil, cancelButtonTitle:"OK")
alert.show()
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}

这个方法创建一个异步的网络请求,当完成的时候,将执行completion block在主线程。当下载完成property list的数据被萃取成一个NSDictionary,然后再次处理一个数组的PhotoRecord的对象。你不能直接使用这里的NSOperation,你应该在主线程中接入它使用NSOperationQueue.mainQueue()

viewDidLoad调用新方法

fetchPhotoDetails()

下一步找到 tableView(_:cellForRowAtIndexPath:)然后替换它用下面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) as! UITableViewCell

//1
if cell.accessoryView == nil {
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
cell.accessoryView = indicator
}
let indicator = cell.accessoryView as! UIActivityIndicatorView

//2
let photoDetails = photos[indexPath.row]

//3
cell.textLabel?.text = photoDetails.name
cell.imageView?.image = photoDetails.image

//4
switch (photoDetails.state){
case .Filtered:
indicator.stopAnimating()
case .Failed:
indicator.stopAnimating()
cell.textLabel?.text = "Failed to load"
case .New, .Downloaded:
indicator.startAnimating()
self.startOperationsForPhotoRecord(photoDetails,indexPath:indexPath)
}

return cell
}

花点时间来通读注释区域下面的解释:

  1. 为了给用户提供反馈,创建UIActivityIndicatorView让后把它设成cell的accessory view。
  2. 数据源包含PhotoRecord实例。抓取正确的数据基于当前行的indexPath
  3. cell的文本标签总是一样的,图片被正确的设置在PhotoRecord中当它被处理的时候,以便你能够设置他们两者,而不管记录的状态。
  4. 检查记录,正确的设置activity indicator和文本,然后开始操作(暂时还没实现)

你可以移除applySepiaFilter的实现,因为那个将不再被调用了,添加下面的方法到类中来开始操作:

1
2
3
4
5
6
7
8
9
10
func startOperationsForPhotoRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
switch (photoDetails.state) {
case .New:
startDownloadForRecord(photoDetails, indexPath: indexPath)
case .Downloaded:
startFiltrationForRecord(photoDetails, indexPath: indexPath)
default:
NSLog("do nothing")
}
}

这里,你将传递一个PhotoRecord类型的实例带有它的index path
依据photo record的状态,你选开始进行下载还是加滤镜的步骤。

注意:下载图片和给图片加滤镜的方法分开实现,因为这里有可能出现当一个图片正在被下载,用户可能把它滚开了,这样你就不必对它应用滤镜操作。当下一次用户又来到相同的哪行时,你不必重新下载图片;你只需对它应用
图片滤镜即可! Efficiency rocks! :]

现在你需要实现你在上面调用的方法。记住你创建的自定义的类,PendingOperations,保持跟踪操作;现在实际上你可以使用它了!添加下面的方法到类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func startDownloadForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
//1
if let downloadOperation = pendingOperations.downloadsInProgress[indexPath] {
return
}

//2
let downloader = ImageDownloader(photoRecord: photoDetails)
//3
downloader.completionBlock = {
if downloader.cancelled {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
})
}
//4
pendingOperations.downloadsInProgress[indexPath] = downloader
//5
pendingOperations.downloadQueue.addOperation(downloader)
}

func startFiltrationForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
if let filterOperation = pendingOperations.filtrationsInProgress[indexPath]{
return
}

let filterer = ImageFiltration(photoRecord: photoDetails)
filterer.completionBlock = {
if filterer.cancelled {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
})
}
pendingOperations.filtrationsInProgress[indexPath] = filterer
pendingOperations.filtrationQueue.addOperation(filterer)
}

Okey!下面是一个快速列表来帮助你立即上面的代码到底做了什么:

  1. 首先,检查特定的indexPath来看这里是否已经有一个操作在downloadsInProgress中。假如有,忽略它。
  2. 假如没有,通过designated initializer创建一个ImageDownloader实例
  3. 添加一个当操作完成时执行的完成block。这是最好的地方让你其余的应用知道一个操作已经完成。注意完成block必须被执行即使这个操作被取消,这相当的重要,所以你需要使用GCD来触发重新载入表视图在主线程。
  4. 添加操作到downloadsInProgress中来保持跟踪一些事情。
  5. 添加操作到下载队列中。这是你实际获取这些操作开始运行的方法–队列需要注意的是一旦你添加了操作它就会执行。

过滤图片的方法遵循下面相同的类型,除了它使用ImageFiltrationfiltrationsInProgress来跟踪操作。作为经验,你应该尝试不要重复这个区域的代码 :]

你做到了!你的工程完成了。构建让后运行看有什么改进在操作上!当你滚动表视图的时候,app并没有停止,还是像它们变得可见一样继续下载图片和给图片加滤镜操作。

classicphotos-stalled-screenshot

原来的图片,现在可以滚动了

是不是很酷?你能看到随做你的进步让你的应用更易于响应能做出的努力 –对用户来说更有趣!

细微的调整

你已经随着这个教程走过漫长的路!你的小项目现在反应灵敏表明了在原来的版本基础上有很多改进。然而,这里任然有一些遗留的细节我们需要考虑。你是想成为一名伟大的程序员,而不仅仅是一名优秀的程序员!

你可能已经注意到了当你滚动表视图时,那些离屏的cell仍然在处理下载和给图片加滤镜的操作。假如你快速滚动,app将忙于下载和给图片处理滤镜的操作,在列表中从最前面甚至到不可见的地方。理想的情况下app应该对离开屏幕的cells就是现在不可见的取消滤镜操作。

难道你没有把取消的规定放进你的代码里? 是的,你做了–现在你应该充分利用它们!:]

回到Xcode,让后打开ListViewController.swift文件。去到tableView(_:cellForRowAtIndexPath:)方法的实现,封装startOperationsForPhotoRecord调用在一个if条件向下面的:

1
2
3
if (!tableView.dragging && !tableView.decelerating) {
self.startOperationsForPhotoRecord(photoDetails, indexPath: indexPath)
}

你需要告诉表视图开始操作仅仅当表视图没有滚动的时候。这实际上是UIScrollView的接口,由于UITableViewUIScrollView的子类,你自动继承了这些接口。

下一步,添加到下面UIScrollView代理方法的实现到类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
//1
suspendAllOperations()
}

override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// 2
if !decelerate {
loadImagesForOnscreenCells()
resumeAllOperations()
}
}

override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// 3
loadImagesForOnscreenCells()
resumeAllOperations()
}

快速走查上面的代码显示在下面:

  1. 当用户开始滚动,你想暂停所有的操作让后看看用户想看到什么。你将要实现suspendAllOperations在一会儿工夫。
  2. 假如decelerate的值是false,那就意味着停止拖拽表视图,因此你想恢复暂停的,因为cell离开屏幕取消的操作,启动在屏幕内cells的操作。你将要一起实现loadImagesForOnscreenCellsresumeAllOperations
  3. 代理方法告诉你表视图已经停止滚动,所以你将做和#2条相同的处理。

现在,添加下面这些遗失的方法的实现到ListViewController.swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func suspendAllOperations () {
pendingOperations.downloadQueue.suspended = true
pendingOperations.filtrationQueue.suspended = true
}

func resumeAllOperations () {
pendingOperations.downloadQueue.suspended = false
pendingOperations.filtrationQueue.suspended = false
}

func loadImagesForOnscreenCells () {
//1
if let pathsArray = tableView.indexPathsForVisibleRows() {
//2
var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys.array)
allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys.array)

//3
var toBeCancelled = allPendingOperations
let visiblePaths = Set(pathsArray as! [NSIndexPath])
toBeCancelled.subtractInPlace(visiblePaths)

//4
var toBeStarted = visiblePaths
toBeStarted.subtractInPlace(allPendingOperations)

// 5
for indexPath in toBeCancelled {
if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
pendingDownload.cancel()
}
pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
pendingFiltration.cancel()
}
pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
}

// 6
for indexPath in toBeStarted {
let indexPath = indexPath as NSIndexPath
let recordToProcess = self.photos[indexPath.row]
startOperationsForPhotoRecord(recordToProcess, indexPath: indexPath)
}
}
}

suspendAllOperationsresumeAllOperations有一个简单的实现。NSOperationQueues能够被暂停,通过设置suspended接口为true。这将暂停队列里面的所有操作–你不能单独暂停一个操作。

loadImagesForOnscreenCells有一点小复杂。这里发生了什么事?

  1. 以一个包含了现在表视图可见的index paths的数组开始开始
  2. 构造一个所有进行中的操作的集合通过结合所有在下载的进度+所有在处理滤镜的进度。
  3. 构造一个index paths集合用来取消操作。开始所有的操作,然后移除可见行的index paths,这样讲留下一个离开屏幕的行正在执行的操作集合
  4. 构造一个index paths集合,需要操作启动,用所有可见行的index paths启动,让后移除它们中在进行的操作。
  5. 遍历那些被取消的操作,取消它们,然后移除它们的引用从PendingOperations
  6. 遍历那些将要开始的操作,然后对他们每个调用startOperationsForPhotoRecord

构建运行然后你发现一个更流畅,资源管理德更好的应用!给你自己一轮掌声!

improved app

原来的相册,载入东西一次一个

注意到当你完成滚动表视图,在可见区域行的cell的图片立即开始处理。

何去何从?

这里是completed version of the project

注意:此教程写于Update 17 April 2015: Updated for Xcode 6.3 and Swift 1.2,现在Swift最新版本2.1使用Xcode7+以上编辑会报错,这里打包一个新语法修改版completed fixed version of the project

假如你完成这个工程应该花时间来真正理解它,恭喜你!你可以认为你自己是一位更有价值iOS开发者了比起在教程刚开始的时候!大多数开发的公司是非常幸运的有一个或者两个人正在知道这个东西。

当时请当心 – 像多层嵌套的blocks,不必要的使用多线程可能让一个工程变得难以理解对维护你代码的人来说。线程可能引入一些难以捉摸的bugs,将永远不出现知道你网络非常慢,或者代码运行在一个更快(或更慢)的设备上,或一个不同数量的核的芯片上时。小心测试,尽量使用Instruments(或者你自己的观察)来确定引入多线程真的有很大改进。

一个有用的特征使用操作时在这里没涉及到就是依赖(dependency)。你可以给一个操作添加一个或者更多的操作的依赖。这个操作不会开始直到它所有依赖的操作完成时。例如:

1
2
3
4
5
6
// MyDownloadOperation is a subclass of NSOperation
let downloadOperation = MyDownloadOperation()
// MyFilterOperation is a subclass of NSOperation
let filterOperation = MyFilterOperation()

filterOperation.addDependency(downloadOperation)

移除依赖:

1
filterOperation.removeDependency(downloadOperation)

这个工程是否能使用依赖简化呢?把你学到的新技能用起来试一试 :]
有件非常重要的事需要注意的就是一个依赖操作将仍然启动假如它依赖的操作被取消,还有它将自然完成。你需要牢记在心。

假如你有任何评论或者问题关于这个教程或者NSOperations,请加Pull request

译者注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

本文目录

  • 前言
  • NSInvocationOperation
  • NSBlokcOperation
  • NSOperation的其他用法
  • 自定义NSOperation
  • 参考资料

前言

1.虽然NSThread也可以实现多线程编程,但是需要我们去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销。我们也可以配合使用NSOperationNSOperationQueue实现多线程编程,实现步骤大致是这样的

  • 先将需要执行的操作封装到一个NSOperation对象中
  • 然后将NSOperation对象添加到NSOperationQueue中
  • 系统会自动将NSOperation中封装的操作放到一条新线程中执行在此过程中,我们根本不用考虑线程的生命周期、同步、加锁等问题下面列举一个应用场景,比如微博的粉丝列表:

微博的粉丝列表

每一行的头像肯定要从新浪服务器下载图片后才能显示的,而且是需要异步下载。这时候你就可以把每一行的图片下载操作封装到一个NSOperation对象中,上面有6行,所以要创建6个NSOperation对象,然后添加到NSOperationQueue中,分别下载不同的图片,下载完毕后,回到对应的行将图片显示出来。

2 .默认情况下,NSOperation并不具备封装操作的能力,必须使用它的子类,使用NSOperation子类的方式有3种:

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义子类继承NSOperation,实现内部相应的方法

这讲先介绍如何用NSOperation封装一个操作,后面再结合NSOperationQueue来使用。

NSInvocationOperation

1
2
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"mj"];
[operation start];
  • 第1行初始化了一个NSInvocationOperation对象,它是基于一个对象和selector来创建操作
  • 第2行调用了start方法,紧接着会马上执行封装好的操作,也就是会调用self的run:方法,并且将@”mj”作为方法参数
  • 这里要注意:默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作。只有将operation放到一个NSOperationQueue中,才会异步执行操作。

NSBlockOperation

a.同步执行一个操作

1
2
3
4
5
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){
NSLog(@"执行了一个新的操作");
}];
// 开始执行任务
[operation start];
  • 第1行初始化了一个NSBlockOperation对象,它是用一个Block来封装需要执行的操作
  • 第2行调用了start方法,紧接着会马上执行Block中的内容
  • 这里还是在当前线程同步执行操作,并没有异步执行

b.并发执行多个操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){
  NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^() {
  NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^() {
  NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^() {
  NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);
}];

// 开始执行任务
[operation start];
  • 第1行初始化了一个NSBlockOperation对象

  • 分别在第5、9、13行通过addExecutionBlock:方法添加了新的操作,包括第1行的操作,一共封装了4个操作

  • 在第18行调用start方法后,就会并发地执行这4个操作,也就是会在不同线程中执行

      1 2013-02-02 21:38:46.102 thread[4602:c07] 又执行了1个新的操作,线程:<NSThread: 0x7121d50>{name = (null), num = 1}
      2 2013-02-02 21:38:46.102 thread[4602:3f03] 又执行了1个新的操作,线程:<NSThread: 0x742e1d0>{name = (null), num = 5}
      3 2013-02-02 21:38:46.102 thread[4602:1b03] 执行第1次操作,线程:<NSThread: 0x742de50>{name = (null), num = 3}
      4 2013-02-02 21:38:46.102 thread[4602:1303] 又执行了1个新的操作,线程:<NSThread: 0x7157bf0>{name = (null), num = 4}
    

可以看出,每个操作所在线程的num值都不一样,说明是不同线程

NSOperation的其他用法

a.取消操作

operation开始执行之后, 默认会一直执行操作直到完成,我们也可以调用cancel方法中途取消操作

[operation cancel];

b.在操作完成后做一些事情

如果想在一个NSOperation执行完毕后做一些事情,就调用NSOperation的setCompletionBlock方法来设置想做的事情

operation.completionBlock = ^() {
    NSLog(@"执行完毕");
};

当operation封装的操作执行完毕后,就会回调Block里面的内容

自定义NSOperation

如果NSInvocationOperationNSBlockOperation不能满足需求,我们可以直接新建子类继承NSOperation,并添加任何需要执行的操作。如果只是简单地自定义NSOperation,只需要重载-(void)main这个方法,在这个方法里面添加需要执行的操作。

下面写个子类DownloadOperation来下载图片

a.继承NSOperation,重写main方法

DownloadOperation.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>
@protocol DownloadOperationDelegate;

@interface DownloadOperation : NSOperation
// 图片的url路径
@property (nonatomic, copy) NSString *imageUrl;
// 代理
@property (nonatomic, assign) id<DownloadOperationDelegate> delegate;

- (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate;
@end

// 图片下载的协议
@protocol DownloadOperationDelegate <NSObject>
- (void)downloadFinishWithImage:(UIImage *)image;
@end

DownloadOperation.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import "DownloadOperation.h"

@implementation DownloadOperation
@synthesize delegate = _delegate;
@synthesize imageUrl = _imageUrl;

// 初始化
- (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate {
if (self = [super init]) {
self.imageUrl = url;
self.delegate = delegate;
}
return self;
}
// 释放内存
- (void)dealloc {
[super dealloc];
[_imageUrl release];
}

// 执行主任务
- (void)main {
// 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
@autoreleasepool {
// ....
}
}
@end
  • 在第22行重载了main方法,等会就把下载图片的代码写到这个方法中
  • 如果这个DownloadOperation是在异步线程中执行操作,也就是说main方法在异步线程调用,那么将无法访问主线程的自动释放池,所以在第24行创建了一个属于当前线程的自动释放池

b.正确响应取消事件

  • 默认情况下,一个NSOperation开始执行之后,会一直执行任务到结束,就比如上面的DownloadOperation,默认会执行完main方法中的所有代码
  • NSOperation提供了一个cancel方法,可以取消当前的操作。
  • 如果是自定义NSOperation的话,需要手动处理这个取消事件。比如,一旦调用了cancel方法,应该马上终止main方法的执行,并及时回收一些资源。
  • 处理取消事件的具体做法是:在main方法中定期地调用isCancelled方法检测操作是否已经被取消,也就是说是否调用了cancel方法,如果返回YES,表示已取消,则立即让main方法返回。
  • 以下地方可能需要调用isCancelled方法:
    1. 在执行任何实际的工作之前,也就是在main方法的开头。因为取消可能发生在任何时候,甚至在operation执行之前。
    2. 执行了一段耗时的操作之后也需要检测操作是否已经被取消
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)main {
// 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
@autoreleasepool {
if (self.isCancelled) return;

// 获取图片数据
NSURL *url = [NSURL URLWithString:self.imageUrl];
NSData *imageData = [NSData dataWithContentsOfURL:url];

if (self.isCancelled) {
url = nil;
imageData = nil;
return;
}

// 初始化图片
UIImage *image = [UIImage imageWithData:imageData];

if (self.isCancelled) {
image = nil;
return;
}

if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {
// 把图片数据传回到主线程
[(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];
}
}
}
  • 在第4行main方法的开头就先判断operation有没有被取消。如果被取消了,那就没有必要往下执行了
  • 经过第8行下载图片后,在第10行也需要判断操作有没有被取消
  • 总之,执行了一段比较耗时的操作之后,都需要判断操作有没有被取消
  • 图片下载完毕后,在第26行将图片数据传递给了代理(delegate)对象

参考资料

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

iOS触摸事件分类

  • 触摸事件
  • 加速事件
  • 远程事件

谁能处理触摸事件?

响应者对象

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件.我们称之为响应者对象.

UIApplication,UIViewController,UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件.

UIResponder

UIResponder内部提供了方法来处理事件

  1. 触摸事件

    一次完成的触摸过程,会经历3个状态;
    UIView的触摸事件处理

    1.一根或多根手指开始触摸view,系统会自动调用view下面的方法:
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; //触摸开始
    2.一根或者多根手指在view上移动,系统会自动调用view下面的方法(随着手指的移动,会持续调用该方法)
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; //触摸移动
    3.一根或者多根手指离开view,系统会自动调用view下面的方法
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; //触摸结束
    4.触摸结束前,某个系统事件(例如电话呼入 )会打断触摸过程,系统会自动调用view下面的方法
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; //触摸取消(可能会经历)
    4个触摸事件的处理方法中,都有 NSSet *touches 和 UIEvent *event 两个参数:

    • 一次完整的触摸过程,只会产生一个事件对象,4个触摸方法都是同一个event参数
    • 如果两根手指同时触摸一个view,那么view只会调用一次 touchesBegan:withEvent: 方法,touches参数中装着两个UITouch对象;
    • 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用两次 touchesBegan:withEvent:方法, 并且每次调用时的touches参数只包含一个UITouch对象;
    • 根据touches中UITouch个数可以判断出使单点触摸还是多点触摸

    提示:touches中存放的都是UITouch对象。

  2. 加速计事件

    • (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    • (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    • (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
  3. 远程控制事件

     - (void)remoteControlReceivedWithEvent:(UIEvent *)event;
    

UITouch

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象;一根手指对应一个UITouch对象
UITouch的作用:

  • 保存跟手指相关的信息,比如触摸的位置、时间、阶段;
  • 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指的触摸位置;
  • 当手指离开屏幕时,系统会销毁相应的UITouch对象。

提示:iPhone开发中,要避免使用双击事件。

UITouch的属性:
触摸产生时所处的窗口:

1
@property(nonatomic,readonly,retain) UIWindow *window;

触摸产生时所处的视图

1
@property(nonatomic,readonly,retain) UIView *view;

短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多地点击

1
@property(nonatomic,readonly) NSUInteger tapCount;

记录了触摸事件产生或变化时的时间,单位是秒

1
@property(nonatomic,readonly) NSTimeInterval timestamp;

当前触摸事件所处的状态

1
2
3
4
5
6
7
8
@property(nonatomic,readonly) UITouchPhase phase;
/*
UITouchPhase是一个枚举类型,包含:
UITouchPhaseBegan(触摸开始)
UITouchPhaseMoved(接触点移动)
UITouchPhaseStationary(接触点无移动)
UITouchPhaseEnded(触摸结束)
UITouchPhaseCancelled(触摸取消)*/

UITouch的方法:

1
- (CGPoint)locationInView:(UIView *)view;

1.返回值表示触摸在view上的位置;

2.这里返回的位置是针对view坐标系的,(以view的左上角为原点(0,0));

3.调用时传入的view参数为nil 的话,返回的是触摸点在UIWindow的位置。

1
- (CGPoint)previousLocationInView:(UIView *)view;

该方法记录了前一个触摸点的位置;

UIEvent

每产生一个事件,就会产生一个UIEvent对象;

UIEvent:称为事件对象,记录事件产生的时刻和类型。

常见属性:
1.事件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@property(nonatomic,readonly) UIEventType  type;
@property(nonatomic,readonly) UIEventSubtype subtype;

typedef
NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
};
typedef
NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,
// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

2.事件产生的时间

1
@property(nonatomic,readonly) NSTimeInterval  timestamp;

UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)。

触摸事件的产生:

  1. 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中;

  2. UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow);

  3. 主窗口会在视图层次结构中找到一个最合适的视图控件来处理触摸事件,这也是整个事件处理过程的第一步;

  4. 找到合适的视图控件后,就会调用视图控件的touches方法来做具体的事件处理。

触摸事件的传递:

触摸事件的传递是从父控件传递到子控件;

如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件。

触摸事件
触摸事件2

UIView不接收触摸事件的三种情况:
不接受用户交互 :

  1. userInteractionEnable = NO;

  2. 隐藏 :hidden = YES;

  3. 透明:alpha = 0.0 ~ 0.01

提示:UIImageView的userInteractionEnable默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的。

触摸事件处理的详细过程:

  1. 用户点击屏幕后产生的一个触摸事件,经过一些列的传递过程后,会找到最合适的视图控件来处理这个事件

  2. 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理

    touchesBegan…

    touchesMoved…

    touchedEnded…

这些touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理

响应者链的事件传递过程:

  1. 如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图;

  2. 在视图层次结构最顶级的视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理。

  3. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象;

  4. 如果UIApplication也不能处理该事件或消息,则将其丢弃

监听触摸事件的做法

如果想监听一个view上面的触摸事件,之前的做法是:

  1. 自定义一个view;

  2. 实现view的touches方法,在方法内部实现具体处理代码。

    通过touches方法监听view触摸事件,有很明显的几个缺点:

    • 必须得自定义view;

    • 由于是在view内部的touches方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听view的触摸事件;

    • 不容易区分用户的具体手势行为。

iOS 3.2之后,苹果推出了手势识别功能(Gesture Recognizer),在触摸事件处理方面,大大简化了开发者的开发难度。

UIGestureRescognizer

为了完成手势识别,必须借助于手势识别器:UIGestureRecognizer 。

利用UIGestureRecognizer,能轻松识别用户在某个view上面做的一些常见手势。

UIGestureRecognizer是一个抽象类,定义了所有的手势基本行为,使用它的子类才能处理具体的手势

  • UITapGestureRecognizer(敲击)
  • UIPinchGestureRecognizer(捏合,用于缩放)
  • UIPanGestureRecognizer(拖拽)
  • UISwipeGestureRecognizer(轻扫)
  • UIRotationGestureRecognizer(旋转)
  • UILongPressGestureRecognizer(长按)

每一个手势识别器的用法都差不多,比如UITapGestureRecognizer的使用步骤如下:

  1. 创建手势识别器对象;

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];

  2. 设置手势识别器对象的具体属性;

     // 连续敲击2次
     tap.numberOfTapsRequired = 2;
     // 需要2根手指一起敲击
     tap.numberOfTouchesRequired = 2;
    
  3. 添加手势识别器到对应的view上

    [self.iconView addGestureRecognizer:tap];

  4. 监听手势的触发

    [tap addTarget:self action:@selector(tapIconView:)];

手势识别的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
// 没有触摸事件发生,所有手势识别的默认状态
UIGestureRecognizerStatePossible,
// 一个手势已经开始但尚未改变或者完成时
UIGestureRecognizerStateBegan,
// 手势状态改变
UIGestureRecognizerStateChanged,
// 手势完成
UIGestureRecognizerStateEnded,
// 手势取消,恢复至Possible状态
UIGestureRecognizerStateCancelled,
// 手势失败,恢复至Possible状态
UIGestureRecognizerStateFailed,
// 识别到手势识别
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

参考资料:傲风凌寒的博客

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com