601、iOS面试题 - BMWB/Fastlane_Swift GitHub Wiki

Swift

1、Swift中struct和class有什么区别?

struct是值引用,更轻量,存放于栈区,class是类型引用,存放于堆区。

struct无法继承,class可继承。

2、Swift中的方法调用有哪些形式?

直接派发、函数表派发、消息机制派发。 派发方式受声明位置,引用类型,特定行为的影响。 为什么Swift有这么多派发形式?为了效率。

参考文章:深入理解 Swift 派发机制

3、Swift和OC有什么区别?

Swift和OC的区别有很多,这里简要总结这几条:

1.swift是静态语言,有类型推断,OC是动态语言。

2.swift面向协议编程,OC面向对象编程

3.swift注重值类型,OC注重引用类型。

4.swift支持泛型,OC只支持轻量泛型

5.swift支持静态派发(效率高)、动态派发(函数表派发、消息派发)方式,OC支持动态派发(消息派发)方式。

6.swift支持函数式编程

7.swift的协议不仅可以被类实现,也可以被struct和enum实现

8.swift有元组类型、支持运算符重载

9.swift引入了命名空间,从此不用再import其他文件

10.swift支持默认参数

11.swift比oc代码更加简洁

4、从OC向Swift迁移的时候遇到过什么问题?

OC项目转Swift指南

5、怎么理解面向协议编程?

面向对象是以对象的视角观察整体结构,万物皆为对象。

面向协议则是用协议的方式组织各个类的关系,Swift底层几乎所有类都构建在协议之上。

面向协议能够解决面向对象的菱形继承,横切关注点和动态派发的安全性等问题。

参考:面向协议编程与 Cocoa 的邂逅 (上)

OC语法

1、Block是如何实现的?Block对应的数据结构是什么样子的?__block的作用是什么?它对应的数据结构又是什么样子的?

block本质是一个对象,底层用struct实现。

数据结构如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};
  • isa 指针,所有对象都有该指针,用于实现对象相关的功能。
  • flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
  • reserved,保留变量。
  • invoke,函数指针,指向具体的 block 实现的函数调用地址。
  • descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
  • variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

__block的作用是可以获取对应变量的指针,使其可以在block内部被修改。通过反编译的代码我们可以看到该对象是这样的:

struct __Block_byref_i_0 {
    void *__isa;
    __Block_byref_i_0 *__forwarding;
    int __flags;
    int __size;
    int val; //变量名
};

在 Objective-C 语言中,一共有 3 种类型的 block:

  • _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  • _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  • _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

对于block的深入了解,可以参考《Objective-C高级编程》第二章或者唐巧的这篇谈Objective-C block的实现

2、GCD中的Block是在堆上还是栈上?

堆上。可以通过block的isa指针确认。

3、NSCoding协议是干什么用的?

一种编码协议,归档时和解档时需要依赖该协议定义的编码和解码方法。Foundation和Cocoa Touch中的大部分类都遵循了这个协议,一般被NSKeyedArchiver做自定义对象持久化时使用。

4、KVO的实现原理

利用Runtime生成一个中间对象,让原对象的isa指针指向它,然后重写setter方法,插入willChangeValueForKey和didChangeValueForKey方法。当属性变化时会调用,会调用这两个方法通知到外界属性变化。

5、NSOperation有哪些特性,比着GCD有哪些优点,它有哪些API?

NSOperation是对GCD的封装,具有面向对象的特点,可以更方便的进行封装,可以设置依赖关系。

API可以查看NSOperation文档。

6、NSNotificaiton是同步还是异步的,如果发通知时在子线程,接收在哪个线程?

同步。子线程。

UI

1、事件响应链是如何传递的?

手势的点击会发生两个重要事情,事件传递和事件响应。

事件传递:从UIApplication开始,到window,再逐步往下层(子视图)找,直到找到最深层的子视图,其为first responder。用到的判断方法是pointInside:withEvent和hitTest:withEvent。

事件响应:从识别到的视图(first responder)开始验证能否响应事件,如果不能就交给其上层(父视图)视图,如果能相应将不再往下传递,如果直到找到UIApplication层还没有相应,那就忽略该次点击。用到的判断方法是touchesBegan:withEvent、touchesMoved:withEvent等。

2、什么是异步渲染?

异步渲染就是在子线程进行绘制,然后拿到主线程显示。

UIView的显示是通过CALayer实现的,CALayer的显示则是通过contents进行的。异步渲染的实现原理是当我们改变UIView的frame时,会调用layer的setNeedsDisplay,然后调用layer的display方法。我们不能在非主线程将内容绘制到layer的context上,但我们单独开一个子线程通过CGBitmapContextCreateImage()绘制内容,绘制完成之后切回主线程,将内容赋值到contents上。

这个步骤可以参照YYText中YYTextAsyncLayer.m文件中的实现方式。

3、layoutsubviews是在什么时机调用的?

  • init初始化不会触发。
  • addSubview时。
  • 设置frame且前后值变化,frame为zero且不添加到指定视图不会触发。
  • 旋转Screen会触发父视图的layoutSubviews。
  • 滚动UIScrollView引起View重新布局时会触发layoutSubviews。

4、一张图片的展示经历了哪些步骤?

这个可以参考我之前写的一篇文章iOS开发图片格式选择 中的前半部分内容。

  1. 加载图片
  2. 图片解码(解压)
  3. 图片渲染

5、什么是离屏渲染,什么情况会导致离屏渲染?

如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。

以阴影为例,为什么它会导致离屏渲染。因为GPU的渲染是遵循“画家算法”,一层一层绘制的,但阴影很特殊,它需要全部内容绘制完成,再根据外轮廓进行绘制。这就导致了,阴影这一层要一直占据一块内存区域,这就导致了离屏渲染。

类似导致离屏渲染的情况还有:

  • cornerRadius+clipsToBounds
  • group opacity 组透明度
  • mask 遮罩
  • UIBlurEffect 毛玻璃效果

优化:

  • 即刻大量应用AsyncDisplayKit(Texture)作为主要渲染框架,对于文字和图片的异步渲染操作交由框架来处理。关于这方面可以看我之前的一些介绍
  • 对于图片的圆角,统一采用“precomposite”的策略,也就是不经由容器来做剪切,而是预先使用CoreGraphics为图片裁剪圆角
  • 对于视频的圆角,由于实时剪切非常消耗性能,我们会创建四个白色弧形的layer盖住四个角,从视觉上制造圆角的效果
  • 对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
  • 对于所有的阴影,使用shadowPath来规避离屏渲染
  • 对于特殊形状的view,使用layer mask并打开shouldRasterize来对渲染结果进行缓存
  • 对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果

有一篇文章详细的讨论了这些情况:关于iOS离屏渲染的深入研究

6、CoreAnimation这个框架的作用什么,它跟UIKit的关系是什么?

CoreAnimation虽然直译是核心动画,但它其实是一个图像渲染框架,动画实现只是它的一部分功能。

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMjg3Nzk5Mi01YTViMDQzYmYwODEyNjE2LnBuZw

看这张图我们可以知道,它是UIKit和AppKit的底层实现,位于Metal、Core Graphics和GPU之上之上。

苹果官方文档:About Core Animation

引用计数

1、ARC方案的原理是什么?它是在什么时候做的隐式添加release操作?

ARC(Automatic Reference Cunting)自动引用计数,意即通过LLVM编译器自动管理对应的引用计数状态。 ARC开启时无需再次键入retain或者release代码。

它是在编译阶段添加retain或者release代码的。

2、循环引用有哪些场景,如何避免?

循环引用及两个及以上对象出现引用环,导致对象无法释放的情况。一般在block,delegate,NSTimer时容易出现这个问题。

解决方案就是让环的其中一环节实现弱引用。

3、为什么当我们在使用block时外面是weak 声明一个weakSelf,还要在block内部使用strong再持有一下?

block外界声明weak是为了实现block对对象的弱持有,而里面的作用是为了保证在进到block时不会发生释放。

4、Autoreleasepool是实现机制是什么?它是什么时候释放内部的对象的?它内部的数据结构是什么样的?当我提到哨兵对象时,会继续问哨兵对象的作用是什么,为什么要设计它?

Autoreleasepool的原理是一个双向列表,它会对加入其中的对象实现延迟释放。当Autoreleasepool调用drain方法时会释放内部标记为autorelease的对象。

class AutoreleasePoolPage {
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};

哨兵对象类似一个指针,指向自动释放池的栈顶位置,它的作用就是用于标记当前自动释放池需要释放内部对象时,释放到那个地方结束,每次入栈时它用于确定添加的位置,然后再次移动到栈顶。

关于自动释放池的底层探究可以看draveness的这篇自动释放池的前世今生 ----深入解析 autoreleasepool

5、哪些对象会放入到Autoreleasepool中?

有两种情况生成的对象会加入到autoreleasepool中:

非alloc/new/copy/mutablecopy 开始的方式初始化时。 id的指针或对象的指针在没有显示指定时

引用计数带来的一次讨论

6、weak的实现原理是什么?当引用对象销毁是它是如何管理内部的Hash表的?(这里要参阅weak源码)

runTime会把对weak修饰的对象放到一个全局的哈希表中,用weak修饰的对象的内存地址为key,weak指针为值,在对象进行销毁时,用通过自身地址去哈希表中查找到所有指向此对象的weak指针,并把所有的weak指针置位nil。

Runtime

1、消息发送的流程是怎样的?

OC中的方法调用会转化成给对象发送消息,发送消息会调用这个方法:

objc_msgSend(receiver, @selector(message))

该过程有以下关键步骤:

  • 先确定调用方法的类已经都加载完毕,如果没加载完毕的话进行加载
  • 从cache中查找方法
  • cache中没有找到对应的方法,则到方法列表中查,查到则缓存
  • 如果本类中查询到没有结果,则遍历所有父类重复上面的查找过程,直到NSObject

2、关联对象时什么情况下会导致内存泄露?

关联对象可以理解就是持有了一个对象,如果是retain等方式的持有,而该对象也持有了本类,那就是导致了循环引用。

3、消息转发的流程是什么?

消息转发是发生在接收者(receiver)没有找到对应的方法(method)的时候,该步骤有如下几个关键步骤:

  • 消息转发的时候,如果是实例方法会走resolveInstanceMethod:,如果是类方法会走resolveClassMethod:,它们的返回值都是Bool,需要我们确定是否进行转发。
  • 如果第一步返回YES,确定转发就会进到下个方法forwardingTargetForSelector,这个方法需要我们指定一个被用receiver。
  • methodSignatureForSelector用于指定方法签名,forwardInvocation用于处理Invocation,进行完整转发。
  • 如果消息转发也没有处理即为无法处理,会调用doesNotRecognizeSelector,引发崩溃。

更多了解可以参考iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding) (消息机制,方法未实现+API不兼容奔溃,模拟多继承)

4、category能否添加属性,为什么?能否添加实例变量,为什么?

可以添加属性,这里的属性指@property,但跟类里的@property又不一样。正常的@property为:实例变量Ivar + Setter + Getter 方法,分类里的@property这三者都没有,需要我们手动实现。

分类是运行时被编译的,这时类的结构已经固定了,所以我们无法添加实例变量。

对于分类自定义Setter和Getter方法,我们可以通过关联对象(Associated Object)进行实现。

5、元类的作用是什么?

元类的作用是存储类方法,同时它也是为了让OC的类结构能够形成闭环。

对于为甚设计元类有以下原因;

  • 在OC的世界里一切皆对象(借鉴于Smalltalk),metaclass的设计就是要为满足这一点。
  • 在OC中Class也是一种对象,它对应的类就是metaclass,metaclass也是一种对象,它的类是root metaclass,在往上根元类(root metaclass)指向自己,形成了一个闭环,一个完备的设计。

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMjg3Nzk5Mi01ZGQxNmJjM2Q4YmVhMTM5LnBuZw

如果不要metaclass可不可以?也是可以的,在objc_class再加一个类方法指针。但是这样的设计会将消息传递的过程复杂化,所以为了消息传递流程的复用,为了一切皆对象的思想,就有了metaclass。

关于这一话题的深入讨论可以参考这两篇文章:

为什么要存在MetaClass

为什么要设计metaclass

6、类方法是存储到什么地方的?类属性呢?

类方法和类属性都是存储到元类中的。

类属性在Swift用的多些,OC中很少有人用到,但其实它也是有的,写法如下:

@interface Person : NSObject
// 在属性类别中加上class
@property (class, nonatomic, copy) NSString *name;
@end
// 调用方式
NSString *temp = Person.name;

需要注意的是跟实例属性不一样,类属性不会自动生成实例变量和setter,getter方法,需要我们手动实现。具体实现方法可以参考这个文章:Objective-C Class Properties

7、讲几个runtime的应用场景

  • hook系统方法进行方法交换。
  • 了解一个类(闭源)的私有属性和方法。
  • 关联对象,实现添加分类属性的功能。
  • 修改isa指针,自定义KVO。

Runloop

1、讲一下对Runloop的理解?

Runloop就是一个运行循环,它保证了在没有任务的时候线程不退出,有任务的时候即使响应。Runloop跟线程,事件响应,手势识别,页面更新,定时器都有着紧密联系。

深入了解推荐ibireme的这篇深入理解RunLoop

2、可以用Runloop实现什么功能?

  • 检测卡顿
  • 线程保活
  • 性能优化,将一些耗时操作放到runloop wait的情况处理。

