No description, website, or topics provided.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.gitignore
LICENSE
README.md

README.md

文章大纲,便于浏览

  1. 01-iOS程序启动过程
  2. 02-浅拷贝-深拷贝
  3. 03-View的生命周期
  4. 04-@property
  5. 05-事件传递和事件响应
  6. 06-KVC
  7. 07-KVO
  8. 08-iOS数据持久化方案

01-iOS-App-startup-process

###一、启动完整过程

1.main函数

2.UIApplicationMain函数

  • 创建UIApplication对象
  • 创建UIApplication的delegate对象

3.delegate对象开始处理(监听)系统事件(没有storyboard)

  • 程序启动完毕的时候, 就会调用代理的:didFinishLaunchingWithOptions:方法
  • application:didFinishLaunchingWithOptions:中创建UIWindow 创建和设置UIWindowrootViewController
  • 显示窗口

4.根据Info.plist获得最主要storyboard的文件名,加载最主要的storyboard(有storyboard)

  • 创建UIWindow
  • 创建和设置UIWindowrootViewController
  • 显示窗口

###二、程序启动原理

1.main函数中执行了一个UIApplicationMain这个函数

int UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);

argc、argv:直接传递给UIApplicationMain进行相关处理即可

2.principalClassName:指定应用程序类名(app的象征),该类必须是UIApplication(或子类)。如果为nil,则用UIApplication类作为默认值

3.delegateClassName:指定应用程序的代理类,该类必须遵守UIApplicationDelegate协议

4.UIApplicationMain函数会根据principalClassName创建UIApplication对象,根据delegateClassName创建一个delegate对象,并将该delegate对象赋值给UIApplication对象中的delegate属性

5.接着会建立应用程序的Main Runloop(事件循环),进行事件的处理(首先会在程序完毕后调用delegate对象的application:didFinishLaunchingWithOptions:方法)

程序正常退出时UIApplicationMain函数才返回

int main(int argc, char * argv[]){ @autoreleasepool { 

/**
* argc: 系统或者用户传入的参数个数
* argv: 系统或者用户传入的实际参数 
* 1.根据传入的第三个参数创建UIApplication对象 
* 2.根据传入的第四个产生创建UIApplication对象的代理
* 3.设置刚刚创建出来的代理对象为UIApplication的代理 
* 4.开启一个事件循环 
**/ 
return UIApplicationMain(argc, argv, @"UIApplication", @"YYAppDelegate"); }}

启动与代理:

02-ShallowCopy-DeepCopy

简要总结一下什么是浅拷贝,什么是深拷贝

深拷贝就是内容拷贝(分为单层拷贝、完全拷贝),深拷贝的之所以分为两类,主要是针对集合类

浅拷贝就是指针拷贝

####一.系统对象的 copy/mutableCopy

NSString *string = @"LiMing";
    
NSString *copyString = [string copy];
    
NSString *mutableString = [string mutableCopy];
    
NSLog(@"string = %p",string);
    
NSLog(@"copyString = %p",copyString);
    
NSLog(@"mutableString = %p ",mutableString);

结论:
1.string 和 copyString 他们只是二个不同的指针,指向内存中的同一块地址,copy 只是指针复制
2.string 和 mutableString 打印出来的地址不同,是因为两个指针指向的地址本就不同,mutableCopy 是内容复制

注意:其他对象 NSArrayNSMutableArrayNSDictionaryNSMutableDictionary 一样适用

规律可以从这张图看出来

####二.自定义对象实现 Copy-MutableCopy

  • copy
GZQPerson *person = [[GZQPerson alloc] init];
person.age = 20;
person.name = @"GZQ";
GZQPerson *copyP = [person copy];  // 这里崩溃

崩溃:

看崩溃信息GZQPerson应该先实现:

- (id)copyWithZone:(NSZone *)zone;

测试:

#import "GZQPerson.h"

@interface GZQPerson ()<NSCopying,NSMutableCopying>

