面试资料整理(整理中~)

来源:互联网 发布:android ndk r10d mac 编辑:程序博客网 时间:2024/06/06 11:36

1.介绍下内存的几大区域?

栈区,堆区,静态区(全局区),常量区,代码区

  • 动态数据区一般就是”堆栈”,栈是线性结构,堆是链式结构. 本地变量在堆栈中.通过堆栈的基地址和偏移量来访问本地变量

    • 动态内存分配有系统根据程序需要即时分配,且分配的大小就是程序要求的大小.
  • 全局变量(一般用static修饰的变量)和静态变量分配在静态区(需要预先分配存储空间)

    • 静态内存分配:分配固定大小的内存分配方法,大多情况下会浪费大量的内存空间,少数情况下,当定义的数组不够大时,会引起越界.

    • 局部变量采用栈的方式存放


      阿里-p6-一面 


      1.介绍下内存的几大区域?

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

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

      4.runloop内部实现逻辑?

      5.你理解的多线程?

      6.GCD执行原理?

      7.怎么防止别人反编译你的app?

      8.YYAsyncLayer如何异步绘制?

      9.优化你是从哪几方面着手?


      1.介绍下内存的几大区域?


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


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



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


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



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


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



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


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


      大致如图:



      例子代码:



      可能被追问的问题一:


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


      局部变量是保存在栈区的


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


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


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


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


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


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


      可能被追问的问题二:


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


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


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


      堆:


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


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


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


      可能被追问的问题三:


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


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


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



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


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


      打个比喻来说:


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


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


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


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


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


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


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


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


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



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


      组件间通信


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



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


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



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


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


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


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



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


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



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


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


      Android 也是采用类似的方式。


      组件生命周期管理


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


      先来看看现在的入口方法



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



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


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


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



      壳工程


      既然已经拆出去了,那拆出去的组件总得有个载体,这个载体就是壳工程,壳工程主要包含一些基础组件和业务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 里就完事了。这样最简单,但也经常会带来编译通不过的问题。而且这种随意的版本升级也不太能保证质量。于是我们就搭建了一套持续集成系统,大概如此



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



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


      大致思路是,业务方如果要升级组件,假设现在的版本是 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 里所有的组件版本号都是固定的。



      周边设施


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


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



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


      公共 UI 组件


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


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



      参考答案一: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地址


      实验



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



      然后我同时实例化了两个Test的对象,打印信息如下



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


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


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


      4.runloop内部实现逻辑?



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



      其内部代码整理如下 :


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


      RunLoop 的底层实现


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



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


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


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


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


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


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



      其中,在硬件层上面的三个组成部分: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()


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



      这些概念可以参考维基百科: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. 适用于UnixLinuxWindows等系统

      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. 使用更加面向对象

      1. 使用语言:OC语言

      2. 使用频率:经常使用

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


      多线程的原理


      同一时间,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系统通常只能开5~8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3~5条最为合理。


      通过案例明白GCD的执行原理


      案例一:



      分析:


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


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



      案例二:



      分析:


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



      案例三:



      案例四:



      分析:


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


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


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


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



      案例五:



      分析:


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


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


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


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


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


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


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


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


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



      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-(CGFloat)tableView:(UITableView*)tableViewheightForRowAtIndexPath:(NSIndexPath*)indexPath;


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


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


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



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


      YYAsyncLayer


      首先介绍里面几个类:


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


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


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



      AsyncDisplay.png


      上图是整体异步绘制的实现思路,后面一步步说明。现在假设需要绘制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之前队列优先级:


      • 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线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新节目)

      在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)

      尽量减小包的大小


      优化方法:


      1. 量化启动时间

      2. 启动速度模块化

      3. 辅助工具(友盟,听云,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.jianshu.com/p/de1418dc031a

      • iOS开发整理发布,转载请联系作者授权



阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 情慕南雨敬深秋 阮佳 慕少的秘宠甜妻 阮佳 慕少的秘宠甜妻阮佳 阮儿 阮元 阮军 阮冰沈墨 阮冰丝沈墨全文免费阅读 阮咸 阮咸之纵 阮姓 阮字怎么读 阮巡 阮朝 阮氏 阮氏金银 阮氏姓名大全 阮氏集团董事长女儿 阮瑀 阮籍 阮籍的故事 阮莞 防不胜防 防沉 防抱死制动系统 adyady9防弊屏映画 防患未然 防鬼口诀 新加坡防网络假信息法 近防炮 1130近防炮 防出轨软件 防的拼音 防老鼠的方法 皮防所 防雨帽 防擦条 防血栓 防踢垫 防结石 橄榄油能防妊娠纹吗