iOS面试旗开得胜之答案篇

以下问题的答案是之前写的一篇文章 iOS面试旗开得胜之问题篇 现在把问题的答案整理了一份出来给大家。希望对大家有所帮助。如果整理的答案有问题,请联系我。shavekevin@gmail.com

1、属性readwrite,readonly,assign,retain,copy,nonatomic 各自什么作用,他们在那种情况下用?

  • readwrite:默认的属性,可读可写,生成setter和getter方法。

  • readonly:只读,只生成getter方法,也就是说不能修改变量。

  • assign:用于声明基本数据类型(int、float)仅设置变量,是赋值属性。

  • retain:持有属性,setter方法将传入的参数先保留,再赋值,传入的参数 引用计数retaincount 会加1

    在堆上开辟一块空间,用指针a指向,然后将指针a赋值(assign)给指针b,等于是a和b同时指向这块堆空间,当a不使用这块堆空间的时候,是否要释放这块堆空间?答案是肯定要的,但是这件堆空间被释放后,b就成了野指针。

    如何避免这样的问题? 这就引出了引用计数器,当a指针这块堆空间的时候,引用计数器+1,当b也指向的时候,引用计数器变成了2,当a不再指向这块堆空间时,release-1,引用计数器为1,当b也不指向这块堆空间时,release-1,引用计数器为0,调用dealloc函数,空间被释放

    总结:当数据类型为int,float原生类型时,可以使用assign。如果是上面那种情况(对象)就是用retain。

  • copy:是赋值特性,setter方法将传入对象赋值一份;需要完全一份新的变量时,直接从堆区拿。

    当属性是 NSString、NSArray、NSDictionary时,既可以用strong 修饰,也可以用copy修饰。当用strong修饰的NSString 指向一个NSMutableString时,如果在不知情的情况下这个NSMutableString的别的引用修改了值,就会出现:一个不可变的字符串却被改变了的情况, 使用copy就不会出现这种情况。

  • nonatomic:非原子性,可以多线程访问,效率高。

  • atomic:原子性,属性安全级别的表示,同一时刻只有一个线程访问,具有资源的独占性,但是效率很低。

  • strong:强引用,引用计数+ 1,ARC下,一个对象如果没有强引用,系统就会释放这个对象。

  • weak:弱引用,不会使引用计数+1.当一个指向对象的强引用都被释放时,这块空间依旧会被释放掉。

    使用场景:在ARC下,如果使用XIB 或者SB 来创建控件,就使用 weak。纯代码创建控件时,用strong修饰,如果想用weak 修饰,就需要先创建控件,然后赋值给用weak修饰的对象。

    查找了一些资料,发现主要原因是,controller需要拥有它自己的view(这个view是所以子控件的父view),因此viewcontroller对view就必须是强引用(strong reference),得用strong修饰view。对于lable,它的父view是view,view需要拥有label,但是controller是不需要拥有label的。如果用strong修饰,在view销毁的情况下,label还仍然占有内存,因为controller还对它强引用;如果用wak修饰,在view销毁的时label的内存也同时被销毁,避免了僵尸指针出现。

    用引用计数回答就是:因为Controller并不直接“拥有”控件,控件由它的父view“拥有”。使用weak关键字可以不增加控件引用计数,确保控件与父view有相同的生命周期。控件在被addSubview后,相当于控件引用计数+1;父view销毁后,所有的子view引用计数-1,则可以确保父view销毁时子view立即销毁。weak的控件在removeFromSuperview后也会立即销毁,而strong的控件不会,因为Controller还保有控件强引用。

    总结归纳为:当控件的父view销毁时,如果你还想继续拥有这个控件,就用srtong;如果想保证控件和父view拥有相同的生命周期,就用weak。当然在大多数情况下用两个都是可以的。

    使用weak的时候需要特别注意的是:先将控件添加到superview上之后再赋值给self,避免控件被过早释放。

2、Objective-C如何对内存管理的,说说你的看法以及你遇到的问题以及解决方法?