性能优化

1、对TableView进行性能优化有哪些方式?

  • 缓存高度
  • 异步渲染
  • 减少离屏渲染

2、Xcode的Instruments都有哪些调试的工具?

  • Activity Monitor(活动监视器):监控进程的CPU、内存、磁盘、网络使用情况。是程序在手机
  • 运行真正占用内存大小
  • Allocations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史
  • Core Animation(图形性能):显示程序显卡性能以及CPU使用情况
  • Core Data:跟踪Core Data文件系统活动
  • Energy Log:耗电量监控
  • File Activity:检测文件创建、移动、变化、删除等
  • Leaks(泄漏):一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录
  • Network:用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接
  • System Usage:记录关于文件读写,sockets,I/O系统活动,输入输出
  • Time Profiler(时间探查):方法执行耗时分析
  • Zombies:测量一般的内存使用,专注于检测过度释放的野指针对象。也提供对象分配统计以及主动分配的内存地址历史

3、讲一下你做过的性能优化的事情。

这个根据自己情况来说吧。

4、如何检测卡顿,都有哪些方法?

  • FPS,通过CADisplayLink计算1s内刷新次数,也可以利用Instruments里的Core Animation。
  • 利用Runloop,实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值
  • 子线程检测,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。参考ANREye的实现

5、缩小包体积有哪些方案?

  • 图片压缩,无用图片删除,webp使用
  • 一些大图可以动态下发
  • 删除无用类,无用方法
  • 减少三方库的依赖

计算机相关

1、项目编译的流程是什么?手机上的应用程序自点击图标开始到首屏内容展示都经历了哪些步骤?

编译流程:

  • 预处理:处理宏定义,删除注释,展开头文件。
  • 词法分析:把代码切成一个个token,比如大小括号等于号还有字符串
  • 语法分析:验证语法是否正确,合成抽象语法树AST
  • 静态分析:查找代码错误
  • 类型检查:动态和静态
  • 目标代码的生成与优化,包括删除多余指令,选择合适的寻址方式,如果开启了bitcode,会做进一步的优化
  • 汇编:由汇编器生成汇编语言
  • 机器码:由汇编语言转成机器码,生成.o文件

应用启动的流程:

启动的前提是完成编译,运行程序即运行编译过后的目标程序,它分为main函数前和main函数后:

main前

  • 加载可执行文件(App的.o文件集合)
  • 加载动态链接库(系统和应用的动态链接库),进行rebase指针调整和bind符号绑定
  • Objc运行时的初始处理,包括Objc相关类的注册,category注册,selector唯一性检查
  • 初始化,包括执行+load()、attribute(constructor)修饰的函数的调用、创建C++静态全局变量

main后

  • 首页初始化所需要配置文件的读写操作
  • 首页界面渲染

2、对于基本数据类型,一般是存储到栈中的,它有没有可能存在堆上,什么情况下会存储到堆上?

  • 栈和堆都是同属一块内存,只不过一个是高地址往低地址存储,一个从低地址往高地址存储,他们并没有严格的界限说一个值只能放在堆上或者栈上。所以基本数据类型也是可以存储到堆上的。
  • 当该基础类型变量被__block捕获时,该变量连同block都会被copy到堆上。

3、数据库中的事务是什么意思?

事务就是访问并操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。如果其中一个步骤出错就要撤销整个操作,回滚到进入事务之前的状态。

4、使用过什么数据库(我回答的Sqlite,Realm),Realm在使用时有哪些注意事项,如何实现批量操作?

  • 对于Realm感兴趣的同学可以看下其官方文档。
  • Realm需要注意的主要就是不能直接跨线程访问同一对象。
  • 批量操作可以在一个单独的事务中执行多个数据库的修改。

5、LRU算法是否了解,如何实现一套LRU算法?

LRU(Least recently used 最近最少使用)算法是一个缓存淘汰算法,其作用就是当缓存很多时,该淘汰哪些内容,见名知意,它的核心思想是淘汰最近使用最少的内容。实现它的关键步骤是:

  • 新数据插入到链表的头部
  • 每当缓存命中时,则将数据移动到链表头部
  • 链表满时,将尾部数据清除

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMjg3Nzk5Mi0xNWEyZWVjMzU2Njg2NzU3LmpwZw

这个算法在SDWebImage和Kingfisher等需要处理缓存的库中都有实现。

6、知道哪些设计模式,怎么理解设计模式的作用?

工厂模式、观察者模式、中介者模式、单例模式。这个根据实际情况说吧。

7、如果有1000万个Int类型的数字,如何对他们排序?

这里的隐藏含义是,内存不够用时如何排序,还有一个隐藏含义是硬盘足够大。这是可以采用分而治之的方法,将数据分成若干块,使每一小块满足当前内容大小,然后对每块内容单独排序,最后采用归并排序对所有块进行排序,就得到了一个有序序列。

8、设计一套数据库方案,实现类似微信的搜索关键词能快速检索出包含该字符串的聊天信息,并展示对应数量(聊天记录的数据量较大)

可以对聊天记录的文本值加上索引。正常情况下数据库搜索都是全量检索的,加上索引之后只会检索满足条件的记录,大大降低检索量。

简历相关问题

1、Lottie实现动画效果的原理是什么?

iOS里的动画基本都是基于CoreAnimation里的API实现的,Lottie也是如此。在AE上实现动画效果,通过插件导出对应的json文件,Lottie的库解析该json,转成对应的系统API方法。图片的引用可以使用Base64编到json里,也可以通过项目集成,通过路径引用。

2、OClint实现静态分析的原理是什么,它是如何做到的?

具体可以参考我之前写的如何通过静态分析提高iOS代码质量

3、MVVM和MVC有什么区别?

对比架构时,可以从是否职责分离,可测试性,可易维护性三个维度对比。

更多对比可以参考我翻译的一篇文章:【译】iOS 架构模式–浅析MVC, MVP, MVVM 和 VIPER

4、静态库和动态库的区别是什么?

  • 静态库:链接时被完整复制到可执行文件中,多次使用就多份拷贝。
  • 动态库:链接时不复制,而是由系统动态加载到内存,内存中只会有一份该动态库。

5、了解Flutter吗?它有没有使用UIKit?它是如何渲染UI的?

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yMjg3Nzk5Mi05NGFkNWQyNmU1N2JhODAwLnBuZw

6、二进制重排的核心依据是什么?

修改链接顺序,减少启动时的缺页中断。

实践步骤可以参考李斌同学的这篇iOS 优化篇 - 启动优化之Clang插桩实现二进制重排

7、如何设计一套切换主题的方案?

核心思路是观察者模式+协议(通知),当获取到主题切换时,通知各个实现了主题协议的类进行更新。

8、AVPlayer和IJKPlayer有什么区别?用IJKPlayer如何实现一个缓存视频列表每条视频前1s的内容?

因为对IJKPlayer和FFmpeg了解的不是很深,这个我也没有确切答案,如果有了解的小伙伴可以评论告知我。

9、类似微博的短视频列表,滑动停留播放,如何实现?

这个主要就是检测contentOffset和屏幕中间位置,设置一些边界条件,处理滑动过程中的切换行为。

10、使用python做过哪些事?如何理解脚本语言?

多语言管理,csv多语言文件读取,然后写入到项目Localizable.strings中;抓取项目中的多语言字符串。

脚本(script) 其实就是一系列指令,计算机看了指令就知道自己该做什么事情。像常见的Python,Shell,Ruby都是脚本语言,他们通常不需要编译,通过解释器运行。

数据结构与算法

1、什么是Hash表,什么是Hash碰撞,解决Hash碰撞有什么方法?

哈希表(Hash Table,也叫散列表),是根据关键码值 (Key-Value) 而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。我们常用的Dictionary就是一种Hash表。

那什么是Hash碰撞呢,我们知道Hash表的查找是通过键值进行定位的,当两个不同的输入对应一个输出时,即为Hash碰撞,也被称为Hash冲突。

如果使用字典的例子你可能联想不到冲突的情况,我们假设另一种情况:假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10。我们使用的hash函数是对9取余。这样的话会出现hash(5)=5,hash(28)=1,hash(19)=1。28和19都对应一个地址,这就出现了Hash冲突。

解决Hash冲突的方式有开放定址法和链地址法。

2、如何遍历二叉树?

二叉树的遍历有三种方式,对于上面这棵二叉树,他们的遍历结果为:

前序遍历:根节点 > 左子节点 > 右子节点。

10,6,4,8,14,12,16

中序遍历:左子节点 > 根节点 > 右子节点。

4,6,8,10,12,14,16

后序遍历:左子节点 > 右子节点 > 根节点。

4,8,6,12,16,14,10

3、简述下快速排序的过程,时间复杂度是多少?

快排的思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行。

一个简单的Swift实现方式如下:

func quicksort<T: Comparable>(_ a: [T]) -> [T] {
  guard a.count > 1 else { return a }

  let pivot = a[a.count/2]
  let less = a.filter { $0 < pivot }
  let equal = a.filter { $0 == pivot }
  let greater = a.filter { $0 > pivot }

  return quicksort(less) + equal + quicksort(greater)
}

快速排序是有好几种的,他们的区别在于如何实现filter和分区基准值的选取。

快排的时间复杂度是O(nlogn),空间复杂度是O(logn)

4、有一个整数数组,如何只遍历一遍就实现让该数组奇数都在前面,偶数都在后面?

这个是《剑指offer》里的一道题,leedcode也有对应题目:剑指offer 21

这个相对比较简单,因为不要求有序,可以采用收尾遍历的方式,进行交换,我这有个参考答案:

func sorted( _ nums: inout [Int]) -> [Int] {
    guard !nums.isEmpty else {
        return []
    }
    var start = 0
    var end = nums.count - 1
    while start < end {
        if nums[start] % 2 != 0 {
            start += 1
            continue
        }
        if nums[end] % 2 == 0 {
            end -= 1
            continue
        }
        (nums[start], nums[end]) = (nums[end], nums[start])
    }
    return nums
}

面试题

101、iOS面试题:聊下HTTP post的body体使用form-urlencoded和multipart/form-data的区别。

1、application/x-www-form-urlencoded: 窗体数据被编码为名称/值对,这是标准且默认的编码格式。当action为get时候,客户端把form数据转换成一个字串append到url后面,用?分割。当action为post时候,浏览器把form数据封装到http body中,然后发送到server。

2、multipart/form-data: multipart表示的意思是单个消息头包含多个消息体的解决方案。multipart媒体类型对发送非文本的各媒体类型是有用的。一般多用于文件上传。 multipart/form-data只是multipart的一种。目前常用的有以下这些类型(注:任何一种执行时无法识别的multipart子类型都被视为子类型"mixed")

102、iOS面试题:让你设计一种机制检测UIViewController的内存泄漏,你会怎么做?

如果Controller被释放了,但其曾经持有过的子对象如果还存在,那么这些子对象就是泄漏的可疑目标。

一个小示例:具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

103、iOS面试题:通过[UIImage imageNamed:]生成的对象什么时候被释放?

使用imageNamed这个方法生成的UIImage对象,会在应用的bundle中寻找图片,如果找到则Cache到系统缓存中,作为内存的cache,而程序员是无法操作cache的,只能由系统自动处理,如果我们需要重复加载一张图片,那这无疑是一种很好的方式,因为系统能很快的从内存的cache找到这张图片,但是试想,如果加载很多很大的图片的时候,内存消耗过大的时候,就会会强制释放内存,即会遇到内存警告(memory warnings).

由于在iOS系统中释放图片的内存比较麻烦,所以冲易产生内存泄露。 像[[UIImageView alloc] init]还有一些其他的 init 方法,返回的都是 autorelease 对象。而 autorelease 不能保证什么时候释放,所以不一定在引用计数为 0 就立即释放,只能保证在 autoreleasepool 结尾的时候释放。

像 UIImage 还有 NSData 这种,大部分情况应该是延迟释放的,可以理解为到 autoreleasepool 结束的时候才释放。

104、iOS面试题:applicationWillEnterForeground和applicationDidBecomeActive都会在哪些场景下被调用?举例越多越好。

1)applicationWillResignActive(将进入后台)

对应applicationWillEnterForeground(将进入前台)

程序将要失去Active状态时调用,比如按下Home键或有电话信息进来,这个方法用来

  • 暂停正在执行的任务;
  • 禁止计时器;
  • 减少OpenGL ES帧率;
  • 若为游戏应暂停游戏;

总结为一个字:停!

2)applicationDidEnterBackground(已经进入后台)

对应applicationDidBecomeActive(已经变成前台)

程序已经进入后台时调用,这个方法用来

  • 释放共享资源;
  • 保存用户数据(写到硬盘);
  • 作废计时器;
  • 保存足够的程序状态以便下次恢复;总结为4个字:释放、保存!

105、iOS面试题:dSYM你是如何分析的

方法1 使用XCode

这种方法可能是最容易的方法了。

  1. 要使用Xcode符号化 crash log,你需要下面所列的3个文件:
  2. crash报告(.crash文件)
  3. 符号文件 (.dsymb文件)
  4. 应用程序文件 (appName.app文件,把IPA文件后缀改为zip,然后解压,Payload目录下的appName.app文件), 这里的appName是你的应用程序的名称。
  5. 把这3个文件放到同一个目录下,打开Xcode的Window菜单下的organizer,然后点击Devices tab,然后选中左边的Device Logs。
  6. 然后把.crash文件拖到Device Logs或者选择下面的import导入.crash文件。
  7. 这样你就可以看到crash的详细log了。