@end

@implementation GZQPerson

- (id)copyWithZone:(NSZone *)zone {

    GZQPerson *person = [[[self class] allocWithZone:zone] init];
    person.age = self.age;
    person.name = self.name;
    return person;
}

- (id)mutableCopyWithZone:(NSZone *)zone {

    GZQPerson *person = [[[self class] allocWithZone:zone] init];
    person.age = self.age;
    person.name = self.name;
    return person;
}

@end

#import "ViewController.h"
#import "GZQPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    
    [super viewDidLoad];

    GZQPerson *person = [[GZQPerson alloc] init];
    person.age = 20;
    person.name = @"GZQ";
    GZQPerson *copyP = [person copy];
    
    NSLog(@"copyP=%p",copyP);
    NSLog(@"person=%p",person);
    NSLog(@"person=%p",copyP.name);
    NSLog(@"person=%p",person.name);
    
}
@end

可以看出虽然指针的地址不同,但是存储的地址是一致的。

####三.copy 本质

property copy 实际上就对name干了这个:

#import <Foundation/Foundation.h>

property copy 实际上就对name干了这个:

- (void)setName:(NSString *)name
{
    _name = [name copy];
}

strong是不执行Copy操作的

@property (nonatomic, strong) NSString *name;

NSMutableString *string = [NSMutableString stringWithFormat:@"深拷贝-浅拷贝"];

GZQPerson *person = [[GZQPerson alloc] init];
person.name = string;

// 可以改变person.name的值,因为其内部没有生成新的对象
[string appendString:@"LALALA"];

NSLog(@"name = %@", person.name);

####四.集合类 Copy MutableCopy 操作

单层深复制,也就是我们经常说的深复制,我这里说的单层深复制是对于集合类所说的(即NSArray,NSDictionary,NSSet),单层深复制指的是只复制了该集合类的最外层,里边的元素没有复制,(即这两个集合类的地址不一样,但是两个集合里所存储的元素的地址是一样的)

完全复制,指的是完全复制整个集合类,也就是说两个集合地址不一样,里边所存储的元素地址也不一样

实现多层完全拷贝也很简单

 NSArray *copyArray = [[NSArray alloc] initWithArray:array copyItems:YES];  // 完全复制

03-View的生命周期

  • 读懂这一张图即可

04-@property

@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

@property 的本质.

@property = ivar + getter + setter;

下面解释下:

“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。

“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为 Objective-C 2.0 的一部分。 而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。 正因为有了这种严格的命名规范,所以 Objective-C 这门语言才能根据名称自动创建出存取方法。其实也可以把属性当做一种关键字,其表示:

编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。 所以你也可以这么说:

@property = getter + setter;

例如下面这个类:

@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

上述代码写出来的类与下面这种写法等效:

@interface Person : NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;
- (NSString *)lastName;
- (void)setLastName:(NSString *)lastName;
@end

propertyruntime中是objc_property_t定义如下:

typedef struct objc_property *objc_property_t;

objc_property是一个结构体,包括nameattributes,定义如下:

struct property_t {
    const char *name;
    const char *attributes;
};

attributes本质是objc_property_attribute_t,定义了property的一些属性,定义如下:

/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

而attributes的具体内容是什么呢?其实,包括:类型,原子性,内存语义和对应的实例变量。

例如:我们定义一个stringproperty

@property (nonatomic, copy) NSString *string;

通过 property_getAttributes(property)获取到attributes并打印出来之后的结果为

T@"NSString",C,N,V_string

其中T就代表类型,可参阅Type EncodingsC就代表CopyN代表nonatomicV就代表对于的实例变量。

ivar、getter、setter 是如何生成并添加到这个类中的?

“自动合成”( autosynthesis)

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译 器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。在前例中,会生成两个实例变量,其名称分别为 _firstName 与 _lastName。也可以在类的实现代码里通过@synthesize 语法来指定实例变量的名字.

@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end

