写点什么

大厂必问 iOS 面试题 - (上)

发布于: 2021 年 05 月 29 日
大厂必问 iOS 面试题 - (上)

本系列面试题为二部分,此节为第一部分


希望能帮到正在面试或者准备面试的你一些帮助!


1、如何追踪 app 崩溃率,如何解决线上闪退


当 iOS 设备上的 App 应用闪退时,操作系统会生成一个 crash 日志,保存在设备上。crash 日志上有很多有用的信息,比如每个正在执行线程的完整堆栈跟踪信息和内存映像,这样就能够通过解析这些信息进而定位 crash 发生时的代码逻辑,从而找到 App 闪退的原因。通常来说,crash 产生来源于两种问题:违反 iOS 系统规则导致的 crash 和 App 代码逻辑 BUG 导致的 crash,下面分别对他们进行分析。


1.1、违反 iOS 系统规则产生 crash 的三种类型


(1) 内存报警闪退


当 iOS 检测到内存过低时,它的 VM 系统会发出低内存警告通知,尝试回收一些内存;如果情况没有得到足够的改善,iOS 会终止后台应用以回收更多内存;最后,如果内存还是不足,那么正在运行的应用可能会被终止掉。


在 Debug 模式下,可以主动将客户端执行的动作逻辑写入一个 log 文件中,这样程序童鞋可以将内存预警的逻辑写入该 log 文件,当发生如下截图中的内存报警时,就是提醒当前客户端性能内存吃紧,可以通过 Instruments 工具中的 Allocations 和 Leaks 模块库来发现内存分配问题和内存泄漏问题。


作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的 iOS 交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享 BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!


如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料: BAT 大厂最新面试题+答案合集(持续更新中) 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障


(2) 响应超时


当应用程序对一些特定的事件(比如启动、挂起、恢复、结束)响应不及时,苹果的 Watchdog 机制会把应用程序干掉,并生成一份相应的 crash 日志。这些事件与下列 UIApplicationDelegate 方法相对应,当遇到 Watchdog 日志时,可以检查上图中的几个方法是否有比较重的阻塞 UI 的动作。


application:didFinishLaunchingWithOptions: applicationWillResignActive:applicationDidEnterBackground: applicationWillEnterForeground:applicationDidBecomeActive:applicationWillTerminate:
复制代码


(3) 用户强制退出


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


1.2、应用逻辑的 Bug


大多数闪退崩溃日志的产生都是因为应用中的 Bug,这种 Bug 的错误种类有很多,比如


  • SEGV:(Segmentation Violation,段违例),无效内存地址,比如空指针,未初始化指针,栈溢出等;

  • SIGABRT:收到 Abort 信号,可能自身调用 abort()或者收到外部发送过来的信号;

  • SIGBUS:总线错误。与 SIGSEGV 不同的是,SIGSEGV 访问的是无效地址(比如虚存映射不到物理内存),而 SIGBUS 访问的是有效地址,但总线访问异常(比如地址对齐问题);

  • SIGILL:尝试执行非法的指令,可能不被识别或者没有权限;

  • SIGFPE:Floating Point Error,数学计算相关问题(可能不限于浮点计算),比如除零操作;

  • SIGPIPE:管道另一端没有进程接手数据;


常见的崩溃原因基本都是代码逻辑问题或资源问题,比如数组越界,访问野指针或者资源不存在,或资源大小写错误等。


1.3、crash 的收集


如果是在 windows 上你可以通过 itools 或 pp 助手等辅助工具查看系统产生的历史 crash 日志,然后再根据 app 来查看。如果是在 Mac 系统上,只需要打开 xcode->windows->devices,选择 device logs 进行查看,如下图,这些 crash 文件都可以导出来,然后再单独对这个 crash 文件做处理分析。


市场上已有的商业软件提供 crash 收集服务,这些软件基本都提供了日志存储,日志符号化解析和服务端可视化管理等服务:


开源的软件也可以拿来收集 crash 日志,比如 Razor,QuincyKit(git 链接)等,这些软件收集 crash 的原理其实大同小异,都是根据系统产生的 crash 日志进行了一次提取或封装,然后将封装后的 crash 文件上传到对应的服务端进行解析处理。很多商业软件都采用了 Plcrashreporter 这个开源工具来上传和解析 crash,比如 HockeyApp,Flurry 和 crittercism 等。