方法2 使用命令行工具symbolicatecrash
  1. 有时候Xcode不能够很好的符号化crash文件。我们这里介绍如何通过symbolicatecrash来手动符号化crash log。
  2. 在处理之前,请依然将“.app“, “.dSYM”和 ".crash"文件放到同一个目录下。现在打开终端(Terminal)然后输入如下的命令:
  3. export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
  4. 然后输入命令:
  5. /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash appName.crash appName.app > appName.log
  6. 现在,符号化的crash log就保存在appName.log中了。
方法3 使用命令行工具atos
  1. 如果你有多个“.ipa”文件,多个".dSYMB"文件,你并不太确定到底“dSYMB”文件对应哪个".ipa"文件,那么,这个方法就非常适合你。
  2. 特别当你的应用发布到多个渠道的时候,你需要对不同渠道的crash文件,写一个自动化的分析脚本的时候,这个方法就极其有用。
  3. 具体方法 请百度

106、iOS面试题:多线程有哪几种?你更倾向于哪一种?

  1. NSThread
  2. Cocoa NSOperation (使用NSOperation和NSOperationQueue)
  3. GCD (Grand Central Dispatch)
1.NSThread:(两种创建方式)
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];

NSThread *myThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil];

[myThread start];

优点:NSThread 比其他两个轻量级。

缺点:需要自己管理线程的生命周期,线程同步,线程同步时对数据的加锁会有一定的系统开销。

2.Cocoa Operation
NSOperationQueue*oprationQueue= [[NSOperationQueuealloc] init];

oprationQueueaddOperationWithBlock:^{

//这个block语句块在子线程中执行

}

优点:不需要关心线程管理,数据同步的事情。 Cocoa Operation 相关的类是 NSOperation ,NSOperationQueue。NSOperation是个抽象类,使用它必须用它的子类,可以实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。创建NSOperation子类的对象,把对象添加到NSOperationQueue队列里执行,我们会把我们的执行操作放在NSOperation中main函数中。

3.GCD Grand Central Dispatch (GCD)

GCD是Apple开发的一个多核编程的解决方法,GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术。它让程序平行排队的特定任务,根据可用的处理资源,安排他们在任何可用的处理器核心上执行任务,一个任务可以是一个函数(function)或者是一个block。 dispatch queue分为下面三种: private dispatch queues,同时只执行一个任务,通常用于同步访问特定的资源或数据。 global dispatch queue,可以并发地执行多个任务,但是执行完成的顺序是随机的。 Main dispatch queue 它是在应用程序主线程上执行任务的。

GCD 扫盲篇

107、iOS面试题:单例的弊端

优点: 1:一个类只被实例化一次,提供了对唯一实例的受控访问。 2:节省系统资源 3:允许可变数目的实例。

缺点: 1:一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。 2:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。 3:滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

108、iOS面试题:App启动过慢,你可能想到的因素有哪些?

1. App启动过程
  1. 解析Info.plist
  • 加载相关信息,例如如闪屏
  • 沙箱建立、权限检查
  1. Mach-O加载
  • 如果是胖二进制文件,寻找合适当前CPU类别的部分
  • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
  • 定位内部、外部指针引用,例如字符串、函数等
  • 执行声明为attribute((constructor))的C函数
  • 加载类扩展(Category)中的方法
  • C++静态对象加载、调用ObjC的 +load 函数
  1. 程序执行
  • 调用main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching
2、影响启动性能的因素
  1. main()函数之前耗时的影响因素
  • 动态库加载越多,启动越慢。
  • ObjC类越多,启动越慢
  • C的constructor函数越多,启动越慢
  • C++静态对象越多,启动越慢
  • ObjC的+load越多,启动越慢
  1. main()函数之后耗时的影响因素
  • 执行main()函数的耗时
  • 执行applicationWillFinishLaunching的耗时
  • rootViewController及其childViewController的加载、view及其subviews的加载

另外参考一下今日头条的启动优化方案

针对于今日头条这个App我们可以优化的点如下:

  • 纯代码方式而不是storyboard加载首页UI。
  • 对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
  • 对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
  • 对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
  • 上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。

109、iOS面试题:怎么防止反编译?

  1. 本地数据加密。 iOS应用防反编译加密技术之一:对NSUserDefaults,sqlite存储文件数据加密,保护帐号和关键信息
  2. URL编码加密。 iOS应用防反编译加密技术之二:对程序中出现的URL进行编码加密,防止URL被静态分析
  3. 网络传输数据加密。 iOS应用防反编译加密技术之三:对客户端传输数据提供加密方案,有效防止通过网络接口的拦截获取数据
  4. 方法体,方法名高级混淆。 iOS应用防反编译加密技术之四:对应用程序的方法名和方法体进行混淆,保证源码被逆向后无法解析代码
  5. 程序结构混排加密。 iOS应用防反编译加密技术之五:对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低

110、iOS面试题:TCP和UDP的区别于联系

  • TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;
  • UDP为用户数据报协议,非连接的不可靠的点到多点的通信;
  • TCP侧重可靠传输,UDP侧重快速传输。

111、iOS面试题:TCP连接的三次握手

第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;

第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)

3788243-4830cacee1f1e7d3

112、iOS面试题:Scoket连接和HTTP连接的区别:

区别:

HTTP协议是基于TCP连接的,是应用层协议,主要解决如何包装数据。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

HTTP连接:短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源。服务器不能主动给客户端响应(除非采用HTTP长连接技术),iPhone主要使用类NSURLConnection。

Socket连接:长连接,客户端跟服务器端直接使用Socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据,一般多用于游戏.Socket默认连接超时时间是30秒,默认大小是8K(理解为一个数据包大小)。

113、iOS面试题:HTTP协议的特点,关于HTTP请求GET和POST的区别

GET和POST的区别:

  • HTTP超文本传输协议,是短连接,是客户端主动发送请求,服务器做出响应,服务器响应之后,链接断开。HTTP是一个属于应用层面向对象的协议,HTTP有两类报文:请求报文和响应报文。
  • HTTP请求报文:一个HTTP请求报文由请求行、请求头部、空行和请求数据4部分组成。
  • HTTP响应报文:由三部分组成:状态行、消息报头、响应正文。
  • GET请求:参数在地址后拼接,没有请求数据,不安全(因为所有参数都拼接在地址后面),不适合传输大量数据(长度有限制,为1024个字节)。

GET提交、请求的数据会附在URL之后,即把数据放置在HTTP协议头中。 以分割URL和传输数据,多个参数用&连接。如果数据是英文字母或数字,原样发送, 如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密。

  • POST请求:参数在请求数据区放着,相对GET请求更安全,并且数据大小没有限制。把提交的数据放置在HTTP包的包体中.

  • GET提交的数据会在地址栏显示出来,而POST提交,地址栏不会改变。

传输数据的大小:

  • GET提交时,传输数据就会受到URL长度限制,POST由于不是通过URL传值,理论上书不受限。

安全性:

  • POST的安全性要比GET的安全性高;

  • 通过GET提交数据,用户名和密码将明文出现在URL上,比如登陆界面有可能被浏览器缓存。

  • HTTPS:安全超文本传输协议(Secure Hypertext Transfer Protocol),它是一个安全通信通道,基于HTTP开发,用于客户计算机和服务器之间交换信息,使用安全套结字层(SSL)进行信息交换,即HTTP的安全版。

114、iOS面试题:ASIHttpRequest、AFNetWorking之间的区别

ASIHttpRequest功能强大,主要是在MRC下实现的,是对系统CFNetwork API进行了封装,支持HTTP协议的CFHTTP,配置比较复杂,并且ASIHttpRequest框架默认不会帮你监听网络改变,如果需要让ASIHttpRequest帮你监听网络状态改变,并且手动开始这个功能。

AFNetWorking构建于NSURLConnection、NSOperation以及其他熟悉的Foundation技术之上。拥有良好的架构,丰富的API及模块构建方式,使用起来非常轻松。它基于NSOperation封装的,AFURLConnectionOperation子类。

ASIHttpRequest是直接操作对象ASIHttpRequest是一个实现了NSCoding协议的NSOperation子类;AFNetWorking直接操作对象的AFHttpClient,是一个实现NSCoding和NSCopying协议的NSObject子类。

同步请求:ASIHttpRequest直接通过调用一个startSynchronous方法;AFNetWorking默认没有封装同步请求,如果开发者需要使用同步请求,则需要重写getPath:paraments:success:failures方法,对于AFHttpRequestOperation进行同步处理。

115、iOS面试题:XML数据解析方式各有什么不同,JSON解析有哪些框架?

  1. XML数据解析的两种解析方式:DOM解析和SAX解析;

DOM解析必须完成DOM树的构造,在处理规模较大的XML文档时就很耗内存,占用资源较多,读入整个XML文档并构建一个驻留内存的树结构(节点树),通过遍历树结构可以检索任意XML节点,读取它的属性和值,通常情况下,可以借助XPath查询XML节点;

SAX与DOM不同,它是事件驱动模型,解析XML文档时每遇到一个开始或者结束标签、属性或者一条指令时,程序就产生一个事件进行相应的处理,一边读取XML文档一边处理,不必等整个文档加载完才采取措施,当在读取解析过程中遇到需要处理的对象,会发出通知进行处理。因此,SAX相对于DOM来说更适合操作大的XML文档。

  1. JSON解析:

性能比较好的主要是第三方的JSONKIT和iOS自带的JSON解析类,其中自带的JSON解析性能最高,只能用于iOS5之后。

116、iOS面试题:SVN的使用

  • SVN=版本控制+备份服务器,可以把SVN当成备份服务器,并且可以帮助你记住每次上服务器的档案内容,并自动赋予每次变更的版本;

  • SVN的版本控制:所有上传版本都会帮您记录下来,也有版本分支及合并等功能。SVN可以让不同的开发者存取同样的档案,并且利用SVN Server作为档案同步的机制,即您有档案更新时,无需将档案寄送给您的开发成员。SVN的存放档案方式是采用差异备份的方式,即会备份到不同的地方,节省硬盘空间,也可以对非文字文件进行差异备份。

  • SVN的重要性:备份工作档案的重要性、版本控管的重要性、伙伴间的数据同步的重要性、备份不同版本是很耗费硬盘空间的;

防止冲突:

  1. 防止代码冲突:不要多人同时修改同一文件,例如:A、B都修改同一个文件,先让A修改,然后提交到服务器,然后B更新下来,再进行修改;

  2. 服务器上的项目文件Xcodeproj,仅让一个人管理提交,其他人只更新,防止文件发生冲突。

117、iOS面试题:如何进行网络消息推送

一种是Apple自己提供的通知服务(APNS服务器)、一种是用第三方推送机制。

首先应用发送通知,系统弹出提示框询问用户是否允许,当用户允许后向苹果服务器(APNS)请求deviceToken,并由苹果服务器发送给自己的应用,自己的应用将DeviceToken发送自己的服务器,自己服务器想要发送网络推送时将deviceToken以及想要推送的信息发送给苹果服务器,苹果服务器将信息发送给应用。

推送信息内容,总容量不超过256个字节;

  1. iOS SDK本身提供的APNS服务器推送,它可以直接推送给目标用户并根据您的方式弹出提示。
  • 优点:不论应用是否开启,都会发送到手机端;
  • 缺点:消息推送机制是苹果服务端控制,个别时候可能会有延迟,因为苹果服务器也有队列来处理所有的消息请求;
  1. 第三方推送机制,普遍使用Socket机制来实现,几乎可以达到即时的发送到目标用户手机端,适用于即时通讯类应用。
  • 优点:实时的,取决于心跳包的节奏;
  • 缺点:iOS系统的限制,应用不能长时间的后台运行,所以应用关闭的情况下这种推送机制不可用。

118、iOS面试题:网络七层协议

应用层: 1.用户接口、应用程序; 2.Application典型设备:网关; 3.典型协议、标准和应用:TELNET、FTP、HTTP

表示层: 1.数据表示、压缩和加密presentation 2.典型设备:网关 3.典型协议、标准和应用:ASCLL、PICT、TIFF、JPEG|MPEG 4.表示层相当于一个东西的表示,表示的一些协议,比如图片、声音和视频MPEG。

会话层: 1.会话的建立和结束; 2.典型设备:网关; 3.典型协议、标准和应用:RPC、SQL、NFS、X WINDOWS、ASP

传输层: 1.主要功能:端到端控制Transport; 2.典型设备:网关; 3.典型协议、标准和应用:TCP、UDP、SPX

网络层: 1.主要功能:路由、寻址Network; 2.典型设备:路由器; 3.典型协议、标准和应用:IP、IPX、APPLETALK、ICMP;

数据链路层: 1.主要功能:保证无差错的疏忽链路的data link; 2.典型设备:交换机、网桥、网卡; 3.典型协议、标准和应用:802.2、802.3ATM、HDLC、FRAME RELAY;

物理层: 1.主要功能:传输比特流Physical; 2.典型设备:集线器、中继器 3.典型协议、标准和应用:V.35、EIA/TIA-232.

119、iOS面试题:关键字volatile有什么含意?并给出三个不同的例子:

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

  • 并行设备的硬件寄存器(如:状态寄存器);

  • 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables);

  • 多线程应用中被几个任务共享的变量。