我为了搞清属性是怎么实现的,曾经反编译过相关的代码,他大致生成了五个东西

1. OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
2. setter 与 getter 方法对应的实现函数
3. ivar_list :成员变量列表
4. method_list :方法列表
5. prop_list :属性列表
也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.

属性可以拥有的特质分为四类:

  • 原子性--- nonatomic 特质,在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用自旋锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。

  • 读/写权限---readwrite(读写)、readonly (只读)

  • 内存管理语义---assign、strong、 weak、unsafe_unretained、copy

  • 方法名---getter= 、setter=

getter=的样式:

@property (nonatomic, getter=isOn) BOOL on;
     
(`setter=`这种不常用,也不推荐使用。故不在这里给出写法。)

setter=一般用在特殊的情境下,比如

在数据反序列化、转模型的过程中,服务器返回的字段如果以 init 开头,所以你需要定义一个 init 开头的属性,但默认生成的 setter 与 getter 方法也会以 init 开头,而编译器会把所有以 init 开头的方法当成初始化方法,而初始化方法只能返回 self 类型,因此编译器会报错。

这时你就可以使用下面的方式来避免编译器报错:

@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;

另外也可以用关键字进行特殊说明,来避免编译器报错

@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;

- (NSString *)initBy __attribute__((objc_method_family(none)));

1. 不常用的:nonnull,null_resettable,nullable

注意:很多人会认为如果属性具备 nonatomic 特质,则不使用 “同步锁”。其实在属性设置方法中使用的是自旋锁,自旋锁相关代码如下:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) 
    {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) 
    {
        newValue = [newValue copyWithZone:nil];
    } 
    else if (mutableCopy) 
    {
        newValue = [newValue mutableCopyWithZone:nil];
    } 
    else 
    {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) 
    {
        oldValue = *slot;
        *slot = newValue;
    } 
    else 
    {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

05-事件传递和事件响应

这部分知识如果自己总结,篇幅较长。可以参考以下文章

06-KVC

Key-Value Coding (KVC)

KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。目前网上关于KVC的文章在非常多,有的只是简单地说了下用法,有的讲得深入但是在使用场景和最佳实践没有说明,我写下这遍文章就是给大家详解一个最完整最详细的KVC。

KVC在iOS中的定义

无论是Swift还是Objective-CKVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要)所以对于所有继承了NSObject在类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC最为重要的四个方法

- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

当然NSKeyValueCoding类别中还有其他的一些方法,下面列举一些

+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,只不过是设值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。

上面的这些方法在碰到特殊情况或者有特殊需求还是会用到的,所以也是可以了解一下。后面的代码示例会有讲到其中的一些方法。 同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。建议有基础的或者英文好的开发者直接去看苹果的官方文档,相信你会对KVC的理解更上一个台阶。

KVC是怎么寻找Key的

KVC是怎么使用的,我相信绝大多数的开发者都很清楚,我在这里就不再写简单的使用KVC来设值和取值的代码了,首页我们来探讨KVC在内部是按什么样的顺序来寻找key的。 当调用setValue:属性值 forKey:``@”name“的代码时,底层的执行机制如下:

  • 程序优先调用set<Key>:属性值方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大清写要符合KVC的全名规则,下同

  • 如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUNdefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为_<key>的成员变量,无论该变量是在类接口部分定义,还是在类实现部分定义,也无论用了什么样的访问修饰符,只在存在以_<key>命名的变量,KVC都可以对该成员变量赋值。

  • 如果该类即没有set<Key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量,

  • 和上面一样,如果该类即没有set<Key>:方法,也没有_<key>_is<Key>成员变量,KVC机制再会继续搜索<key>is<Key>的成员变量。再给它们赋值。

  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUNdefinedKey:方法,默认是抛出异常。

如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用setValue:forUNdefinedKey:方法。

07-KVO

KVO,全称为Key-Value Observing,是iOS中的一种设计模式,用于检测对象的某些属性的实时变化情况并作出响应。当应用场景比较复杂时,多个地方存在crash的危险。