​ Objective-C使用引用计数来管理内存,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。

  • MRC 手动内存计数 (Reference Counted)

    retain 递增保留计数

    release 递减保留计数

    对象被创建出来,对象的保留计数至少为1,若想令某对象继续存活,则调用retain方法。要是不想令其继续存活,就调用release或autorelease。当保留计数归零时,对象就回收了(deallocated),系统会将其占用的内存标记为“可重用”。(拖对象保留计数为1 的时候 调用release或autorelease,不会让计数为0,会直接释放,因为这样可以省一步操作)

    ​注意:对象创建出来之后,并不是说对象此时的保留计数必定是1.在alloc或initWith方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于1.绝不应该说保留计数一定是某个值,只能说你执行的操作是递增了引用计数或递减了引用计数。

    autoreleasepool(自动释放池)

    调用autorelease时,对象的引用计数不会马上递减,而是先对象放进自动释放池,通常是在下一次“事件循环”时递减。

    因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以NSLog语句在使用str对象前不需要手工保留。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留。

  • ARC 自动内存计数(Garbage Collection)

    使用ARC时,引用计数实际还是要执行的,只不过保留与释放操作现在是由ARC自动添加。

    这种方式和java类似,在你的程序的执行过程中。始终有一个高人在背后准确地帮你收拾垃圾,你不用考虑它什么时候开始工作,怎样工作。你只需要明白,我申请了一段内存空间,当我不再使用从而这段内存成为垃圾的时候,我就彻底的把它忘记掉,反正那个高人会帮我收拾垃圾。遗憾的是,那个高人需要消耗一定的资源。

    ARC在调用这些方法时(retain、release、autorelease、dealloc),并不通过普通的Objective-C消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objc_retain。这也是不能覆写retain、release、autorelease的原因。

3、内存管理的几条原则时什么?按照默认法则.哪些关键字生成的对象需要手动释放?在和property结合的时候如何有效的避免内存泄露?

  • 谁申请,谁释放  遵循Cocoa Touch的使用原则;  内存管理主要要避免“过早释放”和“内存泄漏”,对于“过早释放”需要注意@property设置特性时,一定要用对特性关键字,对于“内存泄漏”,一定要申请了要负责释放,要细心。 关键字alloc 或new 生成的对象需要手动释放;  设置正确的property属性,对于retain需要在合适的地方释放,

  • 使用new、alloc或copy方法创建一个对象时,该对象引用计数器为1。如果不需要使用该对象,可以向其发送release或autorelease消息,在其使用完毕时被销毁。

    ​ 如果通过其他方法获取一个对象,则可以假设这个对象引用计数为1,并且被设置为autorelease,不需要对该对象进行清理,如果确实需要retain这个对象,则需要使用完毕后release。

    ​ 如果retain了某个对象,需要release或autorelease该对象,保持retain方法和release方法使用次数相等。

    ​ 使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放。设置为autorelease的对象不需要手动释放,会直接进入自动释放池。

4、MVC设计模式是什么? 你还熟悉什么设计模式?他们和MVC有什么不同的地方?

  • MVC设计模式是什么

    MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

    Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。   通常模型对象负责在数据库中存取数据。

    View(视图)是应用程序中处理数据显示的部分。   通常视图是依据模型数据创建的。

    Controller(控制器)是应用程序中处理用户交互的部分。   通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

  • 你还熟悉什么设计模式?

    • 代理模式

    解决什么场景:当一个类的某些功能需要被别人来实现,但是既不明确是些什么功能,又不明确谁来实现这些功能的时候,委托代理模式就可以派上用场。

    在cocoa框架中的Delegate模式中,委托人往往是框架中的对象(视图中的控件、表视图神马的),代理人往往是视图控制器对象。

    自定义一个delegate模式

    @interface A:UIView
    id transparendValueDelegate;
    @property(nomatic, retain) id transparendValueDelegate;

    @end

    @implementation A
    @synthesize transparendValueDelegate

    -(void)Call
    { 
    NSString* value = @"你好";
    [transparendValueDelegate transparendValue: value];
    }

    @end
    @interface B:UIView

    NSString* value;
    @end

    @implementation B
    -(void)transparendValue:(NSString*)fromValue
    {
    value = fromValue;
    NSLog(@"%@ ,我是B",value); 
    }
    @end