120、iOS面试题:property属性的修饰符的作用

  • getter=getName、setter=setName:设置setter与getter的方法名;
  • readwrite、readonly:设置可供访问级别;
  • assign:方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题;
  • retain:其setter方法对参数进行release旧值再* retain新值,所有实现都是这个顺序;
  • copy:其setter方法进行copy操作,与retain处理流程一样,先对旧值release,再copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。
  • nonatomic:非原子性访问,不加同步, 多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。

121、iOS面试题:XIB与Storyboards的优缺点

优点:

  • XIB:在编译前就提供了可视化界面,可以直接拖控件,也可以直接给控件添加约束,更直观一些,而且类文件中就少了创建控件的代码,确实简化不少,通常每个XIB对应一个类。

  • Storyboard:在编译前提供了可视化界面,可拖控件,可加约束,在开发时比较直观,而且一个storyboard可以有很多的界面,每个界面对应一个类文件,通过storybard,可以直观地看出整个App的结构。

缺点:

  • XIB:需求变动时,需要修改XIB很大,有时候甚至需要重新添加约束,导致开发周期变长。XIB载入相比纯代码自然要慢一些。对于比较复杂逻辑控制不同状态下显示不同内容时,使用XIB是比较困难的。当多人团队或者多团队开发时,如果XIB文件被发动,极易导致冲突,而且解决冲突相对要困难很多。

  • Storyboard:需求变动时,需要修改storyboard上对应的界面的约束,与XIB一样可能要重新添加约束,或者添加约束会造成大量的冲突,尤其是多团队开发。对于复杂逻辑控制不同显示内容时,比较困难。当多人团队或者多团队开发时,大家会同时修改一个storyboard,导致大量冲突,解决起来相当困难。

122、iOS面试题:内存的使用和优化的注意事项

  • 重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用;
  • 尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;
  • 不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多;
  • 选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
  • gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。
  • 延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。
  • 数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。
  • 处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
  • 重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。
  • 避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要;
  • 使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;
  • 正确选择图片加载方式:UIImage加载方式

123、iOS面试题:基于CTMediator的组件化方案,有哪些核心组成?

假如主APP调用某业务A,那么需要以下组成部分:

  • CTMediator类,该类提供了函数 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget; 这个函数可以根据targetName生成对象,根据actionName构造selector,然后可以利用performSelector:withObject:方法,在目标上执行动作。

  • 业务A的实现代码,另外要加一个专门的类,用于执行Target Action 类的名字的格式:Target_%@,这里就是Target_A。 这个类里面的方法,名字都以Action_开头,需要传参数时,都统一以NSDictionary*的形式传入。CTMediator类会创建Target类的对象,并在对象上执行方法。

  • 业务A的CTMediator扩展 扩展里声明了所有A业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。 在扩展的实现里,对Target, Action需要通过硬编码进行指定。由于扩展的负责方和业务的负责方是相同的,所以这个不是问题。

124、iOS面试题:为什么CTMediator方案优于基于Router的方案?

Router的缺点:

  • 在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。
  • 在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。
  • 在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。
  • 为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的。

CTMediator的优点:

  • 调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。
  • 组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。
  • 方便传递各种类型的参数。

125、iOS面试题:MVVM设计模式

轻量级的视图(控制器), 所有的UI逻辑都位于ViewModel中。 易测试性。可以在没有视图的情况下,运行整个应用。

126、iOS面试题:weak修饰的释放则自动被置为nil的实现原理

  • Runtime维护着一个Weak表,用于存储指向某个对象的所有Weak指针
  • Weak表是Hash表,Key是所指对象的地址,Value是Weak指针地址的数组
  • 在对象被回收的时候,经过层层调用,会最终触发下面的方法将所有Weak指针的值设为nil。* runtime源码,objc-weak.m 的 arr_clear_deallocating 函数
  • weak指针的使用涉及到Hash表的增删改查,有一定的性能开销.

127、iOS面试题:HTTPS的加密原理

  • 服务器端用非对称加密(RSA)生成公钥和私钥
  • 然后把公钥发给客户端, 服务器则保存私钥
  • 客户端拿到公钥后, 会生成一个密钥, 这个密钥就是将来客户端和服务器用来通信的钥匙
  • 然后客户端用公钥对密钥进行加密, 再发给服务器
  • 服务器拿到客户端发来的加密后的密钥后, 再使用私钥解密密钥, 到此双方都获得通信的钥匙

128、iOS面试题:你认为开发中那些导致crash?

当iOS设备上的App应用闪退时,操作系统会生成一个crash日志,保存在设备上。crash日志上有很多有用的信息,比如每个正在执行线程的完整堆栈跟踪信息和内存映像,这样就能够通过解析这些信息进而定位crash发生时的代码逻辑,从而找到App闪退的原因。

通常来说,crash产生来源于两种问题:违反iOS系统规则导致的crash和App代码逻辑BUG导致的crash

1.应用逻辑的Bug
  • SEGV:(Segmentation Violation,段违例),无效内存地址,比如空指针,未初始化指针,栈溢出等;
  • SIGABRT:收到Abort信号,可能自身调用abort()或者收到外部发送过来的信号;
  • SIGBUS:总线错误。与SIGSEGV不同的是,SIGSEGV访问的是无效地址(比如虚存映射不到物理内存),而SIGBUS访问的是有效地址,但总线访问异常(比如地址对齐问题);
  • SIGILL:尝试执行非法的指令,可能不被识别或者没有权限;
  • SIGFPE:Floating Point Error,数学计算相关问题(可能不限于浮点计算),比如除零操作;
  • SIGPIPE:管道另一端没有进程接手数据; 常见的崩溃原因基本都是代码逻辑问题或资源问题,比如数组越界,访问野指针或者资源不存在,或资源大小写错误等
2.违反iOS系统规则产生crash的三种类型
  • 内存报警闪退 当iOS检测到内存过低时,它的VM系统会发出低内存警告通知,尝试回收一些内存;如果情况没有得到足够的改善,iOS会终止后台应用以回收更多内存;最后,如果内存还是不足,那么正在运行的应用可能会被终止掉。在Debug模式下,可以主动将客户端执行的动作逻辑写入一个log文件中,这样程序童鞋可以将内存预警的逻辑写入该log文件,当发生如下截图中的内存报警时,就是提醒当前客户端性能内存吃紧,可以通过Instruments工具中的Allocations 和 Leaks模块库来发现内存分配问题和内存泄漏问题。

  • 响应超时 当应用程序对一些特定的事件(比如启动、挂起、恢复、结束)响应不及时,苹果的Watchdog机制会把应用程序干掉,并生成一份相应的crash日志。

  • 用户强制退出 一看到“用户强制退出”,首先可能想到的双击Home键,然后关闭应用程序。不过这种场景一般是不会产生crash日志的,因为双击Home键后,所有的应用程序都处于后台状态,而iOS随时都有可能关闭后台进程,当应用阻塞界面并停止响应时这种场景才会产生crash日志。这里指的“用户强制退出”场景,是稍微比较复杂点的操作:先按住电源键,直到出现“滑动关机”的界面时,再按住Home键,这时候当前应用程序会被终止掉,并且产生一份相应事件的crash日志。

129、iOS面试题:分析下SDWebImage

1.SDWebImage 加载图片的流程

1.入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

2.进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

3.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

4.SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

5.如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

6.根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7.如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

8.如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

9.共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

10.图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

11.connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。

12.connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

13.图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

14.在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

15.imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

16.通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

17.将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

18.SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

19.SDWI 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache,方便使用。

20.SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

3788243-cf9c367301b36091

2. SDImageCache是怎么做数据管理的?

SDImageCache分两个部分,一个是内存层面的,一个是硬盘层面的。内存层面的相当是个缓存器,以Key-Value的形式存储图片。当内存不够的时候会清除所有缓存图片。用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于一周的图片文件。当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有直接返回,没有的话去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。

3.内部做Decoder的原因 (典型的空间换时间)

由于UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像,所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了

130、iOS面试题:SEL和Method和IMP分别说下再谈下对IMP的理解?

Method

先看下定义

runtime.h
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:

- (void)logName
{
    NSLog(@"name");
} 

这段代码,就是一个函数。

我们来看下objc_method这个结构体的内容:

  • SEL method_name 方法名
  • char *method_types 方法类型
  • IMP method_imp 方法实现
  • 在这个结构体重,我们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。

我们接着来看SEL。

SEL

还是先看定义

Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

这里要先说明下selector和SEL的关系,我在写本文的时候,其实搜索的是selector,直到我看到了selector的定义,才发现我理解一直不对。

@property SEL selector;

在文档中,selector的定义都是这样声明,也就是说:selector是SEL的一个实例,只是在iOS中,selector的使用是如此的频繁,我们才会把他当成一个概念。

selector怎么理解呢?我们可以想想股票,比如市场上有如此多公司在纳斯达克上市,而且他们的名字又非常的长,或者有些公司的名称也是相似的,都是**有限公司。那当市场去指定一个股票的时候,效率会非常低,当你着急想买股票的时候,你会跟你的经纪人说:“hi,peter,给我买一百股Tuniu limited liability company的股票吗?”,也许等你说完,经纪人输入完,市场就变化了,所以纳斯达克通常用代码,比如“TOUR”.这里的selector有类似的作用,就是让我们能够快速找到对应的函数。

文档中是这样讲的:

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

在iOS中,runtime会在运行的时候,通过load函数,将所有的methodhash然后map到set中。这样在运行的时候,寻找selector的速度就会非常快,不会因为runtime特性牺牲太多的性能。

selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了method的name,没有参数,所以没法区分不同的method。

比如:

- (void)caculate(NSInteger)num;

- (void)caculate(CGFloat)num;

是会报错的。

我们只能通过命名来区别:

- (void)caculateWithInt(NSInteger)num;

- (void)caculateWithFloat(CGFloat)num;
IMP

指向一个方法实现的指针

/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

这个就比较好理解了,就是指向最终实现程序的内存地址的指针。

综上,在iOS的runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

131、iOS面试题:Autorelease的原理 ?

ARC下面,我们使用@autoreleasepool{}来使用一个Autoreleasepool,实际上UIKit 通过RunLoopObserver 在RunLoop二次Sleep间Autoreleasepool进行Pop和Push,将这次Loop产生的autorelease对象释放 对编译器会编译大致如下:

void *DragonLiContext = objc_ AutoreleasepoolPush();
// {} 的 code 
objc_ AutoreleasepoolPop(DragonLiContext);

释放时机: 当前RunLoop迭代结束时候释放.

132、iOS面试题:ARC的工作原理

  • Automatic Reference Counting,自动引用计数,即ARC,ARC会自动帮你插入retain和release语句,ARC编译器有两部分,分别是前端编译器和优化器
  • 前端编译器:前端编译器会为“拥有的”每一个对象插入相应的release语句。如果对象的所有权修饰符是__strong,那么它就是被拥有的。如果在某个方法内创建了一个对象,前端编译器会在方法末尾自动插入release语句以销毁它。而类拥有的对象(实例变量/属性)会在dealloc方法内被释放。事实上,你并不需要写dealloc方法或调用父类的dealloc方法,ARC会自动帮你完成一切。此外,由编译器生成的代码甚至会比你自己写的release语句的性能还要好,因为编辑器可以作出一些假设。在ARC中,没有类可以覆盖release方法,也没有调用它的必要。ARC会通过直接使用objc_release来优化调用过程。而对于retain也是同样的方法。ARC会调用objc_retain来取代保留消息
  • ARC优化器: 虽然前端编译器听起来很厉害的样子,但代码中有时仍会出现几个对retain和release的重复调用。ARC优化器负责移除多余的retain和release语句,确保生成的代码运行速度高于手动引用计数的代码

133、用户需要上传和下载一个重要的资料文件,应该如何判断用户本次是否上传成功和下载成功了?

  • 用MD5验证文件的完整性!(仅仅通过代码来判断当前次的请求发送结束或者收到数据结束不可以的)
  • 当客户端上传一个文件的时候,在请求body里面添加该文件的MD5值来告诉服务器,服务器接受文件完毕以后通过校验收到的文件的MD5值与请求body里面的MD5值来最终确定本次上传是否成功
  • 当客户端下载一个文件的时候,在响应头里面收到了服务器附带的该文件的MD5值,文件下载结束以后,通过获取下载后文件的MD5值与本次请求服务器返回的响应头中的MD5值做一个比较,来最终判断本次下载是否成功
  • MD5,是一个将任意长度的数据字符串转化成短的固定长度的值的单向操作。任意两个字符串不应有相同的散列值
  • MD5校验可以应用在多个领域,比如说机密资料的检验,下载文件的检验,明文密码的加密等。MD5校验原理举例:如客户往我们数据中心同步一个文件,该文件使用MD5校验,那么客户在发送文件的同时会再发一个存有校验码的文件,我们拿到该文件后做MD5运算,得到的计算结果与客户发送的校验码相比较,如果一致则认为客户发送的文件没有出错,否则认为文件出错需要重新发送。

134、iOS面试题:isa指针的作用

  • 对象的isa指向类,类的isa指向元类(meta class),元类isa指向元类的根类。isa帮助一个对象找到它的方法

  • 是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。 元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。 元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