首先,假设我们的目标是在一个UITableViewController内对tableviewcontentOffset进行实时监测,很容易地使用KVO来实现为。

在初始化方法中加入:

[_tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];

// 在dealloc中移除KVO监听:
[_tableView removeObserver:self forKeyPath:@"contentOffset" context:nil];

// 添加默认的响应回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object
                        change:(NSDictionary *)change 
                       context:(void *)context
{
    [self doSomethingWhenContentOffsetChanges];
}

好了,KVO实现就到此完美结束了,开玩笑,肯定没这么简单的,这样的代码太粗糙了,当你在controller中添加多个KVO时,所有的回调都是走同上述函数,那就必须对触发回调函数的来源进行判断。判断如下:

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object
                        change:(NSDictionary *)change 
                       context:(void *)context
{
    if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) 
    {
        [self doSomethingWhenContentOffsetChanges];
    }
}

你以为这样就结束了吗?答案是否定的!我们假设当前类(在例子中为UITableViewController)还有父类,并且父类也有自己绑定了一些其他KVO呢?我们看到,上述回调函数体中只有一个判断,如果这个if不成立,这次KVO事件的触发就会到此中断了。但事实上,若当前类无法捕捉到这个KVO,那很有可能是在他的superClass,或者super-superClass...中,上述处理砍断了这个链。合理的处理方式应该是这样的:

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object
                        change:(NSDictionary *)change 
                       context:(void *)context
{
    if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) 
    {
        [self doSomethingWhenContentOffsetChanges];
    } 
    else 
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

这样就结束了吗?答案仍旧是否定的。潜在的问题有可能出现在dealloc中对KVO的注销上。KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个kvo,父类在deallocremove了一次,子类又remove了一次的情况下。不要以为这种情况很少出现!当你封装framework开源给别人用或者多人协作开发时是有可能出现的,而且这种crash很难发现。不知道你发现没,目前的代码中context字段都是nil,那能否利用该字段来标识出到底kvosuperClass注册的,还是self注册的?

回答是可以的。我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context@"ThisIsMyKVOContextNotSuper";然后在deallocremove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash

08-iOS数据持久化方案

存储方案

  • plist文件(属性列表)
  • preference(偏好设置)
  • NSKeyedArchiver(归档)
  • SQLite 3
  • CoreData

沙盒

iOS程序默认情况下只能访问程序自己的目录,这个目录被称为“沙盒”。

1.结构

沙盒的目录结构如下:

"应用程序包"
Documents
Library
    Caches
    Preferences
tmp

2.目录特性

虽然沙盒中有这么多文件夹,但是每个文件夹都不尽相同,都有各自的特性。所以在选择存放目录时,一定要认真选择适合的目录。

"应用程序包": 这里面存放的是应用程序的源文件,包括资源文件可执行文件

  • Documents: 最常用的目录,iTunes同步该应用时会同步此文件夹中的内容,适合存储重要数据。
  NSString *path = [[NSBundle mainBundle] bundlePath];
  NSLog(@"%@", path);
  • Library/Caches: iTunes不会同步此文件夹,适合存储体积大,不需要备份的非重要数据。
  NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
  NSLog(@"%@", path);
  • Library/Preferences: iTunes同步该应用时会同步此文件夹中的内容,通常保存应用的设置信息。
  NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
  NSLog(@"%@", path);
  • tmp: iTunes不会同步此文件夹,系统可能在应用没运行时就删除该目录下的文件,所以此目录适合保存应用中的一些临时文件,用完就删除。
  NSString *path = NSTemporaryDirectory();
  NSLog(@"%@", path);

plist文件

plist文件是将某些特定的类,通过XML文件的方式保存在目录中。

可以被序列化的类型只有如下几种:

NSArray;
NSMutableArray;
NSDictionary;
NSMutableDictionary;
NSData;
NSMutableData;
NSString;
NSMutableString;
NSNumber;
NSDate;

