| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
DominickLan
6年前发布

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

   <p><strong>阿里-p6-一面</strong> <strong> </strong></p>    <ul>     <li> <p>1.介绍下内存的几大区域?</p> </li>     <li> <p>2.你是如何组件化解耦的?</p> </li>     <li> <p>3.runtime如何通过selector找到对应的IMP地址</p> </li>     <li> <p>4.runloop内部实现逻辑?</p> </li>     <li> <p>5.你理解的多线程?</p> </li>     <li> <p>6.GCD执行原理?</p> </li>     <li> <p>7.怎么防止别人反编译你的app?</p> </li>     <li> <p>8.YYAsyncLayer如何异步绘制?</p> </li>     <li> <p>9.优化你是从哪几方面着手?</p> </li>    </ul>    <p>1.介绍下内存的几大区域?</p>    <p>1.栈区(stack) 由编译器自动分配并释放,存放函数的参数值,局部变量等。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。[先进后出]</p>    <p>栈空间分静态分配 和动态分配两种。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9049a18dc4bd58b20aee58495b3a6373.jpg"></p>    <p>堆区(heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。</p>    <p>优点是灵活方便,数据适应面广泛,但是效率有一定降低。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a003c6d883889a5838df26b434253d1e.png"></p>    <p>虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存,释放内存匹配是良好程序的基本要素。</p>    <p>3.全局区(静态区) (static) 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c869064c501deb856450ee2441c67207.jpg"></p>    <p>4.文字常量区 存放常量字符串,程序结束后由系统释放;</p>    <p>5.代码区 存放函数的二进制代码</p>    <p>大致如图:</p>    <p><img src="https://simg.open-open.com/show/2f6be2cf9f8816e7dda027ee501e22f2.jpg"></p>    <p><img src="https://simg.open-open.com/show/b270d4b6c6cef8480879db11ab8db66f.jpg"></p>    <p>例子代码:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8fa4501d6c26a4320a662708978a4531.jpg"></p>    <p>可能被追问的问题一:</p>    <p>1.栈区 (stack [stæk]): 由编译器自动分配释放</p>    <p>局部变量是保存在栈区的</p>    <p>方法调用的实参也是保存在栈区的</p>    <p>2.堆区 (heap [hiːp]): 由程序员分配释放,若程序员不释放,会出现内存泄漏,赋值语句右侧 使用 new 方法创建的对象,被创建对象的所有 成员变量!</p>    <p>3.BSS 段 : 程序结束后由系统释放</p>    <p>4.数据段 : 程序结束后由系统释放</p>    <p>5.代码段:程序结束后由系统释放</p>    <p>程序编译链接 后的二进制可执行代码</p>    <p>可能被追问的问题二:</p>    <p>比如申请后的系统是如何响应的?</p>    <p>栈:存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。</p>    <p>注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。</p>    <p>堆:</p>    <p>1.首先应该知道操作系统有一个记录空闲内存地址的链表。</p>    <p>2.当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。</p>    <p>3 .由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中</p>    <p>可能被追问的问题三:</p>    <p>比如:申请大小的限制是怎样的?</p>    <p>栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数 ) ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。</p>    <p>堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e0f8acee76c6fc904aee3e7ea466c378.png"></p>    <p>栈:由系统自动分配,速度较快,不会产生内存碎片</p>    <p>堆:是由alloc分配的内存,速度比较慢,而且容易产生内存碎片,不过用起来最方便</p>    <p>打个比喻来说:</p>    <p>使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。</p>    <p>使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。</p>    <p>2.你是如何组件化解耦的?</p>    <p>实现代码的高内聚低耦合,方便多人多团队开发!</p>    <p>一般需要解耦的项目都会多多少少出现,一下几个情况:</p>    <p>耦合比较严重(因为没有明确的约束,「组件」间引用的现象会比较多)</p>    <p>2.容易出现冲突(尤其是使用 Xib,还有就是 Xcode Project,虽说有脚本可以改善)</p>    <p>3.业务方的开发效率不够高(只关心自己的组件,却要编译整个项目,与其他不相干的代码糅合在一起)</p>    <p>先来看下,组件化之后的一个大概架构</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/062cc8ee87dbda5d43bc102835d15110.jpg"></p>    <p>「组件化」顾名思义就是把一个大的 App 拆成一个个小的组件,相互之间不直接引用。那如何做呢?</p>    <p>组件间通信</p>    <p>以 iOS 为例,由于之前就是采用的 URL 跳转模式,理论上页面之间的跳转只需 open 一个 URL 即可。所以对于一个组件来说,只要定义「支持哪些 URL」即可,比如详情页,大概可以这么做的</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6abc0e1ab5e10aafee809264beb2532f.jpg"></p>    <p>首页只需调用[MGJRouter openURL:@"mgj://detail?id=404"]就可以打开相应的详情页。</p>    <p>那问题又来了,我怎么知道有哪些可用的 URL?为此,我们做了一个后台专门来管理。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cc7059e8216807c1cf1bcc3dcb7f80eb.jpg"></p>    <p>然后可以把这些短链生成不同平台所需的文件,iOS 平台生成 .{h,m} 文件,Android 平台生成 .java 文件,并注入到项目中。这样开发人员只需在项目中打开该文件就知道所有的可用 URL 了。</p>    <p>目前还有一块没有做,就是参数这块,虽然描述了短链,但真想要生成完整的 URL,还需要知道如何传参数,这个正在开发中。</p>    <p>还有一种情况会稍微麻烦点,就是「组件A」要调用「组件B」的某个方法,比如在商品详情页要展示购物车的商品数量,就涉及到向购物车组件拿数据。</p>    <p>类似这种同步调用,iOS 之前采用了比较简单的方案,还是依托于MGJRouter,不过添加了新的方法- (id)objectForURL:,注册时也使用新的方法进行注册</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/92f2014155ea387637e377c8bedd6879.jpg"></p>    <p>使用时NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]这样就拿到了购物车里的商品数。</p>    <p>稍微复杂但更具通用性的方法是使用「协议」<-> 「类」绑定的方式,还是以购物车为例,购物车组件可以提供这么个 Protocol</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/464a8e60ae0e026bc09cebfd82416e51.png"></p>    <p>可以看到通过协议可以直接指定返回的数据类型。然后在购物车组件内再新建个类实现这个协议,假设这个类名为MGJCartImpl,接着就可以把它与协议关联起来[ModuleManagerregisterClass:MGJCartImplforProtocol:@protocol(MGJCart)],对于使用方来说,要拿到这个MGJCartImpl,需要调用[ModuleManagerclassForProtocol:@protocol(MGJCart)]。拿到之后再调用+ (NSInteger)orderCount就可以了。</p>    <p>那么,这个协议放在哪里比较合适呢?如果跟组件放在一起,使用时还是要先引入组件,如果有多个这样的组件就会比较麻烦了。所以我们把这些公共的协议统一放到了PublicProtocolDomain.h下,到时只依赖这一个文件就可以了。</p>    <p>Android 也是采用类似的方式。</p>    <p>组件生命周期管理</p>    <p>理想中的组件可以很方便地集成到主客中,并且有跟AppDelegate一致的回调方法。这也是ModuleManager做的事情。</p>    <p>先来看看现在的入口方法</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a0d4e18c6d0fb14fb9d74ba124299375.jpg"></p>    <p>其中[MGJApp startApp]主要负责一些 SDK 的初始化。[self trackLaunchTime]是我们打的一个点,用来监测从main方法开始到入口方法调用结束花了多长时间。其他的都由ModuleManager搞定,loadModuleFromPlist:pathForResource:方法会读取 bundle 里的一个 plist 文件,这个文件的内容大概是这样的</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8d92c732fa0b0844a8c3176169e10137.jpg"></p>    <p>每个Module都实现了ModuleProtocol,其中有一个- (BOOL)applicaiton:didFinishLaunchingWithOptions:方法,如果实现了的话,就会被调用。</p>    <p>还有一个问题就是,系统的一些事件会有通知,比如applicationDidBecomeActive会有对应的UIApplicationDidBecomeActiveNotification,组件如果要做响应的话,只需监听这个系统通知即可。但也有一些事件是没有通知的,比如- application:didRegisterUserNotificationSettings:,这时组件如果也要做点事情,怎么办?</p>    <p>一个简单的解决方法是在AppDelegate的各个方法里,手动调一遍组件的对应的方法,如果有就执行。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c7f23d215712eacb644eadd1ecaaed46.jpg"></p>    <p>壳工程</p>    <p>既然已经拆出去了,那拆出去的组件总得有个载体,这个载体就是壳工程,壳工程主要包含一些基础组件和业务SDK,这也是主工程包含的一些内容,所以如果在壳工程可以正常运行的话,到了主工程也没什么问题。不过这里存在版本同步问题,之后会说到。</p>    <p>遇到的问题</p>    <p>组件拆分</p>    <p>由于之前的代码都是在一个工程下的,所以要单独拿出来作为一个组件就会遇到不少问题。首先是组件的划分,当时在定义组件粒度时也花了些时间讨论,究竟是粒度粗点好,还是细点好。粗点的话比较有利于拆分,细点的话灵活度比较高。最终还是选择粗一点的粒度,先拆出来再说。</p>    <p>假如要把详情页迁出来,就会发现它依赖了一些其他部分的代码,那最快的方式就是直接把代码拷过来,改个名使用。比较简单暴力。说起来比较简单,做的时候也是挺有挑战的,因为正常的业务并不会因为「组件化」而停止,所以开发同学们需要同时兼顾正常的业务和组件的拆分。</p>    <p>版本管理</p>    <p>我们的组件包括第三方库都是通过 Cocoapods 来管理的,其中组件使用了私有库。之所以选择 Cocoapods,一个是因为它比较方便,还有就是用户基数比较大,且社区也比较活跃(活跃到了会时不时地触发 Github 的 rate limit,导致长时间 clone 不下来···见此),当然也有其他的管理方式,比如 submodule / subtree,在开发人员比较多的情况下,方便、灵活的方案容易占上风,虽然它也有自己的问题。主要有版本同步和更新/编译慢的问题。</p>    <p>假如基础组件做了个 API 接口升级,这个升级会对原有的接口做改动,自然就会升一个中位的版本号,比如原先是 1.6.19,那么现在就变成 1.7.0 了。而我们在 Podfile 里都是用~指定的,这样就会出现主工程的 pod 版本升上去了,但是壳工程没有同步到,然后群里就会各种反馈编译不过,而且这个编译不过的长尾有时能拖上两三天。</p>    <p>然后我们就想了个办法,如果不在壳工程里指定基础库的版本,只在主工程里指定呢,理论上应该可行,只要不出现某个基础库要同时维护多个版本的情况。但实践中发现,壳工程有时会莫名其妙地升不上去,在 podfile 里指定最新的版本又可以升上去,所以此路不通。</p>    <p>还有一个问题是pod update时间过长,经常会在Analyzing Dependency上卡 10 多分钟,非常影响效率。后来排查下来是跟组件的 Podspec 有关,配置了 subspec,且依赖比较多。</p>    <p>然后就是 pod update 之后的编译,由于是源码编译,所以这块的时间花费也不少,接下去会考虑 framework 的方式。</p>    <p>持续集成</p>    <p>在刚开始,持续集成还不是很完善,业务方升级组件,直接把 podspec 扔到 private repo 里就完事了。这样最简单,但也经常会带来编译通不过的问题。而且这种随意的版本升级也不太能保证质量。于是我们就搭建了一套持续集成系统,大概如此</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d8a9d8ad0c703f73e2b795afcbe37c28.jpg"></p>    <p>每个组件升级之前都需要先通过编译,然后再决定是否升级。这套体系看起来不复杂,但在实施过程中经常会遇到后端的并发问题,导致业务方要么集成失败,要么要等不少时间。而且也没有一个地方可以呈现当前版本的组件版本信息。还有就是业务方对于这种命令行的升级方式接受度也不是很高。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9c42fd42b703bb748eff6f7dd9df7979.jpg"></p>    <p>基于此,在经过了几轮讨论之后,有了新版的持续集成平台,升级操作通过网页端来完成。</p>    <p>大致思路是,业务方如果要升级组件,假设现在的版本是 0.1.7,添加了一些 feature 之后,壳工程测试通过,想集成到主工程里看看效果,或者其他组件也想引用这个最新的,就可以在后台手动把版本升到 0.1.8-rc.1,这样的话,原先依赖~> 0.1.7的组件,不会升到 0.1.8,同时想要测试这个组件的话,只要手动把版本调到 0.1.8-rc.1 就可以了。这个过程不会触发 CI 的编译检查。</p>    <p>当测试通过后,就可以把尾部的-rc.n去掉,然后点击「集成」,就会走 CI 编译检查,通过的话,会在主工程的 podfile 里写上固定的版本号 0.1.8。也就是说,podfile 里所有的组件版本号都是固定的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1696c1d8ca2dacab1ed5336add3ccb5c.jpg"></p>    <p>周边设施</p>    <p>基础组件及组件的文档 / Demo / 单元测试</p>    <p>无线基础的职能是为集团提供解决方案,只是在蘑菇街 App 里能 work 是远远不够的,所以就需要提供入口,知道有哪些可用组件,并且如何使用,就像这样(目前还未实现)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4691e67c5ed561e69e53c8fcecfea0d5.jpg"></p>    <p>这就要求组件的负责人需要及时地更新 README / CHANGELOG / API,并且当发生 API 变更时,能够快速通知到使用方。</p>    <p>公共 UI 组件</p>    <p>组件化之后还有一个问题就是资源的重复性,以前在一个工程里的时候,资源都可以很方便地拿到,现在独立出去了,也不知道哪些是公用的,哪些是独有的,索性都放到自己的组件里,这样就会导致包变大。还有一个问题是每个组件可能是不同的产品经理在跟,而他们很可能只关注于自己关心的页面长什么样,而忽略了整体的样式。公共</p>    <p>UI 组件就是用来解决这些问题的,这些组件甚至可以跨 App 使用。(目前还未实现)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/20e6b128dd130b50cccbfc5784181f02.jpg"></p>    <p>参考答案一:http://blog.csdn.net/GGGHub/article/details/52713642</p>    <p>参考答案二:http://limboy.me/tech/2016/03/10/mgj-components.html</p>    <p>3.runtime如何通过selector找到对应的IMP地址?</p>    <p>概述</p>    <p>类对象中有类方法和实例方法的列表,列表中记录着方法的名词、参数和实现,而selector本质就是方法名称,runtime通过这个方法名称就可以在列表中找到该方法对应的实现。</p>    <p>这里声明了一个指向struct objc_method_list指针的指针,可以包含类方法列表和实例方法列表</p>    <p>具体实现</p>    <p>在寻找IMP的地址时,runtime提供了两种方法</p>    <p>IMP class_getMethodImplementation(Class cls, SEL name);IMP method_getImplementation(Method m)</p>    <p>而根据官方描述,第一种方法可能会更快一些</p>    <p>@note \c class_getMethodImplementation may be faster than \c method_getImplementation(class_getInstanceMethod(cls, name)).</p>    <p>对于第一种方法而言,类方法和实例方法实际上都是通过调用class_getMethodImplementation()来寻找IMP地址的,不同之处在于传入的第一个参数不同</p>    <p>类方法(假设有一个类A)</p>    <p>class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));</p>    <p>实例方法</p>    <p>class_getMethodImplementation([A class],@selector(methodName));</p>    <p>通过该传入的参数不同,找到不同的方法列表,方法列表中保存着下面方法的结构体,结构体中包含这方法的实现,selector本质就是方法的名称,通过该方法名称,即可在结构体中找到相应的实现。</p>    <p>struct objc_method {SEL method_namechar *method_typesIMP method_imp}</p>    <p>而对于第二种方法而言,传入的参数只有method,区分类方法和实例方法在于封装method的函数</p>    <p>类方法</p>    <p>Method class_getClassMethod(Class cls, SEL name)</p>    <p>实例方法</p>    <p>Method class_getInstanceMethod(Class cls, SEL name)</p>    <p>最后调用IMP method_getImplementation(Method m)获取IMP地址</p>    <p>实验</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ef100ab4962d53ae5c7d7b2b2d20a1ce.jpg"></p>    <p>这里有一个叫Test的类,在初始化方法里,调用了两次getIMPFromSelector:方法,第一个aaa方法是不存在的,test1和test2分别为实例方法和类方法</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/79d88645c2c0b6a6b9fc7e159cb8e375.png"></p>    <p>然后我同时实例化了两个Test的对象,打印信息如下</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/65500addfdd6e2b550f228bf4cb46bde.jpg"></p>    <p>大家注意图中红色标注的地址出现了8次:0x1102db280,这个是在调用class_getMethodImplementation()方法时,无法找到对应实现时返回的相同的一个地址,无论该方法是在实例方法或类方法,无论是否对一个实例调用该方法,返回的地址都是相同的,但是每次运行该程序时返回的地址并不相同,而对于另一种方法,如果找不到对应的实现,则返回0,在图中我做了蓝色标记。</p>    <p>还有一点有趣的是class_getClassMethod()的第一个参数无论传入objc_getClass()还是objc_getMetaClass(),最终调用method_getImplementation()都可以成功的找到类方法的实现。</p>    <p>而class_getInstanceMethod()的第一个参数如果传入objc_getMetaClass(),再调用method_getImplementation()时无法找到实例方法的实现却可以找到类方法的实现。</p>    <p>4.runloop内部实现逻辑?</p>    <p><strong><img src="https://simg.open-open.com/show/71aa8e0fa2129a14349aa04f6f7cdd1d.jpg"> </strong></p>    <p>苹果在文档里的说明,RunLoop 内部的逻辑大致如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ed04bbc8ace57552aaa94a78004e2a0a.jpg"></p>    <p>其内部代码整理如下 :</p>    <p>可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。</p>    <p>RunLoop 的底层实现</p>    <p>从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5542ade4a33e6ff68ead7281977486f4.png"></p>    <p>苹果官方将整个系统大致划分为上述4个层次:</p>    <p>应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。</p>    <p>应用框架层即开发人员接触到的 Cocoa 等框架。</p>    <p>核心框架层包括各种核心框架、OpenGL 等内容。</p>    <p>Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在opensource.apple.com里找到。</p>    <p>我们在深入看一下 Darwin 这个核心的架构:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3b246f60e2a53871269d154f61380d73.png"></p>    <p>其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。</p>    <p>XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。</p>    <p>BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。</p>    <p>IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。</p>    <p>Mach</p>    <p>本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的</p>    <p>API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach</p>    <p>中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach</p>    <p>的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port)</p>    <p>之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。</p>    <p>Mach 的消息定义是在头文件的,很简单:</p>    <pre>  <code class="language-objectivec">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;</code></pre>    <p>一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,</p>    <p>发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:</p>    <pre>  <code class="language-objectivec">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);</code></pre>    <p>为了实现消息的发送和接收,mach_msg()</p>    <p>函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach</p>    <p>中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg()</p>    <p>函数会完成实际的工作,如下图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/88239e5906a43aaa6b4c9cd44aebabed.png"></p>    <p>这些概念可以参考维基百科:System_call、Trap_(computing)。</p>    <p>RunLoop</p>    <p>的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port</p>    <p>消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在</p>    <p>mach_msg_trap() 这个地方。</p>    <p>关于具体的如何利用 mach port 发送信息,可以看看NSHipster 这一篇文章,或者这里的中文翻译 。</p>    <p>关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian。</p>    <p>苹果用 RunLoop 实现的功能</p>    <p>首先我们可以看一下 App 启动后 RunLoop 的状态:</p>    <p>可以看到,系统默认注册了5个Mode:</p>    <p>1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。</p>    <p>2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。</p>    <p>3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。</p>    <p>4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。</p>    <p>5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。</p>    <p>你可以在这里看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了。</p>    <p>5.你理解的多线程?</p>    <p>1.可能会追问,每种多线程基于什么语言?</p>    <p>2.生命周期是如何管理?</p>    <p>3.你更倾向于哪种?追问至现在常用的两种你的看法是?</p>    <p>第一种:pthread</p>    <p>.特点:</p>    <p>1)一套通用的多线程API</p>    <p>2)适用于Unix\Linux\Windows等系统</p>    <p>3)跨平台\可移植</p>    <p>4)使用难度大</p>    <p>b.使用语言:c语言</p>    <p>c.使用频率:几乎不用</p>    <p>d.线程生命周期:由程序员进行管理</p>    <p>第二种:NSThread</p>    <p>a.特点:</p>    <p>1)使用更加面向对象</p>    <p>2)简单易用,可直接操作线程对象</p>    <p>b.使用语言:OC语言</p>    <p>c.使用频率:偶尔使用</p>    <p>d.线程生命周期:由程序员进行管理</p>    <p>第三种:GCD</p>    <p>a.特点:</p>    <p>1)旨在替代NSThread等线程技术</p>    <p>2)充分利用设备的多核(自动)</p>    <p>b.使用语言:C语言</p>    <p>c.使用频率:经常使用</p>    <p>d.线程生命周期:自动管理</p>    <p>第四种:NSOperation</p>    <p>a.特点:</p>    <p>1)基于GCD(底层是GCD)</p>    <p>2)比GCD多了一些更简单实用的功能</p>    <p>3)使用更加面向对象</p>    <p>b.使用语言:OC语言</p>    <p>c.使用频率:经常使用</p>    <p>d.线程生命周期:自动管理</p>    <p>多线程的原理</p>    <p>同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)</p>    <p>多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)</p>    <p>如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象</p>    <p>思考:如果线程非常非常多,会发生什么情况?</p>    <p>CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源</p>    <p>每条线程被调度执行的频次会降低(线程的执行效率降低)</p>    <p>多线程的优点</p>    <p>能适当提高程序的执行效率</p>    <p>能适当提高资源利用率(CPU、内存利用率)</p>    <p>多线程的缺点</p>    <p>开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能</p>    <p>线程越多,CPU在调度线程上的开销就越大</p>    <p>程序设计更加复杂:比如线程之间的通信、多线程的数据共享</p>    <p>你更倾向于哪一种?</p>    <p>倾向于GCD:</p>    <p>GCD</p>    <p>技术是一个轻量的,底层实现隐藏的神奇技术,我们能够通过GCD和block轻松实现多线程编程,有时候,GCD相比其他系统提供的多线程方法更加有效,当然,有时候GCD不是最佳选择,另一个多线程编程的技术</p>    <p>NSOprationQueue 让我们能够将后台线程以队列方式依序执行,并提供更多操作的入口,这和 GCD 的实现有些类似。</p>    <p>这种类似不是一个巧合,在早期,MacOX</p>    <p>与 iOS 的程序都普遍采用Operation</p>    <p>Queue来进行编写后台线程代码,而之后出现的GCD技术大体是依照前者的原则来实现的,而随着GCD的普及,在iOS 4 与 MacOS X</p>    <p>10.6以后,Operation Queue的底层实现都是用GCD来实现的。</p>    <p>那这两者直接有什么区别呢?</p>    <p>1.GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;</p>    <p>2.在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);</p>    <p>3.NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;</p>    <p>4.我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;</p>    <p>5.在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;</p>    <p>6.我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。</p>    <p>总的来说,Operation</p>    <p>queue</p>    <p>提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation</p>    <p>queue 为我们提供能更多的选择。</p>    <p>倾向于:NSOperation</p>    <p>NSOperation相对于GCD:</p>    <p>1,NSOperation拥有更多的函数可用,具体查看api。NSOperationQueue 是在GCD基础上实现的,只不过是GCD更高一层的抽象。</p>    <p>2,在NSOperationQueue中,可以建立各个NSOperation之间的依赖关系。</p>    <p>3,NSOperationQueue支持KVO。可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)</p>    <p>4,GCD 只支持FIFO 的队列,而NSOperationQueue可以调整队列的执行顺序(通过调整权重)。NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。</p>    <p>使用NSOperation的情况:各个操作之间有依赖关系、操作需要取消暂停、并发管理、控制操作之间优先级,限制同时能执行的线程数量.让线程在某时刻停止/继续等。</p>    <p>使用GCD的情况:一般的需求很简单的多线程操作,用GCD都可以了,简单高效。</p>    <p>从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。</p>    <p>当需求简单,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。</p>    <p>5.你理解的多线程?</p>    <p>1.可能会追问,每种多线程基于什么语言?</p>    <p>2.生命周期是如何管理?</p>    <p>3.你更倾向于哪种?追问至现在常用的两种你的看法是?</p>    <p>第一种:pthread</p>    <p>.特点:</p>    <p>1)一套通用的多线程API</p>    <p>2)适用于Unix\Linux\Windows等系统</p>    <p>3)跨平台\可移植</p>    <p>4)使用难度大</p>    <p>b.使用语言:c语言</p>    <p>c.使用频率:几乎不用</p>    <p>d.线程生命周期:由程序员进行管理</p>    <p>第二种:NSThread</p>    <p>a.特点:</p>    <p>1)使用更加面向对象</p>    <p>2)简单易用,可直接操作线程对象</p>    <p>b.使用语言:OC语言</p>    <p>c.使用频率:偶尔使用</p>    <p>d.线程生命周期:由程序员进行管理</p>    <p>第三种:GCD</p>    <p>a.特点:</p>    <p>1)旨在替代NSThread等线程技术</p>    <p>2)充分利用设备的多核(自动)</p>    <p>b.使用语言:C语言</p>    <p>c.使用频率:经常使用</p>    <p>d.线程生命周期:自动管理</p>    <p>第四种:NSOperation</p>    <p>a.特点:</p>    <p>1)基于GCD(底层是GCD)</p>    <p>2)比GCD多了一些更简单实用的功能</p>    <p>3)使用更加面向对象</p>    <p>b.使用语言:OC语言</p>    <p>c.使用频率:经常使用</p>    <p>d.线程生命周期:自动管理</p>    <p>多线程的原理</p>    <p>同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)</p>    <p>多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)</p>    <p>如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象</p>    <p>思考:如果线程非常非常多,会发生什么情况?</p>    <p>CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源</p>    <p>每条线程被调度执行的频次会降低(线程的执行效率降低)</p>    <p>多线程的优点</p>    <p>能适当提高程序的执行效率</p>    <p>能适当提高资源利用率(CPU、内存利用率)</p>    <p>多线程的缺点</p>    <p>开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能</p>    <p>线程越多,CPU在调度线程上的开销就越大</p>    <p>程序设计更加复杂:比如线程之间的通信、多线程的数据共享</p>    <p>你更倾向于哪一种?</p>    <p>倾向于GCD:</p>    <p>GCD</p>    <p>技术是一个轻量的,底层实现隐藏的神奇技术,我们能够通过GCD和block轻松实现多线程编程,有时候,GCD相比其他系统提供的多线程方法更加有效,当然,有时候GCD不是最佳选择,另一个多线程编程的技术</p>    <p>NSOprationQueue 让我们能够将后台线程以队列方式依序执行,并提供更多操作的入口,这和 GCD 的实现有些类似。</p>    <p>这种类似不是一个巧合,在早期,MacOX</p>    <p>与 iOS 的程序都普遍采用Operation</p>    <p>Queue来进行编写后台线程代码,而之后出现的GCD技术大体是依照前者的原则来实现的,而随着GCD的普及,在iOS 4 与 MacOS X</p>    <p>10.6以后,Operation Queue的底层实现都是用GCD来实现的。</p>    <p>那这两者直接有什么区别呢?</p>    <p>1.GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;</p>    <p>2.在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);</p>    <p>3.NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;</p>    <p>4.我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;</p>    <p>5.在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;</p>    <p>6.我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。</p>    <p>总的来说,Operation</p>    <p>queue</p>    <p>提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation</p>    <p>queue 为我们提供能更多的选择。</p>    <p>倾向于:NSOperation</p>    <p>NSOperation相对于GCD:</p>    <p>1,NSOperation拥有更多的函数可用,具体查看api。NSOperationQueue 是在GCD基础上实现的,只不过是GCD更高一层的抽象。</p>    <p>2,在NSOperationQueue中,可以建立各个NSOperation之间的依赖关系。</p>    <p>3,NSOperationQueue支持KVO。可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)</p>    <p>4,GCD 只支持FIFO 的队列,而NSOperationQueue可以调整队列的执行顺序(通过调整权重)。NSOperationQueue可以方便的管理并发、NSOperation之间的优先级。</p>    <p>使用NSOperation的情况:各个操作之间有依赖关系、操作需要取消暂停、并发管理、控制操作之间优先级,限制同时能执行的线程数量.让线程在某时刻停止/继续等。</p>    <p>使用GCD的情况:一般的需求很简单的多线程操作,用GCD都可以了,简单高效。</p>    <p>从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。</p>    <p>当需求简单,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。</p>    <p>6.GCD执行原理?</p>    <p>GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,很容易理解出这个“池”中的线程是可以重用的,当一段时间后这个线程没有被调用胡话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护(看到这句话是不是很开心?)</p>    <p>而我们程序员需要关心的是什么呢?我们只关心的是向队列中添加任务,队列调度即可。</p>    <ul>     <li> <p>如果队列中存放的是同步任务,则任务出队后,底层线程池中会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程。</p> </li>     <li> <p>如果队列中存放的是异步的任务,(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。</p> </li>     <li> <p>这样就对线程完成一个复用,而不需要每一个任务执行都开启新的线程,也就从而节约的系统的开销,提高了效率。在iOS7.0的时候,使用GCD系统通常只能开5~8条线程,iOS8.0以后,系统可以开启很多条线程,但是实在开发应用中,建议开启线程条数:3~5条最为合理。</p> </li>    </ul>    <p>通过案例明白GCD的执行原理</p>    <p>案例一:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/364f380fbb629ad4ec538b62ac614ddf.jpg"></p>    <p>分析:</p>    <p>首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面,问题来了:</p>    <p>任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面。【既然这样,那干脆就卡在这里吧】这就是死锁。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8a2d532517a82e8d44424b4ed1e1f833.jpg"></p>    <p>案例二:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fb85c16f42918516c8a31e0905af5ecd.jpg"></p>    <p>分析:</p>    <p>首先执行任务1,接下来会遇到一个同步线程,程序会进入等待。等待任务2执行完成以后,才能继续执行任务3。从dispatch_get_global_queue可以看出,任务2被加入到了全局的并行队列中,当并行队列执行完任务2以后,返回到主队列,继续执行任务3。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/dffda7b0fe0c3be6731e591ec73b5bed.jpg"></p>    <p>案例三:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7bde77a20c5e726deb90a4b7d59b47e6.jpg"></p>    <p>案例四:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bac63e6db3f12b600797e59e3bc94d02.jpg"></p>    <p>分析:</p>    <p>首先,将【任务1、异步线程、任务5】加入Main</p>    <p>Queue中,异步线程中的任务是:【任务2、同步线程、任务4】。所以,先执行任务1,然后将异步线程中的任务加入到Global</p>    <p>Queue中,因为异步线程,所以任务5不用等待,结果就是2和5的输出顺序不一定。然后再看异步线程中的任务执行顺序。任务2执行完以后,遇到同步线程。将同步线程中的任务加入到Main</p>    <p>Queue中,这时加入的任务3在任务5的后面。当任务3执行完以后,没有了阻塞,程序继续执行任务4。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5875f6f753114d00c4c5f552f179c2ef.jpg"></p>    <p>案例五:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d6fec3f87b678a4f372e8440e849bf2f.jpg"></p>    <p>分析:</p>    <p>和上面几个案例的分析类似,先来看看都有哪些任务加入了Main Queue:</p>    <p>【异步线程、任务4、死循环、任务5】。</p>    <p>在加入到Global Queue异步线程中的任务有:</p>    <p>【任务1、同步线程、任务3】。第一个就是异步线程,任务4不用等待,</p>    <p>所以结果任务1和任务4顺序不一定。任务4完成后,程序进入死循环,</p>    <p>Main Queue阻塞。但是加入到Global Queue的异步线程不受影响,</p>    <p>继续执行任务1后面的同步线程。同步线程中,将任务2加入到了主线程,</p>    <p>并且,任务3等待任务2完成以后才能执行。这时的主线程,已经被死循环阻塞了。</p>    <p>所以任务2无法执行,当然任务3也无法执行,在死循环后的任务5也不会执行。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4a829dada8bb62913c7b00c25a6506ee.jpg"></p>    <p>7.怎么防止别人动态在你程序生成代码?</p>    <p>(这题是听错了面试官的意思)</p>    <p>面试官意思是怎么防止别人反编译你的app?</p>    <p>1.本地数据加密</p>    <p>iOS应用防反编译加密技术之一:对NSUserDefaults,sqlite存储文件数据加密,保护帐号和关键信息</p>    <p>2.URL编码加密</p>    <p>iOS应用防反编译加密技术之二:对程序中出现的URL进行编码加密,防止URL被静态分析</p>    <p>3.网络传输数据加密</p>    <p>iOS应用防反编译加密技术之三:对客户端传输数据提供加密方案,有效防止通过网络接口的拦截获取数据</p>    <p>4.方法体,方法名高级混淆</p>    <p>iOS应用防反编译加密技术之四:对应用程序的方法名和方法体进行混淆,保证源码被逆向后无法解析代码</p>    <p>5.程序结构混排加密</p>    <p>iOS应用防反编译加密技术之五:对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低</p>    <p>6.借助第三方APP加固,例如:网易云易盾</p>    <p>8.YYAsyncLayer如何异步绘制?</p>    <p>YYAsyncLayer是异步绘制与显示的工具。为了保证列表滚动流畅,将视图绘制、以及图片解码等任务放到后台线程,</p>    <p>YYKitDemo</p>    <p>对于列表主要对两个代理方法的优化,一个与绘制显示有关,另一个与计算布局有关:</p>    <p>Objective-C</p>    <p>1</p>    <p>2-(UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath;</p>    <p>-(CGFloat)tableView:(UITableView*)tableViewheightForRowAtIndexPath:(NSIndexPath*)indexPath;</p>    <p>常规逻辑可能觉得应该先调用tableView : cellForRowAtIndexPath :返回UITableViewCell对象,事实上调用顺序是先返回UITableViewCell的高度,是因为UITableView继承自UIScrollView,滑动范围由属性contentSize来确定,UITableView的滑动范围需要通过每一行的UITableViewCell的高度计算确定,复杂cell如果在列表滚动过程中计算可能会造成一定程度的卡顿。</p>    <p>假设有20条数据,当前屏幕显示5条,tableView : heightForRowAtIndexPath :方法会先执行20次返回所有高度并计算出滑动范围,tableView : cellForRowAtIndexPath :执行5次返回当前屏幕显示的cell个数。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9a14cdd4bca405dfd3fb5bc0e060ba35.jpg"></p>    <p>从图中简单看下流程,从网络请求返回JSON数据,将Cell的高度以及内部视图的布局封装为Layout对象,Cell显示之前在异步线程计算好所有布局对象,并存入数组,每次调用tableView: heightForRowAtIndexPath :只需要从数组中取出,可避免重复的布局计算。同时在调用tableView: cellForRowAtIndexPath :对Cell内部视图异步绘制布局,以及图片的异步绘制解码,这里就要说到今天的主角YYAsyncLayer。</p>    <p>YYAsyncLayer</p>    <p>首先介绍里面几个类:</p>    <p>YYAsyncLayer:继承自CALayer,绘制、创建绘制线程的部分都在这个类。</p>    <p>YYTransaction:用于创建RunloopObserver监听MainRunloop的空闲时间,并将YYTranaction对象存放到集合中。</p>    <p>YYSentinel:提供获取当前值的value(只读)属性,以及- (int32_t)increase自增加的方法返回一个新的value值,用于判断异步绘制任务是否被取消的工具。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/df8dc8ba699c1e2ac935c9be1c9248a0.jpg"></p>    <p>AsyncDisplay.png</p>    <p>上图是整体异步绘制的实现思路,后面一步步说明。现在假设需要绘制Label,其实是继承自UIView,重写+ (Class)layerClass,在需要重新绘制的地方调用下面方法,比如setter,layoutSubviews。</p>    <p>Objective-C</p>    <pre>  <code class="language-objectivec">+(Class)layerClass{  returnYYAsyncLayer.class;  }  -(void)setText:(NSString*)text{  _text=text.copy;  [[YYTransactiontransactionWithTarget:selfselector:@selector(contentsNeedUpdated)]commit];  }  -(void)layoutSubviews{  [superlayoutSubviews];  [[YYTransactiontransactionWithTarget:selfselector:@selector(contentsNeedUpdated)]commit];  }</code></pre>    <p>YYTransaction有selector、target的属性,selector其实就是contentsNeedUpdated方法,此时并不会立即在后台线程去更新显示,而是将YYTransaction对象本身提交保存在transactionSet的集合中,上图中所示。</p>    <p>Objective-C</p>    <pre>  <code class="language-objectivec">+(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];  }</code></pre>    <p>同时在YYTransaction.m中注册一个RunloopObserver,监听MainRunloop在kCFRunLoopCommonModes(包含kCFRunLoopDefaultMode、UITrackingRunLoopMode)下的kCFRunLoopBeforeWaiting和kCFRunLoopExit的状态,也就是说在一次Runloop空闲时去执行更新显示的操作。</p>    <p>kCFRunLoopBeforeWaiting:Runloop将要进入休眠。</p>    <p>kCFRunLoopExit:即将退出本次Runloop。</p>    <p>Objective-C</p>    <pre>  <code class="language-objectivec">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);  });  }</code></pre>    <p>下面是RunloopObserver的回调方法,从transactionSet取出transaction对象执行SEL的方法,分发到每一次Runloop执行,避免一次Runloop执行时间太长。</p>    <pre>  <code class="language-objectivec">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  }];  }</code></pre>    <p>接下来是异步绘制,这里用了一个比较巧妙的方法处理,当使用GCD时提交大量并发任务到后台线程导致线程被锁住、休眠的情况,创建与程序当前激活CPU数量(activeProcessorCount)相同的串行队列,并限制MAX_QUEUE_COUNT,将队列存放在数组中。</p>    <p>YYAsyncLayer.m有一个方法YYAsyncLayerGetDisplayQueue来获取这个队列用于绘制(这部分YYKit中有独立的工具YYDispatchQueuePool)。创建队列中有一个参数是告诉队列执行任务的服务质量quality of service,在iOS8+之后相比之前系统有所不同。</p>    <p>iOS8之前队列优先级:</p>    <pre>  <code class="language-objectivec">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</code></pre>    <p>接下来是关于绘制部分的代码,对外接口YYAsyncLayerDelegate代理中提供- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask方法用于回调绘制的代码,以及是否异步绘制的BOOl类型属性displaysAsynchronously,同时重写CALayer的display方法来调用绘制的方法- (void)_displayAsync:(BOOL)async。</p>    <p>这里有必要了解关于后台的绘制任务何时会被取消,下面两种情况需要取消,并调用了YYSentinel的increase方法,使value值增加(线程安全):</p>    <p>在视图调用setNeedsDisplay时说明视图的内容需要被更新,将当前的绘制任务取消,需要重新显示。</p>    <p>以及视图被释放调用了dealloc方法。</p>    <p>在YYAsyncLayer.h中定义了YYAsyncLayerDisplayTask类,有三个block属性用于绘制的回调操作,从命名可以看出分别是将要绘制,正在绘制,以及绘制完成的回调,可以从block传入的参数BOOL(^isCancelled)(void)判断当前绘制是否被取消。</p>    <pre>  <code class="language-objectivec">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);</code></pre>    <p>下面是部分- (void)_displayAsync:(BOOL)async绘制的代码,主要是一些逻辑判断以及绘制函数,在异步执行之前通过YYAsyncLayerGetDisplayQueue创建的队列,这里通过YYSentinel判断当前的value是否等于之前的值,如果不相等,说明绘制任务被取消了,绘制过程会多次判断是否取消,如果是则return,保证被取消的任务能及时退出,如果绘制完毕则设置图片到layer.contents。</p>    <pre>  <code class="language-objectivec">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</code></pre>    <p>9.优化你是从哪几方面着手?</p>    <p>一、首页启动速度</p>    <p>启动过程中做的事情越少越好(尽可能将多个接口合并)</p>    <p>不在UI线程上作耗时的操作(数据的处理在子线程进行,处理完通知主线程刷新节目)</p>    <p>在合适的时机开始后台任务(例如在用户指引节目就可以开始准备加载的数据)</p>    <p>尽量减小包的大小</p>    <p>优化方法:</p>    <p>量化启动时间</p>    <p>启动速度模块化</p>    <p>辅助工具(友盟,听云,Flurry)</p>    <p>二、页面浏览速度</p>    <p>json的处理(iOS 自带的NSJSONSerialization,Jsonkit,SBJson)</p>    <p>数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)</p>    <p>数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)</p>    <p>内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)</p>    <p>延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载)</p>    <p>算法的优化(核心算法的优化,例如有些app 有个 联系人姓名用汉语拼音的首字母排序)</p>    <p>三、操作流畅度优化:</p>    <p>Tableview 优化(tableview cell的加载优化)</p>    <p>ViewController加载优化(不同view之间的跳转,可以提前准备好数据)</p>    <p>四、数据库的优化:</p>    <p>数据库设计上面的重构</p>    <p>查询语句的优化</p>    <p>分库分表(数据太多的时候,可以分不同的表或者库)</p>    <p>五、服务器端和客户端的交互优化:</p>    <p>客户端尽量减少请求</p>    <p>服务端尽量做多的逻辑处理</p>    <p>服务器端和客户端采取推拉结合的方式(可以利用一些同步机制)</p>    <p>通信协议的优化。(减少报文的大小)</p>    <p>电量使用优化(尽量不要使用后台运行)</p>    <p>六、非技术性能优化</p>    <p>产品设计的逻辑性(产品的设计一定要符合逻辑,或者逻辑尽量简单,否则会让程序员抓狂,有时候用了好大力气,才可以完成一个小小的逻辑设计问题)</p>    <p>界面交互的规范(每个模块的界面的交互尽量统一,符合操作习惯)</p>    <p>代码规范(这个可以隐形带来app 性能的提高,比如 用if else 还是switch ,或者是用!还是 ==)</p>    <p>code review(坚持code Review 持续重构代码。减少代码的逻辑复杂度)</p>    <p>日常交流(经常分享一些代码,或者逻辑处理中的坑)</p>    <p>以上问题加参考答案,部分自己回答(群友回答)+网上博客参考,回答的不好勿喷!</p>    <p>仅供学习使用! 谢谢!</p>    <p> </p>    <p>来自:http://www.cocoachina.com/ios/20171129/21362.html</p>    <p> </p>    
 本文由用户 DominickLan 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1511943431096.html
IOS 面试题 iOS开发 移动开发