135、iOS面试题:与 NSURLConnection 相比,NSURLsession 改进哪些?

  • 可以配置每个 session 的缓存,协议,cookie,以及证书策略(credential policy),甚至跨程序共享这些信息

  • session task。它负责处理数据的加载以及文件和数据在客户端与服务端之间的上传和下载。

  • NSURLSessionTask 与 NSURLConnection 最大的相似之处在于它也负责数据的加载,最大的不同之处在于所有的 task 共享其创造者 NSURLSession 这一公共委托者(common delegate)

136、iOS面试题:使用drawRect有什么影响?

drawRect方法依赖Core Graphics框架来进行自定义的绘制

  • 缺点:它处理touch事件时每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了

  • 这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制

137、iOS面试题:什么时候会报unrecognized selector的异常?如何避免?

  • 当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决,如果还是不行就会报unrecognized selector异常

  • objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下: objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类然后在该类中的方法列表以及其父类方法列表中寻找方法运行 如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会

  • 三次拯救程序崩溃的机会 Method resolution:objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。 如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程 如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发

    Fast forwarding:如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但Normal forwarding转发会创建一个NSInvocation对象,相对Normal forwarding转发更快点,所以这里叫Fast forwarding

    Normal forwarding 这一步是Runtime最后一次给你挽救的机会。 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。 如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。 如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象

138、iOS面试题:iOS中常用的数据存储方式有哪些?

综合

所有的本地持久化数据存储的本质都是写文件,而且只能存到沙盒中。

沙盒机制是苹果的一项安全机制,本质就是系统给每个应用分配了一个文件夹来存储数据,而且每个应用只能访问分配给自己的那个文件夹,其他应用的文件夹是不能访问的。

数据存储的核心都是写文件。主要有四种持久化方式:

  • 属性列表,
  • 对象序列化,
  • SQLite 数据库,
  • CoreData

属性列表: 应用于少量数据存储,比如登陆的用户信息,应用程序配置信息等。只有NSString ,NSArray,NSDictory,NSData,可以WriteToFile;存储的依旧是plist文件,plist文件可以存储的7种数据类型:array,dictory,string,bool,data,date,number。

对象序列化: 最终也是存为属性列表文件,如果程序中,需要存储的时候,直接存储对象比较方便,例如有一个设置类,我们可以把设置类的对象直接存储,就没必要再把里面的每一个属性单独存到文件中。对象序列化是将一个实现了NSCoding协议的对象,通过序列化(NSKeydArchiver)的形式,将对象中的属性抽取出来,转化成二进制流,也就是NSData,NSData可以选择write to file 或者存储到NSUserdefault中。 必须实现的两个方法 encodeWithCoder,initWithCoder。对象序列化的本质就是 对象NSData。

SQLite: 适合大量,重复,有规律的数据存储。而且频繁的读取,删除,过滤数据,这种适合使用数据库 (iOS 使用第三方FMDB)

CoreData: Sqlite叫做关系型数据库,CoreData 是一中OR-Mapping的思想 ,O代表对象Object,R代表relationship,Mapping代表映射,直译过来就是对象关系映射,其实就是把对象的属性和表中的字段自动映射,简化程序员的负担,以面向对象的方式操作数据库。ORMapping是一种思想,CoreData实现了这种思想,在Java中,hibernate 也是对ORMapping的一种实现,只是利用java实现的。

CoreData 本质还是数据库,只不过使用起来更加面向对象,不关注二维的表结构,而是只需要关注对象,纯面向对象的数据操作方式。我们直接使用数据库的时候,如果向数据库中插入数据,一般是把一个对象的属性和数据库中某个表的字段一一对应,然后把对象的属性存储到具体的表字段中.取一条数据的时候,把表中的一行数据取出,同样需要再封装到对象的属性中,这样的方式有点繁琐,不面向对象。CoreData解决的问题就是不需要这个中间的转换过程,看起来是直接把对象存储进去,并且取出来,不关心表的存在,实际内部帮你做好了映射关系。

139、iOS面试题:描述一个ViewController的生命周期

  • 当我们调用UIViewControlller的view时,
  • 系统首先判断当前的 UIViewControlller是否存在* view,如果存在直接返回view,
  • 如果不存在的话,会调用loadview方法,
  • 然后判断loadview方法是否是自定义方法,
  • 如果是自定义方法,就执行自定义方法,
  • 如果不是自定义方法,判断当时视图控制器是否有* xib、stroyboard。
  • 如果有xib、stroyboard 就加载xib、stroyboard。
  • 如果没有创建一个空白的view。
  • 调用viewDidLoad方法。
  • 最后返回view

140、iOS面试题:Block中可以修改全局变量,全局静态变量,局部静态变量吗?

可以.深入研究Block捕获外部变量和__block实现原理

  • 全局变量和静态全局变量的值改变,以及它们被Block捕获进去,因为是全局的,作用域很广
  • 静态变量和自动变量,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量
  • 自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。
  • Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量
  • Block就分为以下3种
  1. _NSConcreteStackBlock:只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。 StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了,是不持有对象的 _NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。比如当Block作为函数返回值的时候,肯定会copy到堆上
  2. _NSConcreteMallocBlock:有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制,是持有对象的
  3. _NSConcreteGlobalBlock:没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束,也不持有对象

ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上

ARC下,Block中引用id类型的数据有没有__block都一样都是retain,而对于基础变量而言,没有的话无法修改变量值,有的话就是修改其结构体令其内部的forwarding指针指向拷贝后的地址达到值的修改

141、iOS面试题:SDWebImage实现原理是什么? 它是如何解决tableView的复用时出现图片错乱问题的呢?

解决tableView复用错乱问题:每次都会调UIImageView+WebCache文件中的 [self sd_cancelCurrentImageLoad];

原理解释参考

  • SDWebImageDownloader
  • 图片的下载操作放在一个NSOperationQueue并发操作队列中,队列默认最大并发数是6
  • 每个图片对应一些回调(下载进度,完成回调等),回调信息会存在downloader的URLCallbacks(一个字典,key是url地址,value是图片下载回调数组)中,URLCallbacks可能被多个线程访问,所以downloader把下载任务放在一个barrierQueue中,并设置屏障保证同一时间只有一个线程访问URLCallbacks。,在创建回调URLCallbacks的block中创建了一个NSOperation并添加到NSOperationQueue中
  • 下载的核心是利用NSURLSession加载数据,每个图片的下载都有一个operation操作来完成,并将这些操作放到一个操作队列中,这样可以实现图片的并发下载。
  • 内存缓存的处理由NSCache对象实现,NSCache类似一个集合的容器,它存储key-value对,类似于nsdictionary类,我们通常使用缓存来临时存储短时间使用但创建昂贵的对象,重用这些对象可以优化新能,同时这些对象对于程序来说不是紧要的,如果内存紧张就会自动释放。
  • 先在内存中放置一份缓存,如果需要缓存到磁盘,将磁盘缓存操作作为一个task放到串行队列中处理,会先检查图片格式是jpeg还是png,将其转换为响应的图片数据,最后吧数据写入磁盘中(文件名是对key值做MD5后的串)。

142、iOS面试题:AFNetworking 底层原理分析

AFNetworking主要是对NSURLSession和NSURLConnection(iOS9.0废弃)的封装,其中主要有以下类: 1). AFHTTPRequestOperationManager:内部封装的是 NSURLConnection, 负责发送网络请求, 使用最多的一个类。(3.0废弃) 2). AFHTTPSessionManager:内部封装是 NSURLSession, 负责发送网络请求,使用最多的一个类。 3). AFNetworkReachabilityManager:实时监测网络状态的工具类。当前的网络环境发生改变之后,这个工具类就可以检测到。 4). AFSecurityPolicy:网络安全的工具类, 主要是针对 HTTPS 服务。 5). AFURLRequestSerialization:序列化工具类,基类。上传的数据转换成JSON格式 (AFJSONRequestSerializer).使用不多。 6). AFURLResponseSerialization:反序列化工具类;基类.使用比较多: 7). AFJSONResponseSerializer; JSON解析器,默认的解析器. 8). AFHTTPResponseSerializer; 万能解析器; JSON和XML之外的数据类型,直接返回二进制数据.对服务器返回的数据不做任何处理. 9). AFXMLParserResponseSerializer; XML解析器;

143、iOS面试题:描述下SDWebImage里面给UIImageView加载图片的逻辑

SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后再替换占位图片。

加载图片的过程大致如下:

首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存 如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来 如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片 下载后的图片会加入缓存中,并写入磁盘中 整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来 SDWebImage原理: 调用类别的方法:

从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过),找到直接使用。 从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中。 从网络上获取,使用,缓存到内存,缓存到沙盒。

144、iOS面试题:分析json、xml 的区别? json、xml 解析 式的底层是如何让处理的

(一)JSON与XML的区别:

(1)可读性方面:基本相同,XML的可读性比较好; (2)可扩展性方面:都具有良好的扩展性; (3)编码难度方面:相对而言,JSON的编码比较容易; (4)解码难度:JSON的解码难度基本为零,XML需要考虑子节点和父节点; (5)数据体积方面:JSON相对于XML来讲,数据体积小,传递的速度比较快; (6)数据交互方面:JSON与javascript的交互更加方便,更容易解析处理,更好的数据交互; (7)数据描述方面:XML对数据描述性比较好 (8)传输速度方面:JSON的速度远远快于XML。

(二)JSON与XML底层实现原理:

(1)JSON底层原理:遍历字符串中的字符,最终根据格式规定的特殊字符,比如{}、[]、:等进行区分,{}号表示字典,[]号表示数组,:号是字典的键和值的分水岭,最终仍是将JSON转化为字典,只不过字典中的值可能是“字典、数组或者字符串而已”。 (2)XML底层原理:XML解析常用的解析方法有两种:DOM解析和SAX解析;DOM采用的是树形结构的方式访问XML文档,而SAX采用的是事件模型;DOM解析把XML文档转化为一个包含其内容的树,并可以对树进行遍历,使用DOM解析器的时候需要处理整个XML文档,所以对内存和性能的要求比较高;SAX在解析XML文档的时候可以触发一系列的事件,当发现给定的tag的时候,他可以激活一个回调方法,告诉该方法指定的标签已经找到,SAX对内存的要求通常会比较低,因为他让开发人员自己来决定所要处理的tag,特别是当开发人员只需要处理文档中所包含部分数据时,SAX这种扩展能力得到了更好的体现。

145、iOS面试题:对程序性能的优化你有什么建议?

1.使用复用机制

2.尽可能设置 View 为不透明

3.避免臃肿的 XIB 文件

4.不要阻塞主线程

5.图片尺寸匹配 UIImageView

6.选择合适的容器

7.启用 GZIP 数据压缩

8.View 的复用和懒加载机制

9、缓存

  • 服务器的响应信息(response)。
  • 图片。
  • 计算值。比如:UITableView 的 row heights。

10.关于图形绘制

11.处理 Memory Warnings

  • 在 AppDelegate 中实现 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。 在 UIViewController 中重载 didReceiveMemoryWarning 方法。 监听 UIApplicationDidReceiveMemoryWarningNotification 通知。

12.复用高开销的对象

13.减少离屏渲染(设置圆角和阴影的时候可以选用绘制的方法)

14.优化 UITableView

  • 通过正确的设置 reuseIdentifier 来重用 Cell。
  • 尽量减少不必要的透明 View。
  • 尽量避免渐变效果、图片拉伸和离屏渲染。
  • 当不同的行的高度不一样时,尽量缓存它们的高度值。
  • 如果 Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
  • 使用 shadowPath 来设置阴影效果。
  • 尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写 drawRect。
  • 尽量优化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。
  • 选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
  • 对于 rowHeight、sectionFooterHeight、sectionHeaderHeight 尽量使用常量。

15.选择合适的数据存储方式 在 iOS 中可以用来进行数据持有化的方案包括:

  • NSUserDefaults。只适合用来存小数据。
  • XML、JSON、Plist 等文件。JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
  • 使用 NSCoding 来存档。NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
  • 使用 SQLite 数据库。可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
  • 使用 CoreData。也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。

16.减少应用启动时间

  • 快速启动应用对于用户来说可以留下很好的印象。尤其是第一次使用时。
  • 保证应用快速启动的指导原则:
  • 尽量将启动过程中的处理分拆成各个异步处理流,比如:网络请求、数据库访问、数据解析等等。
  • 避免臃肿的 XIB 文件,因为它们会在你的主线程中进行加载。重申:Storyboard 没这个问题,放心使用。
  • 注意:在测试程序启动性能的时候,最好用与 Xcode 断开连接的设备进行测试。因为 watchdog 在使用 Xcode 进行调试的时候是不会启动的。

17.使用 Autorelease Pool (内存释放池)

18.imageNamed 和 imageWithContentsOfFile

  • imageNamed:第一次加载图片时会缓存图片到内存,适合使用频繁的图片,imageWithContentsOfFile:不会把图片缓存到内存,每次调用都要重新从磁盘加载一次。

146、iOS面试题:讲讲iOS事件响应链的原理

1、响应者链通常是由视图(UIView)构成的; 2、一个视图的下一个响应者是它视图控制器(UIViewController)(如果有的话),然后再转给它的父视图(Super View); 3、视图控制器(如果有的话)的下一个响应者为其管理的视图的父视图; 4、单例的窗口(UIWindow)的内容视图将指向窗口本身作为它的下一个响应者 需要指出的是,Cocoa Touch应用不像Cocoa应用,它只有一个UIWindow对象,因此整个响应者链要简单一点; 5、单例的应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

