iOS 中级面试题
半年前在知乎浏览到一个帖子,是关于如何面试iOS工程师。由于当时公司正在招聘iOS工程师,自己也面试了不少同学,就饶有兴致的把自己的面试问题清单贴了出去,却意外的引来了不少关注。还有不少同学认真回答并将答案私信于我交流的,还有求隐藏关卡的:(。由于工作太忙,私信和评论后面都没怎么回复,这里一并做下总结回复:p
一份面试题的意义
我把收到的私信和一百多条评论都仔细读了遍,发现大家之所以会关注大致出于以下几个原因:
- 刚毕业的同学最近在找工作面试,想刷题增加面试通过率。
- 有一定的工作经历的同学,想测试下自己的iOS水平,看自己能卖多少钱。
- 本身iOS基础不错,抱着技多不压身心态补充知识的。
- 土豪老板就差一个程序员了,想找份带标准答案的面试题找真爱。
除了第三类同学心态正确外,其他的都高估面试题的作用了。面试题只是武功招式,知识体系才是内功心法。刚入门记住的都是招式,但招式何其多,面试的时候总会有遗漏和盲区,内功心法才是一通万通,能以不变应万变。这份面试题你答不全不能说明你iOS不及格,你全答对了你也不能上天。真正应该关注的是这份题背后所包含的理论知识体系。帖子里还有其他不少优质回答,涵盖候选人心态,习惯,基础知识,产品理解等各个方面,都值得一读。当然啦,既然出了题,就得有答案,就有它的目标群体。主要考察对象是从事iOS开发 1~3年的同学。不需要全部答对,能对一半以上问题侃侃而谈就不错了。
评论区百态
评论区有各路神仙吐槽,有的说难,有的说太容易,还有美工和安卓党出现。大家七嘴八舌的讨论意见很杂,但从中可以看出不少同学心态都不正确。技术这条路无穷无尽,广度和深度的拓展都需要长年累月的积累,不存在什么够用就好了,用的时候再查下,没必要了解这么深。技术人员的视野和耐力决定在这条路上你能够走多远。下面几类同学点名批评:
- 故作无知都机灵的。
- 觉得sqlite太重没必要用的。
- 说太容易不愿意答题的。
- 说都不会但不影响做项目的。
- 说圆角头像让美工切个图就搞定的。
- 说一半不会没必要深究的。
心态不及格。
合格的答案
出乎我意料之外的是有好几位同学都正二八经的答了题,还把答案私信了我。这里贴出其中一份答得还不错的,再后面是答主自己的答案。 过关回答
我的答案
我有过不少面试和被面试的经历,作为面试官出这份面试题从来就不是为了难倒面试者,而是为了多角度全面的了解面试者从而建立信任。面试的时候最担心的是冷场,面试题只不过个引子,我心底里最希望遇到的面试者是能够举一反三,除了回答问题本身之外,还能自信的旁征博引,深谈其背后原理或者相关的知识理论的。问题本身反而并不怎么重要。这份清单里的问题也并不难,这里我列下我的回答以及从我的角度所期望的答案。
什么是arc?(arc是为了解决什么问题诞生的?)
现在有不少程序员是直接从arc上手的,从没接触过mrc,对arc的理解仅仅停留在apple帮助管理内存的层面。这个问题真正想了解的是对内存管理的理解,retain release虽然不用写了,但arc下还是会有内存泄漏野指针crash的bug存在。如果能从retain count这种内存管理策略的角度去阐述arc诞生的意义就算答对了。如果还能扯下其他类型的策略,比如java里的mark and sweep,那就加分点赞。
请解释以下keywords的区别: assign vs weak, __block vs __weak
这道题属于基础语法题,可以网上搜到答案。不过真有不少同学不知道weak在对象释放后会置为nil。__block关键字的理解稍微难点,因为在arc和mrc下含义(对retain count的影响)完全不同。理解了这几个关键字就能应付使用block时引入retain cycle的风险了。这题还在内存管理的范畴之内。
使用atomic一定是线程安全的吗?
看这题的问法不用想答案肯定是NO。有些人说不出所以然,有些人知道通过property的方式使用才能保证安全,还有人知道这个用来做多线程安全会有性能损耗,更有出色的候选人能谈atomic,synchronized,NSLock,pthread mutex,OSSpinLock的差别。好奇宝宝点我。
描述一个你遇到过的retain cycle例子。(别撒谎,你肯定遇到过)
说没遇到过的我很难相信你有过成熟项目的经历。这题答不出了会扣很多很多分。用过block,写过delegate的肯定都踩过坑。
+(void)load; +(void)initialize;有什么用处?
这题属于runtime范畴,我遇到过能说出对runtime的理解却不知道这两个方法的候选人。所以答不出来也没关系,这属于细节知识点,是加分项,能答出两个message各在什么阶段接收就可以了。
为什么其他语言里叫函数调用, objective c里则是给对象发消息(或者谈下对runtime的理解)
这题考查的是objective c这门语言的dynamic特性,需要对比c++这类传统静态方法调用才能理解。最好能说出一个对象收到message之后的完整的流程是如何的。对runtime有完整理解的候选人还能说出oc的对象模型。
什么是method swizzling?
说了解runtime但没听过method swizzling是骗人的。这题很容易搜到答案。定位一些疑难杂症bug,hack老项目实现,阅读第三方源码都有机会接触到这个概念。
UIView和CALayer是啥关系?
能答出UIView是CALayer的delegate就及格了,能说出UIView主要处理事件,CALayer负责绘制就更好,再聊下二者在使用过程中对动画流畅性影响的注意点就superb。UI流畅性是个大话题,推荐看下这两篇文章。中餐,西餐。
如何高性能的给UIImageView加个圆角?(不准说layer.cornerRadius!)
这题讨论的最多,还有说美工切图就搞定的。答主在项目里做过圆角头像的处理,里面的坑还真不少。cornerRadius会导致offscreen drawing有性能问题,美工切图无法适用有背景图的场景,即使加上shouldRasterize也有cache实效问题。正确的做法是切换到工作线程利用CoreGraphic API生成一个offscreen UIImage,再切换到main thread赋值给UIImageView。这里还涉及到UIImageView复用,圆角头像cache缓存(不能每次都去绘制),新旧头像替换等等逻辑。还有其他的实现方式,但思路离不开工作线程与主线程切换。
使用drawRect有什么影响?(这个可深可浅,你至少得用过。。)
不少同学都用过drawRect或者看别人用过,但不知道这个api存在的含义。这不仅仅是另一种做UI的方式。drawRect会利用CPU生成offscreen bitmap,从而减轻GPU的绘制压力,用这种方式最UI可以将动画流畅性优化到极致,但缺点是绘制api复杂,offscreen cache增加内存开销。UI动画流畅性的优化主要平衡CPU和GPU的工作压力。推荐一篇文章:西餐
ASIHttpRequest或者SDWebImage里面给UIImageView加载图片的逻辑是什么样的?(把UIImageView放到UITableViewCell里面问更赞)
很多同学没有读源码的习惯,别人的轮子拿来只是用用却不知道真正的营养都在源代码里面。这两个经典的framework代码并不复杂,很值得一读。能对一个UIImageView怎么通过url展示一张图片有完整的理解。涉及到的知识点也非常多,UITableViewCell的复用,memory cache, disk cache, 多线程切换,甚至http协议本身都需要有一定的涉及。
麻烦你设计个简单的图片内存缓存器(移除策略是一定要说的)
内存缓存是个通用话题,每个平台都会涉及到。cache算法会影响到整个app的表现。候选人最好能谈下自己都了解哪些cache策略及各自的特点。常见的有FIFO,LRU,LRU-2,2Q等等。由于NSCache的缓存策略不透明,一些app开发者会选择自己做一套cache机制,其实并不难。
讲讲你用Instrument优化动画性能的经历吧(别问我什么是Instrument)
Apple的instrument为开发者提供了各种template去优化app性能和定位问题。很多公司都在赶feature,并没有充足的时间来做优化,导致不少开发者对instrument不怎么熟悉。但这里面其实涵盖了非常完整的计算机基础理论知识体系,memory,disk,network,thread,cpu,gpu等等,顺藤摸瓜去学习,是一笔巨大的知识财富。动画性能只是其中一个template,重点还是理解上面问题当中CPU GPU如何配合工作的知识。
loadView是干嘛用的?
不要就简单的告诉我没用过,至少问下我有什么用。。这里是apple给开发者自己设置custom view的位置。说UI熟悉的一定要知道。
viewWillLayoutSubView你总是知道的。。
controller layout触发的时候,开发者有机会去重新layout自己的各个subview。说UI熟悉的一定要知道。
GCD里面有哪几种Queue?你自己建立过串行queue吗?背后的线程模型是什么样的?
两种queue,串行和并行。main queue是串行,global queue是并行。有些开发者为了在工作线程串行的处理任务会自己建立一个serial queue。背后是苹果维护的线程池,各种queue要用线程都是这个池子里取的。GCD大家都用过,但很多关键的概念不少人都理解的模凌两可。串行,并行,同步,异步是GCD的核心概念。
用过coredata或者sqlite吗?读写是分线程的吗?遇到过死锁没?咋解决的?
没用过sqlite是说不过去的。用过CoreData的肯定有很多血泪史要说。多谢线程模型你肯定做过比较选择。死锁是啥肯定也是要知道的,没遇到过至少能举个简单的例子来说明。单个线程可以死锁(main thread里dispatch_sync到main queue),多个线程直接也可以死锁(A,B线程互相持有对方需要的资源且互相等待)。
http的post和get啥区别?(区别挺多的,麻烦多说点)
这个可以说很多。不希望听到的答案有
- 两个差不多,随便用一个。
- post比get安全(其实两个都不安全)
能说下两个http格式有什么不同,各自应用的场景就合格了。更多可以阅读下这个答案。
我知道你大学毕业过后就没接触过算法数据结构了,但是请你一定告诉我什么是Binary search tree? search的时间复杂度是多少?我很想知道!
很多人都很排斥数据结构和算法题,我个人意见是复杂的可以不知道,基础的一定要了解。时间复杂度是什么得知道,list,queue,stack,table,tree这些都要明白是啥。连hash表的概念都不知道怎么能保证在写代码的时候注意性能呢。
隐藏关卡😓
其实当初写这份答案的时候并没有准备什么隐藏关卡,只不过有一些从自己这些年项目经历里总结出来的有深度的知识点,感觉可以难倒不少同学:p。求隐藏关卡的同学真不少,近期我会再准备一份进阶版面试题,权当作隐藏关卡。面向的对象是3~5年iOS开发经验的同学。再次申明下:这只是一份面试题。
一份"有点难"的iOS面试题
之前一时兴致在知乎上出过一份iOS的中级面试题,引起一些关注,不少同学表示对”隐藏关卡“感兴趣。升级版iOS面试题来了,目测难倒90%iOS程序员,目测一大波程序员撸着袖子在靠近。每道题都不难,对知识广度有要求,请摸着良心回答,不要百度。
声明:这份面试题和iOS程序员本身技术水平没任何关联,无论你能否全部答出,都不要对自己产生任何正面或消极的评价,权当做闲暇之余的消遣。
1.NSString如何计算字符的个数?
2.PKI体系当中加密和签名有什么区别?
3.如何自己高效实现NSUserDefault?
4.解释下tcp的慢启动特性。
5.如何用HTTP实现长连接?
6.HTTP2.0针对同一个域名的多个请求,会建立多少个tcp连接?
7.数据库建表的时候索引有什么用?
8.Full Text Search为什么快?
9.iOS下如何实现指定线程数目的线程池?
10.介绍下iOS设备获取唯一设备号的历史变迁。
11.函数式编程当中的 first-class function是什么意思呢?
12.如何使用runtime hook一个class的某个方法,又如何hook某个instance的方法?
13.谈下Objective C都有哪些锁机制,你一般用哪个?
14.聊下HTTP post的body体使用form-urlencoded和multipart/form-data的区别。
15.让你设计一种机制检测UIViewController的内存泄漏,你会怎么做?
16.通过[UIImage imageNamed:]生成的对象什么时候被释放?
17.applicationWillEnterForeground和applicationDidBecomeActive都会在哪些场景下被调用?举例越多越好。
18.如何终止正在运行的工作线程?
19.穷举iOS下所有的本地持久化方案。
20.如果公司强制996,你有什么心里话要对老板说吗?
如果挑战的朋友数量多,后面我会抽空公布自己的答案:)。
欢迎关注公众号:
-
孤城
关于方案一可以看一下老谭的http://www.tanhao.me/code/151113.html/这篇
知乎链接:http://www.zhihu.com/question/19604641
1.什么是arc?(arc是为了解决什么问题诞生的?)
首先解释ARC: automatic reference counting自动引用计数。
ARC几个要点:
在对象被创建时 retain count +1,在对象被release时 retain count -1.当retain count 为0 时,销毁对象。
程序中加入autoreleasepool的对象会由系统自动加上autorelease方法,如果该对象引用计数为0,则销毁。
那么ARC是为了解决什么问题诞生的呢?这个得追溯到MRC手动内存管理时代说起。
MRC下内存管理的缺点:
1.当我们要释放一个堆内存时,首先要确定指向这个堆空间的指针都被release了。(避免提前释放)
2.释放指针指向的堆空间,首先要确定哪些指针指向同一个堆,这些指针只能释放一次。(MRC下即谁创建,谁释放,避免重复释放)
3.模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁去释放。
4.多线程操作时,不确定哪个线程最后使用完毕2.请解释以下keywords的区别: assign vs weak, __block vs __weak
assign适用于基本数据类型,weak是适用于NSObject对象,并且是一个弱引用。
assign其实也可以用来修饰对象,那么我们为什么不用它呢?因为被assign修饰的对象在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil。如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。
而weak修饰的对象在释放之后,指针地址会被置为nil。所以现在一般弱引用就是用weak。
首先__block是用来修饰一个变量,这个变量就可以在block中被修改(参考block实现原理)
__block:使用__block修饰的变量在block代码快中会被retain(ARC下,MRC下不会retain)
__weak:使用__weak修饰的变量不会在block代码块中被retain
同时,在ARC下,要避免block出现循环引用 __weak typedof(self)weakSelf = self;3.__block在arc和非arc下含义一样吗?
是不一样的。
在MRC中__block variable在block中使用是不會retain的
但是ARC中__block則是會Retain的。
取而代之的是用__weak或是__unsafe_unretained來更精確的描述weak reference的目的
其中前者只能在iOS5之後可以使用,但是比較好 (該物件release之後,此pointer會自動設成nil)
而後者是ARC的環境下為了相容4.x的解決方案。
所以上面的範例中__block MyClass* temp = …; // MRC環境下使用 __weak MyClass* temp = …; // ARC但只支援iOS5.0以上的版本 __unsafe_retained MyClass* temp = …; //ARC且可以相容4.x以後的版本
4.使用nonatomic一定是线程安全的吗?()
不是的。
atomic原子操作,系统会为setter方法加锁。 具体使用 @synchronized(self){//code }
nonatomic不会为setter方法加锁。
atomic:线程安全,需要消耗大量系统资源来为属性加锁
nonatomic:非线程安全,适合内存较小的移动设备5.描述一个你遇到过的retain cycle例子。
block中的循环引用:一个viewController
@property (nonatomic,strong)HttpRequestHandler * handler; @property (nonatomic,strong)NSData *data; _handler = [httpRequestHandler sharedManager]; [ downloadData:^(id responseData){ _data = responseData; }];
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
self 拥有_handler, _handler 拥有block, block拥有self(因为使用了self的_data属性,block会copy 一份self)
解决方法:__weak typedof(self)weakSelf = self [ downloadData:^(id responseData){ weakSelf.data = responseData; }];
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
6.+(void)load; +(void)initialize;有什么用处?
在Objective-C中,runtime会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。
共同点:两个方法都只会被调用一次。7.为什么其他语言里叫函数调用, objective c里则是给对象发消息(或者谈下对runtime的理解)
先来看看怎么理解发送消息的含义:
曾经觉得Objc特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把[receiver message]当成简单的方法调用,而无视了“发送消息”这句话的深刻含义。于是[receiver message]会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。
现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。
Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。
Objc Runtime使得C具有了面向对象能力,在程序运行时创建,检查,修改类、对象和它们的方法。可以使用runtime的一系列方法实现。
顺便附上OC中一个类的数据结构 /usr/include/objc/runtime.h
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,r untime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识 long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 #endif } OBJC2_UNAVAILABLE;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
OC中一个类的对象实例的数据结构(/usr/include/objc/objc.h):
typedef struct objc_class *Class; /// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
向object发送消息时,Runtime库会根据object的isa指针找到这个实例object所属于的类,然后在类的方法列表以及父类方法列表寻找对应的方法运行。id是一个objc_object结构类型的指针,这个类型的对象能够转换成任何一种对象。
然后再来看看消息发送的函数:objc_msgSend函数
在引言中已经对objc_msgSend进行了一点介绍,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数了。
检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
如果 cache 找不到就找一下方法分发表。
如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
如果还找不到就要开始进入动态方法解析了,后面会提到。后面还有:
动态方法解析resolveThisMethodDynamically
消息转发forwardingTargetForSelector详情可参考 http://www.jianshu.com/p/620022378e97
8.什么是method swizzling?
Method Swizzling 原理(方法搅拌?)
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
……
归根结底,都是偷换了selector的IMP,如下图所示:详情:http://blog.csdn.net/yiyaaixuexi/article/details/9374411
9.UIView和CALayer是啥关系?
1.UIView是iOS系统中界面元素的基础,所有的界面元素都继承自它。它本身完全是由CoreAnimation来实现的 (Mac下似乎不是这样)。它真正的绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理。 UIView本身,更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame,bounds等 等,实际上内部都是在访问它所包含的CALayer的相关属性。
2.UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的 类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示,例如通过
- (class) layerClass { return ([CAEAGLLayer class]); }
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
=使某个UIView的子类使用GL来进行绘制。
3.UIView的CALayer类似UIView的子View树形结构,也可以向它的layer上添加子layer,来完成某些特殊的表 示。例如下面的代码
grayCover = [[CALayer alloc] init]; grayCover.backgroundColor = [[[UIColor blackColor] colorWithAlphaComponent:0.2] CGColor]; [self.layer addSubLayer: grayCover];
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
会在目标View上敷上一层黑色的透明薄膜。
4.UIView的layer树形在系统内部,被系统维护着三份copy(这段理解有点吃不准)。
- 逻辑树,就是代码里可以操纵的,例如更改layer的属性等等就在这一份。
- 动画树,这是一个中间层,系统正在这一层上更改属性,进行各种渲染操作。
- 显示树,这棵树的内容是当前正被显示在屏幕上的内容。
这三棵树的逻辑结构都是一样的,区别只有各自的属性。
10. 如何高性能的给UIImageView加个圆角?(不准说layer.cornerRadius!)
我觉得应该是使用Quartz2D直接绘制图片,得把这个看看。
步骤:
a、创建目标大小(cropWidth,cropHeight)的画布。b、使用UIImage的drawInRect方法进行绘制的时候,指定rect为(-x,-y,width,height)。
c、从画布中得到裁剪后的图像。
- (UIImage*)cropImageWithRect:(CGRect)cropRect { CGRect drawRect = CGRectMake(-cropRect.origin.x , -cropRect.origin.y, self.size.width * self.scale, self.size.height * self.scale); UIGraphicsBeginImageContext(cropRect.size); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextClearRect(context, CGRectMake(0, 0, cropRect.size.width, cropRect.size.height)); [self drawInRect:drawRect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } @end
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
11. 使用drawRect有什么影响?(这个可深可浅,你至少得用过。。)
drawRect方法依赖Core Graphics框架来进行自定义的绘制,但这种方法主要的缺点就是它处理touch事件的方式:每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例。
12. ASIHttpRequest或者SDWebImage里面给UIImageView加载图片的逻辑是什么样的?
详见SDWebImage的实现流程 http://www.cnblogs.com/6duxz/p/4159572.html
13. 麻烦你设计个简单的图片内存缓存器(移除策略是一定要说的)
图片的内存缓存,可以考虑将图片数据保存到一个数据模型中。所以在程序运行时这个模型都存在内存中。
移除策略:释放数据模型对象。14. 讲讲你用Instrument优化动画性能的经历吧(别问我什么是Instrument)
可以参考iOS App性能优化
15. loadView是干嘛用的?
当你访问一个ViewController的view属性时,如果此时view的值是nil,那么,ViewController就会自动调用loadView这个方法。这个方法就会加载或者创建一个view对象,赋值给view属性。
loadView默认做的事情是:如果此ViewController存在一个对应的nib文件,那么就加载这个nib。否则,就创建一个UIView对象。如果你用Interface Builder来创建界面,那么不应该重载这个方法。
如果你想自己创建view对象,那么可以重载这个方法。此时你需要自己给view属性赋值。你自定义的方法不应该调用super。如果你需要对view做一些其他的定制操作,在viewDidLoad里面去做。
=========================================
根据上面的文档可以知道,有两种情况:
1、如果你用了nib文件,重载这个方法就没有太大意义。因为loadView的作用就是加载nib。如果你重载了这个方法不调用super,那么nib文件就不会被加载。如果调用了super,那么view已经加载完了,你需要做的其他事情在viewDidLoad里面做更合适。
2、如果你没有用nib,这个方法默认就是创建一个空的view对象。如果你想自己控制view对象的创建,例如创建一个特殊尺寸的view,那么可以重载这个方法,自己创建一个UIView对象,然后指定 self.view = myView; 但这种情况也没有必要调用super,因为反正你也不需要在super方法里面创建的view对象。如果调用了super,那么就是浪费了一些资源而已
参考:http://www.cnblogs.com/dyllove98/archive/2013/06/06/3123005.html16. viewWillLayoutSubView你总是知道的。
横竖屏切换的时候,系统会响应一些函数,其中 viewWillLayoutSubviews 和 viewDidLayoutSubviews。
//
- (void)viewWillLayoutSubviews { [self _shouldRotateToOrientation:(UIDeviceOrientation)[UIApplication sharedApplication].statusBarOrientation]; } -(void)_shouldRotateToOrientation:(UIDeviceOrientation)orientation { if (orientation == UIDeviceOrientationPortrait ||orientation == UIDeviceOrientationPortraitUpsideDown) { // 竖屏 } else { // 横屏 } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
通过上述一个函数就知道横竖屏切换的接口了。
注意:viewWillLayoutSubviews只能用在ViewController里面,在view里面没有响应。17. GCD里面有哪几种Queue?你自己建立过串行queue吗?背后的线程模型是什么样的?
1.主队列 dispatch_main_queue(); 串行 ,更新UI
2.全局队列 dispatch_global_queue(); 并行,四个优先级:background,low,default,high
3.自定义队列 dispatch_queue_t queue ; 可以自定义是并行:DISPATCH_QUEUE_CONCURRENT或者串行DISPATCH_QUEUE_SERIAL18. 用过coredata或者sqlite吗?读写是分线程的吗?遇到过死锁没?咋解决的?
19. http的post和get啥区别?(区别挺多的,麻烦多说点)
1.GET请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,参数之间以&相连,如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0%E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如:%E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。
POST把提交的数据则放置在是HTTP包的包体中。2.”GET方式提交的数据最多只能是1024字节,理论上POST没有限制,可传较大量的数据,IIS4中最大为80KB,IIS5中为100KB”??!
以上这句是我从其他文章转过来的,其实这样说是错误的,不准确的:
(1).首先是”GET方式提交的数据最多只能是1024字节”,因为GET是通过URL提交数据,那么GET可提交的数据量就跟URL的长度有直接关系了。而实际上,URL不存在参数上限的问题,HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。
注意这是限制是整个URL长度,而不仅仅是你的参数值数据长度。[见参考资料5]
(2).理论上讲,POST是没有大小限制的,HTTP协议规范也没有进行大小限制,说“POST数据量存在80K/100K的大小限制”是不准确的,POST数据是没有限制的,起限制作用的是服务器的处理程序的处理能力。
3.在ASP中,服务端获取GET请求参数用Request.QueryString,获取POST请求参数用Request.Form。在JSP中,用request.getParameter(\”XXXX\”)来获取,虽然jsp中也有request.getQueryString()方法,但使用起来比较麻烦,比如:传一个test.jsp?name=hyddd&password=hyddd,用request.getQueryString()得到的是:name=hyddd&password=hyddd。在PHP中,可以用GET和_POST分别获取GET和POST中的数据,而REQUEST则可以获取GET和POST两种请求中的数据。值得注意的是,JSP中使用request和PHP中使用_REQUEST都会有隐患,这个下次再写个文章总结。
4.POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存,(2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外,使用GET提交数据还可能会造成Cross-site request forgery攻击。
总结一下,Get是向服务器发索取数据的一种请求,而Post是向服务器提交数据的一种请求,在FORM(表单)中,Method默认为”GET”,实质上,GET和POST只是发送机制不同,并不是一个取一个发!
20. 我知道你大学毕业过后就没接触过算法数据结构了,但是请你一定告诉我什么是Binary search tree? search的时间复杂度是多少?
Binary search tree:二叉搜索树。
主要由四个方法:(用C语言实现或者Python)
1.search:时间复杂度为O(h),h为树的高度2.traversal:时间复杂度为O(n),n为树的总结点数。
3.insert:时间复杂度为O(h),h为树的高度。
4.delete:最坏情况下,时间复杂度为O(h)+指针的移动开销。
可以看到,二叉搜索树的dictionary operation的时间复杂度与树的高度h相关。所以需要尽可能的降低树的高度,由此引出平衡二叉树Balanced binary tree。它要求左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这样就可以将搜索树的高度尽量减小。常用算法有红黑树、AVL、Treap、伸展树等。
Written with StackEdit.
PerpendiculousProgramming, Personal Finance, and Personal musings
2009.09.23
@synchronized, NSLock, pthread, OSSpinLock showdown, done right
Somewhere out there on the internet, there’s a “showdown” between @synchronized, NSLock, pthread mutexes, and OSSpinLock. It aims to measure their performance relative to each other, but uses sloppy code to perform the measuring. As a result, while the performance ordering is correct (@synchronized is the slowest, OSSpinLock is the fastest), the relative cost is severely misrepresented. Herein I attempt to rectify that benchmark.
Locking is absolutely required for critical sections. These arise in multithreaded code, and sometimes their performance can have severe consequences in applications. The problem with the aforementioned benchmark is that it did a bunch of extraneous work while it was locking/unlocking. It was doing the same amount of extraneous work, so the relative order was correct (the fastest was still the fastest, the slowest still the slowest, etc), but it didn’t properly show just how much faster the fastest was.
In the benchmark, the author used autorelease pools, allocated objects, and then released them all. While locking. This is a pretty reasonable use-case, but by no means the only one. For most high-performance, multithreaded code, you’ll spend a _bunch_ of time trying to make the critical sections as small and fast as possible. Large, slow critical sections effectively undo the multithreading speed up by causing threads to block each other out unnecessarily. So when you’ve trimmed the critical sections down to the minimum, another sometimes-justified optimization is to optimize the amount of time spent locking/unlocking itself.
Just to make things exciting though, not all locking primitives are created equal. Two of the 4 mentioned have special properties that can affect how long they take, and how the operate under pressure. I’ll get to that towards the end.
First up, here’s my “no-nonsense” microbench code:
#import <Foundation/Foundation.h> #import <objc/runtime.h> #import <objc/message.h> #import <libkern/OSAtomic.h> #import <pthread.h> #define ITERATIONS (1024*1024*32) static unsigned long long disp=0, land=0; int main() { double then, now; unsigned int i, count; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; OSSpinLock spinlock = OS_SPINLOCK_INIT; NSAutoreleasePool *pool = [NSAutoreleasePool new]; NSLock *lock = [NSLock new]; then = CFAbsoluteTimeGetCurrent(); for(i=0;i<ITERATIONS;++i) { [lock lock]; [lock unlock]; } now = CFAbsoluteTimeGetCurrent(); printf("NSLock: %f sec\n", now-then); then = CFAbsoluteTimeGetCurrent(); IMP lockLock = [lock methodForSelector:@selector(lock)]; IMP unlockLock = [lock methodForSelector:@selector(unlock)]; for(i=0;i<ITERATIONS;++i) { lockLock(lock,@selector(lock)); unlockLock(lock,@selector(unlock)); } now = CFAbsoluteTimeGetCurrent(); printf("NSLock+IMP Cache: %f sec\n", now-then); then = CFAbsoluteTimeGetCurrent(); for(i=0;i<ITERATIONS;++i) { pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); } now = CFAbsoluteTimeGetCurrent(); printf("pthread_mutex: %f sec\n", now-then); then = CFAbsoluteTimeGetCurrent(); for(i=0;i<ITERATIONS;++i) { OSSpinLockLock(&spinlock); OSSpinLockUnlock(&spinlock); } now = CFAbsoluteTimeGetCurrent(); printf("OSSpinlock: %f sec\n", now-then); id obj = [NSObject new]; then = CFAbsoluteTimeGetCurrent(); for(i=0;i<ITERATIONS;++i) { @synchronized(obj) { } } now = CFAbsoluteTimeGetCurrent(); printf("@synchronized: %f sec\n", now-then); [pool release]; return 0; }
We do 5 tests: We test NSLock, NSLock with IMP caching, pthread mutexes, OSSpinLocks, and then finally @synchronized. We simply lock and unlock 33554432 times (that’s 1024*1024*32 for those keeping score at home ;), and see how long it takes. No allocation, no releases, no autorelease pools, nothing. Just pure lock/unlock goodness. I ran the test a few times, and averaged the results (so overall, the results are from something like 100 million lock/unlock cycles each)
- NSLock: 3.5175 sec
- NSLock+IMP Cache: 3.1165 sec
- Mutex: 1.5870 sec
- SpinLock: 1.0893
- @synchronized: 9.9488 sec
![Lock Performance Lock Performance](http://perpendiculo.us/wp-content/uploads/2009/09/LockPerformance.png)
Lock Performance
From the above graph, we can see a couple thing: First, @synchronized is _Really_ expensive — like, 3 times as expensive as anything else. We’ll get into why that is in a moment. Otherwise, we see that NSLock and NSLock+IMP Cache are pretty close — these are built on top of pthread mutexes, but we have to pay for the extra ObjC overhead. Then there’s Mutex (pthread mutexes) and SpinLock — these are pretty close, but even then SpinLock is almost 30% faster than Mutex. We’ll get into that one too. So from top to bottom we have almost an order of magnitude difference between the worst and best.
The nice part about these all is that they all take about the same amount of code — using NSLock takes as many lines as a pthread mutex, and the same number for a spinlock. @synchronized saves a line or two, but with a cost like that it quickly looks unappealing in all but the most trivial of cases.
So, what makes @sychronized and SpinLock so different from the others?
@synchronized is very heavy weight because it has to set up an exception handler, and it actually ends up taking a few internal locks on its way there. So instead of a simple cheap lock, you’re paying for a couple locks/unlocks just to acquire your measly lock. Those take time.
OSSpinLock, on the other hand, doesn’t even enter the kernel — it just keeps reloading the lock, hoping that it’s unlocked. This is terribly inefficient if locks are held for more than a few nanoseconds, but it saves a costly system call and a couple context switches. Pthread mutexes actually use an OSSpinLock first, to keep things running smoothly where there’s no contention. When there is, it resorts to heavier, kernel-level locking/tasking stuff.
So, if you’ve got hotly-contested locks, OSSpinLock probably isn’t for you (unless your critical sections are _Really_ _Fast_). Pthread mutexes are a tiny bit more expensive, but they avoid the power-wasting effects of OSSpinLock.
NSLock is a pretty wrapper on pthread mutexes. They don’t provide much else, so there’s not much point in using them over pthread mutexes.
Of course, standard optimization disclaimers apply: don’t do it until you’re sure you’ve chosen the correct algorithms, have profiled to find hotspots, and have found locking to be one of those hot items. Otherwise, you’re wasting your time on something that’s likely to provide minimal benefits.
6 Comments »
南来地,北往的,上班的,下岗的,走过路过不要错过!
======================个性签名=====================
之前认为Apple 的iOS 设计的要比 Android 稳定,我错了吗?
下载的许多客户端程序/游戏程序,经常会Crash,是程序写的不好(内存泄漏?刚启动也会吗?)还是iOS本身的不稳定!!!
如果在Android手机中可以简单联接到ddms,就可以查看系统log,很容易看到程序为什么出错,在iPhone中如何得知呢?试试Organizer吧,分析一下Device logs,也许有用.
This is very interesting and useful, thanks for the sample code too!
Comment by Zachary Howe — 2012.08.10 @ 12:48 pm
thank you very much, With your article, i finally konw the speed of different sync mechanism
Comment by maple — 2012.11.01 @ 8:05 am
nice article
Comment by Paul — 2012.11.03 @ 11:35 am
Interesting article,
I just thought that I let you know that I tried your code on an iPhone6 Plus (arm64, iOS9.3).
What is interesting is that NSLock is now faster than pthread_mutex (on iOS at least, it is still slower on Mac OS X). So aside from using spin locks, which have their own disadvantages and are only good for special cases as you pointed out, clean Objective-C code (using NSLock) is the fastest way to do it. (Not that I would have used the mutex before just for the relatively small impact on the speed in actual applications.)
I had problems with the NSLock+IMPCache method (it seams the IMP prototype has changed to take 0 parameters, but when I removed the parameters, it crashes at runtime), so I just removed it. I copied the code three times into the viewDidLoad function of a template single view app. I did it three times to be sure that the effect of the fresh start of the app doesn’t negatively influence the measurements.
Here is a typical output:
NSLock: 2.193462 sec
pthread_mutex: 2.603898 sec
OSSpinlock: 1.192254 sec
@synchronized: 8.019554 sec
NSLock: 2.171150 sec
pthread_mutex: 2.605145 sec
OSSpinlock: 1.193469 sec
@synchronized: 7.984741 sec
NSLock: 2.178009 sec
pthread_mutex: 2.604524 sec
OSSpinlock: 1.192347 sec
@synchronized: 7.991478 sec
Comment by Jochen — 2016.03.29 @ 6:00 am
[…] 这篇文章 […]
Pingback by 轻松理解多线程同步基础概念-IT技术 — 2016.05.30 @ 10:06 am
Note: You should absolutely never use spin locks. They’re unsafe because there’s no way to guarantee forward progress if there’s a priority inversion. Mutex is the way to go – if there’s a priority inversion, the kernel can temporarily bump the blocked thread’s priority so that things can move forward.
Comment by Adlai — 2016.07.05 @ 10:39 am
[OC]之 atomic 与 nonatomic的区别
下面我们就主要讲讲atomic 与 nonatomic:
在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不需要同步锁。
请注意!!!!尽管没有名为atomic的特质(如果某属性不具备nonatomic特质,那它就是“原子的”(atomic)),但是仍然可以在属性特质中写明这一点,编译器是不会报错的。
那么atomic与nonatomic的区别又是什么捏?
之前说过滴,具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。
也就是说,如果两个线程同时读取一个属性,那么不论何时,总能看到有效的属性值。
如果不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值的时候,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发证这种情况时,线程读取道德属性值肯能不对。
相信大家都遇到过上述那种情况吧。。。。
一般iOS程序中,所有属性都声明为nonatomic。这样做的原因是:
在iOS中使用同步锁的开销比较大, 这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”(thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才醒。
例如:一个线程在连续多次读取某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读取到不同的属性值。
因此,iOS程序一般都会使用nonatomic属性。但是在Mac OS X程序时, 使用atomic属性通常都不会有性能瓶颈
因为看评论有人问了,所以补充个问题,就是atomic一定是线程安全的么,回答是NO :
nonatomic的内存管理语义是非原子性的,非原子性的操作本来就是线程不安全,而atomic的操作是原子性的,但并不意味着他就是线程安全的,它会增加正确的几率,能够更好的避免线程错误,但仍旧是不安全的。
为了说atomic与nonatomic的本质区别其实也就是在setter方法上的操作不同:
nonatomic的实现:
- (void)setCurrentImage:(UIImage *)currentImage
{
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
// do something
}
}
- (UIImage *)currentImage
{
return _currentImage;
}
atomic的实现:
- (void)setCurrentImage:(UIImage *)currentImage
{
@synchronized(self) {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
// do something
}
}
}
- (UIImage *)currentImage
{
@synchronized(self) {
return _currentImage;
}
}
当使用atomic时,虽然对属性的读和写是原子性的,但是仍然可能出现线程错误:当线程A进行写操作,这时其他线程的读或者写操作会因为等该操作而等待。当A线程的写操作结束后,B线程进行写操作,然后当A线程需要读操作时,却获得了在B线程的值,这就破坏了线程安全,如果有线程C在A线程读操作之前release了该属性,那么还会导致程序崩溃。所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全。
其实无论是否是原子性的只是针对于getter和setter而言,比如用atomic去操作一个NSMutableArray ,如果一个线程循环读数据,一个线程循环写数据,肯定会产生内存问题,这个就跟getter和setter就木有关系了。
在我们所处的互联网世界中,HTTP协议算得上是使用最广泛的网络协议。最近http2.0的诞生使得它再次互联网技术圈关注的焦点。任何事物的消退和新生都有其背后推动的力量。对于HTTP来说,这力量复杂来说是各种技术细节的演进,简单来说是用户体验和感知的进化。用户总是希望网络上的信息能尽可能快的抵达眼球,越快越好,正是这种对“快”对追逐催生了今天的http2.0。
1. HTTP2.0的前世
http2.0的前世是http1.0和http1.1这两兄弟。虽然之前仅仅只有两个版本,但这两个版本所包含的协议规范之庞大,足以让任何一个有经验的工程师为之头疼。http1.0诞生于1996年,协议文档足足60页。之后第三年,http1.1也随之出生,协议文档膨胀到了176页。不过和我们手机端app升级不同的是,网络协议新版本并不会马上取代旧版本。实际上,1.0和1.1在之后很长的一段时间内一直并存,这是由于网络基础设施更新缓慢所决定的。今天的http2.0也是一样,新版协议再好也需要业界的产品锤炼,需要基础设施逐年累月的升级换代才能普及。
1.1 HTTP站在TCP之上
理解http协议之前一定要对TCP有一定基础的了解。HTTP是建立在TCP协议之上,TCP协议作为传输层协议其实离应用层并不远。HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性。比如TCP建立连接时三次握手有1.5个RTT(round-trip time)的延迟,为了避免每次请求的都经历握手带来的延迟,应用层会选择不同策略的http长链接方案。又比如TCP在建立连接的初期有慢启动(slow start)的特性,所以连接的重用总是比新建连接性能要好。
1.1 HTTP应用场景
http诞生之初主要是应用于web端内容获取,那时候内容还不像现在这样丰富,排版也没那么精美,用户交互的场景几乎没有。对于这种简单的获取网页内容的场景,http表现得还算不错。但随着互联网的发展和web2.0的诞生,更多的内容开始被展示(更多的图片文件),排版变得更精美(更多的css),更复杂的交互也被引入(更多的js)。用户打开一个网站首页所加载的数据总量和请求的个数也在不断增加。今天绝大部分的门户网站首页大小都会超过2M,请求数量可以多达100个。另一个广泛的应用是在移动互联网的客户端app,不同性质的app对http的使用差异很大。对于电商类app,加载首页的请求也可能多达10多个。对于微信这类IM,http请求可能仅限于语音和图片文件的下载,请求出现的频率并不算高。
1.2 因为延迟,所以慢
影响一个网络请求的因素主要有两个,带宽和延迟。今天的网络基础建设已经使得带宽得到极大的提升,大部分时候都是延迟在影响响应速度。http1.0被抱怨最多的就是连接无法复用,和head of line blocking这两个问题。理解这两个问题有一个十分重要的前提:客户端是依据域名来向服务器建立连接,一般PC端浏览器会针对单个域名的server同时建立6~8个连接,手机端的连接数则一般控制在4~6个。显然连接数并不是越多越好,资源开销和整体延迟都会随之增大。
连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。
head of line blocking会导致带宽无法被充分利用,以及后续健康请求被阻塞。假设有5个请求同时发出,如下图:
[图1]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/http1.0_blocking.png?raw=true" width="485">
对于http1.0的实现,在第一个请求没有收到回复之前,后续从应用层发出的请求只能排队,请求2,3,4,5只能等请求1的response回来之后才能逐个发出。网络通畅的时候性能影响不大,一旦请求1的request因为什么原因没有抵达服务器,或者response因为网络阻塞没有及时返回,影响的就是所有后续请求,问题就变得比较严重了。
1.3 解决连接无法复用
http1.0协议头里可以设置Connection:Keep-Alive。在header里设置Keep-Alive可以在一定时间内复用连接,具体复用时间的长短可以由服务器控制,一般在15s左右。到http1.1之后Connection的默认值就是Keep-Alive,如果要关闭连接复用需要显式的设置Connection:Close。一段时间内的连接复用对PC端浏览器的体验帮助很大,因为大部分的请求在集中在一小段时间以内。但对移动app来说,成效不大,app端的请求比较分散且时间跨度相对较大。所以移动端app一般会从应用层寻求其它解决方案,长连接方案或者伪长连接方案:
方案一:基于tcp的长链接
现在越来越多的移动端app都会建立一条自己的长链接通道,通道的实现是基于tcp协议。基于tcp的socket编程技术难度相对复杂很多,而且需要自己制定协议,但带来的回报也很大。信息的上报和推送变得更及时,在请求量爆发的时间点还能减轻服务器压力(http短连接模式会频繁的创建和销毁连接)。不止是IM app有这样的通道,像淘宝这类电商类app都有自己的专属长连接通道了。现在业界也有不少成熟的方案可供选择了,google的protobuf就是其中之一。
方案二:http long-polling
long-polling可以用下图表示:
[图2]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/http_polling.png?raw=true" width="356">
客户端在初始状态就会发送一个polling请求到服务器,服务器并不会马上返回业务数据,而是等待有新的业务数据产生的时候再返回。所以连接会一直被保持,一旦结束马上又会发起一个新的polling请求,如此反复,所以一直会有一个连接被保持。服务器有新的内容产生的时候,并不需要等待客户端建立一个新的连接。做法虽然简单,但有些难题需要攻克才能实现稳定可靠的业务框架:
和传统的http短链接相比,长连接会在用户增长的时候极大的增加服务器压力
移动端网络环境复杂,像wifi和4g的网络切换,进电梯导致网络临时断掉等,这些场景都需要考虑怎么重建健康的连接通道。
这种polling的方式稳定性并不好,需要做好数据可靠性的保证,比如重发和ack机制。
polling的response有可能会被中间代理cache住,要处理好业务数据的过期机制。
long-polling方式还有一些缺点是无法克服的,比如每次新的请求都会带上重复的header信息,还有数据通道是单向的,主动权掌握在server这边,客户端有新的业务请求的时候无法及时传送。
方案三:http streaming
http streaming流程大致如下:
[图3]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/http_streaming.png?raw=true" width="363">
同long-polling不同的是,server并不会结束初始的streaming请求,而是持续的通过这个通道返回最新的业务数据。显然这个数据通道也是单向的。streaming是通过在server response的头部里增加"Transfer Encoding: chunked"来告诉客户端后续还会有新的数据到来。除了和long-polling相同的难点之外,streaming还有几个缺陷:
有些代理服务器会等待服务器的response结束之后才会将结果推送到请求客户端。对于streaming这种永远不会结束的方式来说,客户端就会一直处于等待response的过程中。
业务数据无法按照请求来做分割,所以客户端没收到一块数据都需要自己做协议解析,也就是说要做自己的协议定制。
streaming不会产生重复的header数据。
方案四:web socket
WebSocket和传统的tcp socket连接相似,也是基于tcp协议,提供双向的数据通道。WebSocket优势在于提供了message的概念,比基于字节流的tcp socket使用更简单,同时又提供了传统的http所缺少的长连接功能。不过WebSocket相对较新,2010年才起草,并不是所有的浏览器都提供了支持。各大浏览器厂商最新的版本都提供了支持。
1.4 解决head of line blocking
Head of line blocking(以下简称为holb)是http2.0之前网络体验的最大祸源。正如图1中所示,健康的请求会被不健康的请求影响,而且这种体验的损耗受网络环境影响,出现随机且难以监控。为了解决holb带来的延迟,协议设计者设计了一种新的pipelining机制。
http pipelining
pipelining的流程图可以用下图表示:
[图4]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/pipelining.png?raw=true" width="375">
和图一相比最大的差别是,请求2,3,4,5不用等请求1的response返回之后才发出,而是几乎在同一时间把request发向了服务器。2,3,4,5及所有后续共用该连接的请求节约了等待的时间,极大的降低了整体延迟。下图可以清晰的看出这种新机制对延迟的改变:
[图5]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/http_pipelining_performance.png?raw=true" width="625">
不过pipelining并不是救世主,它也存在不少缺陷:
pipelining只能适用于http1.1,一般来说,支持http1.1的server都要求支持pipelining。
只有幂等的请求(GET,HEAD)能使用pipelining,非幂等请求比如POST不能使用,因为请求之间可能会存在先后依赖关系。
head of line blocking并没有完全得到解决,server的response还是要求依次返回,遵循FIFO(first in first out)原则。也就是说如果请求1的response没有回来,2,3,4,5的response也不会被送回来。
绝大部分的http代理服务器不支持pipelining。
和不支持pipelining的老服务器协商有问题。
可能会导致新的Front of queue blocking问题。
正是因为有这么多的问题,各大浏览器厂商要么是根本就不支持pipelining,要么就是默认关掉了pipelining机制,而且启用的条件十分苛刻。可以参考chrome对于pipeling的问题描述。
1.5 其它奇技淫巧
为了解决延迟带来的苦恼,永远都会有聪明的探索者找出新的捷径来。互联网的蓬勃兴盛催生出了各种新奇技巧,我们来依次看下这些“捷径”及各自的优缺点。
Spriting(图片合并)
Spriting指的是将多个小图片合并到一张大的图片里,这样多个小的请求就被合并成了一个大的图片请求,然后再利用js或者css文件来取出其中的小张图片使用。好处显而易见,请求数减少,延迟自然低。坏处是文件的粒度变大了,有时候我们可能只需要其中一张小图,却不得不下载整张大图,cache处理也变得麻烦,在只有一张小图过期的情况下,为了获得最新的版本,不得不从服务器下载完整的大图,即使其它的小图都没有过期,显然浪费了流量。
Inlining(内容内嵌)
Inlining的思考角度和spriting类似,是将额外的数据请求通过base64编码之后内嵌到一个总的文件当中。比如一个网页有一张背景图,我们可以通过如下代码嵌入:
background: url(data:image/png;base64,<data>)
data部分是base64编码之后的字节码,这样也避免了一次多余的http请求。但这种做法也有着和spriting相同的问题,资源文件被绑定到了其它文件,粒度变得难以控制。
Concatenation(文件合并)
Concatenation主要是针对js这类文件,现在前端开发交互越来越多,零散的js文件也在变多。将多个js文件合并到一个大的文件里在做一些压缩处理也可以减小延迟和传输的数据量。但同样也面临着粒度变大的问题,一个小的js代码改动会导致整个js文件被下载。
Domain Sharding(域名分片)
前面我提到过很重要的一点,浏览器或者客户端是根据domain(域名)来建立连接的。比如针对www.example.com只允许同时建立2个连接,但mobile.example.com被认为是另一个域名,可以再建立两个新的连接。依次类推,如果我再多建立几个sub domain(子域名),那么同时可以建立的http请求就会更多,这就是Domain Sharding了。连接数变多之后,受限制的请求就不需要等待前面的请求完成才能发出了。这个技巧被大量的使用,一个颇具规模的网页请求数可以超过100,使用domain sharding之后同时建立的连接数可以多到50个甚至更多。
这么做当然增加了系统资源的消耗,但现在硬件资源升级非常之快,和用户宝贵的等待时机相比起来实在微不足道。
domain sharding还有一大好处,对于资源文件来说一般是不需要cookie的,将这些不同的静态资源文件分散在不同的域名服务器上,可以减小请求的size。
不过domain sharding只有在请求数非常之多的场景下才有明显的效果。而且请求数也不是越多越好,资源消耗是一方面,另一点是由于tcp的slow start会导致每个请求在初期都会经历slow start,还有tcp 三次握手,DNS查询的延迟。这一部分带来的时间损耗和请求排队同样重要,到底怎么去平衡这二者就需要取一个可靠的连接数中间值,这个值的最终确定要通过反复的测试。移动端浏览器场景建议不要使用domain sharding,具体细节参考这篇文章。
2. 开拓者SPDY
http1.0和1.1虽然存在这么多问题,业界也想出了各种优化的手段,但这些方法手段都是在尝试绕开协议本身的缺陷,都有种隔靴搔痒,治标不治本的感觉。直到2012年google如一声惊雷提出了SPDY的方案,大家才开始从正面看待和解决老版本http协议本身的问题,这也直接加速了http2.0的诞生。实际上,http2.0是以SPDY为原型进行讨论和标准化的。为了给http2.0让路,google已决定在2016年不再继续支持SPDY开发,但在http2.0出生之前,SPDY已经有了相当规模的应用,作为一个过渡方案恐怕在还将一段时间内继续存在。现在不少app客户端和server都已经使用了SPDY来提升体验,http2.0在老的设备和系统上还无法使用(iOS系统只有在iOS9+上才支持),所以可以预见未来几年spdy将和http2.0共同服务的情况。
2.1 SPDY的目标
SPDY的目标在一开始就是瞄准http1.x的痛点,即延迟和安全性。我们上面通篇都在讨论延迟,至于安全性,由于http是明文协议,其安全性也一直被业界诟病,不过这是另一个大的话题。如果以降低延迟为目标,应用层的http和传输层的tcp都是都有调整的空间,不过tcp作为更底层协议存在已达数十年之久,其实现已深植全球的网络基础设施当中,如果要动必然伤经动骨,业界响应度必然不高,所以SPDY的手术刀对准的是http。
降低延迟,客户端的单连接单请求,server的FIFO响应队列都是延迟的大头。
http最初设计都是客户端发起请求,然后server响应,server无法主动push内容到客户端。
压缩http header,http1.x的header越来越膨胀,cookie和user agent很容易让header的size增至1kb大小,甚至更多。而且由于http的无状态特性,header必须每次request都重复携带,很浪费流量。
为了增加业界响应的可能性,聪明的google一开始就避开了从传输层动手,而且打算利用开源社区的力量以提高扩散的力度,对于协议使用者来说,也只需要在请求的header里设置user agent,然后在server端做好支持即可,极大的降低了部署的难度。SPDY的设计如下:
[图6]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/spdy.png?raw=true" width="215">
SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将http1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。SPDY的功能可以分为基础功能和高级功能两部分,基础功能默认启用,高级功能需要手动启用。
SPDY基础功能
多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了http1.x holb(head of line blocking)的问题,降低了延迟同时提高了带宽的利用率。
请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
header压缩。前面提到过几次http1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。SPDY对header的压缩率可以达到80%以上,低带宽环境下效果很大。
SPDY高级功能
server推送(server push)。http1.x只能由客户端发起请求,然后服务器被动的发送response。开启server push之后,server通过X-Associated-Content header(X-开头的header都属于非标准的,自定义header)告知客户端会有新的内容推送过来。在用户第一次打开网站首页的时候,server将资源主动推送过来可以极大的提升用户体验。
server暗示(server hint)。和server push不同的是,server hint并不会主动推送内容,只是告诉有新的内容产生,内容的下载还是需要客户端主动发起请求。server hint通过X-Subresources header来通知,一般应用场景是客户端需要先查询server状态,然后再下载资源,可以节约一次查询请求。
2.2 SPDY的成绩
SPDY的成绩可以用google官方的一个数字来说明:页面加载时间相比于http1.x减少了64%。而且各大浏览器厂商在SPDY诞生之后的1年多里都陆续支持了SPDY,不少大厂app和server端框架也都将SPDY应用到了线上的产品当中。
google的官网也给出了他们自己做的一份测试数据。测试对象是25个访问量排名靠前的网站首页,家用网络%1的丢包率,每个网站测试10次取平均值。结果如下:
[图7]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/spdy_result.png?raw=true" width="860">
不开启ssl的时候提升在 27% - 60%,开启之后为39% - 55%。这份测试结果有两点值得特别注意:
连接数的选择
连接到底是基于域名来建立,还是不做区分所有子域名都共享一个连接,这个策略选择上值得商榷。google的测试结果测试了两种方案,看结果似乎是单一连接性能高于多域名连接方式。之所以出现这种情况是由于网页所有的资源请求并不是同一时间发出,后续发出的子域名请求如果能复用之前的tcp连接当然性能更好。实际应用场景下应该也是单连接共享模式表现好。
带宽的影响
测试基于两种带宽环境,一慢一快。网速快的环境下对减小延迟的提升更大,单连接模式下可以提升至60%。原因也比较简单,带宽越大,复用连接的请求完成越快,由于三次握手和慢启动导致的延迟损耗就变得更明显。
出了连接模式和带宽之外,丢包率和RTT也是需要测试的参数。SPDY对header的压缩有80%以上,整体包大小能减少大概40%,发送的包越少,自然受丢包率影响也就越小,所以丢包率大的恶劣环境下SPDY反而更能提升体验。下图是受丢包率影响的测试结果,丢包率超过2.5%之后就没有提升了:
[图8]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/spdy_loss.png?raw=true" width="586">
RTT越大,延迟会越大,在高RTT的场景下,由于SPDY的request是并发进行的,所有对包的利用率更高,反而能更明显的减小总体延迟。测试结果如下:
[图9]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/spdy_rtt.png?raw=true" width="585">
SPDY从2012年诞生到2016停止维护,时间跨度对于网络协议来说其实非常之短。如果HTTP2.0没有出来,google或许能收集到更多业界产品的真实反馈和数据,毕竟google自己的测试环境相对简单。但SPDY也完成了自己的使命,作为一贯扮演拓荒者角色的google应该也早就预见了这样的结局。SPDY对产品网络体验的提升到底如何,恐怕只有各大厂产品经理才清楚了。
3. 救世主HTTP2.0
SPDY的诞生和表现说明了两件事情:一是在现有互联网设施基础和http协议广泛使用的前提下,是可以通过修改协议层来优化http1.x的。二是针对http1.x的修改确实效果明显而且业界反馈很好。正是这两点让IETF(Internet Enginerring Task Force)开始正式考虑制定HTTP2.0的计划,最后决定以SPDY/3为蓝图起草HTTP2.0,SPDY的部分设计人员也被邀请参与了HTTP2.0的设计。
3.1 HTTP2.0需要考虑的问题
HTTP2.0与SPDY的起点不同,SPDY可以说是google的“玩具”,最早出现在自家的chrome浏览器和server上,好不好玩以及别人会不会跟着一起玩对google来说无关痛痒。但HTTP2.0作为业界标准还没出生就是众人瞩目的焦点,一开始如果有什么瑕疵或者不兼容的问题影响可能又是数十年之久,所以考虑的问题和角度要非常之广。我们来看下HTTP2.0一些重要的设计前提:
客户端向server发送request这种基本模型不会变。
老的scheme不会变,使用http://和https://的服务和应用不会要做任何更改,不会有http2://。
使用http1.x的客户端和服务器可以无缝的通过代理方式转接到http2.0上。
不识别http2.0的代理服务器可以将请求降级到http1.x。
因为客户端和server之间在确立使用http1.x还是http2.0之前,必须要要确认对方是否支持http2.0,所以这里必须要有个协商的过程。最简单的协商也要有一问一答,客户端问server答,即使这种最简单的方式也多了一个RTT的延迟,我们之所以要修改http1.x就是为了降低延迟,显然这个RTT我们是无法接受的。google制定SPDY的时候也遇到了这个问题,他们的办法是强制SPDY走https,在SSL层完成这个协商过程。ssl层的协商在http协议通信之前,所以是最适合的载体。google为此做了一个tls的拓展,叫NPN(Next Protocol Negotiation),从名字上也可以看出,这个拓展主要目的就是为了协商下一个要使用的协议。HTTP2.0虽然也采用了相同的方式,不过HTTP2.0经过激烈的讨论,最终还是没有强制HTTP2.0要走ssl层,大部分浏览器厂商(除了IE)却只实现了基于https的2.0协议。HTTP2.0没有使用NPN,而是另一个tls的拓展叫ALPN(Application Layer Protocol Negotiation)。SPDY也打算从NPN迁移到ALPN了。
各浏览器(除了IE)之所以只实现了基于SSL的HTTP2.0,另一个原因是走SSL请求的成功率会更高,被SSL封装的request不会被监听和修改,这样网络中间的网络设备就无法基于http1.x的认知去干涉修改request,http2.0的request如果被意外的修改,请求的成功率自然会下降。
HTTP2.0协议没有强制使用SSL是因为听到了很多的反对声音,毕竟https和http相比,在不优化的前提下性能差了不少,要把https优化到几乎不增加延迟的程度又需要花费不少力气。IETF面对这种两难的处境做了妥协,但大部分浏览器厂商(除了IE)并不买帐,他们只认https2.0。对于app开发者来说,他们可以坚持使用没有ssl的http2.0,不过要承担一个多余的RTT延迟和请求可能被破坏的代价。
3.1 HTTP2.0主要改动
HTTP2.0作为新版协议,改动细节必然很多,不过对应用开发者和服务提供商来说,影响较大的就几点。
新的二进制格式(Binary Format)
http1.x诞生的时候是明文协议,其格式由三部分组成:start line(request line或者status line),header,body。要识别这3部分就要做协议解析,http1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑http2.0的协议解析决定采用二进制格式,实现方便且健壮。
有人可能会觉得基于文本的http调试方便很多,像firebug,chrome,charles等不少工具都可以即时调试修改请求。实际上现在很多请求都是走https了,要调试https请求必须有私钥才行。http2.0的绝大部分request应该都是走https,所以调试方便无法作为一个有力的考虑因素了。curl,tcpdump,wireshark这些工具会更适合http2.0的调试。
http2.0用binary格式定义了一个一个的frame,和http1.x的格式对比如下图:
[图10]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/http1xvs2.png?raw=true" width="455">
http2.0的格式定义更接近tcp层的方式,这张二机制的方式十分高效且精简。length定义了整个frame的开始到结束,type定义frame的类型(一共10种),flags用bit位定义一些重要的参数,stream id用作流控制,剩下的payload就是request的正文了。
虽然看上去协议的格式和http1.x完全不同了,实际上http2.0并没有改变http1.x的语义,只是把原来http1.x的header和body部分用frame重新封装了一层而已。调试的时候浏览器甚至会把http2.0的frame自动还原成http1.x的格式。具体的协议关系可以用下图表示:
[图11]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/http2_frame.png?raw=true" width="455">
连接共享
http2.0要解决的一大难题就是多路复用(MultiPlexing),即连接共享。上面协议解析中提到的stream id就是用作连接共享机制的。一个request对应一个stream并分配一个id,这样一个连接上可以有多个stream,每个stream的frame可以随机的混杂在一起,接收方可以根据stream id将frame再归属到各自不同的request里面。
前面还提到过连接共享之后,需要优先级和请求依赖的机制配合才能解决关键请求被阻塞的问题。http2.0里的每个stream都可以设置又优先级(Priority)和依赖(Dependency)。优先级高的stream会被server优先处理和返回给客户端,stream还可以依赖其它的sub streams。优先级和依赖都是可以动态调整的。动态调整在有些场景下很有用,假想用户在用你的app浏览商品的时候,快速的滑动到了商品列表的底部,但前面的请求先发出,如果不把后面的请求优先级设高,用户当前浏览的图片要到最后才能下载完成,显然体验没有设置优先级好。同理依赖在有些场景下也有妙用。
header压缩
前面提到过http1.x的header由于cookie和user agent很容易膨胀,而且每次都要重复发送。http2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量从而降低延迟。
这里普及一个小知识点。现在大家都知道tcp有slow start的特性,三次握手之后开始发送tcp segment,第一次能发送的没有被ack的segment数量是由initial tcp window大小决定的。这个initial tcp window根据平台的实现会有差异,但一般是2个segment或者是4k的大小(一个segment大概是1500个字节),也就是说当你发送的包大小超过这个值的时候,要等前面的包被ack之后才能发送后续的包,显然这种情况下延迟更高。intial window也并不是越大越好,太大会导致网络节点的阻塞,丢包率就会增加,具体细节可以参考IETF这篇文章。http的header现在膨胀到有可能会超过这个intial window的值了,所以更显得压缩header的重要性。
压缩算法的选择
SPDY/2使用的是gzip压缩算法,但后来出现的两种攻击方式BREACH和CRIME使得即使走ssl的SPDY也可以被破解内容,最后综合考虑采用的是一种叫HPACK的压缩算法。这两个漏洞和相关算法可以点击链接查看更多的细节,不过这种漏洞主要存在于浏览器端,因为需要通过javascript来注入内容并观察payload的变化。
重置连接表现更好
很多app客户端都有取消图片下载的功能场景,对于http1.x来说,是通过设置tcp segment里的reset flag来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。http2.0引入RST_STREAM类型的frame,可以在不断开连接的前提下取消某个request的stream,表现更好。
Server Push
Server Push的功能前面已经提到过,http2.0能通过push的方式将客户端需要的内容预先推送过去,所以也叫“cache push”。另外有一点值得注意的是,客户端如果退出某个业务场景,出于流量或者其它因素需要取消server push,也可以通过发送RST_STREAM类型的frame来做到。
流量控制(Flow Control)
TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。
Nagle Algorithm vs TCP Delayed Ack
tcp协议优化的一个经典场景是:Nagle算法和Berkeley的delayed ack算法的对立。http2.0并没有对tcp层做任何修改,所以这种对立导致的高延迟问题依然存在。要么通过TCP_NODELAY禁用Nagle算法,要么通过TCP_QUICKACK禁用delayed ack算法。貌似http2.0官方建议是设置TCP_NODELAY。
更安全的SSL
HTTP2.0使用了tls的拓展ALPN来做协议升级,除此之外加密这块还有一个改动,HTTP2.0对tls的安全性做了近一步加强,通过黑名单机制禁用了几百种不再安全的加密算法,一些加密算法可能还在被继续使用。如果在ssl协商过程当中,客户端和server的cipher suite没有交集,直接就会导致协商失败,从而请求失败。在server端部署http2.0的时候要特别注意这一点。
3.2 HTTP2.0里的负能量
SPDY和HTTP2.0之间的暧昧关系,以及google作为SPDY的创造者,这两点很容易让阴谋论者怀疑google是否会成为协议的最终收益方。这其实是废话,google当然会受益,任何新协议使用者都会从中受益,至于谁吃肉,谁喝汤看的是自己的本事。从整个协议的变迁史也可以粗略看出,新协议的诞生完全是针对业界现存问题对症下药,并没有google业务相关的痕迹存在,google至始至终只扮演了一个角色:you can you up。
HTTP2.0不会是万金油,但抹了也不会有副作用。HTTP2.0最大的亮点在于多路复用,而多路复用的好处只有在http请求量大的场景下才明显,所以有人会觉得只适用于浏览器浏览大型站点的时候。这么说其实没错,但http2.0的好处不仅仅是multiplexing,请求压缩,优先级控制,server push等等都是亮点。对于内容型移动端app来说,比如淘宝app,http请求量大,多路复用还是能产生明显的体验提升。多路复用对延迟的改变可以参考下这个测试网址。
HTTP2.0对于ssl的依赖使得有些开发者望而生畏。不少开发者对ssl还停留在高延迟,CPU性能损耗,配置麻烦的印象中。其实ssl于http结合对性能的影响已经可以优化到忽略的程度了,网上也有不少文章可以参考。HTTP2.0也可以不走ssl,有些场景确实可能不适合https,比如对代理服务器的cache依赖,对于内容安全性不敏感的get请求可以通过代理服务器缓存来优化体验。
3.3 HTTP2.0的现状
HTTP2.0作为新版本的网络协议肯定需要一段时间去普及,但HTTP本身属于应用层协议,和当年的网络层协议IPV6不同,离底层协议越远,对网络基础硬件设施的影响就越小。HTTP2.0甚至还特意的考虑了与HTTP1.x的兼容问题,只是在HTTP1.x的下面做了一层framing layer,更使得其普及的阻力变小。所以不出意外,HTTP2.0的普及速度可能会远超大部分人的预期。
Firefox 2015年在其浏览器流量中检测到,有13%的http流量已经使用了http2.0,27%的https也是http2.0,而且还处于持续的增长当中。一般用户察觉不到是否使用了http2.0,不过可以装这样一个插件,安装之后如果网站是http2.0的,在地址栏的最右边会有个闪电图标。还可以使用这个网站来测试。对于开发者来说,可以通过Web Developer的Network来查看协议细节,如下图:
[图12]
<img src="https://github.com/music4kid/music4kid.github.io/blob/master/images/firefox_http2.png?raw=true" width="552">
其中Version:HTTP/2.0已经很明确表明协议类型,Firefox还在header里面插入了X-Firefox-Spdy:“h2”,也可以看出是否使用http2.0。
Chrome在2015年检测到的http2.0流量大概有18%。不过这个数字本来会更高,因为Chrome现在很大一部分流量都在试验QUIC(google正在开辟的另一块疆土)。Chrome上也可以使用类似的插件来判断网站是否是使用http2.0。
4. 移动端HTTP现状
4.1 iOS下http现状
iOS系统是从iOS8开始才通过NSURLSession来支持SPDY的,iOS9+开始自动支持http2.0。实际上apple对http2.0非常有信心,推广力度也很大。新版本ATS机制默认使用https来进行网络传输。APN(Apple Push Notifiction)在iOS9上也已经是通过http2.0来实现的了。iOS9 sdk里的NSURLSession默认使用http2.0,而且对开发者来说是完全透明的,甚至没有api来知道到底是用的哪个版本的http协议。
对于开发者来说到底怎么去配置最佳的http使用方案呢?在我看来,因app而异,主要从两方面来考虑:一是app本身http流量是否大而且密集,二是开发团队本身的技术条件。http2.0的部署相对容易很多,客户端开发者甚至不用做什么改动,只需要使用iOS9的SDK编译即可,但缺点是http2.0只能适用于iOS9的设备。SPDY的部署相对麻烦一些,但优点是可以兼顾iOS6+的设备。iOS端的SPDY可以使用twitter开发的CocoaSPDY方案,但有一点需要特别处理:
由于苹果的TLS实现不支持NPN,所以通过NPN协商使用SPDY就无法通过默认443端口来实现。有两种做法,一是客户端和server同时约定好使用另一个端口号来做NPN协商,二是server这边通过request header智能判断客户端是否支持SPDY而越过NPN协商过程。第一种方法会简单一点,不过需要从框架层将所有的http请求都map到另一个port,url mapping可以参考我之前的一篇文章。twitter自己的网站twitter.com使用的是第二种方法。
浏览器端(比如Chrome),server端(比如nginx)都陆续打算放弃支持spdy了,毕竟google官方都宣布要停止维护了。spdy会是一个过渡方案,会随着iOS9的普及会逐步消失,所以这部分的技术投入需要开发团队自己去衡量。
4.2 Android下http现状
android和iOS情况类似,http2.0只能在新系统下支持,spdy作为过渡方案仍然有存在的必要。
对于使用webview的app来说,需要基于chrome内核的webview才能支持spdy和http2.0,而android系统的webview是从android4.4(KitKat)才改成基于chrome内核的。
对于使用native api调用的http请求来说,okhttp是同时支持spdy和http2.0的可行方案。如果使用ALPN,okhttp要求android系统5.0+(实际上,android4.4上就有了ALPN的实现,不过有bug,知道5.0才正式修复),如果使用NPN,可以从android4.0+开始支持,不过NPN也是属于将要被淘汰的协议。
结束语
以上是HTTP从1.x到SPDY,再到HTTP2.0的一些主要变迁技术点。HTTP2.0正处于逐步应用到线上产品和服务的阶段,可以预见未来会有不少新的坑产生和与之对应的优化技巧,HTTP1.x和SPDY也将在一段时间内继续发挥余热。作为工程师,需要了解这些协议背后的技术细节,才能打造高性能的网络框架,从而提升我们的产品体验。
参考链接:
http://http2-explained.haxx.se/content/en/part5.html
https://www.chromium.org/spdy/spdy-whitepaper
你可能感兴趣的文章
讨论区
图片都没有展示出来,楼主修复一下呀
碧青_Kwok · 2月25日
**bold**
_italic_
[link](http://example.com)
> 引用
`code`
- 列表
。同时,被你 @ 的用户也会收到通知
91 声望
发表于于专栏
Peak技术分享
分享移动平台(iOS&Android)开发的技术心得。
1 人关注
相关收藏夹
11 个条目 | 2 人关注
2016年3月iOS面试总结
今年3月中下旬因为个人原因,换了一份工作,期间面试了有4,5家,基本都是D轮或者上市公司,也从他们的面试笔试中看到了自己的一些不足,于是就想写出来和大家分享一下,如果能帮到正在面试的同学更好。从面试题中,其实可以看到一些行业的发展,以及总体人才需求是怎样的了。
一.笔试题
笔试基本都有一两道基础题,比如说UITableView的重用机制,ARC的基本原理,如何避免retain cycle,谈谈对MVC的理解,iOS内存管理机制。这些大家应该都很清楚了。笔试的内容有几种有选择题,问答题,难一点的就是多选题了。我面试了一家就是给了10道多选题,多选,少选,错选都不行,当时做完以后就感觉不是很好,有些题目题干就是一下哪些是对的,然后ABCD依次给4个不同的概念,这种一道题相当于考了4个点。总之遇到这种“恶心”的多选题也不要太慌,静下心来一一甄别应该能拿到不错的成绩。
接下来我说几个我当时答的不怎么好的题目,我当时记了一下,和大家分享一下。
1.进程和线程的区别和联系
这个其实是操作系统的问题,当时一下子把我问的懵了,后来仔细回想了一下,加上自己的理解就答了,下面说说稍微完整的答案,大家可以准备准备,再问这种问题就可以完美作答了。
2.并行和并发的区别
3.谈谈你对Block和delegate的理解
我当时是这么答的,delegate的回调更多的面向过程,而block则是面向结果的。如果你需要得到一条多步进程的通知,你应该使用delegation。而当你只是希望得到你请求的信息(或者获取信息时的错误提示),你应该使用block。(如果你结合之前的3个结论,你会发现delegate可以在所有事件中维持state,而多个独立的block却不能)
4.谈谈instancetype和id的异同
1、相同点
都可以作为方法的返回类型
2、不同点
①instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象;②instancetype只能作为返回值,不能像id那样作为参数
5.category中能不能使用声明属性?为什么?如果能,怎么实现?
这种问题一问,我当时就感觉肯定能实现的,但是实在不知道怎么做,后来回来查了一下,才知道是用到了Runtime的知识了。贴一下答案
给分类(Category)添加属性
利用Runtime实现getter/setter 方法
@interface ClassName (CategoryName) @property (nonatomic, strong) NSString *str; @end //实现文件 #import "ClassName + CategoryName.h" #import <objc/runtime.h> static void *strKey = &strKey; @implementation ClassName (CategoryName) -(void)setStr:(NSString *)str { objc_setAssociatedObject(self, & strKey, str, OBJC_ASSOCIATION_COPY); } -(NSString *)str { return objc_getAssociatedObject(self, &strKey); } @end
6.isKindOfClass和isMemberOfClass的区别
这个题目简单,但是就是当时紧张的情况下,别答反了。
isKindOfClass来确定一个对象是否是一个类的成员,或者是派生自该类的成员
isMemberOfClass只能确定一个对象是否是当前类的成员
7.block里面的如何防止retain cycle
使用弱引用打断block里面的retain cycle
MRC中 _block 是不会引起retain;但在ARC中 _block 则会引起retain。ARC中应该使用_weak 或__unsafe_unretained弱引用
8.iOS多线程有哪几种实现方法?GCD中有哪些队列?分别是并行还是串行?
iOS中多线程编程工具主要3有:
1.NSThread
2.NSOperation
3.GCD
dispatch queue分为下面3种:而系统默认就有一串行队列main_queue和并行队列global_queue:
GCD中有三种队列类型:
The main queue: 与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。
Global queues: 全局队列是并发队列,并由整个进程共享。进程中存在三个全局队列:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入优先级来访问队列。
用户队列: 用户队列 (GCD并不这样称呼这种队列, 但是没有一个特定的名字来形容这种队列,所以我们称其为用户队列) 是用函数 dispatch_queue_create
创建的队列. 这些队列是串行的。正因为如此,它们可以用来完成同步机制, 有点像传统线程中的mutex。
9.谈谈load和initialize的区别
这个题目当时问出来,真的是一下子就傻了,平时虽然用的多,但是真的没有注意比较过他们俩,看来平时学习还是多要问问所以然!
10.Core Data是数据库么?有哪些重要的类?
我当时一看问到是不是的问题,我就留神,感觉应该不是常理的,当时仔细想了想,Core Data确实不是一个数据库,只是把表和OC对象进行的映射,当时并不是进进映射那么简单,底层还是用的Sqlite3进行存储的,所以Core Data不是数据库。
有以下6个重要的类:
(1)NSManagedObjectContext(被管理的数据上下文)
操作实际内容(操作持久层)
作用:插入数据,查询数据,删除数据
(2)NSManagedObjectModel(被管理的数据模型)
数据库所有表格或数据结构,包含各实体的定义信息
作用:添加实体的属性,建立属性之间的关系
操作方法:视图编辑器,或代码
(3)NSPersistentStoreCoordinator(持久化存储助理)
相当于数据库的连接器
作用:设置数据存储的名字,位置,存储方式,和存储时机
(4)NSManagedObject(被管理的数据记录)
相当于数据库中的表格记录
(5)NSFetchRequest(获取数据的请求)
相当于查询语句
(6)NSEntityDescription(实体结构)
相当于表格结构
11. frame和bound 一定都相等么?如果有不等的情况,请举例说明
在上图中,如果蓝色的View旋转了,那么它的frame是外面的大框,而bound是蓝色的边框。
以上是我3月份面试遇到的问到的我一下子没有答全或者没答好的问题,大神全部都会的话请忽略哈。然后还有2个开放性的问题,那基本就是完全考验实力和自己理解的深度了。一个是谈谈你对Runtime的理解,另一个是谈谈你对Runloop的理解,由于我个人这两个理解都不是很深,这里就不贴我的理解了。大家如果也感觉欠缺的,就赶紧去网上多看看吧!
二.机试
这个环节基本都是大公司,或者是复试的时候会出现,因为上机打代码确实很很快区分出谁好谁坏,当然我也面了一家这样的公司,就给一张白纸,全程都是手写代码,这就完全是考验基本功了,因为没了代码补全,没有了编译器告诉你哪里错了,一切都要靠自己的基本功来了。
机试基本就是靠靠算法题了。当然也有算法题在笔试的最后几道题出现,那就看公司面试怎么安排的。
2年前我也是面试iOS,当时对算法和 数据结构要求很低的,很多面试基本都不问这些,今年面试多了这些问题,也让我眼前一亮,也感叹,2年技术发展之快,面试如今都会涉及到算法,不会算法和数据结构的程序员的道路会越走越窄。
算法题,我遇到的都不难,毕竟不是BAT那种公司,简单的就是直接要你写一个算法出来,稍微高级点的就是有一个背景,然后要你解决问题,其实就是和ACM题目一样的,不过就是没有那么复杂。我贴几段问的最多的算法,太难的题只能考自己的算法功底了。
二分查找 θ(logn) 递归方法 int binarySearch1(int a[] , int low , int high , int findNum) { int mid = ( low + high ) / 2; if (low > high) return -1; else { if (a[mid] > findNum) return binarySearch1(a, low, mid - 1, findNum); else if (a[mid] < findNum) return binarySearch1(a, mid + 1, high, findNum); else return mid; } } 非递归方法 int binarySearch2(int a[] , int low , int high , int findNum) { while (low <= high) { int mid = ( low + high) / 2; //此处一定要放在while里面 if (a[mid] < findNum) low = mid + 1; else if (a[mid] > findNum) high = mid - 1; else return mid; } return -1; }
冒泡排序 θ(n^2) void bubble_sort(int a[], int n) { int i, j, temp; for (j = 0; j < n - 1; j++) for (i = 0; i < n - 1 - j; i++) //外层循环每循环一次就能确定出一个泡泡(最大或者最小),所以内层循环不用再计算已经排好的部分 { if(a[i] > a[i + 1]) { temp = a[i]; a[i] = a[i + 1]; a[i + 1] = temp; } } } 快速排序 调用方法 quickSort(a,0,n); θ(nlogn) void quickSort (int a[] , int low , int high) { if (high < low + 2) return; int start = low; int end = high; int temp; while (start < end) { while ( ++start < high && a[start] <= a[low]);//找到第一个比a[low]数值大的位子start while ( --end > low && a[end] >= a[low]);//找到第一个比a[low]数值小的位子end //进行到此,a[end] < a[low] < a[start],但是物理位置上还是low < start < end,因此接下来交换a[start]和a[end],于是[low,start]这个区间里面全部比a[low]小的,[end,hight]这个区间里面全部都是比a[low]大的 if (start < end) { temp = a[start]; a[start]=a[end]; a[end]=temp; } //在GCC编译器下,该写法无法达到交换的目的,a[start] ^= a[end] ^= a[start] ^= a[end];编译器的问题 } //进行到此,[low,end]区间里面的数都比a[low]小的,[end,higt]区间里面都是比a[low]大的,把a[low]放到中间即可 //在GCC编译器下,该写法无法达到交换的目的,a[low] ^= a[end] ^= a[low] ^= a[end];编译器的问题 temp = a[low]; a[low]=a[end]; a[end]=temp; //现在就分成了3段了,由最初的a[low]枢纽分开的 quickSort(a, low, end); quickSort(a, start, high); }
注释我也写上了,这些算法基本上简单的算法题都能应对了。
数据结构的题目我就遇到了链表翻转,实现一个栈的结构,先进后出的,树先跟,中跟,后跟遍历,图的DFS和BFS。代码就不贴了,太长了。如果有忘记的,可以再去翻翻回顾一下。
三.面试
面试基本都是问你之前做过什么项目啦,遇到了哪些问题了,自己如何解决的。谈谈对XXX的看法等等这些问题,只要平时认真完成项目,其实面试反而问的东西更好答,因为都是关于你项目的,这些你最了解和清楚了。
好了,到此就是2016年3月上海地区除了BAT公司,招聘iOS开发工程师的行情了,比2年前,最大的体会就是面试面更广了,要求更高了。现在要求除了会OC,还要懂算法和数据结构,还有要么会ReactNative,或者PhoneGap一系列混合开发的框架,或者熟悉Swift,程序员要一直跟上主流才能不能被时代淘汰。才能具有竞争力。这也是我面试了这些公司的感悟,活到老学到老!最后希望大家都和我交流交流,我也是个iOS菜鸟,请大家多多指教!