1.获得文件路径

NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];

2.存储

NSArray *array = @[@"123", @"456", @"789"];
[array writeToFile:fileName atomically:YES];

3.读取

NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);

4.注意

// 只有以上列出的类型才能使用plist文件存储。
// 存储时使用writeToFile: atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。
// 读取时使用arrayWithContentsOfFile:方法。

Preference

1.使用方法

//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//2.向文件中写入内容
[userDefaults setObject:@"AAA" forKey:@"a"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];

//2.1立即同步
[userDefaults synchronize];

//3.读取文件
NSString *name = [userDefaults objectForKey:@"a"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
NSLog(@"%@, %d, %ld", name, sex, age);

2.注意

// 偏好设置是专门用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。
// 如果没有调用synchronize方法,系统会根据I/O情况不定时刻地保存到文件中。所以如果需要立即写入文件的就必须调用synchronize方法。
// 偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。

NSKeyedArchiver

归档在iOS中是另一种形式的序列化,只要遵循了NSCoding协议的对象都可以通过它实现序列化。由于决大多数支持存储数据的Foundation和Cocoa Touch类都遵循了NSCoding协议,因此,对于大多数类来说,归档相对而言还是比较容易实现的。

1.遵循NSCoding协议

NSCoding协议声明了两个方法,这两个方法都是必须实现的。一个用来说明如何将对象编码到归档中,另一个说明如何进行解档来获取一个新对象。

遵循协议和设置属性

  //1.遵循NSCoding协议 
  @interface Person : NSObject   //2.设置属性
  @property (strong, nonatomic) UIImage *avatar;
  @property (copy, nonatomic) NSString *name;
  @property (assign, nonatomic) NSInteger age;
  @end

实现协议方法

  //解档
  - (id)initWithCoder:(NSCoder *)aDecoder {
      if ([super init]) {
          self.avatar = [aDecoder decodeObjectForKey:@"avatar"];
          self.name = [aDecoder decodeObjectForKey:@"name"];
          self.age = [aDecoder decodeIntegerForKey:@"age"];
      }
      return self;
  }
  
  //归档
  - (void)encodeWithCoder:(NSCoder *)aCoder {
      [aCoder encodeObject:self.avatar forKey:@"avatar"];
      [aCoder encodeObject:self.name forKey:@"name"];
      [aCoder encodeInteger:self.age forKey:@"age"];
  }

特别注意

如果需要归档的类是某个自定义类的子类时,就需要在归档和解档之前先实现父类的归档和解档方法。即 [super encodeWithCoder:aCoder] 和 [super initWithCoder:aDecoder] 方法;

2.使用

需要把对象归档是调用NSKeyedArchiver的工厂方法 archiveRootObject: toFile: 方法。

  NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
  Person *person = [[Person alloc] init];
  person.avatar = self.avatarView.image;
  person.name = self.nameField.text;
  person.age = [self.ageField.text integerValue];
  [NSKeyedArchiver archiveRootObject:person toFile:file];

需要从文件中解档对象就调用NSKeyedUnarchiver的一个工厂方法 unarchiveObjectWithFile: 即可。

  NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
  Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
  if (person) 
  {
     self.avatarView.image = person.avatar;
     self.nameField.text = person.name;
     self.ageField.text = [NSString stringWithFormat:@"%ld", person.age];
  }

3.注意

必须遵循并实现NSCoding协议
保存文件的扩展名可以任意指定
继承时必须先调用父类的归档解档方法

SQLite3

之前的所有存储方法,都是覆盖存储。如果想要增加一条数据就必须把整个文件读出来,然后修改数据后再把整个内容覆盖写入文件。所以它们都不适合存储大量的内容。

1.字段类型

表面上·SQLite·将数据分为以下几种类型:

integer : 整数
real : 实数(浮点数)
text : 文本字符串
blob : 二进制数据,比如文件,图片之类的

实际上SQLite是无类型的。即不管你在创表时指定的字段类型是什么,存储是依然可以存储任意类型的数据。而且在创表时也可以不指定字段类型。SQLite之所以什么类型就是为了良好的编程规范和方便开发人员交流,所以平时在使用时最好设置正确的字段类型!主键必须设置成integer

2. 准备工作

准备工作就是导入依赖库啦,在iOS中要使用SQLite3,需要添加库文件:libsqlite3.dylib并导入主头文件,这是一个C语言的库,所以直接使用SQLite3还是比较麻烦的。

3.使用

1.创建数据库并打开

操作数据库之前必须先指定数据库文件和要操作的表,所以使用SQLite3,首先要打开数据库文件,然后指定或创建一张表。

//  打开数据库并创建一个表
- (void)openDatabase 
{
   //1.设置文件名
   NSString *filename = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
   
   //2.打开数据库文件,如果没有会自动创建一个文件
   NSInteger result = sqlite3_open(filename.UTF8String, &_sqlite3);
   
   if (result == SQLITE_OK) 
   {
       NSLog(@"打开数据库成功!");
       
       //3.创建一个数据库表
       char *errmsg = NULL;
       sqlite3_exec(_sqlite3, "CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)", NULL, NULL, &errmsg);
       
       if (errmsg) 
       {
           NSLog(@"错误:%s", errmsg);
       } 
       else 
       {
           NSLog(@"创表成功!");
       }
   } 
   else 
   {
       NSLog(@"打开数据库失败!");
   }
}
2.执行指令

使用 sqlite3_exec() 方法可以执行任何SQL语句,比如创表、更新、插入和删除操作。但是一般不用它执行查询语句,因为它不会返回查询到的数据。

// 往表中插入1000条数据
- (void)insertData 
{
    NSString *nameStr;
    NSInteger age;
    
    for (NSInteger i = 0; i < 1000; i++) 
    {
      nameStr = [NSString stringWithFormat:@"Bourne-%d", arc4random_uniform(10000)];
      age = arc4random_uniform(80) + 20;
      NSString *sql = [NSString stringWithFormat:@"INSERT INTO t_person (name, age) VALUES('%@', '%ld')", nameStr, age];
      char *errmsg = NULL;
      sqlite3_exec(_sqlite3, sql.UTF8String, NULL, NULL, &errmsg);
      if (errmsg) 
      {
          NSLog(@"错误:%s", errmsg);
      }
    }
    NSLog(@"插入完毕!");   
}
3.查询指令

前面说过一般不使用 sqlite3_exec() 方法查询数据。因为查询数据必须要获得查询结果,所以查询相对比较麻烦。示例代码如下:

// sqlite3_prepare_v2() : 检查sql的合法性
// sqlite3_step() : 逐行获取查询结果,不断重复,直到最后一条记录
// sqlite3_coloum_xxx() : 获取对应类型的内容,iCol对应的就是SQL语句中字段的顺序,从0开始。根据实际查询字段的属性,使用sqlite3_column_xxx取得对应的内容即可。
// sqlite3_finalize() : 释放stmt

// 从表中读取数据到数组中
- (void)readData 
{
   NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1000];
   char *sql = "select name, age from t_person;";
   sqlite3_stmt *stmt;
   NSInteger result = sqlite3_prepare_v2(_sqlite3, sql, -1, &stmt, NULL);
   
   if (result == SQLITE_OK) 
   {
       while (sqlite3_step(stmt) == SQLITE_ROW) 
       {
           char *name = (char *)sqlite3_column_text(stmt, 0);
           NSInteger age = sqlite3_column_int(stmt, 1);
           //创建对象
           Person *person = [Person personWithName:[NSString stringWithUTF8String:name] Age:age];
           [mArray addObject:person];
       }
       self.dataList = mArray;
   }
   sqlite3_finalize(stmt);
}