147、iOS面试题:假如Controller太臃肿,如何优化?

1.将网络请求抽象到单独的类中 方便在基类中处理公共逻辑; 方便在基类中处理缓存逻辑,以及其它一些公共逻辑; 方便做对象的持久化。

2.将界面的封装抽象到专门的类中 构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。

3.构造 ViewModel 借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程。

4.专门构造存储类 专门来处理本地数据的存取。

148、iOS面试题:介绍下App启动的完成过程?

  1. App启动过程 • 解析Info.plist ▪ 加载相关信息,例如如闪屏 ▪ 沙箱建立、权限检查

• Mach-O加载 ▪ 如果是胖二进制文件,寻找合适当前CPU类别的部分 ▪ 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法) ▪ 定位内部、外部指针引用,例如字符串、函数等 ▪ 执行声明为attribute((constructor))的C函数 ▪ 加载类扩展(Category)中的方法 ▪ C++静态对象加载、调用ObjC的 +load 函数

• 程序执行 · 1.main函数 · 2.执行UIApplicationMain函数 ·   1.创建UIApplication对象 ·   2.创建UIApplicationDelegate对象并复制 ·   3.读取配置文件info.plist,设置程序启动的一些属性,(关于info.plist的内容可网上搜索下) ·   4.创建应用程序的Main Runloop循环 · 3.UIApplicationDelegate对象开始处理监听到的事件 ·   1.程序启动成功之后,首先调用application:didFinishLaunchingWithOptions:方法, ·   如果info.plist文件中配置了启动storyboard文件名,则加载storyboard文件。 ·   如果没有配置,则根据代码来创建UIWindow--->UIWindow的rootViewController-->显示

149、iOS面试题:哈希原理

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

哈希概念:哈希表的本质是一个数组,数组中每一个元素称为一个箱子(bin),箱子中存放的是键值对。

150、iOS面试题:哈希存储过程

1.根据 key 计算出它的哈希值 h。

2.假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中。

3.如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突。

在使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。

哈希表还有一个重要的属性: 负载因子(load factor),它用来衡量哈希表的空/满程度,一定程度上也可以体现查询的效率,计算公式为:

负载因子 = 总键值对数 / 箱子个数

负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将自动扩容。

哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。

哈希表的扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。

基于以上总结,细心的朋友可能会发现哈希表的两个问题:

1.如果哈希表中本来箱子就比较多,扩容时需要重新哈希并移动数据,性能影响较大。

2.如果哈希函数设计不合理,哈希表在极端情况下会变成线性表,性能极低。

3.block和函数指针的理解;

相似点:

函数指针和Block都可以实现回调的操作,声明上也很相似,实现上都可以看成是一个代码片段。

函数指针类型和Block类型都可以作为变量和函数参数的类型。(typedef定义别名之后,这个别名就是一个类型)

不同点:

函数指针只能指向预先定义好的函数代码块(可以是其他文件里面定义,通过函数参数动态传入的),函数地址是在编译链接时就已经确定好的。

Block本质是Objective-C对象,是NSObject的子类,可以接收消息。

函数里面只能访问全局变量,而Block代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过__block访问指示符修饰的局部变量还可以在block代码块里面进行修改)。

从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而block实际上是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。

151、iOS面试题:NSDate\CFAbsoluteTimeGetCurrent\CACurrentMediaTime的区别

1.从框架的角度

NSDate 属于Foundation CFAbsoluteTimeGetCurrent 属于 Core Foundation CACurrentMediaTime 属于 QuartzCore

2.本质区别

NSDate 或 CFAbsoluteTimeGetCurrent 返回的系统时钟时间将会从网络时钟同步. 从时钟偏移量的角度 mach_absolute_time() 和 CACurrentMediaTime 基于内建时钟.能够更精确的测试时间,并且不会根据外部的时间变化而变化.(例如,时区变化\夏时制),它和系统的upTime有关.系统重启后,CACurrentMediaTime 也会重新设置. 3.常用用法

NSDate \CFAbsoluteTimeGetCurrent 常用于日常时间,时间戳表示,与服务器之间的数据交互. CFAbsoluteTimeGetCurrent 相当于 [NSDate date]timeIntervalSinceReferenceDate]; CACurrentMediaTime 常用测试代码效率 4.项目使用场景

分析: 在购物车点击"+"和"-"时.如果说立即进行服务器同步的话,代价是非常高!所以会采用"延时提交"的方法.

思路: 连续点击的时候,不提交服务器.点击停止时,进行同步.

连续点击如何判断了??

采用的是判断2次点击的时间差,如果时间差小于一定的数值(自己根据项目需求判定).那么即可判断是连续点击的操作.

152、iOS面试题:讲一下MVC和MVVM,MVP?

简单来说MVC(Model View Controller)就是模型(Model)- 视图(View)-控制器(Controller)的缩写,Model是用来处理数据,View是用来展示界面,Cotroller是用来调节他们两者之间的交互。 这个是最常用的。但是View和Model之间的直接交互,就导致了View和Model之间的耦合性比较大。

MVP (Model View Presenter)是MVC模式的变种,使用Presenter代替了Controller,而且改变了数据流向 View和Model之间不再直接进行交互,而是通过Presenter来进行的。总体来说Presenter同时持有View和Model。

优点:整体框架分层清晰,降低了耦合度。

缺点:需要加入Presenter来作为协调Model和View的桥梁,同时也导致了Presenter的臃肿。在维护起来不方便。

MVVM(Model View View-Model ViewModel)其实是对MVP的一种改进,他将Presenter替换成ViewModel, 并通过双向数据绑定来实现视图和数据的交互。

优点:使其数据流向更加清晰(脑补一下就是云对雨,x对风,大陆对长空)。一一对应起来。

缺点:这种架构没有统一的实现方式,每个人各有特色。我接触过得各有不同。而且有一定的学习成本。(不懂得人去学习也得一周左右吧,不抬杠,学过之后再来回话。"大牛请忽略")

153、iOS面试题:为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?

1、因为用weak修饰的对象引用计数不会+1.好吧。说白了就是不会造成循环引用。0.0~!

2、delegate偏重于用户交互的回调。dataSource偏重于数据的回调。

3、首先共同点都是实现回调的方式。算了直白点说就是一个一对一,一个一对多。一个编写简单,一个实现繁复。 对于什么时候用block什么时候用代理简单来说就是公共接口,方法较多,回调类型较多可以选择用代理。对于异步和简单的回调用block好一点。

154、iOS面试题:进程和线程的区别?同步异步的区别?并行和并发的区别?

1.进程和线程的区别   线程和进程的区别主要在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式的影响下不会对其他进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等同于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。   线程是CPU独立运行和独立调度的基本单位(可以理解为一个进程中执行的代码片段)。   进程是资源分配的基本单位(进程是一块包含了某些资源的内存区域)。   进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程是线程的容器,真正完成代码执行的线程,而进程则作为线程的执行环境。一个程序至少包含一个进程,一个进程至少包含一个线程,一个进程中的所有线程共享当前进程所拥有的资源。

2.同步异步的区别   异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。   异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。

3.并行和并发的区别   并发行和并行性的区别可以用馒头做比喻。前者相当于一个人同时吃三个馒头和三个人同时吃一个馒头。   并发性(Concurrence):指两个或两个以上的事件或活动在同一时间间隔内发生。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。   并行性(parallelism)指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。   区别:一个处理器同时处理多个任务和多个处理器或者是多核的处理器同时处理多个不同的任务。   前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生。   两者的联系:并行的事件或活动一定是并发的,但反之并发的事件或活动未必是并行的。并行性是并发性的特例,而并发性是并行性的扩展。

155、iOS面试题:如何提升 tableview 的流畅度?

本质上是降低 CPU、GPU 的工作,从这两个大的方面去提升性能。

  • CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制
  • GPU:纹理的渲染

卡顿优化在 CPU 层面

  • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用 CALayer 取代 UIView
  • 不要频繁地调用 UIView 的相关属性,比如 frame、bounds、transform 等属性,尽量减少不必要的修改
  • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
  • Autolayout 会比直接设置 frame 消耗更多的 CPU 资源
  • 图片的 size 最好刚好跟 UIImageView 的 size 保持一致
  • 控制一下线程的最大并发数量
  • 尽量把耗时的操作放到子线程
  • 文本处理(尺寸计算、绘制)
  • 图片处理(解码、绘制)

卡顿优化在 GPU层面

  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
  • GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用 CPU 资源进行处理,所以纹理尽量不要超过这个尺寸
  • 尽量减少视图数量和层次
  • 减少透明的视图(alpha<1),不透明的就设置 opaque 为 YES
  • 尽量避免出现离屏渲染

iOS 保持界面流畅的技巧

1.预排版,提前计算

  • 在接收到服务端返回的数据后,尽量将 CoreText 排版的结果、单个控件的高度、cell 整体的高度提前计算好,将其存储在模型的属性中。需要使用时,直接从模型中往外取,避免了计算的过程。

  • 尽量少用 UILabel,可以使用 CALayer 。避免使用 AutoLayout 的自动布局技术,采取纯代码的方式

2.预渲染,提前绘制

  • 例如圆形的图标可以提前在,在接收到网络返回数据时,在后台线程进行处理,直接存储在模型数据里,回到主线程后直接调用就可以了

  • 避免使用 CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。

3.异步绘制

4.全局并发线程

5.高效的图片异步加载

156、iOS面试题:编程中的六大设计原则?

1.单一职责原则

通俗地讲就是一个类只做一件事

  • CALayer:动画和视图的显示。
  • UIView:只负责事件传递、事件响应。

2.开闭原则

对修改关闭,对扩展开放。 要考虑到后续的扩展性,而不是在原有的基础上来回修改

3.接口隔离原则

使用多个专门的协议、而不是一个庞大臃肿的协议

  • UITableviewDelegate
  • UITableViewDataSource

4.依赖倒置原则

抽象不应该依赖于具体实现、具体实现可以依赖于抽象。 调用接口感觉不到内部是如何操作的

5.里氏替换原则

父类可以被子类无缝替换,且原有的功能不受任何影响

  • 例如 KVO

6.迪米特法则

一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合

157、iOS面试题:NSOperation 与 GCD 的主要区别?

  1. GCD 的核心是 C 语言写的系统服务,执行和操作简单高效,因此 NSOperation 底层也通过 GCD 实现,换个说法就是 NSOperation 是对 GCD 更高层次的抽象,这是他们之间最本质的区别。因此如果希望自定义任务,建议使用 NSOperation;

  2. 依赖关系,NSOperation 可以设置两个 NSOperation 之间的依赖,第二个任务依赖于第一个任务完成执行,GCD 无法设置依赖关系,不过可以通过dispatch_barrier_async来实现这种效果;

  3. KVO(键值对观察),NSOperation 和容易判断 Operation 当前的状态(是否执行,是否取消),对此 GCD 无法通过 KVO 进行判断;

  4. 优先级,NSOperation 可以设置自身的优先级,但是优先级高的不一定先执行,GCD 只能设置队列的优先级,无法在执行的 block 设置优先级;

  5. 继承,NSOperation 是一个抽象类,实际开发中常用的两个类是 NSInvocationOperation 和 NSBlockOperation ,同样我们可以自定义 NSOperation,GCD 执行任务可以自由组装,没有继承那么高的代码复用度;

  6. 效率,直接使用 GCD 效率确实会更高效,NSOperation 会多一点开销,但是通过 NSOperation 可以获得依赖,优先级,继承,键值对观察这些优势,相对于多的那么一点开销确实很划算,鱼和熊掌不可得兼,取舍在于开发者自己;

158、iOS面试题:请说明并比较以下关键词:strong, weak, assign, copy

strong 表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为 0 则不会被销毁。当然强行将其设为 nil 可以销毁它。

weak 表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。

assign 主要用于修饰基本数据类型,如 NSInteger 和 CGFloat,这些数值主要存在于栈上。

weak 一般用来修饰对象,assign 一般用来修饰基本数据类型。原因是assign 修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成野指针。

copy 与 strong 类似。不同之处是 strong 的复制是多个指针指向同一个地址,而 copy 的复制每次会在内存中拷贝一份对象,指针指向不同地址。copy 一般用在修饰有可变对应类型的不可变对象上,如 NSString , NSArray , NSDictionary 。

Objective-C 中,基本数据类型的默认关键字是 atomic , readwrite , assign ;普通属性的默认关键字是 atomic , readwrite , strong 。

159、iOS面试题:Socket原理

1、套接字(socket)概念

套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

应 用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应 用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

2 、建立socket连接

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

连 接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户 端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

3、Socket连接与TCP连接

创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

4、Socket连接与HTTP连接

由 于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用 中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

很 多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给 客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以 保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

160、iOS面试题:请说明并比较以下关键词:__weak,__block

__weak与weak基本相同。前者用于修饰变量(variable),后者用于修饰属性(property)。__weak 主要用于防止block中的循环引用。

__block也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。__block用于修饰某些block内部将要修改的外部变量。

__weak和__block的使用场景几乎与block息息相关。而所谓block,就是Objective-C对于闭包的实现。闭包就是没有名字的函数,或者理解为指向函数的指针。

161、iOS面试题:什么是ARC?

ARC全称是 Automatic Reference Counting,是Objective-C的内存管理机制。简单地来说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

ARC的使用是为了解决对象retain和release匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。

以前需要手动的通过retain去为对象获取内存,并用release释放内存。所以以前的操作称为MRC (Manual Reference Counting)。