由于自己的 crash 信息太长,找了一张示例:


1)crash 标识是应用进程产生 crash 时的一些标识信息,它描述了该 crash 的唯一标识(E838FEFB-ECF6-498C-8B35-D40F0F9FEAE4),所发生的硬件设备类型(iphone3,1 代表 iphone4),以及 App 进程相关的信息等;


2)基本信息描述的是 crash 发生的时间和系统版本;


3)异常类型描述的是 crash 发生时抛出的异常类型和错误码;


4)线程回溯描述了 crash 发生时所有线程的回溯信息,每个线程在每一帧对应的函数调用信息(这里由于空间限制没有全部列出);


5)二进制映像是指 crash 发生时已加载的二进制文件。以上就是一份 crash 日志包含的所有信息,接下来就需要根据这些信息去解析定位导致 crash 发生的代码逻辑, 这就需要用到符号化解析的过程(洋名叫:symbolication)。


1.4、解决线上闪退


首先保证,发布前充分测试。发布后依然有闪退现象,查看崩溃日志,及时修复并发布。


2、什么是事件响应链,点击屏幕时是如何互动的,事件的传递。


事件响应链


对于 iOS 设备用户来说,他们操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。对应的事件类型有以下三种:


1、触屏事件(Touch Event)


2、运动事件(Motion Event)


3、远端控制事件(Remote-Control Event)


2.1、响应者链(Responder Chain)


响应者对象(Responder Object),指的是有响应和处理事件能力的对象。响应者链就是由一系列的响应者对象构成的一个层次结构。


UIResponder 是所有响应对象的基类,在 UIResponder 类中定义了处理上述各种事件的接口。我们熟悉的 UIApplication、 UIViewController、UIWindow 和所有继承自 UIView 的 UIKit 类都直接或间接的继承自 UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。


响应者链有以下特点:


1、响应者链通常是由视图(UIView)构成的;


2、一个视图的下一个响应者是它视图控制器(UIViewController)(如果有的话),然后再转给它的父视图(Super View);


3、视图控制器(如果有的话)的下一个响应者为其管理的视图的父视图;


4、单例的窗口(UIWindow)的内容视图将指向窗口本身作为它的下一个响应者


需要指出的是,Cocoa Touch 应用不像 Cocoa 应用,它只有一个 UIWindow 对象,因此整个响应者链要简单一点;


5、单例的应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向 nil,以结束整个循环。


2.2、点击屏幕时是如何互动的


iOS 系统检测到手指触摸(Touch)操作时会将其打包成一个 UIEvent 对象,并放入当前活动 Application 的事件队列,单例的 UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent:方法寻找此次 Touch 操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。


UIWindow 实例对象会首先在它的内容视图上调用 hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用 pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果 pointInside:withEvent:返回 YES,则继续逐级调用,直到找到 touch 操作发生的位置,这个视图也就是要找的 hit-test view。


hitTest:withEvent:方法的处理流程如下:首先调用当前视图的 pointInside:withEvent:方法判断触摸点是否在当前视图内;若返回 NO,则 hitTest:withEvent:返回 nil;若返回 YES,则向当前视图的所有子视图(subviews)发送 hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从 subviews 数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则 hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则 hitTest:withEvent:方法返回自身(self)。


事件的传递和响应分两个链:


传递链:由系统向离用户最近的 view 传递。UIKit –> active app’s event queue –> window –> root view –>……–>lowest view


响应链:由离用户最近的 view 向系统传递。initial view –> super view –> …..–> view controller –> window –> Application


3、Run Loop 是什么,使用的目的,何时使用和关注点


Run Loop 是一让线程能随时处理事件但不退出的机制。RunLoop 实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。


OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。


线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。


3.1、系统默认注册了 5 个 Mode:


  • kCFRunLoopDefaultMode: App 的默认 Mode,通常主线程是在这个 Mode 下运行的。

  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。

  • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。


3.2、Run Loop 的四个作用:


  • 使程序一直运行接受用户输入

  • 决定程序在何时应该处理哪些 Event

  • 调用解耦

  • 节省 CPU 时间


主线程的 run loop 默认是启动的。iOS 的应用程序里面,程序启动后会有一个如下的 main() 函数:


 int main(int argc, char *argv[]) {        @autoreleasepool {          return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));       }  }
复制代码


