日期: 2021 年 6 月 9 日

iOS–UIWebView清除缓存和Cookie

1.清除缓存和cookie

– (void)cleanCacheAndCookie{
//清除cookies
NSHTTPCookie *cookie;
NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (cookie in [storage cookies]){
[storage deleteCookie:cookie];
}

2.清除UIWebView的缓存

[[NSURLCache sharedURLCache] removeAllCachedResponses];
NSURLCache * cache = [NSURLCache sharedURLCache];
[cache removeAllCachedResponses];
[cache setDiskCapacity:0];
[cache setMemoryCapacity:0];
}

iOS–APP 迭代开发版本号的规则

在iOS中简单的版本号是怎样管理的呢? 首先我们的App*版本首次上线, 比如以1.0.0为首次上线的版本号:

1.上线后突然发现一个严重的Bug那我们就要修复更新版本, 此时我们的版本号为1.0.1
所以说如果修复Bug或者优化功能, 我们只修改叠加第三位数字, 其他不变

2.如果有了新的需求, 在原来的基础上增加了一个新功能, 那么我们的版本号变为1.1.0, 需要清空第三位数字为0, 来叠加修改第二位数字

3.如果App需求功能大改, 更新量非常大, 那我们的版本号变为2.0.0, 需要叠加修改*位数字, 清空其他数字为0

iOS–本地存储加密

1.本地存储加密:重要的数据名称。

2.代码安全问题,现在已有工具可以反编译出C语言代码(逆向工程)

3.解决办法:代码混淆

iOS本地数据存储安全

iOS本地数据存储安全
移动APP通常会在设备本地存储一些数据,这可以为程序执行、更良好地性能或离线访问提供支持。由于移动设备使用地越来越广泛,设备失窃的风险也越来越大,因此不安全的本地数据存储已成为移动APP安全的主要威胁之一。

攻击者可以通过一些方式获取到存储在iOS设备上的敏感信息,主要有以下方法:

恶意程序
借助iOS系统的安全弱点,攻击者可以设计出一种远程偷取iPhone上文件的恶意程序。

备份
当iPhone连接至iTunes后,如果iPhone信任了所连接的电脑后,iTunes将自动对设备上的所有数据进行备份。通过备份,敏感数据也将会保存到电脑上。因此,攻击者如果可以接触到那台电脑,则可以通过备份文件读取到敏感信息。

物理接触
用户iPhone的丢失或被盗非常常见。在这两种情形下,攻击者都将可以物理接触到设备,并读取设备上存储的敏感信息。

在iOS上,移动APP可以将信息输出或存储到以下一个或多个文件中:
* Plist文件
* SQLite数据库文件
* Keychain文件
* 缓存文件
* 日志文件

Plist文件
功能模块介绍
属性列表(Plist,Property List)是一种结构化的二进制格式文件,包含了内嵌键值对的可执行bundle的基本配置信息。Plist文件主要用于存储App的用户设置及配置信息,例如,游戏类App经常会在Plist文件中存储游戏等级和分数信息。一般来说,App会将存储用户数据的Plist文件保存在“[App home目录]/documents/”目录下。Plist文件可以是XML格式或二进制格式。

风险描述
Plist文件主要用于存储用户设置及App的配置信息,但App可能使用Plist文件存储明文的用户名、密码或其它一些个人敏感信息。而保存在Plist文件中的二进制格式文件数据则可以使用Plist文件编辑器(如plutil)进行查看或修改,即使在一个没有越狱的设备上,plist文件也可以通过工具iExplorer获取。对于以编码、未加密或弱加密形式存储的敏感信息就可能会导致敏感信息泄露了。

WordPress的iOS版App曾经就在Plist文件中存储了明文的用户名和密码,如下图所示,后来WordPress紧急修复了它。

%title插图%num

安全方案
1) 尽量不要在iOS设备的Plist文件中保存敏感信息(如证件号、银行卡号、详细住址及其各对应的编码格式等);
2) 对于有些APP功能需求,如果一定需要在iOS设备本地保存敏感信息,则可采用iOS提供的加密接口(如CommonCrypto)进行安全加密后保存。

SQLite存储
功能模块介绍
SQLite是一种自包含、可嵌入、0配置的SQL数据库引擎的跨平台C库文件。它的表、触发器和视图整个数据库都包含在一个硬盘文件中。SQLite数据库提供了所有标准的SQL结构,包括select、insert、update和delete。创建SQLite数据库文件时,可以添加任意文件后缀,也可以不添加后缀,常见的后缀一般有“.sqlitedb”和“.db”。APP一般会将其保存在“[App home目录]/documents/”目录下。由于SQLite的轻便、稳定和小巧,它已成为一个在iOS设备上存储数据的优秀解决方案。

风险描述
信息泄露
iOS自带的SQLite数据库没有内置的加密支持,因此,许多iOS APP会直接以明文格式将许多敏感数据存储在SQLite数据库中,除非APP自身对数据进行加密后再存储。例如,为提供离线的邮件访问功能,Gmail的iOS APP以明文方式将所有邮件存储在SQLite数据库中。一旦可以物理访问到设备或其备份文件,存储在SQLite中未加密的敏感信息容易被泄露。
如下图,某银行APP就将用户的登录手机号、登录密码、手势密码全部都以明文方式存在了客户端本地的SQLite数据库中。

%title插图%num

数据恢复
除上述提到的信息泄露途径外,SQLite的数据恢复同样也会导致信息泄露。在iOS中,恢复被删除的SQLite数据库记录比恢复被删除的文件更为容易。因为如果删除一条记录,SQLite仅会将该记录标记为已删除,但不会清除它们。只要SQLite数据库文件本身没被删除,数据库中被删除的记录则会一直保留在SQLite文件的未分配空间内,直到新的记录覆盖它们。

攻击者可以使用strings命令打印SQLite数据库文件中数据,这其中就包括了被删除的数据。以下为整个恢复过程的演示。
(1)创建一个名为messages.sqlite的SQLite数据库,并插入测试数据。

%title插图%num

(2)重新连接数据库,删除几条记录,然后再查看数据库记录,已无法查看到被删除的记录。

%title插图%num
(3)使用strings命令查看该数据库文件,可以看到被删除的记录被打印出来。

%title插图%num

安全方案
方案1,*简单的方法就是尽量不在客户端的SQLite数据库中保存敏感信息;

方案2,如果确实需要将某些敏感信息保存在SQLite数据库中时,可以结合使用以下几种方案:

(1) 数据加密:使用如AES256加密算法对数据进行安全加密后再存入SQLite中;
(2) 整库加密:可使用第三方的SQLite扩展库,对数据库进行整体的加密。
(3) 数据覆盖:在删除SQLite数据库某条记录之前,可以使用垃圾数据update一下该条目。这样即使有人尝试从SQLite文件中恢复已删除的数据库时,他们也无法获取到实际的数据。

对于方案2中提到的三种子方案,也各有优缺点:

%title插图%num

键盘缓存
功能模块介绍
为提供自动填充和纠正的功能,iOS系统的自带键盘会缓存用户的输入信息。其会保存一个接近600个单词的列表,存放在“Library/Keyboard/ en_GB-dynamic-text.dat”或“/private/var/mobile/Library/Keyboard/dynamic-text.dat”文件中(iOS版本不同,位置及文件名会略有不同)。

风险描述
这个功能会带来一个安全问题:它会明文存储用户在输入框中输入过的所有信息,如用户名、密码短语、安全问题回答等。由于键盘会缓存这些输入框信息,使得在开始输入一个如安全问题答案的时候,缓存会帮助攻击者自动完成该问题的答案输入。
要想查看该键盘缓存,可以将上述如“en_GB-dynamic-text.dat”文件复制到电脑上,并使用十六进制编辑器打开。下图就是一个键盘缓存的使用16进制浏览的截图,可以看到明文的输入信息(this is a test)。

%title插图%num
图 3. 键盘缓存记录的输入

安全方案
1, 首先,对于以下位置或方式的输入,iOS不会对其输入内容进行缓存:
* 在标记为secure的字段、passwords字段内输入的内容不会缓存;
* 输入只包括数字的字符串不会被缓存,这也即意味着银行卡号、信用卡号是安全的,(iOS 5之前的版本会缓存);
* 非常短的输入,如只有1或者2个字母组成的单词不会被缓存;
* 禁用了自动纠正功能的文本框会阻止输入内容被缓存;

2, 可以在不需要缓存的文本框处禁用自动纠正功能。如下代码所示:

UITextField *textField = [[UITextField alloc] initWithFrame: frame ];
textField.autocorrectionType = UITextAutocorrectionTypeNo;
1
2
3, 输入框也可以被标记为密码输入类型,使得输入变得更加安全,防止缓存。如:

textField.secureTextEntry = YES;
1
4,在所有敏感信息输入处均使用自定义键盘,当然自定义键盘也不能缓存用户输入。(这可能会影响用户体验)

另外除了文本输入的地方,在iOS系统上,当数据被复制到粘贴板上的时候,也会被进行明文缓存,而且粘贴板内容所有APP均可访问。为禁用文本框的复制/粘贴功能,使得用户无法在某些地方进行复制和粘贴,可在该文本输入的地方添加以下方法:

-(BOOL)canPerformAction:(SEL)action withSender:(id)sender {
UIMenuController *menuController = [UIMenuController sharedMenuController];
if (menuController) {
menuController.menuVisible = NO;
}
return NO;
}

应用快照缓存
功能模块介绍
当一个应用在后台被挂起时,iOS会生成一个当前屏幕的快照,当应用被重新唤起时,可以快速还原该APP之前的内容,以提高用户的使用体验。

风险描述
然而,这样做也可能会导致一些应用数据的泄露。应用快照保存在“/var/mobile/Containers/Data/Application/XXXXXXX-XXXXXXXXX-XXXXXXXXX/Library/Caches/Snapshots/”目录下。恶意APP可通过读取该文件并发送至远程服务端,从而获得其快照内容信息。
例如,以下快照截图就是在花旗银行APP上查看邮件时直接按下home按键后截图的内容。

%title插图%num

安全方案
要防止这种信息泄露途径,屏幕内容就必须在iOS系统进行屏幕快照之前进行隐藏或模糊化处理,而iOS系统也提供了许多回调方法来提示程序将被挂起。例如以下两个方法:
* (void)applicationWillResignActive:(UIApplication *)application
应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件
* (void)applicationDidEnterBackground:(UIApplication *)application
程序将被推送到后台。
下图为APP压入后台的过程,右边则是可供APP回调的方法,可利用其实现自己的一些需求:

%title插图%num
图 5. 应用进入后台的过程

方案一:
一个简单方法就是设置关键窗口的hidden属性为YES,这样当前在屏幕上显示的内容将被隐藏,返回一个空白的快照来替代任何内容。

[UIApplication sharedApplication].keyWindow.hidden=YES;
1
注意:如果在当前窗口后面有其他的窗口,当关键窗口被隐藏时,那些窗口将会被显示出来。所以当使用这种方法时要确保也隐藏了其他窗口。。
当应用程序即将进入非活动状态时(如接到一个电话或切换到其他应用程序时),applicationWillResignActive方法会被回调。因此可以用以下代码来隐藏窗口。

-(void)applicationWillResignActive:(UIApplication *)application
{
[UIApplication sharedApplication].keyWindow.hidden=YES;
}