162、iOS面试题:ARC的底层原理,怎么实现自动释放的,和MRC的区别是什么?

ARC管理原则:只要一个对象没有被强指针修饰就会被销毁,默认局部变量对象都是强指针,存放到堆里面,只是局部变量的强指针会在代码块结束后释放,对应所指向的内存空间也会被销毁。

MRC没有strong,weak,局部变量对象就是相当于基本数据类型。MRC给成员属性赋值,一定要使用set方法,不能直接访问下划线成员属性赋值,因为使用下划线是直接赋值(如_name = name),而set方法会多做影响引用计数方面的事情,比如retain。

163、iOS面试题:苹果为什么推出ARC?

在MRC时代,我们要想保持一个对象,只要“retain”。现在的ARC是不需要了,现在只需用一个指针指向这个对象,无非2种情况:第一:指针为空时,对象被释放咯。第二:指针不为空,对象会一直保存在堆里,如果当指针指向一个新的值时,原来的对象会被release一次,这个系统会在合适的时候自动帮我们搞掂,不需我们关心。

而在ARC时,只要对象指针被置空,就会释放。否则,对象就会一直保持在堆上。当将指针指向新值时,原来的对象会被release 一次。

164、iOS面试题:有了线程,你觉得为什么还要有runloop?,runloop和线程有什么关系?

解析:关于为什么要,我觉得runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。

关于这两者的更多关系:

runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。 runloop在第一次获取时被创建,在线程结束时被销毁。 对于主线程来说,runloop在程序一启动就默认创建好了。 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。

165、iOS面试题:objc中向一个nil对象发送消息将会发生什么?

首先,需要搞明白2个问题:

  • 什么是isa指针
  • 消息传递机制

isa指针是用于对象指向类对象,类对象指向元类对象的一个指针。而类对象和元类对象中又分别存放对象方法和类方法。 在消息传递机制中,就是通过isa指针来寻找到方法的实际调用地址的。

objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。

166、iOS面试题:常用的设计模式

1 代理模式 应用场景:当一个类的某些功能需要由别的类来实现,但是又不确定具体会是哪个类实现。 优势:解耦合 敏捷原则:开放-封闭原则

实例:

tableview的 数据源delegate,通过和protocol的配合,完成委托诉求。 列表row个数delegate 自定义的delegate 2 观察者模式 应用场景:一般为model层对,controller和view进行的通知方式,不关心谁去接收,只负责发布信息。 优势:解耦合 敏捷原则:接口隔离原则,开放-封闭原则

实例:

Notification通知中心,注册通知中心,任何位置可以发送消息,注册观察者的对象可以接收。 kvo,键值对改变通知的观察者,平时基本没用过。 3 MVC模式 应用场景:是一中非常古老的设计模式,通过数据模型,控制器逻辑,视图展示将应用程序进行逻辑划分。 优势:使系统,层次清晰,职责分明,易于维护 敏捷原则:对扩展开放-对修改封闭

实例:

model-即数据模型,view-视图展示,controller进行UI展现和数据交互的逻辑控制。 4 单例模式 应用场景:确保程序运行期某个类,只有一份实例,用于进行资源共享控制。 优势:使用简单,延时求值,易于跨模块 敏捷原则:单一职责原则

实例:

[UIApplication sharedApplication]。 注意事项:确保使用者只能通过 getInstance方法才能获得,单例类的唯一实例。 java,C++中使其没有公有构造函数,私有化并覆盖其构造函数。 object c中,重写allocWithZone方法,保证即使用户用 alloc方法直接创建单例类的实例, 返回的也只是此单例类的唯一静态变量。

5 策略模式 应用场景:定义算法族,封装起来,使他们之间可以相互替换。 优势:使算法的变化独立于使用算法的用户 敏捷原则:接口隔离原则;多用组合,少用继承;针对接口编程,而非实现。

实例:

排序算法,NSArray的sortedArrayUsingSelector;经典的鸭子会叫,会飞案例。 注意事项: 1.剥离类中易于变化的行为,通过组合的方式嵌入抽象基类 2.变化的行为抽象基类为,所有可变变化的父类 3.用户类的最终实例,通过注入行为实例的方式,设定易变行为防止了继承行为方式,导致无关行为污染子类。完成了策略封装和可替换性。 6 工厂模式 应用场景:工厂方式创建类的实例,多与proxy模式配合,创建可替换代理类。 优势:易于替换,面向抽象编程,application只与抽象工厂和易变类的共性抽象类发生调用关系。 敏捷原则:DIP依赖倒置原则

实例:

项目部署环境中依赖多个不同类型的数据库时,需要使用工厂配合proxy完成易用性替换 注意事项:项目初期,软件结构和需求都没有稳定下来时,不建议使用此模式,因为其劣势也很明显,增加了代码的复杂度,增加了调用层次,增加了内存负担。所以要注意防止模式的滥用。

167、iOS面试题:单例会有什么弊端?

主要优点:

1、提供了对唯一实例的受控访问。 2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。 3、允许可变数目的实例。 主要缺点:

1、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。 2、单例类的职责过重,在一定程度上违背了“单一职责原则”。 3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

168、iOS面试题:你会如何存储用户的一些敏感信息,如登录的token

使用keychain来存储,也就是钥匙串,使用keychain需要导入Security框架

iOS的keychain服务提供了一种安全的保存私密信息(密码,序列号,证书等)的方式,每个iOS程序都有一个独立的keychain存储。相对于 NSUserDefaults、文件保存等一般方式,keychain保存更为安全,而且keychain里保存的信息不会因App被删除而丢失,所以在 重装App后,keychain里的数据还能使用。从iOS 3.0开始,跨程序分享keychain变得可行。

如何需要在应用里使 用使用keyChain,我们需要导入Security.framework ,keychain的操作接口声明在头文件SecItem.h里。直接使用SecItem.h里方法操作keychain,需要写的代码较为复杂,为减轻 咱们程序员的开发,我们可以使用一些已经封装好了的工具类,下面我会简单介绍下我用过的两个工具类:KeychainItemWrapper和 SFHFKeychainUtils。

自定义一个keychain的类

CSKeyChain.h

@interface CSKeyChain : NSObject

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service;

+ (void)save:(NSString *)service data:(id)data;

+ (id)load:(NSString *)service;

+ (void)delete:(NSString *)service;

@end

CSKeyChain.m

#import "CSKeyChain.h"
#import<Security/Security.h>

@implementation CSKeyChain

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (__bridge_transfer id)kSecClassGenericPassword,(__bridge_transfer id)kSecClass,
            service, (__bridge_transfer id)kSecAttrService,
            service, (__bridge_transfer id)kSecAttrAccount,
            (__bridge_transfer id)kSecAttrAccessibleAfterFirstUnlock,(__bridge_transfer id)kSecAttrAccessible,
            nil];
}

+ (void)save:(NSString *)service data:(id)data {
    // 获得搜索字典
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    // 添加新的删除旧的
    SecItemDelete((__bridge_retained CFDictionaryRef)keychainQuery);
    // 添加新的对象到字符串
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge_transfer id)kSecValueData];
    // 查询钥匙串
    SecItemAdd((__bridge_retained CFDictionaryRef)keychainQuery, NULL);
}

+ (id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    // 配置搜索设置
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(__bridge_transfer id)kSecReturnData];
    [keychainQuery setObject:(__bridge_transfer id)kSecMatchLimitOne forKey:(__bridge_transfer id)kSecMatchLimit];
    
    CFDataRef keyData = NULL;
    
    if (SecItemCopyMatching((__bridge_retained CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge_transfer NSData *)keyData];
        } @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        } @finally {
        }
    }
    
    return ret;
}

+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((__bridge_retained CFDictionaryRef)keychainQuery);
}

@end

在别的类实现存储,加载,删除敏感信息方法

// 用来标识这个钥匙串
static NSString * const KEY_IN_KEYCHAIN = @"com.cs.app.allinfo";
// 用来标识密码
static NSString * const KEY_PASSWORD = @"com.cs.app.password";

+ (void)savePassWord:(NSString *)password {
    NSMutableDictionary *passwordDict = [NSMutableDictionary dictionary];
    [passwordDict setObject:password forKey:KEY_PASSWORD];
    [CSKeyChain save:KEY_IN_KEYCHAIN data:passwordDict];
}

+ (id)readPassWord {
    NSMutableDictionary *passwordDict = (NSMutableDictionary *)[CSKeyChain load:KEY_IN_KEYCHAIN];
    return [passwordDict objectForKey:KEY_PASSWORD];
}

+ (void)deletePassWord {
    [CSKeyChain delete:KEY_IN_KEYCHAIN];
}

169、iOS面试题:UIScrollView大概是如何实现的,它是如何捕捉、响应手势的?

UIScrollView在滚动过程当中,其实是在修改原点坐标。当手指触摸后, scroll view会暂时拦截触摸事件,使用一个计时器。假如在计时器到点后没有发生手指移动事件,那么 scroll view 发送 tracking events 到被点击的 subview。假如在计时器到点前发生了移动事件,那么 scroll view 取消 tracking 自己发生滚动。

首先了解下UIScrollView对于touch事件的接收处理原理:

  • UIScrollView应该是重载了hitTest 方法,并总会返回itself 。所以所有的touch 事件都会进入到它自己里面去了。内部的touch事件检测到这个事件是不是和自己相关的,或者处理或者除递给内部的view。
  • 为了检测touch是处理还是传递,UIScrollView当touch发生时会生成一个timer。
  • 如果150ms内touch未产生移动,它就把这个事件传递给内部view
  • 如果150ms内touch产生移动,开始scrolling,不会传递给内部的view。(例如, 当你touch一个table时候,直接scrolling,你touch的那行永远不会highlight。)
  • 如果150ms内touch未产生移动并且UIScrollView开始传递内部的view事件,但是移动足够远的话,且canCancelContentTouches = YES,UIScrollView会调用touchesCancelled方法,cancel掉内部view的事件响应,并开始scrolling。(例如, 当你touch一个table, 停止了一会,然后开始scrolling,那一行就首先被highlight,但是随后就不在高亮了)

170、iOS面试题:如何实现夜间模式?

1.准备两套资源,分别对应日间模式和夜间模式。

2.在系统全局保存一个变量(BOOL isNight),根据用户的操作改变这个变量的值;

3.把每个需要被改变的view, viewcontroller加入通知中心中监听(NeedTransferToNight和NeedTransferToDay)事件;

4.默认为日间模式,isNight = YES.

5.当用户点击夜间按钮时,如果isNight == YES, 讲此变量的值置为NO,通知中心发布NeedTransferToNight通知,所有需要被改变的view和viewcontroller在监听到此事 件时使用夜间资源重新绘制自身。其他view在初始化时如果发现isNight为YES.则使用夜间资源初始化自身。(反之亦然)

6.运行程序,可以看到夜间模式。

171、iOS面试题:如何捕获异常?

在app启动时(didFinishLaunchingWithOptions),添加一个异常捕获的监听。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

    return YES;
}

实现捕获异常日志并保存到本地的方法。

