美文网首页iOS面试iOS开发之常用技术点面试相关
iOS面试题:阿里-P6一面-参考思路

iOS面试题:阿里-P6一面-参考思路

作者: th先生 | 来源:发表于2017-12-04 20:51 被阅读38次

    iOS面试题:腾讯一面以及参考思路:
    http://www.jianshu.com/p/c2048ae9d799
    iOS面试题:腾讯二面以及参考思路:
    http://www.jianshu.com/p/bf9c663550b7

    阿里-p6-一面
    • 1.介绍下内存的几大区域?
    • 2.你是如何组件化解耦的?
    • 3.runtime如何通过selector找到对应的IMP地址
    • 4.runloop内部实现逻辑?
    • 5.你理解的多线程?
    • 6.GCD执行原理?
    • 7.怎么防止别人反编译你的app?
    • 8.YYAsyncLayer如何异步绘制?
    • 9.优化你是从哪几方面着手?
    1.介绍下内存的几大区域?

    1.栈区(stack) 由编译器自动分配并释放,存放函数的参数值,局部变量等。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。[先进后出]

    栈空间分静态分配 和动态分配两种。


    栈空间.jpg

    堆区(heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。

    优点是灵活方便,数据适应面广泛,但是效率有一定降低。


    堆区.png

    虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存,释放内存匹配是良好程序的基本要素。

    3.全局区(静态区) (static) 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放。


    全局.jpg

    4.文字常量区 存放常量字符串,程序结束后由系统释放;

    5.代码区 存放函数的二进制代码

    大致如图:


    五大内存区.jpg
    内存区.jpg

    例子代码:


    例1.jpg
    可能被追问的问题一:

    1.栈区 (stack [stæk]): 由编译器自动分配释放

    局部变量是保存在栈区的

    方法调用的实参也是保存在栈区的

    2.堆区 (heap [hiːp]): 由程序员分配释放,若程序员不释放,会出现内存泄漏,赋值语句右侧 使用 new 方法创建的对象,被创建对象的所有 成员变量!

    3.BSS 段 : 程序结束后由系统释放

    4.数据段 : 程序结束后由系统释放

    5.代码段:程序结束后由系统释放

    程序编译链接 后的二进制可执行代码

    可能被追问的问题二:

    比如申请后的系统是如何响应的?

    栈:存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。

    注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

    堆:

    1.首先应该知道操作系统有一个记录空闲内存地址的链表。

    2.当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

    3 .由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中

    可能被追问的问题三:

    比如:申请大小的限制是怎样的?

    栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数 ) ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。


    内存地址.png

    栈:由系统自动分配,速度较快,不会产生内存碎片

    堆:是由alloc分配的内存,速度比较慢,而且容易产生内存碎片,不过用起来最方便

    打个比喻来说:

    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    2.你是如何组件化解耦的?

    实现代码的高内聚低耦合,方便多人多团队开发!

    一般需要解耦的项目都会多多少少出现,一下几个情况:

    耦合比较严重(因为没有明确的约束,「组件」间引用的现象会比较多)

    2.容易出现冲突(尤其是使用 Xib,还有就是 Xcode Project,虽说有脚本可以改善)

    3.业务方的开发效率不够高(只关心自己的组件,却要编译整个项目,与其他不相干的代码糅合在一起)

    先来看下,组件化之后的一个大概架构


    组件化.jpg

    「组件化」顾名思义就是把一个大的 App 拆成一个个小的组件,相互之间不直接引用。那如何做呢?

    组件间通信

    以 iOS 为例,由于之前就是采用的 URL 跳转模式,理论上页面之间的跳转只需 open 一个 URL 即可。所以对于一个组件来说,只要定义「支持哪些 URL」即可,比如详情页,大概可以这么做的

    例2.jpg
    首页只需调用[MGJRouter openURL:@"mgj://detail?id=404"]就可以打开相应的详情页。

    那问题又来了,我怎么知道有哪些可用的 URL?为此,我们做了一个后台专门来管理。


    图3.jpg

    然后可以把这些短链生成不同平台所需的文件,iOS 平台生成 .{h,m} 文件,Android 平台生成 .java 文件,并注入到项目中。这样开发人员只需在项目中打开该文件就知道所有的可用 URL 了。

    目前还有一块没有做,就是参数这块,虽然描述了短链,但真想要生成完整的 URL,还需要知道如何传参数,这个正在开发中。

    还有一种情况会稍微麻烦点,就是「组件A」要调用「组件B」的某个方法,比如在商品详情页要展示购物车的商品数量,就涉及到向购物车组件拿数据。

    类似这种同步调用,iOS 之前采用了比较简单的方案,还是依托于MGJRouter,不过添加了新的方法- (id)objectForURL:,注册时也使用新的方法进行注册

    图4.jpg
    使用时NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]这样就拿到了购物车里的商品数。

    稍微复杂但更具通用性的方法是使用「协议」 <-> 「类」绑定的方式,还是以购物车为例,购物车组件可以提供这么个 Protocol


    图5.png

    可以看到通过协议可以直接指定返回的数据类型。然后在购物车组件内再新建个类实现这个协议,假设这个类名为MGJCartImpl,接着就可以把它与协议关联起来[ModuleManagerregisterClass:MGJCartImplforProtocol:@protocol(MGJCart)],对于使用方来说,要拿到这个MGJCartImpl,需要调用[ModuleManagerclassForProtocol:@protocol(MGJCart)]。拿到之后再调用+ (NSInteger)orderCount就可以了。

    那么,这个协议放在哪里比较合适呢?如果跟组件放在一起,使用时还是要先引入组件,如果有多个这样的组件就会比较麻烦了。所以我们把这些公共的协议统一放到了PublicProtocolDomain.h下,到时只依赖这一个文件就可以了。

    Android 也是采用类似的方式。

    组件生命周期管理

    理想中的组件可以很方便地集成到主客中,并且有跟AppDelegate一致的回调方法。这也是ModuleManager做的事情。

    先来看看现在的入口方法


    图6.jpg

    其中[MGJApp startApp]主要负责一些 SDK 的初始化。[self trackLaunchTime]是我们打的一个点,用来监测从main方法开始到入口方法调用结束花了多长时间。其他的都由ModuleManager搞定,loadModuleFromPlist:pathForResource:方法会读取 bundle 里的一个 plist 文件,这个文件的内容大概是这样的


    图7.jpg
    每个Module都实现了ModuleProtocol,其中有一个- (BOOL)applicaiton:didFinishLaunchingWithOptions:方法,如果实现了的话,就会被调用。

    还有一个问题就是,系统的一些事件会有通知,比如applicationDidBecomeActive会有对应的UIApplicationDidBecomeActiveNotification,组件如果要做响应的话,只需监听这个系统通知即可。但也有一些事件是没有通知的,比如- application:didRegisterUserNotificationSettings:,这时组件如果也要做点事情,怎么办?

    一个简单的解决方法是在AppDelegate的各个方法里,手动调一遍组件的对应的方法,如果有就执行。


    图8.jpg

    壳工程

    既然已经拆出去了,那拆出去的组件总得有个载体,这个载体就是壳工程,壳工程主要包含一些基础组件和业务SDK,这也是主工程包含的一些内容,所以如果在壳工程可以正常运行的话,到了主工程也没什么问题。不过这里存在版本同步问题,之后会说到。

    遇到的问题

    组件拆分

    由于之前的代码都是在一个工程下的,所以要单独拿出来作为一个组件就会遇到不少问题。首先是组件的划分,当时在定义组件粒度时也花了些时间讨论,究竟是粒度粗点好,还是细点好。粗点的话比较有利于拆分,细点的话灵活度比较高。最终还是选择粗一点的粒度,先拆出来再说。

    假如要把详情页迁出来,就会发现它依赖了一些其他部分的代码,那最快的方式就是直接把代码拷过来,改个名使用。比较简单暴力。说起来比较简单,做的时候也是挺有挑战的,因为正常的业务并不会因为「组件化」而停止,所以开发同学们需要同时兼顾正常的业务和组件的拆分。

    版本管理

    我们的组件包括第三方库都是通过 Cocoapods 来管理的,其中组件使用了私有库。之所以选择 Cocoapods,一个是因为它比较方便,还有就是用户基数比较大,且社区也比较活跃(活跃到了会时不时地触发 Github 的 rate limit,导致长时间 clone 不下来···见此),当然也有其他的管理方式,比如 submodule / subtree,在开发人员比较多的情况下,方便、灵活的方案容易占上风,虽然它也有自己的问题。主要有版本同步和更新/编译慢的问题。

    假如基础组件做了个 API 接口升级,这个升级会对原有的接口做改动,自然就会升一个中位的版本号,比如原先是 1.6.19,那么现在就变成 1.7.0 了。而我们在 Podfile 里都是用~指定的,这样就会出现主工程的 pod 版本升上去了,但是壳工程没有同步到,然后群里就会各种反馈编译不过,而且这个编译不过的长尾有时能拖上两三天。

    然后我们就想了个办法,如果不在壳工程里指定基础库的版本,只在主工程里指定呢,理论上应该可行,只要不出现某个基础库要同时维护多个版本的情况。但实践中发现,壳工程有时会莫名其妙地升不上去,在 podfile 里指定最新的版本又可以升上去,所以此路不通。

    还有一个问题是pod update时间过长,经常会在Analyzing Dependency上卡 10 多分钟,非常影响效率。后来排查下来是跟组件的 Podspec 有关,配置了 subspec,且依赖比较多。

    然后就是 pod update 之后的编译,由于是源码编译,所以这块的时间花费也不少,接下去会考虑 framework 的方式。

    持续集成

    在刚开始,持续集成还不是很完善,业务方升级组件,直接把 podspec 扔到 private repo 里就完事了。这样最简单,但也经常会带来编译通不过的问题。而且这种随意的版本升级也不太能保证质量。于是我们就搭建了一套持续集成系统,大概如此


    图10.jpg

    每个组件升级之前都需要先通过编译,然后再决定是否升级。这套体系看起来不复杂,但在实施过程中经常会遇到后端的并发问题,导致业务方要么集成失败,要么要等不少时间。而且也没有一个地方可以呈现当前版本的组件版本信息。还有就是业务方对于这种命令行的升级方式接受度也不是很高。


    图11.jpg
    基于此,在经过了几轮讨论之后,有了新版的持续集成平台,升级操作通过网页端来完成。

    大致思路是,业务方如果要升级组件,假设现在的版本是 0.1.7,添加了一些 feature 之后,壳工程测试通过,想集成到主工程里看看效果,或者其他组件也想引用这个最新的,就可以在后台手动把版本升到 0.1.8-rc.1,这样的话,原先依赖~> 0.1.7的组件,不会升到 0.1.8,同时想要测试这个组件的话,只要手动把版本调到 0.1.8-rc.1 就可以了。这个过程不会触发 CI 的编译检查。

    当测试通过后,就可以把尾部的-rc.n去掉,然后点击「集成」,就会走 CI 编译检查,通过的话,会在主工程的 podfile 里写上固定的版本号 0.1.8。也就是说,podfile 里所有的组件版本号都是固定的。


    图12.jpg

    周边设施

    基础组件及组件的文档 / Demo / 单元测试

    无线基础的职能是为集团提供解决方案,只是在蘑菇街 App 里能 work 是远远不够的,所以就需要提供入口,知道有哪些可用组件,并且如何使用,就像这样(目前还未实现)


    图13.jpg

    这就要求组件的负责人需要及时地更新 README / CHANGELOG / API,并且当发生 API 变更时,能够快速通知到使用方。

    公共 UI 组件

    组件化之后还有一个问题就是资源的重复性,以前在一个工程里的时候,资源都可以很方便地拿到,现在独立出去了,也不知道哪些是公用的,哪些是独有的,索性都放到自己的组件里,这样就会导致包变大。还有一个问题是每个组件可能是不同的产品经理在跟,而他们很可能只关注于自己关心的页面长什么样,而忽略了整体的样式。公共

    UI 组件就是用来解决这些问题的,这些组件甚至可以跨 App 使用。(目前还未实现)

    图14.jpg
    参考答案一:http://blog.csdn.net/GGGHub/article/details/52713642

    参考答案二:http://limboy.me/tech/2016/03/10/mgj-components.html

    3.runtime如何通过selector找到对应的IMP地址?

    概述

    类对象中有类方法和实例方法的列表,列表中记录着方法的名词、参数和实现,而selector本质就是方法名称,runtime通过这个方法名称就可以在列表中找到该方法对应的实现。

    这里声明了一个指向struct objc_method_list指针的指针,可以包含类方法列表和实例方法列表

    具体实现

    在寻找IMP的地址时,runtime提供了两种方法

    IMP class_getMethodImplementation(Class cls, SEL name);IMP method_getImplementation(Method m)

    而根据官方描述,第一种方法可能会更快一些

    @note \c class_getMethodImplementation may be faster than \c method_getImplementation(class_getInstanceMethod(cls, name)).

    对于第一种方法而言,类方法和实例方法实际上都是通过调用class_getMethodImplementation()来寻找IMP地址的,不同之处在于传入的第一个参数不同

    类方法(假设有一个类A)

    class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));

    实例方法

    class_getMethodImplementation([A class],@selector(methodName));

    通过该传入的参数不同,找到不同的方法列表,方法列表中保存着下面方法的结构体,结构体中包含这方法的实现,selector本质就是方法的名称,通过该方法名称,即可在结构体中找到相应的实现。

    struct objc_method {SEL method_namechar *method_typesIMP method_imp}

    而对于第二种方法而言,传入的参数只有method,区分类方法和实例方法在于封装method的函数

    类方法

    Method class_getClassMethod(Class cls, SEL name)

    实例方法

    Method class_getInstanceMethod(Class cls, SEL name)

    最后调用IMP method_getImplementation(Method m)获取IMP地址

    实验


    图3.1.jpg

    这里有一个叫Test的类,在初始化方法里,调用了两次getIMPFromSelector:方法,第一个aaa方法是不存在的,test1和test2分别为实例方法和类方法


    图3.2.png
    然后我同时实例化了两个Test的对象,打印信息如下
    图3.3.jpg

    大家注意图中红色标注的地址出现了8次:0x1102db280,这个是在调用class_getMethodImplementation()方法时,无法找到对应实现时返回的相同的一个地址,无论该方法是在实例方法或类方法,无论是否对一个实例调用该方法,返回的地址都是相同的,但是每次运行该程序时返回的地址并不相同,而对于另一种方法,如果找不到对应的实现,则返回0,在图中我做了蓝色标记。

    还有一点有趣的是class_getClassMethod()的第一个参数无论传入objc_getClass()还是objc_getMetaClass(),最终调用method_getImplementation()都可以成功的找到类方法的实现。

    而class_getInstanceMethod()的第一个参数如果传入objc_getMetaClass(),再调用method_getImplementation()时无法找到实例方法的实现却可以找到类方法的实现。

    4.runloop内部实现逻辑?
    图4.1.jpg

    苹果在文档里的说明,RunLoop 内部的逻辑大致如下:


    图4.2.jpg

    其内部代码整理如下 :

    可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

    RunLoop 的底层实现

    从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。


    图4.3.png

    苹果官方将整个系统大致划分为上述4个层次:

    应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。

    应用框架层即开发人员接触到的 Cocoa 等框架。

    核心框架层包括各种核心框架、OpenGL 等内容。

    Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在opensource.apple.com里找到。

    我们在深入看一下 Darwin 这个核心的架构:


    图4.4.png

    其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。

    XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。

    BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。

    IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。

    Mach

    本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的

    API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach

    中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach

    的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port)

    之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

    Mach 的消息定义是在头文件的,很简单:

    typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    } mach_msg_base_t;
    typedef struct {
    mach_msg_bits_t msgh_bits;
    mach_msg_size_t msgh_size;
    mach_port_t msgh_remote_port;
    mach_port_t msgh_local_port;
    mach_port_name_t msgh_voucher_port;
    mach_msg_id_t msgh_id;
    } mach_msg_header_t;
    

    一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,

    发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:

    mach_msg_return_t mach_msg(
    mach_msg_header_t *msg,
    mach_msg_option_t option,
    mach_msg_size_t send_size,
    mach_msg_size_t rcv_size,
    mach_port_name_t rcv_name,
    mach_msg_timeout_t timeout,
    mach_port_name_t notify);
    

    为了实现消息的发送和接收,mach_msg()

    函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach

    中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg()

    函数会完成实际的工作,如下图:


    图4.5.png

    这些概念可以参考维基百科:System_call、Trap_(computing)。

    RunLoop

    的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port

    消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在

    mach_msg_trap() 这个地方。

    关于具体的如何利用 mach port 发送信息,可以看看NSHipster 这一篇文章,或者这里的中文翻译 。

    关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian。

    苹果用 RunLoop 实现的功能

    首先我们可以看一下 App 启动后 RunLoop 的状态:

    可以看到,系统默认注册了5个Mode:

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

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

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

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

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

    你可以在这里看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了。

    5.你理解的多线程?

    1.可能会追问,每种多线程基于什么语言?

    2.生命周期是如何管理?

    3.你更倾向于哪种?追问至现在常用的两种你的看法是?

    第一种:pthread

    .特点:

    1)一套通用的多线程API

    2)适用于Unix\Linux\Windows等系统

    3)跨平台\可移植

    4)使用难度大

    b.使用语言:c语言

    c.使用频率:几乎不用

    d.线程生命周期:由程序员进行管理

    第二种:NSThread

    a.特点:

    1)使用更加面向对象

    2)简单易用,可直接操作线程对象

    b.使用语言:OC语言

    c.使用频率:偶尔使用

    d.线程生命周期:由程序员进行管理

    第三种:GCD

    a.特点:

    1)旨在替代NSThread等线程技术

    2)充分利用设备的多核(自动)

    b.使用语言:C语言

    c.使用频率:经常使用

    d.线程生命周期:自动管理

    第四种:NSOperation

    a.特点:

    1)基于GCD(底层是GCD)

    2)比GCD多了一些更简单实用的功能

    3)使用更加面向对象

    b.使用语言:OC语言

    c.使用频率:经常使用

    d.线程生命周期:自动管理

    多线程的原理

    同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)

    多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)

    如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象

    思考:如果线程非常非常多,会发生什么情况?

    CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源

    每条线程被调度执行的频次会降低(线程的执行效率降低)

    多线程的优点

    能适当提高程序的执行效率

    能适当提高资源利用率(CPU、内存利用率)

    多线程的缺点

    开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能

    线程越多,CPU在调度线程上的开销就越大

    程序设计更加复杂:比如线程之间的通信、多线程的数据共享

    你更倾向于哪一种?

    倾向于GCD:

    GCD

    技术是一个轻量的,底层实现隐藏的神奇技术,我们能够通过GCD和block轻松实现多线程编程,有时候,GCD相比其他系统提供的多线程方法更加有效,当然,有时候GCD不是最佳选择,另一个多线程编程的技术

    NSOprationQueue 让我们能够将后台线程以队列方式依序执行,并提供更多操作的入口,这和 GCD 的实现有些类似。

    这种类似不是一个巧合,在早期,MacOX

    与 iOS 的程序都普遍采用Operation

    Queue来进行编写后台线程代码,而之后出现的GCD技术大体是依照前者的原则来实现的,而随着GCD的普及,在iOS 4 与 MacOS X

    10.6以后,Operation Queue的底层实现都是用GCD来实现的。

    那这两者直接有什么区别呢?

    1.GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;

    2.在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);

    3.NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;

    4.我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;

    5.在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;

    6.我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。

    总的来说,Operation

    queue

    提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation

    queue 为我们提供能更多的选择。

    倾向于:NSOperation

    NSOperation相对于GCD:
    1,NSOperation拥有更多的函数可用,具体查看api。NSOperationQueue 是在GCD基础上实现的,只不过是GCD更高一层的抽象。

    2,在NSOperationQueue中,可以建立各个NSOperation之间的依赖关系。

    3,NSOperationQueue支持KVO。可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)

    4,GCD 只支持FIFO 的队列,而NSOperationQueue可以调整队列的执行顺序(通过调整权重)。NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。

    使用NSOperation的情况:各个操作之间有依赖关系、操作需要取消暂停、并发管理、控制操作之间优先级,限制同时能执行的线程数量.让线程在某时刻停止/继续等。

    使用GCD的情况:一般的需求很简单的多线程操作,用GCD都可以了,简单高效。

    从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。

    当需求简单,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。

    6.GCD执行原理?

    GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护(看到这句话是不是很开心?)

    而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。

    如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。
    如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。
    这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开58条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:35条最为合理。
    通过案例明白GCD的执行原理

    案例一:


    案例1.jpg

    分析:

    首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面,问题来了:

    任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。【既然这样,那干脆就卡在这里吧】这就是死锁。


    图6.2.jpg

    案例二:


    案例2.jpg
    分析:

    首先执行任务1,接下来会遇到一个同步线程,程序会进入等待。等待任务2执行完成以后,才能继续执行任务3。从dispatch_get_global_queue可以看出,任务2被加入到了全局的并行队列中,当并行队列执行完任务2以后,返回到主队列,继续执行任务3。


    图6.4.jpg

    案例三:


    案例三.jpg
    案例四:
    案例四.jpg

    分析:

    首先,将【任务1、异步线程、任务5】加入Main

    Queue中,异步线程中的任务是:【任务2、同步线程、任务4】。所以,先执行任务1,然后将异步线程中的任务加入到Global

    Queue中,因为异步线程,所以任务5不用等待,结果就是2和5的输出顺序不一定。然后再看异步线程中的任务执行顺序。任务2执行完以后,遇到同步线程。将同步线程中的任务加入到Main

    Queue中,这时加入的任务3在任务5的后面。当任务3执行完以后,没有了阻塞,程序继续执行任务4。


    图6.6.jpg

    案例五:


    案例五.jpg
    分析:

    和上面几个案例的分析类似,先来看看都有哪些任务加入了Main Queue:

    【异步线程、任务4、死循环、任务5】。

    在加入到Global Queue异步线程中的任务有:

    【任务1、同步线程、任务3】。第一个就是异步线程,任务4不用等待,

    所以结果任务1和任务4顺序不一定。任务4完成后,程序进入死循环,

    Main Queue阻塞。但是加入到Global Queue的异步线程不受影响,

    继续执行任务1后面的同步线程。同步线程中,将任务2加入到了主线程,

    并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。

    所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。

    图6.8.jpg
    7.怎么防止别人动态在你程序生成代码?

    (这题是听错了面试官的意思)

    面试官意思是怎么防止别人反编译你的app?

    1.本地数据加密

    iOS应用防反编译加密技术之一:对NSUserDefaults,sqlite存储文件数据加密,保护帐号和关键信息

    2.URL编码加密

    iOS应用防反编译加密技术之二:对程序中出现的URL进行编码加密,防止URL被静态分析

    3.网络传输数据加密

    iOS应用防反编译加密技术之三:对客户端传输数据提供加密方案,有效防止通过网络接口的拦截获取数据

    4.方法体,方法名高级混淆

    iOS应用防反编译加密技术之四:对应用程序的方法名和方法体进行混淆,保证源码被逆向后无法解析代码

    5.程序结构混排加密

    iOS应用防反编译加密技术之五:对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低

    6.借助第三方APP加固,例如:网易云易盾

    8.YYAsyncLayer如何异步绘制?

    YYAsyncLayer是异步绘制与显示的工具。为了保证列表滚动流畅,将视图绘制、以及图片解码等任务放到后台线程,

    YYKitDemo

    对于列表主要对两个代理方法的优化,一个与绘制显示有关,另一个与计算布局有关:

    Objective-C

    1

    2-(UITableViewCell)tableView:(UITableView)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath;

    -(CGFloat)tableView:(UITableView)tableViewheightForRowAtIndexPath:(NSIndexPath)indexPath;

    常规逻辑可能觉得应该先调用tableView : cellForRowAtIndexPath :返回UITableViewCell对象,事实上调用顺序是先返回UITableViewCell的高度,是因为UITableView继承自UIScrollView,滑动范围由属性contentSize来确定,UITableView的滑动范围需要通过每一行的UITableViewCell的高度计算确定,复杂cell如果在列表滚动过程中计算可能会造成一定程度的卡顿。

    假设有20条数据,当前屏幕显示5条,tableView : heightForRowAtIndexPath :方法会先执行20次返回所有高度并计算出滑动范围,tableView : cellForRowAtIndexPath :执行5次返回当前屏幕显示的cell个数。


    图8.1.jpg

    从图中简单看下流程,从网络请求返回JSON数据,将Cell的高度以及内部视图的布局封装为Layout对象,Cell显示之前在异步线程计算好所有布局对象,并存入数组,每次调用tableView: heightForRowAtIndexPath :只需要从数组中取出,可避免重复的布局计算。同时在调用tableView: cellForRowAtIndexPath :对Cell内部视图异步绘制布局,以及图片的异步绘制解码,这里就要说到今天的主角YYAsyncLayer。

    YYAsyncLayer

    首先介绍里面几个类:

    YYAsyncLayer:继承自CALayer,绘制、创建绘制线程的部分都在这个类。

    YYTransaction:用于创建RunloopObserver监听MainRunloop的空闲时间,并将YYTranaction对象存放到集合中。

    YYSentinel:提供获取当前值的value(只读)属性,以及- (int32_t)increase自增加的方法返回一个新的value值,用于判断异步绘制任务是否被取消的工具。


    图8.2.jpg

    上图是整体异步绘制的实现思路,后面一步步说明。现在假设需要绘制Label,其实是继承自UIView,重写+ (Class)layerClass,在需要重新绘制的地方调用下面方法,比如setter,layoutSubviews。

    Objective-C

    +(Class)layerClass{
    returnYYAsyncLayer.class;
    }
    -(void)setText:(NSString*)text{
    _text=text.copy;
    [[YYTransactiontransactionWithTarget:selfselector:@selector(contentsNeedUpdated)]commit];
    }
    -(void)layoutSubviews{
    [superlayoutSubviews];
    [[YYTransactiontransactionWithTarget:selfselector:@selector(contentsNeedUpdated)]commit];
    }
    

    YYTransaction有selector、target的属性,selector其实就是contentsNeedUpdated方法,此时并不会立即在后台线程去更新显示,而是将YYTransaction对象本身提交保存在transactionSet的集合中,上图中所示。

    Objective-C

    +(YYTransaction*)transactionWithTarget:(id)targetselector:(SEL)selector{
    if(!target||!selector)returnnil;
    YYTransaction*t=[YYTransactionnew];
    t.target=target;
    t.selector=selector;
    returnt;
    }
    -(void)commit{
    if(!_target||!_selector)return;
    YYTransactionSetup();
    [transactionSetaddObject:self];
    }
    

    同时在YYTransaction.m中注册一个RunloopObserver,监听MainRunloop在kCFRunLoopCommonModes(包含kCFRunLoopDefaultMode、UITrackingRunLoopMode)下的kCFRunLoopBeforeWaiting和kCFRunLoopExit的状态,也就是说在一次Runloop空闲时去执行更新显示的操作。

    kCFRunLoopBeforeWaiting:Runloop将要进入休眠。

    kCFRunLoopExit:即将退出本次Runloop。

    Objective-C

    staticvoidYYTransactionSetup(){
    staticdispatch_once_tonceToken;
    dispatch_once(&onceToken,^{
    transactionSet=[NSMutableSetnew];
    CFRunLoopRefrunloop=CFRunLoopGetMain();
    CFRunLoopObserverRefobserver;
    observer=CFRunLoopObserverCreate(CFAllocatorGetDefault(),
    kCFRunLoopBeforeWaiting|kCFRunLoopExit,
    true,// repeat
    0xFFFFFF,// after CATransaction(2000000)
    YYRunLoopObserverCallBack,NULL);
    CFRunLoopAddObserver(runloop,observer,kCFRunLoopCommonModes);
    CFRelease(observer);
    });
    }
    
    

    下面是RunloopObserver的回调方法,从transactionSet取出transaction对象执行SEL的方法,分发到每一次Runloop执行,避免一次Runloop执行时间太长。

    Objective-C
    staticvoidYYRunLoopObserverCallBack(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity,void*info){
    if(transactionSet.count==0)return;
    NSSet*currentSet=transactionSet;
    transactionSet=[NSMutableSetnew];
    [currentSetenumerateObjectsUsingBlock:^(YYTransaction*transaction,BOOL*stop){
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [transaction.targetperformSelector:transaction.selector];
    #pragma clang diagnostic pop
    }];
    }
    

    接下来是异步绘制,这里用了一个比较巧妙的方法处理,当使用GCD时提交大量并发任务到后台线程导致线程被锁住、休眠的情况,创建与程序当前激活CPU数量(activeProcessorCount)相同的串行队列,并限制MAX_QUEUE_COUNT,将队列存放在数组中。

    YYAsyncLayer.m有一个方法YYAsyncLayerGetDisplayQueue来获取这个队列用于绘制(这部分YYKit中有独立的工具YYDispatchQueuePool)。创建队列中有一个参数是告诉队列执行任务的服务质量quality of service,在iOS8+之后相比之前系统有所不同。

    iOS8之前队列优先级:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    DISPATCH_QUEUE_PRIORITY_HIGH 2高优先级
    DISPATCH_QUEUE_PRIORITY_DEFAULT 0默认优先级
    DISPATCH_QUEUE_PRIORITY_LOW (-2)低优先级
    DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN后台优先级
    iOS8+之后:
    QOS_CLASS_USER_INTERACTIVE 0x21, 用户交互(希望尽快完成,不要放太耗时操作)
    QOS_CLASS_USER_INITIATED 0x19, 用户期望(不要放太耗时操作)
    QOS_CLASS_DEFAULT 0x15, 默认(用来重置对列使用的)
    QOS_CLASS_UTILITY 0x11, 实用工具(耗时操作,可以使用这个选项)
    QOS_CLASS_BACKGROUND 0x09, 后台
    QOS_CLASS_UNSPECIFIED 0x00, 未指定
    Objective-C
    /// Global display queue, used for content rendering.
    staticdispatch_queue_tYYAsyncLayerGetDisplayQueue(){
    #ifdef YYDispatchQueuePool_h
    returnYYDispatchQueueGetForQOS(NSQualityOfServiceUserInitiated);
    #else
    #define MAX_QUEUE_COUNT 16
    staticintqueueCount;
    staticdispatch_queue_tqueues[MAX_QUEUE_COUNT];//存放队列的数组
    staticdispatch_once_tonceToken;
    staticint32_tcounter=0;
    dispatch_once(&onceToken,^{
    //程序激活的处理器数量
    queueCount=(int)[NSProcessInfoprocessInfo].activeProcessorCount;
    queueCount=queueCountMAX_QUEUE_COUNT?MAX_QUEUE_COUNT: queueCount);
    if([UIDevicecurrentDevice].systemVersion.floatValue>=8.0){
    for(NSUIntegeri=0;i
    

    接下来是关于绘制部分的代码,对外接口YYAsyncLayerDelegate代理中提供- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask方法用于回调绘制的代码,以及是否异步绘制的BOOl类型属性displaysAsynchronously,同时重写CALayer的display方法来调用绘制的方法- (void)_displayAsync:(BOOL)async。

    这里有必要了解关于后台的绘制任务何时会被取消,下面两种情况需要取消,并调用了YYSentinel的increase方法,使value值增加(线程安全):

    在视图调用setNeedsDisplay时说明视图的内容需要被更新,将当前的绘制任务取消,需要重新显示。

    以及视图被释放调用了dealloc方法。

    在YYAsyncLayer.h中定义了YYAsyncLayerDisplayTask类,有三个block属性用于绘制的回调操作,从命名可以看出分别是将要绘制,正在绘制,以及绘制完成的回调,可以从block传入的参数BOOL(^isCancelled)(void)判断当前绘制是否被取消。

    Objective-C
    @property(nullable,nonatomic,copy)void(^willDisplay)(CALayer*layer);
    @property(nullable,nonatomic,copy)void(^display)(CGContextRefcontext,CGSizesize,BOOL(^isCancelled)(void));
    @property(nullable,nonatomic,copy)void(^didDisplay)(CALayer*layer,BOOLfinished);
    

    下面是部分- (void)_displayAsync:(BOOL)async绘制的代码,主要是一些逻辑判断以及绘制函数,在异步执行之前通过YYAsyncLayerGetDisplayQueue创建的队列,这里通过YYSentinel判断当前的value是否等于之前的值,如果不相等,说明绘制任务被取消了,绘制过程会多次判断是否取消,如果是则return,保证被取消的任务能及时退出,如果绘制完毕则设置图片到layer.contents。

    Objective-C
    if(async){//异步
    if(task.willDisplay)task.willDisplay(self);
    YYSentinel*sentinel=_sentinel;
    int32_tvalue=sentinel.value;
    NSLog(@" --- %d ---",value);
    //判断当前计数是否等于之前计数
    BOOL(^isCancelled)()=^BOOL(){
    returnvalue!=sentinel.value;
    };
    CGSizesize=self.bounds.size;
    BOOLopaque=self.opaque;
    CGFloatscale=self.contentsScale;
    CGColorRefbackgroundColor=(opaque&&self.backgroundColor)?CGColorRetain(self.backgroundColor): NULL;
    if(size.width
    
    9.优化你是从哪几方面着手?

    一、首页启动速度

    启动过程中做的事情越少越好(尽可能将多个接口合并)

    不在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 持续重构代码。减少代码的逻辑复杂度)

    日常交流(经常分享一些代码,或者逻辑处理中的坑)

    文章转载:http://www.cocoachina.com/ios/20171129/21362.html

    相关文章

      网友评论

        本文标题:iOS面试题:阿里-P6一面-参考思路

        本文链接:https://www.haomeiwen.com/subject/mrnxixtx.html