重点是 UIApplicationMain() 函数,这个方法会为 main thread 设置一个 NSRunLoop 对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。


对其它线程来说,run loop 默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个 Cocoa 程序的线程中,都可以通过:


NSRunLoop   *runloop = [NSRunLoop currentRunLoop];
复制代码


来获取到当前线程的 run loop。


一个 run loop 就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。


NSRunLoop 是一种更加高明的消息处理模式,他就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在 NSRunLoop 中每一个消息就被打包在 input source 或者是 timer source 中了。使用 run loop 可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。


RunLoop


3.3、什么时候使用 run loop


仅当在为你的程序创建辅助线程的时候,你才需要显式运行一个 run loop。Run loop 是程序主线程基础设施的关键部分。所以,Cocoa 和 Carbon 程序提供了代码运行主程序的循环并自动启动 run loop。IOS 程序中 UIApplication 的 run 方法(或 Mac OS X 中的 NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。类似的,RunApplicationEventLoop 函数为 Carbon 程序启动主循环。如果你使用 xcode 提供的模板创建你的程序,那你永远不需要自己去显式的调用这些例程。


对于辅助线程,你需要判断一个 run loop 是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的 run loop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动 run loop。Run loop 在你要和线程有更多的交互时才需要,比如以下情况:


使用端口或自定义输入源来和其他线程通信


使用线程的定时器


Cocoa 中使用任何 performSelector…的方法


使线程周期性工作


3.4、关注点


3.4.1、Cocoa 中的 NSRunLoop 类并不是线程安全的


我们不能再一个线程中去操作另外一个线程的 run loop 对象,那很可能会造成意想不到的后果。不过幸运的是 CoreFundation 中的不透明类 CFRunLoopRef 是线程安全的,而且两种类型的 run loop 完全可以混合使用。Cocoa 中的 NSRunLoop 类可以通过实例方法:


 - (CFRunLoopRef)getCFRunLoop;
复制代码


获取对应的 CFRunLoopRef 类,来达到线程安全的目的。


3.4.2、Run loop 的管理并不完全是自动的。


我们仍必须设计线程代码以在适当的时候启动 run loop 并正确响应输入事件,当然前提是线程中需要用到 run loop。而且,我们还需要使用 while/for 语句来驱动 run loop 能够循环运行,下面的代码就成功驱动了一个 run loop:


BOOL isRunning = NO;do {     isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]];} while (isRunning);
复制代码


3.4.3、Run loop 同时也负责 autorelease pool 的创建和释放


在使用手动的内存管理方式的项目中,会经常用到很多自动释放的对象,如果这些对象不能够被即时释放掉,会造成内存占用量急剧增大。Run loop 就为我们做了这样的工作,每当一个运行循环结束的时候,它都会释放一次 autorelease pool,同时 pool 中的所有自动释放类型变量都会被释放掉。


4、ARC 和 MRC


Objective-c 中提供了两种内存管理机制 MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分别提供对内存的手动和自动管理,来满足不同的需求。Xcode 4.1 及其以前版本没有 ARC。


在 MRC 的内存管理模式下,与对变量的管理相关的方法有:retain,release 和 autorelease。retain 和 release 方法操作的是引用记数,当引用记数为零时,便自动释放内存。并且可以用 NSAutoreleasePool 对象,对加入自动释放池(autorelease 调用)的变量进行管理,当 drain 时回收内存。


(1) retain,该方法的作用是将内存数据的所有权附给另一指针变量,引用数加 1,即 retainCount+= 1;


(2) release,该方法是释放指针变量对内存数据的所有权,引用数减 1,即 retainCount-= 1;


(3) autorelease,该方法是将该对象内存的管理放到 autoreleasepool 中。


在 ARC 中与内存管理有关的标识符,可以分为变量标识符和属性标识符,对于变量默认为__strong,而对于属性默认为 unsafe_unretained。也存在 autoreleasepool。


其中 assign/retain/copy 与 MRC 下 property 的标识符意义相同,strong 类似与 retain,assign 类似于 unsafe_unretained,strong/weak/unsafe_unretained 与 ARC 下变量标识符意义相同,只是一个用于属性的标识,一个用于变量的标识(带两个下划短线__)。所列出的其他的标识符与 MRC 下意义相同。


5、线程和进程