另一个重要的地方是把这些代码加入到aplicationDidEnterBackground方法中,此方法会在APP被压入后台但在屏幕快照被调用前被调用。

-(void)applicationDidEnterBackground:(UIApplication *)application
{
[UIApplication sharedApplication].keyWindow.hidden=YES;
}

方案二:
除隐藏当前界面内容外,还可以对当前界面所展示的内容进行模糊化处理。在上述的回调方法中,通过使用iOS的毛玻璃(blur glass)技术,可以达到程序后台运行界面的模糊化效果。
由于此部分代码较多,故不在此列出,感兴趣的同学可以参考下这两篇文章:http://blog.csdn.net/jp940110jpjp/article/details/44307515、http://www.code4app.com/ios/后台模糊效果/54912828933bf0d5388b4f25

应用日志
功能模块介绍
基于iOS APP程序开发排错的需要,开发人员一般都会写一些数据到日志中,而这些数据就可能包括证件号、登录用户名和密码、认证token或其它的一些敏感信息。

风险描述
应用程序的错误日志是不被应用程序的沙盒隔离保护的,一个APP产生的错误日志可以被另一个APP读取。此外,在越狱iOS设备上,APP还可获取到其他应用输出所有的日志信息。因此,如果一个APP使用日志功能输出了某些敏感信息,那么恶意APP就能够读取到这些信息,并可将其发送到一个远程服务器上。另外,可以直接从AppStore上下载安装“console”应用,查看iOS系统及APP输出的错误日志信息。可以在“/var/log/”目录下找到iOS的日志文件。
如下图所示,某个APP在日志中输出了用户名及登录密码:

%title插图%num
图 6. 应用日志敏感信息泄露

安全方案
不要在APP的日志中记录或打印敏感信息,并且在正式发布的时候,确保关闭了日志打印开关。

keychain存储
功能模块介绍
Keychain是一个拥有有限访问权限的SQLite数据库(AES256加密),可以为多种应用程序或网络服务存储少量的敏感数据(如用户名、密码、加密密钥等)。如保存身份和密码,以提供透明的认证,使得不必每次都提示用户登录。在iPhone上,Keychain存放在“/private/var/Keychains/keychain-2.db”SQLite数据库。
Keychain数据库包含了一些Keychain条目,每个条目都由加密的数据和一系列未加密的描述属性组成,Keychain的条目类型(kSecClass)决定了其关联的一些描述属性。在iOS系统中,Keychain条目被分为5种类型:

%title插图%num
其中,数字身份=证书+密钥。

在iOS的Keychain中,所有的Keychain条目都被存储在Keychain SQLite数据库的4张表中:genp、inet、cert和keys。genp数据表存储了普通密码的Keychain条目,inet数据表存储了网络密码的Keychain条目,cert和keys数据表分别存储了证书和密钥的Keychain条目。

%title插图%num
图 7. Keychain数据表
Keychain的数据库内容使用了设备唯一的硬件密钥进行加密,该硬件密钥无法从设备上导出。因此,存储在Keychain中的数据只能在该台设备上读取,而无法复制到另一台设备上解密后读取。

%title插图%num
图 8. Keychain文件格式

iOS APP的Keychain数据是存储在应用沙箱外面的,各APP的keychain数据内容为逻辑隔离,由系统进程securityd实施访问控制。一个应用默认无法读取到另一个应用在Keychain中存储的数据。在iOS系统中,每个APP都附带一个唯一的应用标识符,而Keychain服务则使用这个应用标识符限制其对其它Keychain数据的访问。默认情况下,APP只能访问与他们的应用标识符相关联的数据。为了在多个APP间能够共享Keychain信息,Apple引入了Keychain访问组概念:拥有相同Keychain访问组标识符的应用,可以共享Keychain数据。APP的Keychain访问权限(即标识符)被加密嵌入在了APP的二进制文件中,但可以通过使用grep或sed命令将其从该文件中提取出来。

通过SSH连接到iPhone上,进入到应用的home目录中(ios8:/var/mobile/Containers/Bundle/Application/[unique-id]/),运行以下命令:
sed –n ‘//,/<\/dict>/p’ [AppDirectory]/[ApplicationBinary]
例如:以下命令列出了QQ应用的keychain权限:

%title插图%num
图 9. QQ应用的keychain权限
上述输出表明QQ应用在存储或访问Keychain数据时,使用“BJFH5U299Z.com.tencent.generickeychain”这一Keychain访问组权限。

当应用向Keychain中添加一个条目时,该应用的应用标识符或Keychain 访问组权限也将被自动获取并添加到该条目的agrp列(访问组)。之后,当某个应用尝试访问该条Keychain条目时,Keychain服务通过该Keychain条目中对应的agrp值验证应用标识符或Keychain访问组,以判断是否允许访问。下图显示了keychain-2.db样例文件,圈出了QQ的Keychain访问组权限。

%title插图%num
图 10. QQ的keychain条目

随着iOS引入了数据保护机制,存储在Keychain中的数据被另一层与用户密码(passcode)相关联的加密机制保护着。数据保护加密密钥(保护类密钥-protection class keys)是由一个设备硬件密钥和一个由用户密码衍生的密钥共同产生的。可以通过向Keychain接口的SecItemAdd或SecItemUpdate方法的kSecAttrAccessible属性提供一个可访问常量来启用Keychain的数据保护。该可访问常量值决定一个Keychain条目在何时可被应用访问,同时也决定某个Keychain条目是否允许移动到另一台设备上。
以下是Keychain条目的数据保护可访问常量列表:
(1)kSecAttrAccessibleWhenUnlocked:Keychain条目只能在设备被解锁后被访问,此时用于解密Keychain条目的数据保护类密钥只在设备被解锁后才会被加载到内存中,并且当设备锁定后,加密密钥将在10s钟内自动清除。
(2)kSecAttrAccessibleAfterFirstUnlock:Keychain条目可以在设备*次解锁到重启过程中被访问,此时用于解密Keychain条目的数据保护类密钥只有在用户重启并解锁设备后才会被加载到内存中,并且该密钥将一直保留在内存中,直到下一次设备重启。
(3)kSecAttrAccessibleAlways:Keychain条目即使在设备锁定时也可被访问,此时用于解密Keychain条目的数据保护类密钥一直被加载在内存中。
(4)kSecAttrAccessibleWhenUnlockedThisDeviceOnly:Keychain条目只能在设备被解锁后被访问,并且无法在设备间移动。
(5)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:Keychain条目可在设备*次解锁后访问,但无法在设备间移动
(6)kSecAttrAccessibleAlwaysThisDeviceOnly:Keychain条目即使在设备锁定时也可被访问,但无法再设备将移动
Keychain条目的数据保护可访问常量被映射到各Keychain表(genp、inet..)中的pdmn列(protection domain)。下表显示了keychain数据保护可访问常量和pdmn值之间的映射关系。

%title插图%num

下图显示了keychain-2.db样例文件,红色方框为pdmn列。

%title插图%num
图 11. pdmn样例

风险描述
为避免每次提示用户登录,APP很可能会在Keychain中直接存储明文的认证信息(如WiFi、邮箱密码)。而Keychain_dumper工具可用于导出越狱iOS设备上的所有Keychain条目,它使用“*”通配符格式的Keychain访问组权限,因而可以访问到设备上所有的Keychain条目。
APP在添加Keychain条目时可能也会设置数据保护的可访问常量,如前文所述,该可访问常量决定了Keychain条目何时才能被访问。而数据保护机制又是与用户密码相关联的,只有在当用户设置iOS密码时,它才会保护数据。如下为导出Keychain数据的演示过程:
(1)当设置了iOS密码且未解锁时,无法访问到Keychain中的数据

%title插图%num
(2)解锁后,(或未设置iOS密码)即可访问到Keychain中的数据。如下图,*个是连接过的WiFi密码,第二个是在系统自带邮件应用中登录过的邮箱密码。

%title插图%num

安全方案
一旦攻击者能够物理接触到没有设置密码的iOS设备时,他就可以通过越狱该设备,运行如keychain_dumper这样的工具,读取到设备所有的Keychain条目,获取里面存储的明文信息。而即使在应用向Keychain中存储数据时使用了数据保护的接口(即上文提到的keychain数据保护可访问常量),未设置用户密码的Keychain数据仍没有被有效地保护着。为保证Keychain中存储的数据的安全,可采用以下建议:
1, 尽量不在Keychain中直接存储明文的敏感信息。
2, 向Keychain中存储数据时,不要使用kSecAttrAccessibleAlways,而是使用更安全的kSecAttrAccessibleWhenUnlocked或kSecAttrAccessibleWhenUnlockedThisDeviceOnly选项。
3, 如果必须要存储,则可先检测用户是否设置了设备密码,并进行相应的风险提示。
4, 对用户来说,想要保护Keychain中的数据,就是设置更强的密码,iOS默认允许4位数字口令(从0-9999),结合iOS的某些漏洞或设置,很容易被人快速暴力破解,而设置字母加数字的密码则会让破解过程花费更多的时间。

iOS数据持久化设计探讨(NSCache,PINCache,YYCache,CoreData,FMDB,WCDB,Realm)