void UncaughtExceptionHandler(NSException *exception){
    
    // 异常日志获取
    NSArray  *excpArr = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    
    NSString *excpCnt = [NSString stringWithFormat:@"exceptionType: %@ \n reason: %@ \n stackSymbols: %@",name,reason,excpArr];
    
    // 日常日志保存(可以将此功能单独提炼到一个方法中)
    NSArray  *dirArr  = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *dirPath = dirArr[0];
    NSString *logDir = [dirPath stringByAppendingString:@"/CrashLog"];
    
    BOOL isExistLogDir = YES;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:logDir]) {
        isExistLogDir = [fileManager createDirectoryAtPath:logDir withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    if (isExistLogDir) {
        // 此处可扩展
        NSString *logPath = [logDir stringByAppendingString:@"/crashLog.txt"];
        [excpCnt writeToFile:logPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }
}

172、iOS面试题:frame与center bounds的关系

###frame属性是相对于父容器的定位坐标。 bounds属性针对于自己,指明大小边框,默认点为(0,0),而宽和高与frame宽和高相等。 center属性是针对与frame属性的中心点坐标。 当frame变化时,bounds和center相应变化。 当bounds变化时,frame会根据新bounds的宽和高,在不改变center的情况下,进行重新设定。 center永远与frame相关,指定frame的中心坐标!

174、iOS面试题:断点续传如何实现的?

断点续传的理解可以分为两部分:一部分是断点,一部分是续传。

  • 断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当 某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。

  • 续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。

使用多线程断点续传下载的时候,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,多个线程并发可以占用服务器端更多资源,从而加快下载速度。

在下载(或上传)过程中,如果网络故障、电量不足等原因导致下载中断,这就需要使用到断点续传功能。下次启动时,可以从记录位置(已经下载的部分)开始,继续下载以后未下载的部分,避免重复部分的下载。断点续传实质就是能记录上一次已下载完成的位置。

断点续传的过程

1.断点续传需要在下载过程中记录每条线程的下载进度; 2.每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库; 3.在每次向文件中写入数据之后,在数据库中更新下载进度; 4.下载完成之后删除数据库中下载记录。

175、iOS面试题:通知,代理,KVO的区别,以及通知的多线程问题

  1. delegate

当我们第一次编写ios应用时,我们注意到不断的在使用“delegate”,并且贯穿于整个SDK。delegation模式不是IOS特有的模式,而是依赖与你过去拥有的编程背景。针对它的优势以及为什么经常使用到,这种模式可能不是很明显的。

delegation的基本特征是:一个controller定义了一个协议(即一系列的方法定义)。该协议描述了一个delegate对象为了能够响应一个controller的事件而必须做的事情。协议就是delegator说,“如果你想作为我的delegate,那么你就必须实现这些方法”。实现这些方法就是允许controller在它的delegate能够调用这些方法,而它的delegate知道什么时候调用哪种方法。delegate可以是任何一种对象类型,因此controller不会与某种对象进行耦合,但是当该对象尝试告诉委托事情时,该对象能确定delegate将响应。

delegate的优势:

1.非常严格的语法。所有将听到的事件必须是在delegate协议中有清晰的定义。 2.如果delegate中的一个方法没有实现那么就会出现编译警告/错误 3.协议必须在controller的作用域范围内定义 4.在一个应用中的控制流程是可跟踪的并且是可识别的; 5.在一个控制器中可以定义定义多个不同的协议,每个协议有不同的delegates 6.没有第三方对象要求保持/监视通信过程。 7.能够接收调用的协议方法的返回值。这意味着delegate能够提供反馈信息给controller 缺点:

1.需要定义很多代码:1.协议定义;2.controller的delegate属性;3.在delegate本身中实现delegate方法定义 2.在释放代理对象时,需要小心的将delegate改为nil。一旦设定失败,那么调用释放对象的方法将会出现内存crash 3.在一个controller中有多个delegate对象,并且delegate是遵守同一个协议,但还是很难告诉多个对象同一个事件,不过有可能。 2. notification

在iOS应用开发中有一个”Notification Center“的概念。它是一个单例对象,允许当事件发生时通知一些对象。它允许我们在低程度耦合的情况下,满足控制器与一个任意的对象进行通信的目的。这种模式的基本特征是为了让其他的对象能够接收到在该controller中发生某种事件而产生的消息,controller用一个key(通知名称)。这样对于controller来说是匿名的,其他的使用同样的key来注册了该通知的对象(即观察者)能够对通知的事件作出反应。

通知优势:

1.不需要编写多少代码,实现比较简单; 2.对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单 3.controller能够传递context对象(dictionary),context对象携带了关于发送通知的自定义的信息 缺点:

1.在编译期不会检查通知是否能够被观察者正确的处理; 2.在释放注册的对象时,需要在通知中心取消注册; 3.在调试的时候应用的工作以及控制过程难跟踪; 4.需要第三方对喜爱那个来管理controller与观察者对象之间的联系; 5.controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况; 6.通知发出后,controller不能从观察者获得任何的反馈信息 3. KVO

KVO是一个对象能够观察另外一个对象的属性的值,并且能够发现值的变化。前面两种模式更加适合一个controller与任何其他的对象进行通信,而KVO更加适合任何类型的对象侦听另外一个任意对象的改变(这里也可以是controller,但一般不是controller)。这是一个对象与另外一个对象保持同步的一种方法,即当另外一种对象的状态发生改变时,观察对象马上作出反应。它只能用来对属性作出反应,而不会用来对方法或者动作作出反应。

优点:

1.能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步; 2.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现; 3.能够提供观察的属性的最新值以及先前值; 4.用key paths来观察属性,因此也可以观察嵌套对象; 5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察 缺点:

1.我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查; 2.对属性重构将导致我们的观察代码不再可用; 3.复杂的“IF”语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向; 4.当释放观察者时不需要移除观察者。 总结:

从上面的分析中可以看出3中设计模式都有各自的优点和缺点。其实任何一种事物都是这样,问题是如何在正确的时间正确的环境下选择正确的事物。下面就讲讲如何发挥他们各自的优势,在哪种情况下使用哪种模式。注意使用任何一种模式都没有对和错,只有更适合或者不适合。每一种模式都给对象提供一种方法来通知一个事件给其他对象,而且前者不需要知道侦听者。在这三种模式中,我认为KVO有最清晰的使用案例,而且针对某个需求有清晰的实用性。而另外两种模式有比较相似的用处,并且经常用来给controller间进行通信。那么我们在什么情况使用其中之一呢?

根据我开发iOS应用的经历,我发现有些过分的使用通知模式。我个人不是很喜欢使用通知中心。我发现用通知中心很难把握应用的执行流程。UserInfo dictionaries的keys到处传递导致失去了同步,而且在公共空间需要定义太多的常量。对于一个工作于现有的项目的开发者来说,如果过分的使用通知中心,那么很难理解应用的流程。

我觉得使用命名规则好的协议和协议方法定义对于清晰的理解controllers间的通信是很容易的。努力的定义这些协议方法将增强代码的可读性,以及更好的跟踪你的app。代理协议发生改变以及实现都可通过编译器检查出来,如果没有将会在开发的过程中至少会出现crash,而不仅仅是让一些事情异常工作。甚至在同一事件通知多控制器的场景中,只要你的应用在controller层次有着良好的结构,消息将在该层次上传递。该层次能够向后传递直至让所有需要知道事件的controllers都知道。

当然会有delegation模式不适合的例外情况出现,而且notification可能更加有效。例如:应用中所有的controller需要知道一个事件。然而这些类型的场景很少出现。另外一个例子是当你建立了一个架构而且需要通知该事件给正在运行中应用。

根据经验,如果是属性层的时间,不管是在不需要编程的对象还是在紧紧绑定一个view对象的model对象,我只使用观察。对于其他的事件,我都会使用delegate模式。如果因为某种原因我不能使用delegate,首先我将估计我的app架构是否出现了严重的错误。如果没有错误,然后才使用notification。

176、iOS面试题:你一般是如何优化你的APP的?

一、首页启动速度

启动过程中做的事情越少越好(尽可能将多个接口合并) 不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新) 在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据) 尽量减小包的大小 优化方法: 量化启动时间 启动速度模块化 辅助工具(友盟,听云,Flurry) 二、页面浏览速度

json的处理(iOS 自带的NSJSONSerialization,Jsonkit,SBJson) 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录) 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度) 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定) 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载) 算法的优化(核心算法的优化,例如有些app 有个 联系人姓名用汉语拼音的首字母排序) 三、操作流畅度优化:

Tableview 优化(tableview cell的加载优化) ViewController加载优化(不同view之间的跳转,可以提前准备好数据) 四、数据库的优化:

数据库设计上面的重构 查询语句的优化 分库分表(数据太多的时候,可以分不同的表或者库) 五、服务器端和客户端的交互优化:

客户端尽量减少请求 服务端尽量做多的逻辑处理 服务器端和客户端采取推拉结合的方式(可以利用一些同步机制) 通信协议的优化。(减少报文的大小) 电量使用优化(尽量不要使用后台运行) 六、非技术性能优化

产品设计的逻辑性(产品的设计一定要符合逻辑,或者逻辑尽量简单,否则会让程序员抓狂,有时候用了好大力气,才可以完成一个小小的逻辑设计问题) 界面交互的规范(每个模块的界面的交互尽量统一,符合操作习惯) 代码规范(这个可以隐形带来app 性能的提高,比如 用if else 还是switch ,或者是用!还是 ==) code review(坚持code Review 持续重构代码。减少代码的逻辑复杂度) 日常交流(经常分享一些代码,或者逻辑处理中的坑)

177、iOS面试题:push Notification原理

  • 本地推送:不需要联网也可以推送,是开发人员在APP内设定特定的时间来提醒用户干什么

  • 远程推送:需要联网,用户的设备会于苹果APNS服务器形成一个长连接,用户设备会发送uuid和Bundle idenidentifier给苹果服务器,苹果服务器会加密生成一个deviceToken给用户设备,然后设备会将deviceToken发送给APP的服务器,服务器会将deviceToken存进他们的数据库,这时候如果有人发送消息给我,服务器端就会去查询我的deviceToken,然后将deviceToken和要发送的信息发送给苹果服务器,苹果服务器通过deviceToken找到我的设备并将消息推送到我的设备上,这里还有个情况是如果APP在线,那么APP服务器会于APP产生一个长连接,这时候APPF服务器会直接通过deviceToken将消息推送到设备上

178、iOS面试题:iOS 中内省的几个方法?

对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。

OC运行时内省的4个方法:

判断对象类型:

-(BOOL) isKindOfClass:            判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass:      判断是否是这个类的实例

判断对象/类是否有这个方法

-(BOOL) respondsToSelector:                      判读实例是否有这样方法
+(BOOL) instancesRespondToSelector:      判断类是否有这个方法

在 Objective-C 中,id类型类似于(void*) ,可以指向任何类的对象,但在运行时对象的类型不再是id,而是该对象真正所属的类。

Person *person = [[Person alloc] init];  
NSArray *arr = @[person];
id  obj = arr[0];    //OC集合中取出的对象都是id类型
此时可通过
BOOL  isPersonClass = [obj  isKindOfClass: [Person class] ];
来判断obj是否Person类型或其子类的对象。

在 Objective-C 中,用父类类型定义的指针,可以指向其子类的对象,但在运行时对象真实类型会是子类。

//例如 Boy是Person的子类,现定义:
Person  *p = [[Boy alloc] init];
可通过 BOOL  isBoy = [p  isMemberOfClass: [Boy class] ]; 
判断Person *类型的p是否是Boy类型。

179、class方法和objc_getClass方法有什么区别?

1.当参数obj为Object实例对象 object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。

2.当参数obj为Class类对象 object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针;[obj class]返回的则是其本身。

3.当参数obj为Metaclass类对象 object_getClass(obj)返回元类对象中的isa指针,因为元类对象的isa指针指向根类,所有返回的是根类对象的地址指针;[obj class]返回的则是其本身。

4.obj为Rootclass类对象 object_getClass(obj)返回根类对象中的isa指针,因为跟类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针;[obj class]返回的则是其本身。

总结: 经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

180、iOS面试题:一个int变量被__block修饰与否的区别?

没有修饰,被block捕获,是值拷贝。 使用__block修饰,会生成一个结构体,复制int的引用地址。达到修改数据。

参考:iOS面试题:一个int变量被__block修饰与否的区别?

181、iOS面试题:什么是离屏渲染?什么情况下会触发?该如何应对?

离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作。

离屏渲染出发的场景有以下:

圆角 (maskToBounds并用才会触发) 图层蒙版 阴影 光栅化 为什么要有离屏渲染?

大家高中物理应该学过显示器是如何显示图像的:需要显示的图像经过CRT电子枪以极快的速度一行一行的扫描,扫描出来就呈现了一帧画面,随后电子枪又会回到初始位置循环扫描,形成了我们看到的图片或视频。

为了让显示器的显示跟视频控制器同步,当电子枪新扫描一行的时候,准备扫描的时发送一个水平同步信号(HSync信号),显示器的刷新频率就是HSync信号产生的频率。然后CPU计算好frame等属性,将计算好的内容交给GPU去渲染,GPU渲染好之后就会放入帧缓冲区。然后视频控制器会按照HSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,就显示出来了。具体的大家自行查找资料或询问相关专业人士,这里只参考网上资料做一个简单的描述。

离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了。

由于垂直同步的机制,如果在一个 HSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

为什么要避免离屏渲染?

CPU GPU 在绘制渲染视图时做了大量的工作。离屏渲染发生在 GPU 层面上,会创建新的渲染缓冲区,会触发 OpenGL 的多通道渲染管线,图形上下文的切换会造成额外的开销,增加 GPU 工作量。如果 CPU GPU 累计耗时 16.67 毫秒还没有完成,就会造成卡顿掉帧。

圆角属性、蒙层遮罩 都会触发离屏渲染。指定了以上属性,标记了它在新的图形上下文中,在未愈合之前,不可以用于显示的时候就出发了离屏渲染。

在OpenGL中,GPU有2种渲染方式

On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作 Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作 离屏渲染消耗性能的原因

需要创建新的缓冲区 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕 哪些操作会触发离屏渲染?

光栅化,layer.shouldRasterize = YES 遮罩,layer.mask 圆角,同时设置 layer.masksToBounds = YES、layer.cornerRadius大于0 考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片 阴影,layer.shadowXXX,如果设置了 layer.shadowPath 就不会产生离屏渲染

182、iOS面试题:反射是什么?可以举出几个应用场景么?

183、iOS面试题:关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将其指针置空么?

184、iOS面试题:Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

185、iOS面试题:KVC的赋值和取值过程是怎样的?原理是什么?

186、iOS面试题:iOS中UITableViewCell的重用机制原理?

187、iOS面试题:RunLoop剖析

188、iOS面试题:内存管理、自动释放池与循环引用

189、iOS面试题:剖析Block

190、iOS面试题:SDWebImage原理

191、iOS面试题:如何高性能的给 UIImageView 加个圆角?

192、iOS面试题:了解CoreData

193、iOS面试题:简述内存管理基本原则

194、iOS面试题:GCD死锁问题解读

195、iOS面试题:谈一谈网络中的 session 和 cookie?

196、iOS面试题:UIWindow,UIView,CALayer的区别

197、iOS面试题:事件传递和响应机制

198、iOS面试题:UIView block动画实现原理

199、iOS面试题:MVVM和MVC的区别

200、iOS面试题:NSCache,NSDictionary,NSArray的区别

参考:

https://www.jianshu.com/p/e709fde38de3 https://juejin.cn/post/6990228005293408270 https://leetcode.com/problems/longest-substring-without-repeating-characters/