进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小 只与处理机的位数有关,一个 16 位长处理机的进程空间大小为 216 ,而 32 位处理机的进程空间大小为 232 。进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。


线程,在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,——无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。


进程的执行过程是线状的,尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。这是进程宏观上的执行过程。而进程又可有单线程进程与多线程进程两种。我们知道,进程有 一个进程控制块 PCB ,相关程序段 和 该程序段对其进行操作的数据结构集 这三部分,单线程进程的执行过程在宏观上是线性的,微观上也只有单一的执行过程;而多线程进程在宏观上的执行过程同样为线性的,但微观上却可以有多个执行操作(线程),如不同代码片段以及相关的数据结构集。**线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。**除了 CPU 之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。与进程控制表和 PCB 相似,每个线程也有自己的线程控制表 TCB ,而这个 TCB 中所保存的线程状态信息则要比 PCB 表少得多,这些信息主要是相关指针用堆栈(系统栈和用户栈),寄存器中的状态数据。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。


线程可以有效地提高系统的执行效率,但并不是在所有计算机系统中都是适用的,如某些很少做进程调度和切换的实时系统。使用线程的好处是有多个任务需要处理机处理时,减少处理机的切换时间;而且,线程的创建和结束所需要的系统开销也比进程的创建和结束要小得多。最适用使用线程的系统是多处理机系统和网络系统或分布式系统。


6、平常常用的多线程处理方式及优缺点


iOS 有四种多线程编程的技术,分别是:NSThread,Cocoa NSOperation,GCD(全称:Grand Central Dispatch),pthread。


6.1、四种方式的优缺点介绍:


1)NSThread 优点:NSThread 比其他两个轻量级。缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。


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


3)GCD(全优点):Grand Central dispatch(GCD)是 Apple 开发的一个多核编程的解决方案。在 iOS4.0 开始之后才能使用。GCD 是一个替代 NSThread, NSOperationQueue,NSInvocationOperation 等技术的很高效强大的技术。


4)pthread 是一套通用的多线程 API,适用于 Linux\Windows\Unix,跨平台,可移植,使用 C 语言,生命周期需要程序员管理,iOS 开发中使用很少。


6.2、GCD 线程死锁


GCD 确实好用 ,很强大,相比 NSOpretion 无法提供 取消任务的功能。


如此强大的工具用不好可能会出现线程死锁。 如下代码:


- (void)viewDidLoad{ [super viewDidLoad];     NSLog(@"=================4");dispatch_sync(dispatch_get_main_queue(),              ^{ NSLog(@"=================5"); }); NSLog(@"=================6");}
复制代码


6.3、GCD Queue 分为三种:


1,The main queue :主队列,主线程就是在个队列中。


2,Global queues : 全局并发队列。


3,用户队列:是用函数 dispatch_queue_create 创建的自定义队列


6.4、dispatch_sync 和 dispatch_async 区别:


dispatch_async(queue,block) async 异步队列,dispatch_async


函数会立即返回, block 会在后台异步执行。


dispatch_sync(queue,block) sync 同步队列,dispatch_sync


函数不会立即返回,及阻塞当前线程,等待 block 同步执行完成。


6.5、分析上面代码:


viewDidLoad 在主线程中, 及在 dispatch_get_main_queue() 中,执行到 sync 时 向


dispatch_get_main_queue()插入 同步 threed。sync 会等到 后面 block 执行完成才返回, sync 又再 dispatch_get_main_queue() 队列中,它是串行队列,sync 是后加入的,前一个是主线程,所以 sync 想执行 block 必须等待主线程执行完成,主线程等待 sync 返回,去执行后续内容。照成死锁,sync 等待 mainThread 执行完成, mianThread 等待 sync 函数返回。下面例子:


- (void)viewDidLoad{ [super viewDidLoad]; dispatch_async(dispatch_get_global_queue(0, 0), ^{                NSLog(@"=================1");              dispatch_sync(dispatch_get_main_queue(), ^{               NSLog(@"=================2"); }); NSLog(@"=================3"); });}
复制代码


程序会完成执行,为什么不会出现死锁。


首先: async 在主线程中 创建了一个异步线程 加入 全局并发队列,async 不会等待 block 执行完成,立即返回,


1,async 立即返回, viewDidLoad 执行完毕,及主线程执行完毕。