使用时:

    A* a = [[A alloc] init];
    B* b = [[B alloc] init];
    a. transparendValueDelegate = b;//设置A代理委托对象为B
    [a Call];

这样就会输出:

**你好,我是B**

 委托模式关键就在于一个“**被”**字。这个B是很被动的,随时就会被你A Call一下。
  • 观察者模式

    观察者模式本质上时一种发布-订阅模型,用以消除具有不同行为的对象之间的耦合,通过这一模式,不同对象可以协同工作,同时它们也可以被复用于其他地方Observer从Subject订阅通知,ConcreteObserver实现重现ObServer并将其重载其update方法。一旦SubJect的实例需要通知Observer任何新的变更,Subject会发送update消息来通知存储在其内部类中所注册的Observer、在ConcreteObserverupdate方法的实际实现中,Subject的内部状态可被取得并进行后续处理。

    通知

    在Cocoa Touch框架中NSNotificationCenterNSNotification对象实现了一对多的模型。通过NSNotificationCenter可以让对象之间进行通讯,即便这些对象之间并不认识。

    KVO

    KVO是Cocoa提供的一种称为键值观察的机制,对象可以通过它得到其他对象特定属性的变更通知。而这个机制是基于NSKeyValueObserving非正式些,Cocoa通过这个协议为所有遵循协议的对象提供了一种自动化的属性监听的功能。

    虽然通知KVO都可以对观察者进行实现,但是他们之间还是略有不同的,由上面的例子我们可以看出通知是由一个中心对象为所有观察者提供变更通知,主要是广义上关注程序事件,而KVO则是被观察的对象直接想观察者发送通知,主要是绑定于特定对象属性的值。

  • 单例模式

    单例设计模式确保对于一个给定的类只有一个实例存在,这个实例有一个全局唯一的访问点。它通常采用懒加载的方式在第一次用到实例的时候再去创建它。

    注意:苹果大量使用了此模式。例如:[NSUserDefaults standardUserDefaults], [UIApplication sharedApplication], [UIScreen mainScreen], [NSFileManager defaultManager],所有的这些方法都返回一个单例对象。

    有一些情况下,只有一个实例显得非常合理。举例来说,你不需要有多个Logger的实例,除非你想去写多个日志文件。或者一个全局的配置处理类:实现线程安全的方式访问共享实例是容易的,比如一个配置文件,有好多个类同时修改这个文件。

  • 工厂模式

    正式的解释是:在基类中定义创建对象的一个接口,让子类决定实例化哪个类。工厂方法让一个类的实例化延迟到子类中进行。工厂方法要解决的问题是对象的创建时机,它提供了一种扩展的策略,很好地符合了开放封闭原则。工厂方法也叫做虚构造器(Virtual Constructor)。

    通过工厂方法创建工厂对象,然后在工厂类中定义创建基类的子类对象的方法并通过外部传入的条件判断去创建哪一个子类对象,不过由于OC是运行时语言,所以工厂类虽然提供了创建子类对象的方法,但是在编译时期并不能确定对象类型,编译时期创建的子类对象类型是基类类型,真正的类型在运行时期由子类来确定,也即此时确定为子类类型。

    优点

    极大地优化了代码,如果需要100个子类对象,不用再一直调用alloc方法去创建,而是直接通过其工厂类的一句代码即可实现,提高了对代码的复用性。同时,也可以将大量的操作放到工厂类中去处理,业务类中只负责去调用创建需要的对象即可。

    缺点

    因为它的实现条件之一必须存在继承关系,所以模式中工厂类集中了所有的创建逻辑,形成一个庞大的全能类,当所有的类不是继承自同一个父类的时候扩展比较困难。

5、浅复制和深复制的区别?

  • 浅拷贝

    浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间。

    浅拷贝只是对对象的简单拷贝,让几个对象共用一片内存,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。

    在 iOS 里面, 使用retain 关键字进行引用计数,就是一种更加保险的浅拷贝。他既让几个指针共用同一片内存空间,又可以在release 由于计数的存在,不会轻易的销毁内存,达到更加简单使用的目的。

  • 深拷贝

    深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。

    copy 与 retain 的区别:

    copy 是创建一个新对象,retain 是创建一个指针,引用对象计数加一。 copy属性标识两个对象内容相同,新的对象retain count为1, 与旧有对象引用计数无关,旧有对象没有变化。copy减少对象对上下文的依赖。

    iOS提供了copy和mutableCopy方法,顾名思义,copy就是复制了一个imutable的对象,而mutableCopy就是复制了一个mutable的对象。以下将举几个例子来说明。 这里指的是NSString, NSNumber等等一类的对象。

