面试总结
基础部分
设计模式 1). MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。 2). MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。 3). 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。 4). 观察者模式:KVO是典型的观察者模式,观察某个属性的状态,状态发生变化时通知观察者。 5). 委托模式:代理+协议的组合。实现1对1的反向传值操作。 6). 工厂模式:通过一个类方法,批量的根据已有模板生产对象。
@property中有哪些属性关键字?/ @property 后面可以有哪些修饰符? 属性可以拥有的特质分为四类: 1.原子性--- nonatomic 特质 2.读/写权限---readwrite(读写)、readonly (只读) 3.内存管理语义---assign、strong、 weak、unsafe_unretained、copy 4.方法名---getter= 、setter= 5.不常用的:nonnull,null_resettable,nullable
属性关键字 readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用? 1). readwrite 是可读可写特性。需要生成getter方法和setter方法。 2). readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。 3). assign 是赋值特性。setter方法将传入参数赋值给实例变量;仅设置变量时,assign用于基本数据类型。 4). retain(MRC)/strong(ARC) 表示持有特性。setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。 5). copy 表示拷贝特性。setter方法将传入对象复制一份,需要完全一份新的变量时。 6). nonatomic 非原子操作。不写的话默认就是atomic。atomic 和 nonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样。对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。 不过atomic可并不能保证线程安全。
怎么用 copy 关键字? 用途:
- NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
- block 也经常使用 copy 关键字。
说明: block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。
用@property声明的 NSString / NSArray / NSDictionary 经常使用 copy 关键字,为什么?如果改用strong关键字,可能造成什么问题? 答:用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
- 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。
- 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
总结:使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。
浅拷贝和深拷贝的区别? 浅拷贝:只复制指向对象的指针,而不复制引用对象本身。 深拷贝:复制引用对象本身。内存中存在了两份独立对象本身,当修改A时,A_copy不变。
系统对象的 copy 与 mutableCopy 方法 不管是集合类对象(NSArray、NSDictionary、NSSet ... 之类的对象),还是非集合类对象(NSString, NSNumber ... 之类的对象),接收到copy和mutableCopy消息时,都遵循以下准则:
- copy 返回的是不可变对象(immutableObject);如果用copy返回值调用mutable对象的方法就会crash。
- mutableCopy 返回的是可变对象(mutableObject)。 3.只有对不可变对象进行copy操作是指针复制(浅复制),其它情况都是内容复制(深复制)!
*这个写法会出什么问题:@property (nonatomic, copy) NSMutableArray arr; 问题:添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃。 //如:-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460 // copy后返回的是不可变对象(即 arr 是 NSArray 类型,NSArray 类型对象不能调用 NSMutableArray 类型对象的方法) 原因:是因为 copy 就是复制一个不可变 NSArray 的对象,不能对 NSArray 对象进行添加/修改。
如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter? 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。 具体步骤:
- 需声明该类遵从 NSCopying 协议
- 实现 NSCopying 协议的方法。 // 该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone; // 注意:使用 copy 修饰符,调用的是copy方法,其实真正需要实现的是 “copyWithZone” 方法。
@synthesize 和 @dynamic 分别有什么作用? @property有两个对应的词,一个是@synthesize(合成实例变量),一个是@dynamic。 如果@synthesize和@dynamic都没有写,那么默认的就是 @synthesize var = _var; // 在类的实现代码里通过 @synthesize 语法可以来指定实例变量的名字。(@synthesize var = _newVar;)
- @synthesize 的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
- @dynamic 告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成(如,@dynamic var)。
常见的 Objective-C 的数据类型有那些,和C的基本数据类型有什么区别?如:NSInteger和int Objective-C的数据类型有NSString,NSNumber,NSArray,NSMutableArray,NSData等等,这些都是class,创建后便是对象,而C语言的基本数据类型int,只是一定字节的内存空间,用于存放数值;NSInteger是基本数据类型,并不是NSNumber的子类,当然也不是NSObject的子类。NSInteger是基本数据类型Int或者Long的别名(NSInteger的定义typedef long NSInteger),它的区别在于,NSInteger会根据系统是32位还是64位来决定是本身是int还是long。
Objective-C 如何对内存管理的,说说你的看法和解决方法? 答:Objective-C的内存管理主要有三种方式ARC(自动内存计数)、手动内存计数、内存池。 1). 自动内存计数ARC:由Xcode自动在App编译阶段,在代码中添加内存管理代码。 2). 手动内存计数MRC:遵循内存谁申请、谁释放;谁添加,谁释放的原则。 3). 内存释放池Release Pool:把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有的内存空间也被自动释放掉。内存池的释放操作分为自动和手动。自动释放受runloop机制影响。
KVO 和 KVC 1). KVC(Key-Value-Coding):键值编码 是一种通过字符串间接访问对象的方式(即给属性赋值) 举例说明: stu.name = @"张三" // 点语法给属性赋值 [stu setValue:@"张三" forKey:@"name"]; // 通过字符串使用KVC方式给属性赋值 2). KVO(key-Value-Observing):键值观察机制 他提供了观察某一属性变化的方法,极大的简化了代码。 KVO只能被KVC触发,包括使用setValue:forKey:方法和点语法。 // 通过下方方法为属性添加KVO观察
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// 当被观察的属性发送变化时,会自动触发下方方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
KVC 和 KVO 的 keyPath 可以是属性、实例变量、成员变量。 KVC的底层实现 当一个对象调用setValue方法时,方法内部会做以下操作: 1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。 2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接赋值。 3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。 4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。 这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
KVO的底层实现 基于runtime机制,生成新的类,重写set方法,在set方法处理完后触发观察事件 参考
ViewController生命周期 按照执行顺序排列:
- initWithCoder:通过nib文件初始化时触发。
- awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
- loadView:开始加载视图控制器自带的view。
- viewDidLoad:视图控制器的view被加载完成。
- viewWillAppear:视图控制器的view将要显示在window上。
- updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
- viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
- viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
- viewDidAppear:视图控制器的view已经展示到window上。
- viewWillDisappear:视图控制器的view将要从window上消失。
- viewDidDisappear:视图控制器的view已经从window上消失。
谓词 谓词就是通过NSPredicate给定的逻辑条件作为约束条件,完成对数据的筛选。 //定义谓词对象,谓词对象中包含了过滤条件(过滤条件比较多) NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30]; //使用谓词条件过滤数组中的元素,过滤之后返回查询的结果 NSArray *array = [persons filteredArrayUsingPredicate:predicate];
修改一个类的私有属性 1). 一种是通过KVC获取。 2). 通过runtime访问并修改私有属性。
下面的代码输出什么? @implementation Son : Father
- (id)init { if (self = [super init]) { NSLog(@"%@", NSStringFromClass([self class])); // Son NSLog(@"%@", NSStringFromClass([super class])); // Son } return self; } @end // 解析: self 是类的隐藏参数,指向当前调用方法的这个类的实例。 super是一个Magic Keyword,它本质是一个编译器标示符,和self是指向的同一个消息接收者。 不同的是:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类里的。 上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *obj 这个对象。
数据存储 数据存储有四种方案:NSUserDefault、KeyChain、File、DB。 其中File有三种方式:writeToFile:atomically:、Plist、NSKeyedAchiever(归档) DB包括:SQLite、FMDB、CoreData
沙盒结构
- AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
- Documents:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。iCloud备份目录。(这里不能存缓存文件,否则上架不被通过)
- Library 目录:这个目录下有两个子目录: Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好. Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。 可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
- tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。
延迟执行 performSelector、NSTimer、NSThread线程的sleep、GCD
UITableView 的优化 1). 正确的复用cell; 2). 设计统一规格的Cell; 3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法; 4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口; 5). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用! 6). 减少子视图的层级关系; 7). 尽量使所有的视图不透明化以及做切圆操作; 8). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示; 9). 使用调试工具分析问题。
进阶部分
从APP启动到main
从可执行文件(Mach-O文件)获取dyld的路径并加载, dyld初始化运行环境,开启缓存策略,加载并链接相关依赖库和可执行文件, 调用每个依赖库的初始化方法(runtime被初始化), runtime会对项目中所有类进行类结构初始化,调用所有load方法。 最后dyld返回main函数地址,main函数被调用。
load和initialize
- 在加载阶段,如果类(类别)实现了load方法,系统就会调用它,load方法不参与覆写机制
- 惰性调用。在首次使用某个类之前,系统会向其发送initialize消息,通常应该在里面判断当前要初始化的类,防止子类未覆写initialize的情况下调用两次。如果类未实行initialize会调用父类的
- load与initialize方法都应该实现得精简一些,有助于保持应用程序的响应能力,也能减少引入“依赖环”(interdependency cycle)的几率
- 无法在编译期设定的全局常量,可以放在initialize方法里初始化;load 方法中最常用的就是方法交换method swizzling 5.两者的执行都会阻塞其他线程
消息机制(方法的调用流程)
1、编译器会先将代码[obj method]转化为objc_msgSend(obj, @selector (method))函数去执行。 2、在objc_msgSend()函数中,首先通过obj的isa指针找到(对象)obj对应的(类)class。 3、在class中会先去cache中 通过SEL查找对应函数method(cache中method列表是以SEL为key通过hash表来存储的),若 cache中未找到。再去class中的消息列表methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
runtime 一套C语言库,方法都是转成了 objc_msgSend 函数 1、互换方法的实现,改变selector和IMP的对应关系 void method_exchangeImplementations(Method m1, Method m2) 2、动态添加方法 resolveClassMethod,class_addMethod 3、动态添加属性 objc_setAssociatedObject 4、获取类中所有的成员变量和属性 class_copyIvarList、class_getProperty 参考
消息转发 参考 当向一个对象发送一条消息,但它并没有实现的时候会尝试做消息转发。 顺序如下,某一个处理中找到方法定义不再执行后面的处理。 处理一:
- (BOOL)resolveInstanceMethod:(SEL)sel
- (BOOL)resolveClassMethod:(SEL)sel 处理二:
- (id)forwardingTargetForSelector:(SEL)aSelector 处理三:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
反射机制 1). class反射 通过类名的字符串形式实例化对象。 Class class = NSClassFromString(@"student"); Student *stu = [[class alloc] init]; 将类名变为字符串。 Class class =[Student class]; NSString className = NSStringFromClass(class); 2). SEL的反射 通过方法的字符串形式实例化方法。 SEL selector = NSSelectorFromString(@"setName"); [stu performSelector:selector withObject:@"Mike"]; 将方法变成字符串。 NSStringFromSelector(@selector(setName:));
逆向传值 delegate:委托、一对一、强关联。 Notification:通知、一对多、全局监控、弱关联。 单例、 block:匿名函数,比delegate灵活,访问其他函数内的变量,将栈上的变量拷贝到堆上。 extern、 KVO:属性监听
-all_load 、-ObjC、-force_load
-ObjC:将静态库中的所有文件加载进来(包括类别的)。 64位ios应用中,链接器的bug造成需添加-all_load、-force_load(加载某个包),Xcode4.2之后不再需要。
isa指针问题 isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。
block的注意点 参考 1). 在block内部使用外部指针且会造成循环引用情况下,需要用**week修饰外部指针: **weak typeof(self) weakSelf = self; 2). 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下。 strong typeof(self) strongSelf = weakSelf; 3). 如果需要在block内部改变外部栈区变量的话,需要在用block修饰外部变量。 4).Block类型: NSStackBlock 存储于栈区 NSGlobalBlock 存储于程序数据区 NSMallocBlock 存储于堆区
5).Block属性的特性限定符一般都是 copy,因为其setter方法中会拷贝一份新的副本到堆区。
多线程
pthread、NSThread、GCD、NSOperation GCD 基于C语言的底层API,GCD主要与block结合使用,代码简洁高效、易用,对于不复杂的多线程操作,会节省代码量。 NSOperation 属于Objective-C类,是基于GCD更高一层的封装。复杂任务一般用NSOperation实现。 NSOperationQueue用来管理子类化的NSOperation对象,控制其线程并发数目。GCD和NSOperation都可以实现对线程的管理,区别是 NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。
RunLoop 从字面上看,就是运行循环,跑圈, 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer), 一个线程对应一个RunLoop,基本作用就是保持程序的持续运行,处理app中的各种事件。 通过runloop,有事运行,没事就休息,可以节省cpu资源,提高程序性能。
主线程的run loop默认是启动的 RunLoop详细
用伪代码写一个线程安全的单例模式 static id _instance;
-
(id)allocWithZone:(struct _NSZone *)zone { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [super allocWithZone:zone]; }); return _instance; }
-
(instancetype)sharedData { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [[self alloc] init]; }); return _instance; }
-
(id)copyWithZone:(NSZone *)zone { return _instance; }
Core开头的系列
UI框架的底层有CoreAnimation,CoreAnimation的底层有CoreGraphics。 CoreText可以解决复杂文字内容排版问题。CoreImage可以处理图片,为其添加各种效果。
如何重写类方法 1、在子类中实现一个同基类名字一样的静态方法 2、在调用的时候不要使用类名调用,而是使用[self class]的方式调用。原理,用类名调用是早绑定,在编译期绑定,用[self class]是晚绑定,在运行时决定调用哪个方法。
id和NSObject*的区别 id是一个 objc_object 结构体指针,定义是 typedef struct objc_object *id id可以理解为指向对象的指针。所有oc的对象 id都可以指向,编译器不会做类型检查,id调用任何存在的方法都不会在编译阶段报错,当然如果这个id指向的对象没有这个方法,该崩溃还是会崩溃的。 NSObject *指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。 不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject *可指向的类型是id的子集。
static关键字的作用
1.在函数体内定义的static他的作用域为该函数体,该变量在内存中只被分配一次,因此,其值在下次调用时仍维持上次的值不变; 2.在模块内的static全局变量可以被模块内所有函数访问,但是不能被模块外的其他函数访问; 3.在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝,也就是说只要是该类的对象,那么该对象的中被static修饰的成员变量都指向同一块地址。
4.修饰局部变量: 4.1.延长局部变量的生命周期,程序结束才会销毁。 4.2.局部变量只会生成一份内存,只会初始化一次。 4.3.改变局部变量的作用域。
5.修饰全局变量: 5.1.只能在本文件中访问,修改全局变量的作用域,生命周期不会改 5.2.避免重复定义全局变量
6.不允许使用static修饰实例变量和方法
使用 Swift 语言编程的优缺点 总的来说,我认为使用 Swift 来作为编程语言的优点还是要远远大于缺点的,而且很多缺点苹果也在逐渐改善。 优点: 1、简洁的语法 2、更强的类型安全 3、函数式编程的支持 Swift 语言本身提供了对函数式编程的支持。 Objc 本身是不支持的,但是可以通过引入 ReactiveCocoa 这个库来支持函数式编程。 4、编写 OS X 下的自动化脚本
缺点 1、App体积变大 使用Swift 后, App 体积大概增加 5-8 M 左右,对体积大小敏感的慎用。 体积变大的原因是因为 Swift 还在变化,所以 Apple 没有在 iOS 系统里放入 Swift 的运行库,反而是每个 App 里都要包含其对应的 Swift 运行库。 2、Xcode 支持不够好 如果你是使用 Xcode经常卡住或者崩溃想必你是肯定碰到过了,这个是目前使用 Swift 最让人头疼的事情,即使是到现在XCode 9, 有时候也会遇到这种问题,所以要看你的承受力了…… 3、第三方库的支持不够多 目前确实 Swift 编写的第三方库确实不多,但可以通过桥接的方式来使用 Objc 的三方库,基本上没有太大问题。现在已经改善很多了… 4、语言版本更新带来的编译问题 语言本身还在发展,所以每次版本更新后都会出现编译不过的情况(至少到目前为止还是),但是自从 4.0 版本发布后,改动没有 beta 时候那么大了,而且根据 Xcode 提示基本就可以解决语法变动导致的编译问题了。
事件传递响应链 参考
响应链 当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链
查找响应者 1、当程序中发现触摸事件之后,系统会将事件加入到UIApplication的任务队列 2、UIApplication会将事件发送给我们最底层的窗口UIWindow,UIWindow将事件发送给根控制器(或者根视图)使用hittest方法进行遍历。 3、如果控制器能处理事件的响应,而且触摸点在自己的身上,则在当前视图的子视图中进行查找。 4、如果不在子视图的范围内,则返回NO,如果在子视图的范围内,继续递归到下一层视图进行查找。 5、直到找到的视图内包含触摸点坐标并且当前视图无子视图时,返回当前视图为相应者
应用:使用hitTest方法扩大触摸对象的响应热区,将事件传给其他对象
转自 http://blog.cocoachina.com/article/70096
- load 方法调用顺序?
父类先于子类调用
类先于分类调用
启动优化部分
run- arguments - Environment Variables 增加key DYLD_PRINT_STATISTICS
控制台会打印 pre-main 的耗时
查看耗时代码
利用xocde Product -> Profile 选择 Time Profile
https://blog.csdn.net/Hello_Hwc/article/details/84311933