2,同时,全局并发队列立即执行异步 block , 打印 1, 当执行到 sync 它会等待 block 执行完成才返回, 及等待 dispatch_get_main_queue() 队列中的 mianThread 执行完成, 然后才开始调用 block 。因为 1 和 2 几乎同时执行,因为 2 在全局并发队列上, 2 中执行到 sync 时 1 可能已经执行完成或 等了一会,mainThread 很快退出, 2 等已执行后继续内容。如果阻塞了主线程,2 中的 sync 就无法执行啦,mainThread 永远不会退出, sync 就永远等待着。


7、大量数据表的优化方案


1.对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。


2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:


 select id from t where num is null
复制代码


最好不要给数据库留 NULL,尽可能的使用 NOT NULL 填充数据库.


备注、描述、评论之类的可以设置为 NULL,其他的,最好不要使用 NULL。


不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段, null 不占用空间。


可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:


select id from t where num=0
复制代码


3.应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。


4.应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描,如:


 select id from t where num=10 or Name='admin'
复制代码


可以这样查询:


 select id from t where num=10 union all select id from t where Name='admin'
复制代码


5.in 和 not in 也要慎用,否则会导致全表扫描,如:


select id from t where num in (1,2,3)
复制代码


对于连续的数值,能用 between 就不要用 in 了:


 select id from t where num between 1 and 3
复制代码


很多时候用 exists 代替 in 是一个好的选择:


 select num from a where num in (select num from b)
复制代码


用下面的语句替换:


 select num from a where exists (select 1 from b where num=a.num)
复制代码


6.下面的查询也将导致全表扫描:


select id from t where name like '%abc%'
复制代码


若要提高效率,可以考虑全文检索。


7.如果在 where 子句中使用参数,也会导致全表扫描。因为 SQL 只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:


select id from t where num=@num
复制代码


可以改为强制查询使用索引:


select id from t with (index(索引名)) where num=@num
复制代码


8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:


select id from t where num/2=100
复制代码


应改为:


select id from t where num=100*2
复制代码


9.应尽量避免在 where 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:


 select id from t where substring(name,1,3)='abc' -–name以abc开头的id select id from t where datediff(day,createdate,'2015-11-30')=0 -–'2015-11-30' --生成的id
复制代码


应改为:


select id from t where name like'abc%' select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'
复制代码


10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。


11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。


12.不要写一些没有意义的查询,如需要生成一个空表结构:


select col1,col2 into #t from t where1=0
复制代码


这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:


create table #t(…)
复制代码


13.Update 语句,如果只更改 1、2 个字段,不要 Update 全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志。


14.对于多张大数据量(这里几百条就算大了)的表 JOIN,要先分页再 JOIN,否则逻辑读会很高,性能很差。


15.select count(*) from table;这样不带任何条件的 count 会引起全表扫描,并且没有任何业务意义,是一定要杜绝的。


16.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。


17.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。


18.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连 接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。


19.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。


20.任何地方都不要使用


  select * from t
复制代码


用具体的字段列表代替“*”,不要返回用不到的任何字段。


21.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。


22.避免频繁创建和删除临时表,以减少系统表资源的消耗。临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件, 最好使用导出表。


23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先 create table,然后 insert。


24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。


25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过 1 万行,那么就应该考虑改写。


26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。


27.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。


28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。


29.尽量避免大事务操作,提高系统并发能力。


30.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。


实际案例分析:拆分大的 DELETE 或 INSERT 语句,批量提交 SQL 语句


如果你需要在一个在线的网站上去执行一个大的 DELETE 或 INSERT 查询,你需要非常小心,要避免你的操作让你的整个网站停止相应。因为这两个操作是会锁表的,表一锁住了,别的操作都进不来了。


Apache 会有很多的子进程或线程。所以,其工作起来相当有效率,而我们的服务器也不希望有太多的子进程,线程和数据库链接,这是极大的占服务器资源的事情,尤其是内存。


如果你把你的表锁上一段时间,比如 30 秒钟,那么对于一个有很高访问量的站点来说,这 30 秒所积累的访问进程/线程,数据库链接,打开的文件数,可能不仅仅会让你的 WEB 服务崩溃,还可能会让你的整台服务器马上挂了。


所以,如果你有一个大的处理,你一定把其拆分,使用 LIMIT oracle(rownum),sqlserver(top)条件是一个好的方法。下面是一个 mysql 示例:


while(1){//每次只做1000条mysql_query(“delete from logs where log_date <= ’2015-11-01’ limit 1000”);if(mysql_affected_rows() == 0){//删除完成,退出!break;}//每次暂停一段时间,释放表让其他进程/线程访问。usleep(50000)}
复制代码


8、常用到的动画库


Facebook 开源动画库 Pop 的 GitHub 主页:facebook/pop · GitHub,介绍:Playing with Pop (i)


Canvas 项目主页:Canvas - Simplify iOS Development,介绍:Animate in Xcode Without Code


拿 Canvas 来和 Pop 比其实不大合适,虽然两者都自称「动画库」,但是「库」这个词的含义有所区别。本质上 Canvas 是一个「动画合集」而 Pop 是一个「动画引擎」。


先说 Canvas。Canvas 的目的是「Animate in Xcode Without Code」。开发者可以通过在 Storyboard 中指定 User Defined Runtime Attributes 来实现一些 Canvas 中预设的动画,也就是他网站上能看到的那些。但是除了更改动画的 delay 和 duration 基本上不能调整其他的参数。


Pop 就不一样了。如果说 Canvas 是对 Core Animation 的封装,Pop 则是对 Core Animation(以及 UIDynamics)的再实现。


Pop 语法上和 Core Animation 相似,效果上则不像 Canvas 那么生硬(时间四等分,振幅硬编码)。这使得对 Core Animation 有了解的程序员可以很轻松地把原来的「静态动画」转换成「动态动画」。


同时 Pop 又往前多走了一步。既然动画的本质是根据时间函数来做插值,那么理论上任何一个对象的任何一个值都可以用来做插值,而不仅仅是 Core Animation 里定死的那一堆大小、位移、旋转、缩放等 animatable properties。


9、Restful 架构


REST 是一种架构风格,其核心是面向资源,REST 专门针对网络应用设计和开发方式,以降低开发的复杂性,提高系统的可伸缩性。REST 提出设计概念和准则为:


1.网络上的所有事物都可以被抽象为资源(resource)

2.每一个资源都有唯一的资源标识(resource identifier),对资源的操作不会改变这些标识

3.所有的操作都是无状态的


REST 简化开发,其架构遵循 CRUD 原则,该原则告诉我们对于资源(包括网络资源)只需要四种行为:创建,获取,更新和删除就可以完成相关的操作和处理。您可以通过统一资源标识符(Universal Resource Identifier,URI)来识别和定位资源,并且针对这些资源而执行的操作是通过 HTTP 规范定义的。其核心操作只有 GET,PUT,POST,DELETE。


由于 REST 强制所有的操作都必须是 stateless 的,这就没有上下文的约束,如果做分布式,集群都不需要考虑上下文和会话保持的问题。极大的提高系统的可伸缩性。


RESTful 架构:


(1)每一个 URI 代表一种资源;


(2)客户端和服务器之间,传递这种资源的某种表现层;


(3)客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。


10、请分析下 SDWebImage 的原理


这个类库提供一个 UIImageView 类别以支持加载来自网络的远程图片。具有缓存管理、异步下载、同一个 URL 下载次数控制和优化等特征。


10.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 可以预先下载图片,方便后续使用。


10.2、SDWebImage 库的作用


通过对 UIImageView 的类别扩展来实现异步加载替换图片的工作。


主要用到的对象:


1、UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调


2、SDWebImageManager,对图片进行管理的中转站,记录那些图片正在读取。


向下层读取 Cache(调用 SDImageCache),或者向网络读取对象(调用 SDWebImageDownloader) 。


实现 SDImageCache 和 SDWebImageDownloader 的回调。


3、SDImageCache,根据 URL 的 MD5 摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)


实现图片和内存清理工作。


4、SDWebImageDownloader,根据 URL 向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)


其他类:


SDWebImageDecoder,异步对图像进行了一次解压⋯⋯


1、SDImageCache 是怎么做数据管理的?


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


2、为啥必须做 Decoder?


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


这种做法是典型的空间换时间的做法。


本篇文章对你有帮助的话,记得点赞哦~


您的关注点赞将是我不断持续更新的动力!

用户头像

公众号:iOSer 2019.03.12 加入

专注移动端新技术和行业发展,不定期推送最新的 iOS资讯以及精品技术文章。

评论

发布
暂无评论
大厂必问 iOS 面试题 - (上)