目录
一、目标
二、数据持久化的目的
三、数据持久化方式分类
1、内存缓存
2、磁盘缓存
四、缓存策略(常见缓存算法)
1、FIFO(First in First out)
2、LFU(Least Frequently Used)
3、 LRU (LeastRecentlyUsed)
4、 LRU-K (LeastRecentlyUsed)
5、2Q(Two queues)
6、MQ(Multi Queue)
五、iOS端可供选择的数据持久化方案
1. 内存缓存
2. 磁盘缓存
3. 应该用哪种缓存方案
六、内存、磁盘数据持久化方案对比
1、可选方案详解
1.1、NSCache
1.1.1、特性
1.1.2、实现
1.2、TMCache
1.3、PINCache
1.3.1、特性:
1.3.2、实现
1.4、YYCache
1.4.1、特性:
1.4.2、实现
2、内存缓存方案对比
2.1、性能
2.1、对比
3、磁盘缓存方案对比
3.1、性能
3.2、对比
七、数据库缓存
1.1、背景
1.2、对比
1.2.1、[FMDB](https://github.com/ccgus/fmdb)
1.2.2、[WCDB](https://github.com/Tencent/wcdb)
1.2.3、[Realm](https://realm.io/cn/docs/objc/latest/)
1.3 数据库存取性能测试
1.4、数据库方案对比
八、持久化在项目中的应用(小结)
1、 图片缓存
2、 简单key-value存取
3、 数据库
4、 其它
一、目标
了解移动端的数据持久化方式和对应的使用场景,提供相关技术选型做技术储备。

二、数据持久化的目的
快速展示,提升体验
已经加载过的数据,用户下次查看时,不需要再次从网络(磁盘)加载,直接展示给用户
节省用户流量(节省服务器资源)
对于较大的资源数据进行缓存,下次展示无需下载消耗流量
同时降低了服务器的访问次数,节约服务器资源。(图片)
离线使用。
用户浏览过的数据无需联网,可以再次查看。
部分功能使用解除对网络的依赖。(百度离线地图、图书阅读器)
无网络时,允许用户进行操作,等到下次联网时同步到服务端。
记录用户操作
草稿:对于用户需要花费较大成本进行的操作,对用户的每个步骤进行缓存,用户中断操作后,下次用户操作时直接继续上次的操作。
已读内容标记缓存,帮助用户识别哪些已读。
搜索记录缓存

三、数据持久化方式分类
在移动端的数据持久化方式总体可以分为以下两类:

1、内存缓存
定义

对于使用频率比较高的数据,从网络或者磁盘加载数据到内存以后,使用后并不马上销毁,下次使用时直接从内存加载。

案例

iOS系统图片加载——[UIImage imageNamed:@“imageName”]
网络图片加载三方库:SDWebImage
2、磁盘缓存
定义

将从网络加载的、用户操作产生的数据写入到磁盘,用户下次查看、继续操作时,直接从磁盘加载使用。

案例

用户输入内容草稿缓存(如:评论、文本编辑)
网络图片加载三方库:SDWebImage
搜索历史缓存
四、缓存策略(常见缓存算法)
在缓存设计中,由于硬件设备的存储空间不是无限的,我们期望存储空间不要占用过多,仅能缓存有限的数据,但是我们希望获得更高的命中率。想达到这一目的。通常需要借助缓存算法来实现。

1、FIFO(First in First out)
实现原理:

FIFO 先进先出的核心思想如果一个数据*先进入缓存中,则应该*早淘汰掉。类似实现一个按照时间先后顺序的队列来管理缓存,将淘汰*早访问的数据缓存。

示意图:

%title插图%num

目录
一、目标
二、数据持久化的目的
三、数据持久化方式分类
1、内存缓存
2、磁盘缓存
四、缓存策略(常见缓存算法)
1、FIFO(First in First out)
2、LFU(Least Frequently Used)
3、 LRU (LeastRecentlyUsed)
4、 LRU-K (LeastRecentlyUsed)
5、2Q(Two queues)
6、MQ(Multi Queue)
五、iOS端可供选择的数据持久化方案
1. 内存缓存
2. 磁盘缓存
3. 应该用哪种缓存方案
六、内存、磁盘数据持久化方案对比
1、可选方案详解
1.1、NSCache
1.1.1、特性
1.1.2、实现
1.2、TMCache
1.3、PINCache
1.3.1、特性:
1.3.2、实现
1.4、YYCache
1.4.1、特性:
1.4.2、实现
2、内存缓存方案对比
2.1、性能
2.1、对比
3、磁盘缓存方案对比
3.1、性能
3.2、对比
七、数据库缓存
1.1、背景
1.2、对比
1.2.1、[FMDB](https://github.com/ccgus/fmdb)
1.2.2、[WCDB](https://github.com/Tencent/wcdb)
1.2.3、[Realm](https://realm.io/cn/docs/objc/latest/)
1.3 数据库存取性能测试
1.4、数据库方案对比
八、持久化在项目中的应用(小结)
1、 图片缓存
2、 简单key-value存取
3、 数据库
4、 其它
一、目标
了解移动端的数据持久化方式和对应的使用场景,提供相关技术选型做技术储备。

二、数据持久化的目的
快速展示,提升体验
已经加载过的数据,用户下次查看时,不需要再次从网络(磁盘)加载,直接展示给用户
节省用户流量(节省服务器资源)
对于较大的资源数据进行缓存,下次展示无需下载消耗流量
同时降低了服务器的访问次数,节约服务器资源。(图片)
离线使用。
用户浏览过的数据无需联网,可以再次查看。
部分功能使用解除对网络的依赖。(百度离线地图、图书阅读器)
无网络时,允许用户进行操作,等到下次联网时同步到服务端。
记录用户操作
草稿:对于用户需要花费较大成本进行的操作,对用户的每个步骤进行缓存,用户中断操作后,下次用户操作时直接继续上次的操作。
已读内容标记缓存,帮助用户识别哪些已读。
搜索记录缓存

三、数据持久化方式分类
在移动端的数据持久化方式总体可以分为以下两类:

1、内存缓存
定义

对于使用频率比较高的数据,从网络或者磁盘加载数据到内存以后,使用后并不马上销毁,下次使用时直接从内存加载。

案例

iOS系统图片加载——[UIImage imageNamed:@“imageName”]
网络图片加载三方库:SDWebImage
2、磁盘缓存
定义

将从网络加载的、用户操作产生的数据写入到磁盘,用户下次查看、继续操作时,直接从磁盘加载使用。

案例

用户输入内容草稿缓存(如:评论、文本编辑)
网络图片加载三方库:SDWebImage
搜索历史缓存
四、缓存策略(常见缓存算法)
在缓存设计中,由于硬件设备的存储空间不是无限的,我们期望存储空间不要占用过多,仅能缓存有限的数据,但是我们希望获得更高的命中率。想达到这一目的。通常需要借助缓存算法来实现。

1、FIFO(First in First out)
实现原理:

FIFO 先进先出的核心思想如果一个数据*先进入缓存中,则应该*早淘汰掉。类似实现一个按照时间先后顺序的队列来管理缓存,将淘汰*早访问的数据缓存。

示意图:

%title插图%num

问题:

LFU仅维护各项的被访问频率信息,对于某缓存项,如果该项在过去有着*高的访问频率而*近访问频率较低,当缓存空间已满时该项很难被从缓存中替换出来,进而导致命中率下降。

3、 LRU (LeastRecentlyUsed)
实现原理:

LRU 是一种应用广泛的缓存算法。该算法维护一个缓存项队列,队列中的缓存项按每项的*后被访问时间排序。当缓存空间已满时,将处于队尾,即删除*后一次被访问时间距现在*久的项,将新的区段放入队列首部。

示意图:
%title插图%num

问题:

LRU算法仅维护了缓存块的访问时间信息,没有考虑被访问频率等因素,当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降。例如对于VoD(视频点播)系统,用户已经访问过的数据不会重复访问等场景。

4、 LRU-K (LeastRecentlyUsed)
实现原理:

相比LRU,其核心思想是将“*近使用过1次”的判断标准扩展为“*近使用过K次”。具体来说它多维护一个队列,记录所有缓存数据被访问的历史。仅当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间*大的数据。

示意图:

%title插图%num

问题:

需要额外的空间来存储访问历史,维护两个队列增加了算法的复杂度,提升了CPU等消耗。

5、2Q(Two queues)
实现原理:

2Q算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。

示意图:
%title插图%num

问题:

需要两个队列,但两个队列本身都比较简单,2Q算法和LRU-2算法命中率、内存消耗都比较接近,但对于*后缓存的数据来说,2Q会减少一次从原始存储读取数据或者计算数据的操作。

6、MQ(Multi Queue)
实现原理:

MQ算法根据优先级(访问频率)将数据划分为多个LRU队列,其核心思想是:优先缓存访问次数多的数据。

示意图:
%title插图%num

问题:

多个队列需要额外的空间来存储缓存,维护多个队列增加了算法的复杂度,提升了CPU等消耗。

五、iOS端可供选择的数据持久化方案
1. 内存缓存
实现内存缓存的技术手段包括苹果官方提供的NSURLCache,NSCache,还有性能和API上比较有优势的开源缓存库YYCache、PINCache等。

2. 磁盘缓存
NSUserDefault

适合小规模数据,弱业务相关数据的缓存。

keychain

Keychain是苹果提供的带有可逆加密的存储机制,普遍用在各种存用户名、密码的需求上。另外,Keychain是系统级存储,还可以被iCloud同步,即使App被删除,Keychain数据依然保留,用户下次安装App,可以直接读取,通常会用来存储用户唯一标识串。所以需要加密、同步iCloud的敏感小数据,一般使用Keychain存取。

文件存储

Plist:一般结构化的数据可以Plist的方式去持久化
archive:Archive方式可以存取遵循协议的数据,比较方便的是存取使用的都是对象,不过中间的序列化和反序列化需要花费一定的性能,可以在想要使用对象直接进行磁盘存取时使用。
Stream:指文件存储,一般用来存图片、视频文件等数据
数据库存储

数据库适合存取一些关系型的数据;可以在有大量的条件查询排序类需求时使用。

Core Data:苹果官方封装的ORM(Object Relational Mapping)
FMDB:github*受欢迎的iOS sqlite 封装开源库之一
WCDB:微信团队在自己使用的sqlite封装基础上的开源实现,具有ORM(Object Relational Mapping)的特性,支持iOS、Android。
Realm:由Y Combinator孵化的创业团队开源出来的一款跨平台(iOS、Android)移动数据库。
3. 应该用哪种缓存方案
根据需求选择:

简单数据存储直接写文件、key-value存取即可。
需要按照一些条件查找、排序等需求的,可以使用sqlite等关系型存储方式。
敏感性高的数据,加密存储。
不希望App删除后清除的小容量数据(用户名、密码、token)存keychain。
六、内存、磁盘数据持久化方案对比
1、可选方案详解
1.1、NSCache
苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key,内部实现了内存警告处理(仅应用在后台时,会移除一部分缓存)。

1.1.1、特性
属性
名称
delegate:obj从cache移除时,通知代理
countLimit:存储数限制
costLimit:存储空间开销值限制(不精确)
evictsObjectsWithDiscardedContent(自动回收废弃内容,没看到这个属性的使用场景)
方法
使用key同步存、取、删
删除所有内容
1.1.2、实现
NSCacheEntry:内部类,将key-value转换成改实体,用来实现双向链表存储结构
key:键
value:值
cost:开销
prevByCost:上个节点
nextByCost:下个节点
NSCacheKey:对存取使用的key的封装,用于实现存取使用不支持NSCopy协议的object
value:存取使用的key的值
_entries:NSDictionary,使用它以键值对形式存取NSCacheEntry实例
_head:双向链表头节点,链表按cost升序排序;setObject触发costLimit/countLimit trim时,从根节点开始删除
NSLock:实现读写线程安全
1.2、TMCache
TMCache *初由 Tumblr 开发,但现在已经不再维护了。TMMemoryCache 实现了很多 NSCache 并没有提供的功能,比如数量限制、总容量限制、存活时间限制、内存警告或应用退到后台时清空缓存等。TMMemoryCache 在设计时,主要目标是线程安全,它把所有读写操作都放到了同一个 concurrent queue 中,然后用 dispatch_barrier_async 来保证任务能顺序执行。它错误的用了大量异步 block 回调来实现存取功能,以至于产生了很大的性能和死锁问题。
由于该库很久不再维护,不做详细对比。

1.3、PINCache
Tumblr 宣布不在维护 TMCache 后,由 Pinterest 维护和改进的一个缓存SDK。它的功能和接口基本和 TMCache 一样,但修复了性能和死锁的问题。它同样也用 dispatch_semaphore 来保证线程安全,但去掉了dispatch_barrier_async,避免了线程切换带来的巨大开销,也避免了可能的死锁。

1.3.1、特性:
PINCaching(protocal)

属性
名称
方法
同步/异步使用key存、取、删、判断存在、设置ttl时长、存储空间消耗值
同步/异步删除指定日期之前的数据(磁盘缓存指创建日期)
同步/异步删除过期数据
同步/异步删除所有数据
PINMemoryCache

属性
totalCost:已经使用的总开销
costLimit:开销(内存)使用限制(每次赋值时,触发trim)
ageLimit:统一生命周期限制(每次赋值时,触发trim;GCD timer循环触发)
ttlCache:是否ttl,配置此项,获取数据只会返回生命周期存活状态的数据
removeAllObjectsOnMemoryWarning
removeAllObjectsOnEnteringBackground
将要/已经添加、移除缓存对象block监听
将要/已经移除缓存所有对象block监听
已经接收内存警告、已经进入后台block监听
方法
同步/异步删除数据到指定的cost以下
同步/异步删除在指定日期之前的数据,继续删除数据到指定的cost以下(trimToCostLimitByDate)
同步/异步遍历所有缓存数据
内部实现
通过NSMutableDictionary保存需要缓存的数据,通过额外的NSMutableDictionary来保存createdDates(创建时间)、accessDates(*近访问时间)、costLimit、ageLimit等信息
使用互斥锁保证多线程安全
使用PINOperationQueue实现异步操作
setObject触发costLimit trim时,对accessDates进行排序,实现LRU策略
PINDiskCache

属性
prefix:缓存名前缀
cacheURL:缓存路径url
byteCount:硬盘已存储数据大小
byteLimit:*大硬盘存储空间限制,默认50M(每次赋值时,触发trim)使用时注意,丢数据时不清楚为什么
ageLimit:同PINMemoryCache;默认30天
writingProtectionOption:
ttlCache:同PINMemoryCache
removeAllObjectsOnMemoryWarning(同PINMemoryCache)
removeAllObjectsOnEnteringBackground(同PINMemoryCache)
将要/已经添加、移除缓存对象block监听(同PINMemoryCache)
将要/已经移除缓存所有对象block监听(同PINMemoryCache)
已经接收内存警告、已经进入后台block监听(同PINMemoryCache)
支持对key进行自定义编码和解码(默认移除特殊字符.:/%)
支持对数据进行自定义序列化和反序列化(默认NSKeyedArchiver,需要遵守NSCoding协议)
方法
lockFileAccessWhileExecutingBlockAsync、synchronouslyLockFileAccessWhileExecutingBlock:执行完所有文件写操作后回调block
fileURLForKey:获取指定文件的fileUrl
同步/异步删除数据到指定的cost以下(同PINMemoryCache)
同步/异步删除在指定日期之前的数据,继续删除数据到costLimit以下(同PINMemoryCache)
同步/异步遍历所有缓存数据(同PINMemoryCache)
内部实现
通过PINDiskCacheMetadata保存数据信息:createdDate、lastModifiedDate、size、ageLimit;初始化时,加载所有文件的metadata,保存在一个NSMutableDictionary中,通过fileKey存取;
读取文件获取createdDate、lastModifiedDate、size等信息回写metadata;setxattr、removexattr、getxattr存储ageLimit信息,回写metadata
trimDiskToSize:按照文件大小降序排序删除,先删大文件
trimDiskToSizeByDate:按*近修改时间升序排序,先删较长时间未访问的(LRU)
trimToDate:删除创建日期在指定日期之前的文件(按修改时间倒序)
使用互斥锁保证多线程安全:
使用PINOperationQueue实现异步操作
对accessDates进行排序,实现LRU策略
PINCache

属性
diskByteCount:设置diskCache,byteCount
diskCache:磁盘缓存
memoryCache:内存缓存
方法
仅有初始化方法及 的实现
实现
二级缓存实现:先取内存;后取磁盘,取磁盘同时更新内存
使用同一个PINOperationQueue实现异步操作
PINOperationGroup来实现内存缓存和磁盘缓存结束回调
1.3.2、实现
PINOperationQueue(async任务通过自定义的PINOperationQueue实现)
pthread_mutex PTHREAD_MUTEX_RECURSIVE(添加operation,线程安全)
dispatch_queue:
DISPATCH_QUEUE_SERIAL:并发数1时,直接使用串行队列执行;使用串行队列保证对信号量数据操作是安全的(修改并发数时,修改信号量数量)
DISPATCH_QUEUE_CONCURRENT:执行block中的耗时操作
dispatch_group:阻塞当前线程,用来实现 waitUntilAllOperationsAreFinished
dispatch_semaphore:并发数量控制,并发数为大于1时使用。
PINOperationGroup
dispatch_group_enter、dispatch_group_leave、dispatch_group_notify,来回调group结束block
LRU淘汰
每次设置新的object时,超出costLimit部分,根据访问时间倒序删除
线程安全
pthread_mutex_lock 互斥?
PINOperationQueue 实现多线程队列任务
1.4、YYCache
大神郭曜源开源的一个内存缓存实现,YYCache是对标PINCache实现的,实现了PINCache大部分的能力,同时做了一些针对性性能优化。
内存缓存相对于 PINMemoryCache 来说,去掉了异步访问的接口,尽量优化了同步访问的性能,用 OSSpinLock pthread_mutex_t互斥锁来保证线程安全。另外,缓存内部用双向链表和 NSDictionary 实现了 LRU 淘汰算法。
磁盘缓存支持设置文件尺寸阈值来控制写磁盘还是存数据库。

1.4.1、特性:
YYMemoryCache

属性
name:名称
totalCount:缓存数
totalCost:已经使用的总开销
countLimit:缓存数限制(并非严格限制,GCD timer定时触发后台线程trim)
costLimit:开销(内存)使用限制(并非严格限制,GCD timer定时触发后台线程trim)
ageLimit:统一生命周期限制(并非严格限制,GCD timer定时触发后台线程trim)
autoTrimInterval:定时触发trim时长,默认5s
shouldRemoveAllObjectsOnMemoryWarning
shouldRemoveAllObjectsWhenEnteringBackground
releaseOnMainThread:是否允许主线程销毁内存键值对,默认NO;注意,指定该值为YES后,YYMemoryCache的缓存只有回到主线程才把缓存的对象销毁,即执行release操作。
releaseAsynchronously:是否异步线程销毁内存键值对,默认YES
已经接收内存警告、已经进入后台block监听
方法
同步使用key存、取、删、判断存在、设置每个存储内存开销值
同步/异步删除所有缓存(根据releaseOnMainThread、releaseAsynchronously决定)
同步trim删除数据到指定的count以下
同步trim删除数据到指定的cost以下(从tail开始移除,即移除*近未访问数据)
同步trim删除在指定日期之前的数据
内部实现
_YYLinkedMapNode:链表节点,key、value、pre、next、cost、time(CACurrentMediaTime,*近访问时间)信息保存
_YYLinkedMap:*终使用_YYLinkedMap的节点通过链表方式执行增、删、改操作
dic、totalCost、totalCount、head(MRU)、tail(LRU)、releaseOnMainThread、releaseAsynchronously
insertNodeAtHead
bringNodeToHead
removeNode
removeTailNode
removeAll
链表*新访问的放在头结点,便于执行trim操作,直接从尾节点开始删除
使用pthread_mutex_t互斥锁保证线程安全
使用DISPATCH_QUEUE_SERIAL执行增加obj缓存触发costLimit情况下的trim任务
YYDiskCache

属性
name:缓存名
path:缓存路径
inlineThreshold:控制保存sqlite或文件的阈值,大于该值存文件,默认20KB
customArchiveBlock、customUnarchiveBlock:对数据进行自定义序列化和反序列化(默认NSKeyedArchiver,需要遵守NSCoding协议)
customFileNameBlock:根据key名称对文件名做自定义
countLimit:同YYMemoryCache;默认无限制
costLimit:同YYMemoryCache,这里指真实的磁盘存储大小;默认无限制
ageLimit:同YYMemoryCache;默认无限制
freeDiskSpaceLimit:磁盘可缓存*小剩余空间限制;默认0
autoTrimInterval:同YYMemoryCache,默认60s
errorLogsEnabled:错误日志
方法
同步/异步使用key存、取、判存、删数据
同步/异步删除所有数据
异步删除所有数据并在block回调进度
同步/异步获取totalCount、totalCost
同步/异步trimToCount、trimToCost、trimToAge
为指定object绑定extendedData
内部实现
使用dispatch_semaphore_t:信号量设置为1,作为锁使用了
使用dispatch_queue_t:DISPATCH_QUEUE_CONCURRENT,异步线程执行trim、CRUD等
注意:这导致所有的异步操作回调block都是在异步线程,没在主线程
_globalInstances:NSMapTable缓存了所有初始化的diskCache实例,key strong,value weak
YYKVStorage
属性
path:缓存路径
type:YYKVStorageTypeFile、YYKVStorageTypeSQLite、YYKVStorageTypeMixed
errorLogsEnabled
方法
保存key-value数据
根据key删除key-value数据;删除超过指定size的数据(访问时间倒序删除,每次删除16个);删除指定时间之前的数据(同);删除数据到整体储存空间到指定size内;删除数据到整体储存数量到指定count内;删除所有数据
使用key取数据
判断指定key是否存在数据;获取存储数量;获取存储占用size
实现
内部使用selite存取数据
删除所有数据:先移动到指定的trash目录下,然后后台删除trash目录?移动文件比删除文件更快?
DISPATCH_QUEUE_SERIAL:后台删除trash
YYCache

属性
name:名称
memoryCache:内存缓存
diskCache:磁盘缓存
方法
同步/异步使用key存、取、判存、删除数据
同步/异步删除所有数据
异步删除所有数据并在block回调进度
实现
二级缓存:先取内存,再取磁盘
异步操作直接使用globalQueue执行了。
1.4.2、实现
磁盘存取:封装YYKVStorage执行文件读写、seqlite操作,具体的存取操作交给它完成
内存LRU淘汰:每次设置新的object时,超出costLimit部分,根据访问时间倒序删除(借助链表)
线程安全
pthread_mutex_lock 互斥? 实现内存缓存线程安全
dispatch_semaphore_t:信号量设置为1,作为锁使用了
2、内存缓存方案对比
2.1、性能
YYCache的读写性能均较为优秀。NSCache和PINCache各有优劣。

摘自YYCache 设计思路的单线程性能测试图:

%title插图%num

  • 我的性能测试图:

性能测试说明:

在YYCache Demo基础上进行的性能测试,使用的debug包,并不代表真实使用性能情况。

%title插图%num

2.1、对比
SDK API能力 易用性 实现 优缺点 是否维护
NSCache 同步存、取、删,设置costLimit,countLimit、delegate(仅触发trim删除时通知) 中 NSLock实现线程安全,内部将key-value信息转换为链表对象实体,使用NSDictionary存取实体,触发trim时使用链表按cost降序删除;应用后台状态触发内存警告清除部分存储 官方较可靠,但缺乏拓展,功能不完善,性能一般 apple维护中
PINMemoryCache 同步/异步存、取、删、判存、执行trim、遍历所有已存储数据;设置costLimit、ageLimit、ttlCache(超时数据不返回,清除)、removeAllObjectsOnMemoryWarning、removeAllObjectsOnEnteringBackground;添加删除key-value block回调;应用进后台、内存警告block回调; 高 使用pthread_mutex_t互斥锁实现线程安全,使用NSDictionary存取实体,使用额外的NSDictionary存取实体的创建时间、更新时间、cost、ageLimit等信息,来实现相关能力,使用GCDtimer来定时trim 功能完善,易用性高,面向协议实现,整体架构清晰,根据存储的更新时间实现了LRU策略,但内部存储拆分了多个NSDictionary,导致性能下降 Pinterest维护中
YYMemoryCache 同步存、取、删、判存、trim;设置countLimit、costLimit、ageLimit、autoTrimInterval、shouldRemoveAllObjectsOnMemoryWarning、shouldRemoveAllObjectsWhenEnteringBackground、应用进入后台/接收内存警告block监听 高 使用pthread_mutex_t互斥锁实现线程安全,使用_YYLinkedMapNode内部类实体存储键值对信息来实现双向列表存储结构,数据按访问时间降序排序,基于此实现LRU cache 功能完善,易用性高,实现了LRU策略,性能高;但未抽象相关协议,内存和磁盘缓存重复度高 作者已不在维护
3、磁盘缓存方案对比
3.1、性能
小数据存取YYCache完胜。20KB以上文件存取YYCache较快。

摘自YYCache 设计思路的性能测试图:

%title插图%num

  • 我的性能测试

性能测试说明:
在YYCache Demo基础上进行的性能测试,使用的debug包,并不代表真实使用性能情况。

%title插图%num

%title插图%num

3.2、对比
SDK API能力 易用性 实现 优缺点 是否维护
PINDiskCache 同步/异步存、取、删、判断存在、执行trim date/size/sizeByDate;设置byteLimit、ageLimit、ttlCache(超时数据不返回,清除)、NSDataWritingOptions(文件写入模式),设置data自定义序列化block、key的自定义编解码block;添加删除key-value block回调;删除所有数据回调;获取缓存url、空间占用大小,单个文件的存储fileUrl;执行指定操作等待文件写入锁定打开;遍历所有的已存储文件 高 使用pthread_mutex_t互斥锁实现读写线程安全,使用pthread_cond_t实现文件读写保护,使用PINDiskCacheMetadata将文件信息保存在内存中方便快速读取,使用NSDictionary用key存取实体,,使用GCDtimer来定时trim,使用dispatch_semaphore_t控制并发实现自定义OperationQueue,按顺序执行缓存队列任务 功能完善,易用性高,面向协议实现,整体架构清晰,trim操作根据存储的更新时间实现了LRU策略 Pinterest维护中
YYDiskCache 同步/异步存、取、删、判断存在、执行trim count/cost/age、获取totalCost、totalCount;设置inlineThreshold、countLimit、costLimit、ageLimit、freeDiskSpaceLimit、autoTrimInterval;设置data自定义序列化block、fileName自定义的block 高 使用dispatch_semaphore_t信号量实现线程安全;使用YYKVStorageItem内部类实体存储键值对key、value、filename、size、modTime、accessTime、extendedData等信息;由YYKVStorage实现具体文件存取,根据sqlite存取小空间数据速度优于直接文件读写的特性,设置存取方式阈值,空间小于阈值数据直接存sqlite,超过的阈值的数据索引信息存sqlite,数据存文件,基于此小数据存取性能较PINDiskCache提升数倍 功能完善,易用性高,实现了LRU策略,性能高;实现文件不同存储策略更高效;但未抽象相关协议,内存和磁盘缓存重复度高 作者已不在维护
七、数据库缓存
1.1、背景
原生的sqlite使用十分繁琐,需要大量的代码来完成一项sql操作,并且是c语言的API,对OC或者其它语言开发者并不友好,假如你想执行一个sql,需要做类似下面的操作:

– (void)example {
sqlite3 *conn = NULL;
//1. 打开数据库
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@”MyDatabase.db”];
int result = sqlite3_open(path.UTF8String, &conn);
if (result != SQLITE_OK) {
sqlite3_close(conn);
return;
}
const char *createTableSQL =
“CREATE TABLE t_test_table (int_col INT, float_col REAL, string_col TEXT)”;
sqlite3_stmt* stmt = NULL;
int len = strlen(createTableSQL);
//2. 准备创建数据表,如果创建失败,需要用sqlite3_finalize释放sqlite3_stmt对象,以防止内存泄露。
if (sqlite3_prepare_v2(conn,createTableSQL,len,&stmt,NULL) != SQLITE_OK) {
if (stmt)
sqlite3_finalize(stmt);
sqlite3_close(conn);
return;
}
//3. 通过sqlite3_step命令执行创建表的语句。对于DDL和DML语句而言,sqlite3_step执行正确的返回值只有SQLITE_DONE。
//对于SELECT查询而言,如果有数据返回SQLITE_ROW,当到达结果集末尾时则返回SQLITE_DONE。
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_finalize(stmt);
sqlite3_close(conn);
return;
}
//4. 释放创建表语句对象的资源。
sqlite3_finalize(stmt);
printf(“Succeed to create test table now.\n”);
//5. 构造查询表数据的sqlite3_stmt对象。
const char* selectSQL = “SELECT * FROM TESTTABLE WHERE 1 = 0”;
sqlite3_stmt* stmt2 = NULL;
if (sqlite3_prepare_v2(conn,selectSQL,strlen(selectSQL),&stmt2,NULL) != SQLITE_OK) {
if (stmt2)
sqlite3_finalize(stmt2);
sqlite3_close(conn);
return;
}
//6. 根据select语句的对象,获取结果集中的字段数量。
int fieldCount = sqlite3_column_count(stmt2);
printf(“The column count is %d.\n”,fieldCount);
//7. 遍历结果集中每个字段meta信息,并获取其声明时的类型。
for (int i = 0; i < fieldCount; ++i) {
//由于此时Table中并不存在数据,再有就是SQLite中的数据类型本身是动态的,所以在没有数据时无法通过sqlite3_column_type函数获取,此时sqlite3_column_type只会返回SQLITE_NULL,
//直到有数据时才能返回具体的类型,因此这里使用了sqlite3_column_decltype函数来获取表声明时给出的声明类型。
string stype = sqlite3_column_decltype(stmt2,i);
stype = strlwr((char*)stype.c_str());
//数据类型以决定字段亲缘性的规则解析
if (stype.find(“int”) != string::npos) {
printf(“The type of %dth column is INTEGER.\n”,i);
} else if (stype.find(“char”) != string::npos
|| stype.find(“text”) != string::npos) {
printf(“The type of %dth column is TEXT.\n”,i);
} else if (stype.find(“real”) != string::npos
|| stype.find(“floa”) != string::npos
|| stype.find(“doub”) != string::npos ) {
printf(“The type of %dth column is DOUBLE.\n”,i);
}
}
sqlite3_finalize(stmt2);
sqlite3_close(conn);
}

由于sqlite在移动端不易直接使用,所以衍生出了许多对seqlite的封装,包括以下被大家所熟知的流行库,它们的*终实现都指向sqlite:

CoreData:苹果基于sqlite封装的ORM(Object Relational Mapping)的数据库,直接对象映射————由于CoreData的性能较差和学习成本较高,坑又不少(见唐巧老师的我为什么不喜欢 Core Data一文),下文不做详细介绍
FMDB:iOS端github使用*广的针对OC对sqlite的封装,支持队列操作
WCDB:微信技术团队开源的对sqlite操作的封装,支持对象和数据库映射,ORM数据库的一种实现,比FMDB更高效
有一个特例,它通过自建搜索引擎实现了一套ORM数据存储:

Realm:realm团队 对sqlite的封装 通过自建搜索引擎实现的一套移动端数据库,也是ORM数据库的一种实现,是一个 MVCC 数据库
1.2、对比
sqlite数据库的使用包括增、删、改、查等基本操作,同时在项目中运用,还需要数据转模型、数据库通过增删表、字段和数据迁移完成版本升级等操作,下文通过对这些操作在各个流行库中的使用示例来对比各个库的易用性。

1.2.1、FMDB
FMDB是对sqlite的面向OC的封装,把c语言对sql的操作封装成OC风格代码。主要有以下特点:

OC风格,省去了大量重复、冗余的C语言代码
提供了多线程安全的数据库操作方法,保证数据的一致性
相比CoreData、Realm等更加轻量。
支持事务
支持全文检索(fts subspec)
支持对WAL(Write ahead logging)模式执行checkpoint操作
FMDB基本操作示例:

// 建表
NSString *sql = [NSString stringWithFormat:@”CREATE TABLE IF NOT EXISTS t_test_1 (‘%@’ INTEGER PRIMARY KEY AUTOINCREMENT,’%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ TEXT NOT NULL, ‘%@’ INTEGER NOT NULL, ‘%@’ FLOAT NOT NULL)”, KEY_ID, KEY_MODEL_ID, KEY_MODEL_NAME, KEY_SERIES_ID, KEY_SERIES_NAME, KEY_TITLE, KEY_PRICE, KEY_DEALER_PRICE, KEY_SALES_STATUS, KEY_IS_SELECTED, KEY_DATE];
FMDatabaseQueue *_dbQueue = [FMDatabaseQueue databaseQueueWithPath:@”path”];
[_dbQueue inDatabase:^(FMDatabase *db) {
BOOL result = [db executeUpdate:sql];
if (result) {
//
}
}];

// 插入一条数据
NSString *insertSql = [NSString stringWithFormat:@”INSERT INTO ‘t_test_1′(%@,%@,%@,%@,%@,%@,%@,%@,%@,%@) VALUES(\”%@\”,\”%@\”,\”%@\”,\”%@\”,\”%@\”,\”%@\”,\”%@\”,\”%@\”,%d,%.2f)”, KEY_MODEL_ID, KEY_MODEL_NAME, KEY_SERIES_ID, KEY_SERIES_NAME, KEY_TITLE, KEY_PRICE, KEY_DEALER_PRICE, KEY_SALES_STATUS, KEY_IS_SELECTED, KEY_DATE, model.model_id, model.model_name, model.Id, model.Name, model.title, model.price, model.dealer_price, model.sales_status, isSelected,time];
[_dbQueue inDatabase:^(FMDatabase *db) {
BOOL result = [db executeUpdate:sql];
if (result) {
//
}
}];

// 更新
NSString *sql = @”UPDATE t_userData SET userName = ? , userAge = ? WHERE id = ?”;
[_dbQueue inDatabase:^(FMDatabase *db) {
BOOL res = [db executeUpdate:sql,_nameTxteField.text,_ageTxteField.text,_userId];
if (result) {
//
}
}];

// 删除
NSString *str = [NSString stringWithFormat:@”DELETE FROM t_userData WHERE id = %ld”,userid];
[_dbQueue inDatabase:^(FMDatabase *db) {
BOOL res = [db executeUpdate:str];
if (res) {
//
}
}];

// 查找
[_dbQueue inDatabase:^(FMDatabase *db) {
FMResultSet *resultSet = [db executeQuery:@”SELECT * FROM message”];
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
while ([resultSet next]) {
Message *message = [[Message alloc] init];
message.localID = [resultSet intForColumnIndex:0];
message.content = [resultSet stringForColumnIndex:1];
message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
[messages addObject:message];
}
}];

1.2.2、WCDB
WCDB是微信技术团队内部在微信app sqlite使用实践抽取的一套开源封装,主要具有以下特点:

通过宏定义的方式实现了ORM映射关系,根据映射关系完成建表、数据库新增字段、修改字段名(绑定别名)、数据初始化绑定等操作
自研了WINQ的语法,大部分场景不需要直接写原生sqlite语句,易用性高
内部实现了安全的多线程读写操作(写操作还是串行)和数据库初始化优化,提升了性能(微信iOS SQLite源码优化实践)
提供了其它较多场景的解决方案:

错误统计
性能统计
损坏修复(微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧)
反注入
加密
在WCDB内,ORM(Object Relational Mapping)是指

将一个ObjC的类,映射到数据库的表和索引;
将类的property,映射到数据库表的字段;
这一过程。通过ORM,可以达到直接通过Object进行数据库操作,省去拼装过程的目的。

WCDB基本操作示例:

//Message.h
@interface Message : NSObject

@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;
@property(assign) int unused; //You can only define the properties you need

@end
//Message.mm
#import “Message.h”
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE(Message, content)
WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE(Message, modifiedTime)

WCDB_PRIMARY(Message, localID)

WCDB_INDEX(Message, “_index”, createTime)

@end
//Message+WCTTableCoding.h
#import “Message.h”
#import <WCDB/WCDB.h>

@interface Message (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)
WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)

@end

// 建表
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
/*
CREATE TABLE messsage (localID INTEGER PRIMARY KEY,
content TEXT,
createTime BLOB,
modifiedTime BLOB)
*/
BOOL result = [database createTableAndIndexesOfName:@”message”
withClass:Message.class];
//插入
Message *message = [[Message alloc] init];
message.localID = 1;
message.content = @”Hello, WCDB!”;
message.createTime = [NSDate date];
message.modifiedTime = [NSDate date];
/*
INSERT INTO message(localID, content, createTime, modifiedTime)
VALUES(1, “Hello, WCDB!”, 1496396165, 1496396165);
*/
BOOL result = [database insertObject:message
into:@”message”];
//删除
//DELETE FROM message WHERE localID>0;
BOOL result = [database deleteObjectsFromTable:@”message”
where:Message.localID > 0];
//修改
//UPDATE message SET content=”Hello, Wechat!”;
Message *message = [[Message alloc] init];
message.content = @”Hello, Wechat!”;
BOOL result = [database updateRowsInTable:@”message”
onProperties:Message.content
withObject:message];
//查询
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [database getObjectsOfClass:Message.class
fromTable:@”message”
orderBy:Message.localID.order()];

1.2.3、Realm
Realm团队 基于sqlite封装 自建搜索引擎实现的一套ORM数据库操作模式,它是MVCC 数据库,主要具有以下特点:

对象就是一切(ORM映射)
MVCC 数据库
Realm 采用了零拷贝 架构
自动更新对象和查询
String & Int 优化(String转换为枚举,类似OC tagged point,)
崩溃保护(系统异常崩溃时,通过copy-on-wirte机制保存了你已经修改的内容)
真实的懒加载(使用时才从磁盘加载真实数据)
内部加密(引擎层内建了加密)
文档详细,且有中文版
社区活跃,Stackoverflow能解决你几乎所有问题
跨平台,支持iOS、Android
提供Mac版Realm Browser,查看数据很方便
简便的数据库版本升级。Realm可以配置数据库版本,进行判断升级。
支持KVC/KVO
支持监听属性变化通知(写入操作触发通知)
限制:

类名长度*大57个UTF8字符。
属性名长度*大63个UTF8字符。
NSData及NSString属性不能保存超过16M数据。
对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。
多线程访问时需要新建新的Realm对象。
Realm对象的 Setters & Getters 不能被重载
Realm没有自增属性。也就是没有自增主键,如果需要,需要自己去赋值,如果只要求unique,那么可以设为[[NSUUID UUID] UUIDString]
所有的数据模型必须直接继承自RealmObject。这阻碍我们利用数据模型中的任意类型的继承。(如JsonModel)
Realm不支持集合类型,仅有一个集合RLMArray,服务端返回的数组数据需要自己转换。支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊类型标记的NSNumber。
Realm基本操作示例:

// 定义模型的做法和定义常规 Objective‑C 类的做法类似
@interface Dog : RLMObject
@property NSString *name;
@property NSData *picture;
@property NSInteger age;
@end
@implementation Dog
@end
RLM_ARRAY_TYPE(Dog)

Dog *mydog = [[Dog alloc] init];
mydog.name = @”Rex”;
mydog.age = 1;
mydog.picture = nil; // 该属性是可空的
NSLog(@”Name of dog: %@”, mydog.name);

RLMRealm *realm = [RLMRealm defaultRealm];
[Dog createOrUpdateInRealm:realm withValue:mydog];

// 查找;找到小于2岁名叫Rex的所有狗
RLMResults<Dog *> *puppies = [Dog objectsWhere:@”age < 2 ADN name = ‘Rex'”];
puppies.count; // => 0 因为目前还没有任何狗狗被添加到了 Realm 数据库中

// 存储
[realm transactionWithBlock:^{
[realm addObject:mydog];
}];

// 检索结果会实时更新
puppies.count; // => 1

/// 删除数据
[realm transactionWithBlock:^{
[realm deleteObject:mydog];
}];

//修改数据
[realm transactionWithBlock:^{
theDog.age = 1;
}];

// 可以在任何一个线程中执行检索、更新操作
dispatch_async(dispatch_queue_create(“background”, 0), ^{
@autoreleasepool {
Dog *theDog = [[Dog objectsWhere:@”age == 1″] firstObject];
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
theDog.age = 3;
[realm commitWriteTransaction];
}
});

1.3 数据库存取性能测试
性能测试说明:

测试数据见下方。由于样本比较少(仅1种数据),只进行了部分写入和读取操作,并不能完全反应某个SDK的综合性能,仅作为参考。

测试数据和测试结果见下图:

顺序插入1W条数据:

使用事务插入1W条数据:

读取1W条数据:

多线程(2条)插入共2W条数据:

1.4、数据库方案对比
SDK 优点 缺点 是否维护
FMDB 较为轻量级的sqlite封装,API较原生使用方便许多,对SDK本省的学习成本较低,基本支持sqlite的所有能力,如事务、FTS等 不支持ORM,需要每个编码人员写具体的sql语句,没有较多的性能优化,数据库操作相对复杂,关于数据加密、数据库升级等操作需要用户自己实现 是
WCDB 跨平台;sqlite的深度封装,支持ORM,基类支持自己继承,不需要用户直接写sql,上手成本低,基本支持sqlite的所有能力;内部较多的性能优化;文档较完善;拓展实现了错误统计、性能统计、损坏修复、反注入、加密等诸多能力,用户需要做的事情较少 内部基于c++实现,基类需要.mm后缀(或者通过category解决),需要额外的宏来标记model和数据库的映射关系 是
REALM 跨平台;支持ORM;文档十分完善;MVCC的实现;零拷贝提升性能;API十分友好;提供了配套可视化工具 不是基于sqlite的关系型数据库,不能或很难建立表之间的关联关系,项目中遇到类似场景可能较难解决; 基类只能继承自RLMObject,不能自由继承,不方便实现类似JsonModel等属性绑定 是
性能数据:

八、持久化在项目中的应用(小结)
1、 图片缓存
以SDWebImage(KingFisher)为代表的图片缓存库基本都实现了二级缓存、队列下载、异步解压、Category拓展等能力,常用的图片加载展示需求都可以使用它们来完成。

2、 简单key-value存取
系统的如NSCache、NSKeyedArchive等缓存功能能满足基本的存取需求,但是并不易用。
PINCache 和 YYCache 等这些三方库拓展了相当多的能力来满足大部分的使用场景,并且内部通过LRU等策略来提升效率,同时内部实现了二级缓存来加快加载速度,可以考率直接使用。
其中PINCache虽然在一些测试数据上性能并不如YYCache,但是可以看到github的PINCache*近依然有更新,而YYCache已经两年没有代码提交了,issue没有处理,遇到问题需要自己处理。
如果考虑维护成本的比例高一些,不妨使用PINCache,反之使用YYCache。

3、 数据库
Core Data (本人未使用过)由于入门门槛高、坑多等原因导致口碑并不太好,这里就不推荐尝试了。
FMDB可以说经过了大量iOS App的验证,它虽然在一些扩展能力上并不尽人意,但是其稳定性久经考验,基于sqlite实现,不改变表结构数据的情况下,便于直接迁移到如WCDB等实现。
WCDB和Realm同样都是支持ORM的,基本不需要写sql语句就能完成增删改查,都跨平台,扩展了如加密、数据升级等很多便捷的封装,用起来都比FMDB更爽。
但两者相较,假如你真的想使用ORM,我更推荐WCDB,因为Realm的搜索引擎暂不支持关联表查询是硬伤,而WCDB是基于sqlite的,支持直接使用sql语句查询,如果业务中遇到类似场景无法解决,还需要从Realm迁移到sqlite花费的力气就大了。
除此之外,微信团队本身就在使用WCDB,他们在数亿用户量的情况下遇到的性能、数据损坏等问题比我们要多得多,他们做的优化也就更多,而这些优化,你使用WCDB就可以体验到。

4、 其它
封装
无论你使用哪个三方库进行缓存实现,*好做一层封装,这样便于你在想要切换别的实现时,直接内部做好数据迁移,对于使用方完全无感知迁移,或者仅需要其做*少的工作,而不是全量的替换
区分用户目录存储
每个用户都使用单独的文件夹来存储他的数据,对数据库也一样,这样做的好处在于,用户数据不会相互污染(比如数据库中存在复杂的多表关联关系时,会使你的sql语句变得很复杂,提升了你区分用户出错的概率),也便于进行数据诊断。
单例
建议对于某个时间段的数据操作都交给一个对象去做,内部来保证多线程读写安全,降低出错的概率。
用户切换的处理
由于区分用户存储目录,切换登录用户时,需要我们切换数据存取的实例,此时,不要马上销毁上个实例,上个实例可能还有未完成的读写任务,等待完成或中断其操作后再销毁。
#参考

文章
iOS架构师之路:本地持久化方案
IOS(数据持久化1)
iOS应用架构谈 本地持久化方案及动态部署
常见缓存算法和缓存策略
缓存淘汰算法–LRU算法
iOS缓存框架-PINCache解读
IOS 缓存管理之 PINCache 使用
YYCache 设计思路
Sqlite学习笔记(四)&&SQLite-WAL原理
微信iOS SQLite源码优化实践
微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧
数据库的设计:深入理解 Realm 的多线程处理机制
Realm 核心数据库引擎探秘
Realm数据库 从入门到“放弃”
使用Realm的一些总结
Realm、WCDB与SQLite移动数据库性能对比测试
Realm、WCDB与SQLite移动数据库性能测试
开源库
wcdb
realm
PINCache
YYCache

iOS中URL编码那些事

文章目录
前言
一、编码的原因和范围
1. 为什么要编码
2. 转码范围
二、编码实现
1. 使用`CoreFoundation`对`url参数`进行encode
2. 使用`Foundation`框架对`完整url`进行encode
三、拓展——`stringByAddingPercentEncodingWithAllowedCharacters`用法
四、参考
前言
在iOS程序中,访问一些HTTP/HTTPS的资源服务时,如果url中存在中文或者特殊字符时,会导致无法正常的访问到资源或服务,想要解决这个问题,需要对url进行编码。

一、编码的原因和范围
1. 为什么要编码
网络标准RFC 1738规定url中只能包含英文字母和阿拉伯数字,以及一些特殊字符:

“…Only alphanumerics [0-9a-zA-Z], the special characters “$-_.+!*'(),” [not including the quotes – ed], and reserved characters used for their reserved purposes may be used unencoded within a URL.”
1
“只有字母和数字[0-9a-zA-Z]、和特殊符号”$-_.+!*’(),”[不包括双引号]、及某些保留字,才可以不经过编码直接用于URL。”

此时如果url中包含如汉字或者其他特殊字符则需要对它进行编码,编码的意义在于,假如url的参数中的中文或特殊字符在发送到服务端时,服务端无法解析它的真正意义,会导致服务端不能理解客户端的请求。

如:
url中的保留字?表示后面连接的是一些请求参数,而参数中如果也包含?,服务端就不知道从哪个?之后是参数;
url中的保留字&用来连接并列的参数项,参数中包含&时,服务端依然无法判断。

2. 转码范围
ASCII 的控制字符

这些字符都是不可打印的,自然需要进行转化。

一些非ASCII字符

这些字符自然是非法的字符范围。转化也是理所当然的了。

一些保留字符

很明显*常见的就是“&”了,这个如果出现在url中了,那你认为是url中的一个字符呢,还是特殊的参数分割用的呢?

就是一些不安全的字符了。

例如:空格。为了防止引起歧义,需要被转化为“+”。

二、编码实现
1. 使用CoreFoundation对url参数进行encode
使用到的API

CFStringRef CFURLCreateStringByAddingPercentEscapes(CFAllocatorRef allocator, CFStringRef originalString, CFStringRef charactersToLeaveUnescaped, CFStringRef legalURLCharactersToBeEscaped, CFStringEncoding encoding)
1
代码示例:

– (void)encodeUrl {
// p1=%+&sd
NSString *para1 = [self encodeParameter:@”%+&sd”];
// p2=我是参数2
NSString *para2 = [self encodeParameter:@”我是参数2″];
NSString *encodeUrl = [NSString stringWithFormat:@”https://www.xingshulin.com?p1=%@&p2=%@”, para1, para2];
NSLog(@”%@”, encodeUrl);
}

– (NSString *)encodeParameter:(NSString *)originalPara {
CFStringRef encodeParaCf = CFURLCreateStringByAddingPercentEscapes(NULL, (__bridge CFStringRef)originalPara, NULL, CFSTR(“!*'();:@&=+$,/?%#[]”), kCFStringEncodingUTF8);
NSString *encodePara = (__bridge NSString *)(encodeParaCf);
CFRelease(encodeParaCf);
return encodePara;
}

编码结果:

https://www.xingshulin.com?p1=%25%2B%26sd&p2=%E6%88%91%E6%98%AF%E5%8F%82%E6%95%B02

可以看到转码对象中,除了中文正常转码外,特殊字符只要包含在!*'();:@&=+$,/?%#[]这些字符范围内的都进行了转码。

注意:

此方法适用于,url前缀不包含中文以及其它非法字符的情况,只需要对参数进行编码即可。

2. 使用Foundation框架对完整url进行encode
方法1:(不推荐)
使用到的API

– (nullable NSString *)stringByAddingPercentEscapesUsingEncoding:(NSStringEncoding)enc

代码示例:

NSString *originalUrl = @”https://www.xingshulin.com我是中文?p1=abc&p2=我是参数2″;
NSString *encodeUrl = [originalUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@”%@”, encodeUrl);

编码结果:

https://www.xingshulin.com%E6%88%91%E6%98%AF%E4%B8%AD%E6%96%87?p1=abc&p2=%E6%88%91%E6%98%AF%E5%8F%82%E6%95%B02

可以看到转码对象中,除了中文正常转码外,特殊字符只要包含在!*'();:@&=+$,/?%#[]这些字符范围内的都进行了转码。

注意:

此方法适用于url或者参数中包含中文以及其它非法字符的情况,但不适用于参数包含保留字和其他特殊字符的情况。

方法2:(推荐)
使用到的API

– (nullable NSString *)stringByAddingPercentEncodingWithAllowedCharacters:(NSCharacterSet *)allowedCharacters
1
代码示例:

NSString *originalUrl = @”https://www.xingshulin.com我是中文?p1=abc&p2=我是参数2″;
NSCharacterSet *encodeUrlSet = [NSCharacterSet URLQueryAllowedCharacterSet];
NSString *encodeUrl = [originalUrl stringByAddingPercentEncodingWithAllowedCharacters:encodeUrlSet];
NSLog(@”%@”, encodeUrl);

编码结果:

https://www.xingshulin.com%E6%88%91%E6%98%AF%E4%B8%AD%E6%96%87?p1=abc&p2=%E6%88%91%E6%98%AF%E5%8F%82%E6%95%B02

可以看到转码对象中,除了中文正常转码外,特殊字符只要包含在!*'();:@&=+$,/?%#[]这些字符范围内的都进行了转码。

注意:

与方法1相同。

对比
之所以推荐方法2,是由于方法1已经在iOS9中被苹果废弃,而且1支持的字符比较少,只对 #%^{}[]|\”<> 加空格 共14个字符编码,不包括 &? 等符号。

三、拓展——stringByAddingPercentEncodingWithAllowedCharacters用法
stringByAddingPercentEncodingWithAllowedCharacters接收NSCharacterSet对象,几种常用的NSCharacterSet:

URLFragmentAllowedCharacterSet “#%<>[\]^`{|}

URLHostAllowedCharacterSet “#%/<>?@\^`{|}

URLPasswordAllowedCharacterSet “#%/:<>?@[\]^`{|}

URLPathAllowedCharacterSet “#%;<>?[\]^`{|}

URLQueryAllowedCharacterSet “#%<>[\]^`{|}

URLUserAllowedCharacterSet “#%/:<>?@[\]^`

iOS中Get请求带中文参数的编码问题

*近测试为了方便直接使用get接收参数,但是在iOS中使用的时候,get中的中文参数却存在乱码问题,需要转换以下才可以使用:

NSString * unicodeStr = [@”yourParam” stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSString * url = [NSString stringWithFormat:@”%@?testparam=%@”,@”https://api.yoururl.com/test”,unicodeStr];

2021年5月份居民消费价格同比上涨1.3% 环比下降0.2%

  2021年5月份,全国居民消费价格同比上涨1.3%。其中,城市上涨1.4%,农村上涨1.1%;食品价格上涨0.3%,非食品价格上涨1.6%;消费品价格上涨1.6%,服务价格上涨0.9%。1­­—5月平均,全国居民消费价格比去年同期上涨0.4%。   5月份,全国居民消费价格环比下降0.2%。其中,城市下降0.1%,农村下降0.3%;食品价格下降1.7%,非食品价格上涨0.2%;消费品价格下降0.4%,服务价格上涨0.2%。    一、各类商品及服务价格同比变动情况 5月份,食品烟酒类价格同比上涨0.8%,影响CPI(居民消费价格指数)上涨约0.24个百分点。食品中,蛋类价格上涨14.3%,影响CPI上涨约0.08个百分点;水产品价格上涨13.8%,影响CPI上涨约0.25个百分点;鲜菜价格上涨5.4%,影响CPI上涨约0.10个百分点;鲜果价格上涨1.4%,影响CPI上涨约0.03个百分点;粮食价格上涨0.8%,影响CPI上涨约0.01个百分点;畜肉类价格下降11.3%,影响CPI下降约0.46个百分点,其中猪肉价格下降23.8%,影响CPI下降约0.50个百分点。     其他七大类价格同比六涨一降。其中,交通通信、教育文化娱乐、居住价格分别上涨5.5%、1.5%和0.7%,衣着、生活用品及服务、医疗保健价格分别上涨0.4%、0.4%和0.2%;其他用品及服务价格下降0.9%。    二、各类商品及服务价格环比变动情况 5月份,食品烟酒类价格环比下降1.0%,影响CPI下降约0.29个百分点。食品中,畜肉类价格下降6.0%,影响CPI下降约0.23个百分点,其中猪肉价格下降11.0%,影响CPI下降约0.20个百分点;鲜菜价格下降5.6%,影响CPI下降约0.11个百分点;鲜果价格下降2.7%,影响CPI下降约0.05个百分点;水产品价格上涨3.0%,影响CPI上涨约0.06个百分点;蛋类价格上涨2.8%,影响CPI上涨约0.02个百分点。    其他七大类价格环比五涨一平一降。其中,其他用品及服务、交通通信价格分别上涨0.6%和0.4%,衣着、教育文化娱乐、居住价格分别上涨0.3%、0.3%和0.1%;医疗保健价格持平;生活用品及服务价格下降0.1%。  2021年5月份居民消费价格主要数据    环比涨跌幅 (%) 同比涨跌幅 (%) 1—5月 同比涨跌幅(%) 居民消费价格 -0.2 1.3 0.4   其中:城市 -0.1 1.4 0.4      农村 -0.3 1.1 0.4   其中:食品 -1.7 0.3 0.1      非食品 0.2 1.6 0.5   其中:消费品 -0.4 1.6 0.6      服务 0.2 0.9 0.2   其中:不包括食品和能源 0.1 0.9 0.3 按类别分       一、食品烟酒 -1.0 0.8 0.6   粮  食 0.0 0.8 1.3   食 用 油 0.4 8.1 6.9   鲜  菜 -5.6 5.4 3.7   畜 肉 类 -6.0 -11.3 -7.6    其中:猪  肉 -11.0 -23.8 -16.2       牛  肉 -0.3 4.8 3.9       羊  肉 -0.2 9.7 8.3   水 产 品 3.0 13.8 7.8   蛋  类 2.8 14.3 5.5   奶  类 -0.1 2.4 1.9   鲜  果 -2.7 1.4 2.5   卷  烟 0.1 1.0 0.9   酒  类 0.2 2.3 1.8 二、衣着 0.3 0.4 0.0   服  装 0.3 0.5 0.1   鞋  类 0.3 0.0 -0.4 三、居住 0.1 0.7 0.1   租赁房房租 0.1 0.3 -0.1   水电燃料 0.1 1.2 0.4 四、生活用品及服务 -0.1 0.4 0.1   家用器具 0.2 1.0 0.0   家庭服务 0.2 2.4 2.4 五、交通通信 0.4 5.5 1.2   交通工具 0.0 -0.9 -1.6   交通工具用燃料 1.6 21.3 5.4   交通工具使用和维修 0.2 1.5 1.6   通信工具 -0.4 6.5 5.7   通信服务 0.0 -0.3 -0.3   邮递服务 0.0 -0.1 -0.3 六、教育文化娱乐 0.3 1.5 0.7   教育服务 0.0 1.9 1.8   旅  游 1.9 -1.0 -4.9 七、医疗保健 0.0 0.2 0.3   中  药 0.1 1.4 1.5   西  药 0.0 -1.0 -1.6   医疗服务 0.0 0.6 0.7 八、其他用品及服务 0.6 -0.9 -1.1    附注   1.指标解释   居民消费价格指数(Consumer Price Index,简称CPI)是度量居民生活消费品和服务价格水平随着时间变动的相对数,综合反映居民购买的生活消费品和服务价格水平的变动情况。   2.统计范围   居民消费价格统计调查涵盖全国城乡居民生活消费的食品烟酒、衣着、居住、生活用品及服务、交通通信、教育文化娱乐、医疗保健、其他用品及服务等8大类、268个基本分类的商品与服务价格。   3.调查方法   采用抽样调查方法抽选确定调查网点,按照“定人、定点、定时”的原则,直接派人到调查网点或从互联网采集原始价格。数据来源于全国31个省(区、市)约500个市县、近10万家价格调查点,包括商场(店)、超市、农贸市场、服务网点和互联网电商等。   4.数据说明   由于“四舍五入”原因,有时会出现合计数据与分类数据高值或低值相同的情况。   5.基期轮换   按照统计制度规定,我国CPI每五年进行一次基期轮换,2021年1月开始编制和发布以2020年为基期的CPI。与上轮基期相比,新基期的调查分类目录、代表规格品和调查网点均有调整,分类权数也有变化,以反映居民消费结构的*新变动。经测算,本次基期轮换对CPI各月同比指数的影响平均约为0.03个百分点。 

2021年5月份工业生产者出厂价格同比上涨9.0% 环比上涨1.6%

  2021年5月份,全国工业生产者出厂价格同比上涨9.0%,环比上涨1.6%;工业生产者购进价格同比上涨12.5%,环比上涨1.9%。1—5月平均,工业生产者出厂价格比去年同期上涨4.4%,工业生产者购进价格上涨5.9%。     一、工业生产者价格同比变动情况   工业生产者出厂价格中,生产资料价格上涨12.0%,涨幅比上月扩大2.9个百分点,影响工业生产者出厂价格总水平上涨约8.87个百分点。其中,采掘工业价格上涨36.4%,原材料工业价格上涨18.8%,加工工业价格上涨7.4%。生活资料价格上涨0.5%,涨幅扩大0.2个百分点,影响工业生产者出厂价格总水平上涨约0.14个百分点。其中,食品价格上涨2.2%,衣着价格下降0.6%,一般日用品价格上涨0.5%,耐用消费品价格下降0.8%。     工业生产者购进价格中,有色金属材料及电线类价格上涨28.0%,黑色金属材料类价格上涨26.8%,燃料动力类价格上涨20.7%,化工原料类价格上涨17.0%。   二、工业生产者价格环比变动情况   工业生产者出厂价格中,生产资料价格上涨2.1%,涨幅比上月扩大0.9个百分点,影响工业生产者出厂价格总水平上涨约1.58个百分点。其中,采掘工业价格上涨6.5%,原材料工业价格上涨2.1%,加工工业价格上涨1.7%。生活资料价格上涨0.1%,涨幅与上月相同,影响工业生产者出厂价格总水平上涨约0.03个百分点。其中,食品和衣着价格均持平,一般日用品价格上涨0.4%,耐用消费品价格上涨0.1%。   工业生产者购进价格中,黑色金属材料类价格上涨5.1%,有色金属材料及电线类价格上涨4.1%,燃料动力类价格上涨2.8%,化工原料类价格上涨1.3%。 2021年5月工业生产者价格主要数据    环比涨跌幅 (%) 同比涨跌幅 (%) 1—5月 同比涨跌幅(%) 一、工业生产者出厂价格 1.6 9.0 4.4   生产资料 2.1 12.0 5.8    采掘 6.5 36.4 15.4    原材料 2.1 18.8 9.0    加工 1.7 7.4 3.7   生活资料 0.1 0.5 0.1    食品 0.0 2.2 1.8    衣着 0.0 -0.6 -0.9    一般日用品 0.4 0.5 0.2    耐用消费品 0.1 -0.8 -1.3 二、工业生产者购进价格 1.9 12.5 5.9   燃料、动力类 2.8 20.7 5.4   黑色金属材料类 5.1 26.8 16.5   有色金属材料及电线类 4.1 28.0 17.3   化工原料类 1.3 17.0 7.1   木材及纸浆类 0.7 6.7 3.0   建筑材料及非金属类 2.1 4.6 0.0   其它工业原材料及半成品类 0.9 3.2 1.4   农副产品类 -0.2 6.9 5.8   纺织原料类 0.6 4.2 1.4 三、主要行业出厂价格       煤炭开采和洗选业 10.6 29.7 13.8 石油和天然气开采业 1.7 99.1 22.1 黑色金属矿采选业 7.4 48.0 34.5 有色金属矿采选业 2.2 16.7 13.0 非金属矿采选业 1.1 2.0 1.1 农副食品加工业 -0.2 5.6 5.1 食品制造业 0.4 1.6 0.9 酒、饮料及精制茶制造业 0.2 1.9 1.9 烟草制品业 0.1 0.5 0.5 纺织业 0.4 2.9 0.1 纺织服装、服饰业 -0.1 -0.3 -0.7 木材加工和木、竹、藤、棕、草制品业 0.3 0.8 0.2 造纸和纸制品业 0.4 7.4 3.0 印刷和记录媒介复制业 0.9 0.8 -0.4 石油、煤炭及其他燃料加工业 4.4 34.3 11.0 化学原料和化学制品制造业 1.8 20.9 10.8 医药制造业 0.1 -0.8 -0.3 化学纤维制造业 -0.9 18.4 8.2 橡胶和塑料制品业 0.2 3.2 1.1 非金属矿物制品业 1.3 2.4 -0.7 黑色金属冶炼和压延加工业 6.4 38.1 22.5 有色金属冶炼和压延加工业 4.4 30.4 19.7 金属制品业 1.6 7.0 3.7 通用设备制造业 0.3 0.9 0.3 汽车制造业 0.0 -0.7 -0.7 铁路、船舶、航空航天和其他运输设备制造业 0.3 0.4 0.1 计算机、通信和其他电子设备制造业 0.5 -0.8 -1.6 电力、热力生产和供应业 -0.3 -0.5 -1.0 燃气生产和供应业 -1.1 2.2 1.5 水的生产和供应业 0.2 1.1 0.7    附注   1.指标解释   工业生产者价格指数包括工业生产者出厂价格指数(Producer Price Index for Industrial Products, 简称PPI)和工业生产者购进价格指数。   工业生产者出厂价格指数反映工业企业产品*次出售时的出厂价格的变化趋势和变动幅度。   工业生产者购进价格指数反映工业企业作为中间投入产品的购进价格的变化趋势和变动幅度。   2.统计范围   工业生产者出厂价格统计调查涵盖40个工业行业大类、1300多个基本分类的工业产品价格;工业生产者购进价格统计调查涵盖9大类、800多个基本分类的工业产品价格。   3.调查方法   工业生产者价格调查采取重点调查与典型调查相结合的调查方法,涉及全国4万多家工业企业。   4.统计标准   工业行业划分标准的依据是《国民经济行业分类》(GB/T4754-2017)。   5.数据说明   由于“四舍五入”原因,有时会出现合计数据与分类数据高值或低值相同的情况。   6.基期轮换   根据统计制度规定,我国工业生产者价格统计调查每五年进行一次基期轮换,2021年1月开始编制和发布以2020年为基期的价格指数。与上轮基期相比,新基期的调查分类目录、代表规格品和调查企业均有调整,分类权数也有变化,以反映工业生产结构的*新变动。经测算,基期轮换对各月同比指数影响平均约为0.05个百分点,在统计可接受范围内。 

国家统计局城市司高级统计师董莉娟解读2021年5月份CPI和PPI数据

2021年5月份CPI总体稳定  PPI环比同比均上涨 ——国家统计局城市司高级统计师董莉娟解读2021年5月份CPI和PPI数据   国家统计局今天发布了2021年5月份全国CPI(居民消费价格指数)和PPI(工业生产者出厂价格指数)数据。对此,国家统计局城市司高级统计师董莉娟进行了解读。   一、CPI环比下降同比上涨   5月份,各地区各部门继续做好保供稳价各项工作,居民消费价格总体稳定。   从环比看,CPI下降0.2%,降幅比上月收窄0.1个百分点。其中,食品价格下降1.7%,降幅收窄0.7个百分点,影响CPI下降约0.31个百分点。食品中,生猪生产不断恢复,猪肉供给持续增加,价格继续下降11.0%;鲜菜和鲜果供应充足,价格分别下降5.6%和2.7%;受供给减少、需求增加及饲料成本上涨等因素影响,淡水鱼价格继续上涨9.9%;受蛋鸡存栏量下降、饲料成本上涨及气温升高产蛋率下降等因素影响,鸡蛋价格上涨3.3%。非食品价格上涨0.2%,涨幅与上月相同,影响CPI上涨约0.16个百分点。非食品中,因国际原油价格上涨,国内汽油和柴油价格分别上涨1.7%和1.9%;受原材料价格上涨影响,电冰箱、电视机、台式计算机和住房装潢材料等工业消费品价格均有上涨,涨幅在0.3%—1.2%之间;夏装新品上市,服装价格上涨0.3%。   从同比看,CPI上涨1.3%,涨幅比上月扩大0.4个百分点,主要是去年价格变动的翘尾影响扩大所致。其中,食品价格由上月下降0.7%转为上涨0.3%,影响CPI上涨约0.05个百分点。食品中,猪肉价格下降23.8%,降幅比上月扩大2.4个百分点;淡水鱼价格上涨33.7%,涨幅扩大7.3个百分点;鸡蛋和食用植物油价格分别上涨17.6%和9.2%,涨幅均有扩大。非食品价格上涨1.6%,涨幅比上月扩大0.3个百分点,影响CPI上涨约1.28个百分点。非食品中,交通通信价格上涨5.5%,涨幅扩大0.6个百分点,其中飞机票、汽油和柴油价格分别上涨32.3%、22.0%和24.2%;教育文化娱乐和居住价格分别上涨1.5%和0.7%,涨幅均有扩大。   据测算,在5月份1.3%的同比涨幅中,去年价格变动的翘尾影响约为0.7个百分点,比上月扩大0.6个百分点;新涨价影响约为0.6个百分点,比上月回落0.2个百分点。扣除食品和能源价格的核心CPI同比上涨0.9%,涨幅比上月扩大0.2个百分点。   二、PPI环比同比均上涨   5月份,国际原油、铁矿石、有色金属等大宗商品价格大幅上涨,国内需求稳定恢复,我国工业品价格继续上涨。   从环比看,PPI上涨1.6%,涨幅比上月扩大0.7个百分点。其中,生产资料价格上涨2.1%,涨幅扩大0.9个百分点;生活资料价格上涨0.1%,涨幅与上月相同。国际原油价格波动上行,带动国内石油相关行业价格上涨,其中石油和天然气开采业价格上涨1.7%,涨幅扩大1.3个百分点;石油、煤炭及其他燃料加工业价格由降转涨,上涨4.4%。受火电厂“迎峰度夏、增加储备”影响,动力煤需求较旺,拉动煤炭开采和洗选业价格上涨10.6%,涨幅扩大7.8个百分点。受铁矿石、焦炭等原材料成本上升影响,黑色金属冶炼和压延加工业价格上涨6.4%,涨幅扩大0.8个百分点。国际市场铜、铝等有色金属价格上涨较多,影响国内有色金属冶炼和压延加工业价格上涨4.4%,涨幅扩大2.3个百分点。此外,电气机械和器材制造业价格上涨1.3%,涨幅扩大0.4个百分点。   从同比看,PPI上涨9.0%,涨幅比上月扩大2.2个百分点。其中,生产资料价格上涨12.0%,涨幅扩大2.9个百分点;生活资料价格上涨0.5%,扩大0.2个百分点。主要行业价格涨幅均有所扩大,其中石油和天然气开采业价格上涨99.1%,扩大13.3个百分点;黑色金属冶炼和压延加工业价格上涨38.1%,扩大8.1个百分点;石油、煤炭及其他燃料加工业价格上涨34.3%,扩大10.5个百分点;有色金属冶炼和压延加工业价格上涨30.4%,扩大3.5个百分点;煤炭开采和洗选业价格上涨29.7%,扩大16.4个百分点;化学原料和化学制品制造业价格上涨20.9%,扩大3.4个百分点。   据测算,在5月份9.0%的同比涨幅中,去年价格变动的翘尾影响约为3.0个百分点,比上月扩大0.5个百分点;新涨价影响约为6.0个百分点,扩大1.7个百分点。 

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速