NSString *string = @”dddd";

NSString *stringCopy = [string copy];

NSMutableString *stringDCopy = [string mutableCopy];

[stringMCopy appendString:@``"!!"``];

查看内存可以发现,string和stringCopy指向的是同一块内存区域(weak reference),引用计数没有发生改变。而stringMCopy则是我们所说的真正意义上的复制,系统为其分配了新内存,是两个独立的字符串内容是一样的。

  • 当然在 ios 中并不是所有的对象都支持copy,mutableCopy,遵守NSCopying协议的类可以发送copy消息,遵守NSMutableCopying协议的类才可以发送mutableCopy消息。

    copy构造

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

    MyObj *copy = [[[self class] allocWithZone :zone] init];

    copy->name = [_name copy];

    copy->imutableStr = [_imutableStr copy];

    copy->age = age;

    return copy;

}

mutableCopy构造

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

    MyObj *copy = NSCopyObject(self, 0, zone);

    copy->name = [_name mutableCopy];

    copy->age = age;

    return copy;

}

6、什么是KVO和KVC?他们的使用场景是什么?

  • KVC

    KVC,即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问对象的属性。KVO 就是基于 KVC 实现的关键技术之一。

    一个对象拥有某些属性。比如说,一个 Person 对象有一个 name 和一个 address 属性。以 KVC 说法,Person 对象分别有一个 value 对应他的 name 和 address 的 key。 key 只是一个字符串,它对应的值可以是任意类型的对象。从最基础的层次上看,KVC 有两个方法:一个是设置 key 的值,另一个是获取 key 的值。

    说白了就是通过指定的key获得想要的值value。而不是通过调用Setter、Getter方法访问。

    • 注意

    (1). key的值必须正确,如果拼写错误,会出现异常

    (2). 当key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来

    (3). 因为类key反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去

    (4). NSArray/NSSet等都支持KVC

    • 底层原理

    • 当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:

      1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值 2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(key).如果有,直接给该成员属性进行赋值 3.如果没有成员属性key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值 4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法。

    • 使用场景

    1、赋值:setValue:forkey。

    2、字典转模型:KVC,使用setValuesForKeysWithDictionary:方法,该方法默认根据字典中每个键值对,调用setValue:forKey方法

    缺点:字典中的键值对必须与模型中的键值对完全对应,否则程序会崩溃

    3、取值:valueForKey

  • KVO

    Key-Value Observing (KVO) 建立在 KVC 之上,它能够观察一个对象的 KVC key path 值的变化。说白了就是你关心的一个值改变了,你就会得到通知。你就可以在你想处理的地方处理这个值。

    1、为对象添加一个观察者(监听器)

    2、设置监听事件

    3、取消监听

    • 底层实现原理
    • KVO是基于runtime机制实现的
    • 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
    • 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
    • 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
    • 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。
    • 补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

  • 主要分为三大步
    • 第一步:寻找该属性有没有setsetter方法?有,就直接赋值
    • 第二步:寻找有没有该属性的成员属性?有,就直接赋值
    • 第三步:寻找有没有该属性带下划线的成员属性?有,就直接赋值

7、通知和协议有哪些不同之处?

  • 通知

    需要有一个通知中心:NSNotificationCenter,自定义通知的话需要给一个名字,然后监听。

    • 优点: 通知的发送者和接受者都不需要知道对方。可以指定接收通知的具体方法。通知名可以是任何字符串。
    • 缺点: 较键值观察(KVO)需要多点代码,在删掉前必须移除监听者。
  • 协议

    通过setDelegate来设置代理对象,最典型的例子是常用的 TableView.

    • 优点:支持它的类有详尽和具体信息。
    • 缺点:该类必须支持委托。某一时间只能有一个委托连接到某一对象。