4.总结

总得来说,SQLite3的使用还是比较麻烦的,因为都是些c语言的函数,理解起来有些困难。不过在一般开发过程中,使用的都是第三方开源库 FMDB,封装了这些基本的c语言方法,使得我们在使用时更加容易理解,提高开发效率。

FMDB

1.简介

FMDB是iOS平台的SQLite数据库框架,它是以OC的方式封装了SQLite的C语言API,它相对于cocoa自带的C语言框架有如下的优点: 使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码 对比苹果自带的Core Data框架,更加轻量级和灵活 提供了多线程安全的数据库操作方法,有效地防止数据混乱

2.核心类

FMDB有三个主要的类:

// FMDatabase
一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句

// FMResultSet
使用FMDatabase执行查询后的结果集

// FMDatabaseQueue
用于在多线程中执行多个查询或更新,它是线程安全的

3.打开数据库

和c语言框架一样,FMDB通过指定SQLite数据库文件路径来创建FMDatabase对象,但FMDB更加容易理解,使用起来更容易,使用之前一样需要导入sqlite3.dylib。打开数据库方法如下:

NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.db"];
FMDatabase *database = [FMDatabase databaseWithPath:path]; 
   
if (![database open]) 
{
    NSLog(@"数据库打开失败!");
}

值得注意的是,Path的值可以传入以下三种情况:

// 具体文件路径,如果不存在会自动创建
// 空字符串@"",会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除
// nil,会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁

4.更新

在FMDB中,除查询以外的所有操作,都称为“更新”, 如:create、drop、insert、update、delete等操作,使用executeUpdate:方法执行更新:

//常用方法有以下 3 种:   
- (BOOL)executeUpdate:(NSString*)sql, ...

- (BOOL)executeUpdateWithFormat:(NSString*)format, ...

- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments

//示例
[database executeUpdate:@"CREATE TABLE IF NOT EXISTS t_person(id integer primary key autoincrement, name text, age integer)"]; 
  
//或者  
[database executeUpdate:@"INSERT INTO t_person(name, age) VALUES(?, ?)", @"Bourne", [NSNumber numberWithInt:42]];

5.查询

查询方法也有3种,使用起来相当简单:

- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments

查询示例:

//1.执行查询
FMResultSet *result = [database executeQuery:@"SELECT * FROM t_person"];

//2.遍历结果集
while ([result next]) 
{
    NSString *name = [result stringForColumn:@"name"];
    int age = [result intForColumn:@"age"];
}

6.线程安全

在多个线程中同时使用一个 FMDatabase 实例是不明智的。不要让多个线程分享同一个FMDatabase实例,它无法在多个线程中同时使用。 如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题。所以,请使用 FMDatabaseQueue,它是线程安全的。以下是使用方法:

创建队列。

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

// 使用队列
[queue inDatabase:^(FMDatabase *database)
{    
          [database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_1", [NSNumber numberWithInt:1]];    
          [database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_2", [NSNumber numberWithInt:2]];    
          [database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_3", [NSNumber numberWithInt:3]];      
          FMResultSet *result = [database executeQuery:@"select * from t_person"];    
         while([result next]) {   
         }    
}];

// 而且可以轻松地把简单任务包装到事务里:
[queue inTransaction:^(FMDatabase *database, BOOL *rollback) {    
          [database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_1", [NSNumber numberWithInt:1]];    
          [database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_2", [NSNumber numberWithInt:2]];    
          [database executeUpdate:@"INSERT INTO t_person(name, age) VALUES (?, ?)", @"Bourne_3", [NSNumber numberWithInt:3]];      
          FMResultSet *result = [database executeQuery:@"select * from t_person"];    
             while([result next]) {   
             }   
           //回滚
           *rollback = YES;  
    }];

FMDatabaseQueue 后台会建立系列化的GCD队列,并执行你传给GCD队列的块。这意味着 你从多线程同时调用调用方法,GCD也会按它接收的块的顺序来执行了。