8、在iOS应用有哪些方式保存本地数据?他们都应用在哪些场景?

  • 沙盒

    • Documents:保存用户产生的数据,iTunes同步设备的时候会备份该目录。用户产生的数据就是指用户在使用当前app的时候保存的一些数据,比如保存app中的图片、保存下载的文件等。
    • Library:这个目录下有2个文件夹,一个是Caches、一个是Preferences,Caches主要保存缓存数据,比如SDWebImage把缓存的图片就存放到该目录下。当程序退出后,改目录保存的文件一直存在。 PreferencesXcode6之前保存的是偏好设置,比如NSUserDefaults保存的文件。但是Xcode6以上就保存到/Users/用户名/Library/ Developer/CoreSimulator/Devices/模拟器UDID/data/Library/Preferences/文件夹下。
    • tmp:保存程序中的临时数据,当程序退出后系统会自动删除tmp中所有的文件。
  • NSUserDefaults

    • NSUserDefaults 是个单例对象,在整个程序的生命周期中都只有一个实例。

    • NSUserDefaults 保存的数据类型:NSNumber, 基本数据类型(int,NSInter,float,double,CGFlat......), NSString, NSData, NSArray, NSDictionary, NSURL。

    • NSUserDefaults:一般保存配置信息,比如用户名、密码、是否保存用户名和密码、是否离线下载等一些配置条件信息。

    • 用法

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

    //保存值(key值同名的时候会覆盖的)  

    [defaults setObject:@"用户名" forKey:kUsernameKey];

    //立即保存

    [defaults synchronize];

    //取值

    NSString *username = [defaults objectForKey:kUsernameKey];
  • 保存的一些方法
    //保存NSInteger
    [defaults setInteger:(NSInteger) forKey:(nonnull NSString *)];
    //保存BOOL
    [defaults setBool:(BOOL) forKey:(nonnull NSString *)];
    //保存NSURL
    [defaults setURL:(nullable NSURL *) forKey:(nonnull NSString *)];
    //保存float
    [defaults setFloat:(float) forKey:(nonnull NSString *)];
    //保存double
    [defaults setDouble:(double) forKey:(nonnull NSString *)];
  • 取值方法
    [defaults integerForKey:(nonnull NSString *)];
    [defaults boolForKey:(nonnull NSString *)];
    [defaults URLForKey:(nonnull NSString *)];
    [defaults floatForKey:(nonnull NSString *)];
    [defaults doubleForKey:(nonnull NSString *)];
  • 删除方法
    [defaults removeObjectForKey:(nonnull NSString *)];
  • 归档(序列化)

    • 一般保存自定义的对象,但是只有遵守NSCoding的类才能只用归档。

    • 准守NSCoding协议必须要实现两个require方法

    • (void)encodeWithCoder:(NSCoder *)aCoder //归档会触发

    • - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder //解归档会触发

    • Coding 类具体实现:

    @interface Coding : NSObject<NSCoding> @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age;

   #import "Coding.h"
    #import <objc/runtime.h>
    @implementation Coding
     /**
     *  根据类动画获取类的所有属性,不要忘记导入#import <objc/runtime.h>
     *
     *  @param cls <#cls description#>
     *
     *  @return <#return value description#>
     */
    - (NSArray *)perperiesWithClass:(Class)cls
    {

        NSMutableArray *perperies = [NSMutableArray array];

        unsigned int outCount;
        //动态获取属性
        objc_property_t *properties = class_copyPropertyList(cls, &outCount);

        //遍历person类的所有属性
        for (int i = 0; i < outCount; i++)
        {
            objc_property_t property = properties[i];
            const char *name = property_getName(property);

            NSString *s = [[NSString alloc] initWithUTF8String:name];

            [perperies addObject:s];

        }

        return perperies;
    }

    /**
     *  归档会触发
     *
     *  @param aCoder <#aCoder description#>
     */
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
        for (NSString *perperty in [self perperiesWithClass:[self class]])
        {
            [aCoder encodeObject:perperty forKey:perperty];
        }
    }

    /**
     *  解归档会触发
     *
     *  @param aDecoder <#aDecoder description#>
     *
     *  @return <#return value description#>
     */
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        if (self = [super init])
        {
            for (NSString *perperty in [self perperiesWithClass:[self class]])
            {
                [self setValue:[aDecoder decodeObjectForKey:perperty] forKey:perperty];;
            }

        }

        return self;
    }

    @end
  • 归档具体使用
    Coding *coding1 = [[Coding alloc] init];
     coding1.name = @"小明";
     coding1.age = 12;

     Coding *coding2 = [[Coding alloc] init];
     coding1.name = @"小王";
     coding1.age = 20;

     NSArray *array = @[coding1, coding2];

     //保存对象转化为二进制数据(一定是可变对象)
     NSMutableData *data = [NSMutableData data];

     //1.初始化
     NSKeyedArchiver *archivier = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
     //2.归档
     [archivier encodeObject:array forKey:@"key"];

     //3.完成归档
     [archivier finishEncoding];

     //4.保存
     [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"data"];
  • 解归档的具体使用:
    //1.获取保存的数据
     NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"data"];

     //2.初始化解归档对象
     NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

     //3.解归档
     NSArray *persons = [unarchiver decodeObjectForKey:@"key"];

     //4.完成解归档
     [unarchiver finishDecoding];
  • plist文件保存

    • 一般在iOS用plist保存,plist本身就是XML文件,名字后缀为.plist

    • plist主要保存的数据类型为NSStringNSNumberNSDataNSArrayNSDictionary

    • 具体实现:

    //把字典写入到plist文件,比如文件path为:~/Documents/data.plist
    [dictionary writeToFile:path atomically:YES];
    //把数组写入到plist文件中
    [array writeToFile:path atomically:YES];
  • 读取数据
    NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:(nonnull NSString *)]];
    NSDictionary *dictionary =  [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:(nullable NSString *) ofType:(nullable NSString *)]];
    NSArray *array = [NSArray arrayWithContentsOfURL:[NSURL fileURLWithPath:(nonnull NSString *)]];
    NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:(nullable NSString *) ofType:(nullable NSString *)]];
  • 数据库

    • iOS用的sqlite3, 使用sqlite3需要配置库文件libsqlite3.tbd或者导入libsqlite3.0.tbd,这两个库导入任何一个都可以

    • 保存大量数据可以优先考虑用数据库,sql语句对查询操作有优化作用,所以从查询速度或者插入效率都是很高的。

    • sqlite使用步骤:

    • 指定数据库路径。

    • 创建sqlite3对象并且打开数据库。

    • 创建表。

    • 对数据库操作,包括增删改查。

    • 关闭数据库。

    • 具体实现:

    • 数据库路径

      ``` //返回数据库路径,保存到Cache目录下 -(NSString *)databasePath { NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];

      return [path stringByAppendingPathComponent:@"contacts.db"]; } ```

    • 创建sqlite3对象并且打开数据库,如果数据库打开成功,就创建表。

      //数据库对象
        sqlite3 *contactDB;

        const char *path = [[self databasePath] UTF8String];

       if (sqlite3_open(path, &contactDB) == SQLITE_OK)
       {
            char *errMsg;
            const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS(ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT,PHONE TEXT)";
           //执行语句
          if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) != SQLITE_OK)
            {
                    //创建表失败
             }
       }
       else 
        {
                //打开数据库失败
        }
        sqlite3_close(contactDB);
  • 代码解释:

    • sqlite3_open:打开指定路径的数据库,如果数据库不存在,就会创建一个新的数据库。
    • SQLITE_OK 是一个常量,表示打开数据库成功。
    • contactDB 就是数据库对象。
    • sqlite3_exec就是执行sql语句方法。
    • sqlite3_close关闭数据库,一般暂时不用数据库的时候手动关闭,防止资源浪费。

      • 保存数据到数据库
      //是一个抽象类型,是一个句柄,在使用过程中一般以它的指针进行操作
      sqlite3_stmt *statement;

      //数据库路径 
      const char *path = [[self databasePath] UTF8String];

      //使用的时候打开数据库
      if (sqlite3_open(path, &contactDB) == SQLITE_OK)
      {
          NSString *insertSQL = [NSString stringWithFormat:@"INSERT INTO CONTACTS (name,address,phone) VALUES(\"%@\",\"%@\",\"%@\")",name.text,address.text,phone.text];

          const char *insert_stmt = [insertSQL UTF8String];
         // 这个函数将sql文本转换成一个准备语句(prepared statement)对象,同时返回这个对象的指针。这个接口需要一个数据库连接指针以及一个要准备的包含SQL语句的文本。它实际上并不执行这个SQL语句,它仅仅为执行准备这个sql语句
          sqlite3_prepare_v2(contactDB, insert_stmt, -1, &statement, NULL);
          //执行这个sql
          if (sqlite3_step(statement) == SQLITE_DONE)
          {
              //TODO:已存储到数据库;
          }
          else
          {
              //TODO:保存失败
          }
          //销毁statement对象
          sqlite3_finalize(statement);
          //关闭数据库
          sqlite3_close(contactDB);
      }
  • 查询操作
      //数据库路径
      const char *path = [[self databasePath] UTF8String];
      //查询结果集对象句柄
      sqlite3_stmt *statement;

      //打开数据库
      if (sqlite3_open(path, &contactDB) == SQLITE_OK)
      {
          //查询的sql语句
          NSString *querySQL = [NSString stringWithFormat:@"SELECT address,phone from contacts where name=\"%@\"",name.text];
          const char *query_stmt = [querySQL UTF8String];

          //执行查询sql语句
          if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK) 
          {
              //遍历每条数据
              if (sqlite3_step(statement) == SQLITE_ROW) 
              {
                  //获取每条数据的字段。
                  NSString *addressField = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, 0)];
                  address.text = addressField;

                  NSString *phoneField = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, 1    )];
                  phone.text = phoneField;

                  //TODO:已查到结果
              }
              else
              {
                  //TODO:未查到结果
              }
              sqlite3_finalize(statement);
          }

          sqlite3_close(contactDB);
      }
  • CoreData

    • CoreData提供了一种“对象-关系映射”的功能,能将OC对象转化成数据,保存Sqlite中。

    • CoreData的好处就是能够合理管理内存,避免sql语句的麻烦(不用写sql语句)。

    • CoreData构成

    • NSManagedObjectContext:被管理的数据上下文,主要作用:插入、查询、删除。

    • NSManagedObjectModel:数据库所有的表结构和数据结构,包含各个实体的定义的信息。主要作用就是添加实体、实体属性,建立属性之间的关系。
    • NSPersistentStoreCoordinator持久化存储助理对象,相当于数据库的连接器。主要作用就是设置存储的名字、位置、存储方式。
    • NSFetchRequest相当于select语句。查询封装对象。
    • NSEntityDescription实体结构对象,相当于表格结构。
    • 后缀为xxx.xcdatamodeld文件,编译后为xxx.momd的文件。

    • 保存数据

    - (NSManagedObjectContext *)context
    {
     AppDelegate *app = [UIApplication sharedApplication].delegate;

     return app.managedObjectContext;
    }
    //创建Person对象
    /*
    insertNewObjectForEntityForName:就是创建的实体名字。
    inManagedObjectContext:上下文,appDelegate里面已经创建完成。
    */
     Person *person = [NSEntityDescription
                       insertNewObjectForEntityForName:@"Person"
                       inManagedObjectContext:[self context]];

     //赋值
     [person setValue:@"小王" forKey:@"name"];
     [person setValue:@(35) forKey:@"age"];

     //保存
     if (![[self context] save:nil])
     {
        //TODO:保存失败
     }
  • 查询
    //创建查询对象
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];

    #if 0
        //条件查询
        //NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<=35"];
        //查询名字带有王的
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like[cd]'*王*'"];
    //设置查询条件
        request.predicate = predicate;
    #endif

        //排序
        NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:NO];
     //设置排序条件
        request.sortDescriptors = @[sort];

        //执行查询
        NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];

       //遍历查询结果
        for (Person *p in objectArray)
        {
            NSLog(@"%@ - %@",[p valueForKey:@"name"],[p valueForKey:@"age"]);
        }
  • 修改
    //先查询要修改的对象
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];

        //设置查询条件
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name='小王' and age = 35"];
        request.predicate = predicate;

        //执行查询
        NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];

        //遍历要修改的对象
        for (Person *p in objectArray)
        {
            //修改(修改内存数据,没有同步数据库)
            [p setValue:@(45) forKey:@"age"];
        }
        //同步数据库
        [[self context] save:nil];
  • 删除
    //查询要删除的数据
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];

        //设置查询条件
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name='小王'"];
        request.predicate = predicate;

        //执行查询
        NSArray *objectArray = [[self context] executeFetchRequest:request error:nil];

        //遍历删除
        for (Person *p in objectArray)
        {
            //删除内存中的数据
            [[self context] deleteObject:p];
         }

        //同步数据库
        [[self context] save:nil];
  • 当app更新版本,并且表结构有修改,需要版本升级和数据迁移操作,否则app就是崩掉。

    • KeyChain
  • 钥匙串(英文: KeyChain)是苹果公司Mac OS中的密码管理系统。

  • 一个钥匙串可以包含多种类型的数据:密码(包括网站,FTP服务器,SSH帐户,网络共享,无线网络,群组软件,加密磁盘镜像等),私钥,电子证书和加密笔记等。

  • iOS的KeyChain服务提供了一种安全的保存私密信息(密码,序列号,证书等)的方式。每个iOS程序都有一个独立的KeyChain存储。从iOS 3.0开始,跨程序分享KeyChain变得可行。

  • 当应用程序被删除后,保存到KeyChain里面的数据不会被删除,所以KeyChain是保存到沙盒范围以外的地方。

  • KeyChain的所有数据也都是以key-value的形式存储的,这和NSDictionary的存储方式一样。

  • 相比于NSUserDefaults来说,KeyChain保存更为安全,而且KeyChain里面保存的数据不会因为app删除而丢失。

  • 基本使用

    为了使用方便,我们使用github上封装好的类KeychainItemWrapperSFHFKeychainUtils

      // 初始化一个保存用户帐号的KeychainItemWrapper 
      KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"Your Apple ID" accessGroup:@"YOUR_APP_ID.com.yourcompany.AppIdentifier"];
      //保存帐号
      [wrapper setObject:@"<帐号>" forKey:(id)kSecAttrAccount];  
      //保存密码
      [wrapper setObject:@"<帐号密码>" forKey:(id)kSecValueData];

      //从keychain里取出帐号密码
      NSString *password = [wrapper objectForKey:(id)kSecValueData];

      //清空设置
      [wrapper resetKeychainItem];
  • 上面代码的setObject: forKey: 里参数forKey的值应该是Security.framework里头文件SecItem.h里定义好的key

  • SFHFKeychainUtils就3个方法:

      //获取密码密码
      +(NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
      //存储密码
      +(BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
      //删除密码
      +(BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
  • 参数说明

    • username:因为KeyChain保存也是以键值对存在,所以这个可以看作key,根据key取value.
    • forServiceName :这个就是组的名字,可以理解为KeyChain保存是分组保存。一般要唯一哦,命名可以使用YOURAPPID.com.yourcompany.AppIdentifier。

      • 如果两个应用的usernameserviceName参数一样,那么这两个app会共用KeyChain里面的数据,也就是可以共享密码。

      • KeyChain还有一个用途,就是替代UDID。UDID已经被废除了,所以只能用UUID代替,所以我们可以把UUID用KeyChain保存。

      //创建一个uuid
      NSString *uuidString = [self uuidString];
      //31C75924-1D2E-4AF0-9C67-96D6929B1BD3

      [SFHFKeychainUtils storeUsername:kKeyChainKey andPassword:uuidString forServiceName:kKeyChainGroupKey updateExisting:NO error:nil];

      -(NSString *)uuidString
      {
        //创建一个uuid
        CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
        CFStringRef stringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef);

        NSString *uuidString = (__bridge NSString *)(stringRef);

        CFRelease(uuidRef);

        return uuidString;
      }

答案整理:少聪:github &&简书

QQ技术交流群:214541576

微信公众号:shavekevin

热爱生活,分享快乐。好记性不如烂笔头。多写,多记,多实践,多思考。

comments powered by Disqus