杂七杂八几个问题
最近被朋友问到的问题,总结了一下问题分享出来(鶸的分享通常都是一本正经地胡说八道😂),问题之间基本上没什么关联,也没有顺序,纯粹就是总结一下最近被问到的问题。
怎么看待视图圆角的性能优化?
视图圆角的性能优化并不是什么新鲜的东西,不知道为什么最近两年特别火,我在网上也看了很多篇这方面的文章,感觉挺失望的,每篇文章的内容基本上都是千篇一律,内容基本上都是说,设置视图的圆角(视图设置圆角不一定会引起离屏渲染,这里默认是会离屏渲染的情况)会引发GPU做离屏渲染的操作,GPU的使用率会大大增加,因此可能会出现屏幕掉帧的情况,所以要避免GPU的离屏渲染,转交给CPU处理。每次看到这些内容的时候我都想问,难道转交给CPU之后,离屏渲染的工作就消失了吗?我不是太明白为什么每篇文章基本上都是呼吁把离屏渲染转交给CPU来完成,难道转交给CPU来做就不会有性能问题?离屏渲染的工作,不是GPU就是CPU来完成,这本来就是一个折中的选择,下面通过分析一个简单的页面来看看这种选择(以下分析当提到CPU处理时,忽略多线程优化,均在主线程处理)。
原图 离屏渲染图首先我们需要简单地分析这个页面的资源分布情况。CPU的基本工作:创建视图,视图布局,文本绘制,图片解压加载;GPU的基本工作:头像视图的离屏渲染,渲杂整个页面。按照这样的分布,假设当前页面的CPU占用率是60%,而GPU占用率是25%,而屏幕并没有掉帧但处于掉帧的边缘,在这种情况下把GPU离屏渲染的工作转交给CPU来完成后,CPU的占用率可能达到了70%,而GPU的占用率只有20%,原本处于掉帧的边缘的状况被打破了,因为CPU的压力过大导致出现屏幕掉帧的情况,所以,面对这样的情况,我们还是坚持要把离屏渲染的工作转交给CPU来完成?当然,这样的假设可能很极端,实际情况可能要好得多,即使真的把离屏渲染的工作转交给CPU来完成也不会出现掉帧(即使因为CPU而掉帧,还可以通过异步处理的方式进行优化),但是这样的资源分配真的合理吗?
要不要将GPU离屏渲染的工作转交给CPU来完成,是需要根据实际情况折中选择的,对于怎么选择,我个人的标准是,首先要看当前页面是动态还是静态,如果只是一个静态页面,那就不需要去考虑性能优化的问题;如果是一个动态页面,要看这个页面在程序中的位置,如果只是一个无关紧要且使用率低的页面,也可以不去考虑掉帧问题;如果是一个很关键且使用率高的页面,就需要先测试当前页面的性能瓶颈在于GPU还是CPU(怎么测试?网上有挺多这方面的资料,动手查一下吧),分析当前页面的资源分布情况,如果只是GPU的压力大,可以将部分GPU的工作转交给CPU来完成,如果是GPU和CPU的压力都很大,那么就需要你进行折中选择的时候了,面对这种选择,我不认为会有一份明确的指南告诉你怎么做是对的,也不可能存在这样的指南,因为任何的折中选择都是权衡后的选择,是否可以快速作出正确的选择,我觉得这个时候就需要吃经验了,尽管如此,还是有一些经验可以作为参考(本文不展开讲,以后可能会写详细的优化文章)。
考虑当前页面是否有很多且需要快速响应的CPU任务(如:点击事件,必须在主线程处理的UI任务等),如果是,选择其它方式优化GPU或者选择CPU并行处理的方式;考虑你自己或你的团队是否有能力驾驭并行处理(如果没有,还是串行处理吧);考虑实现成本和价值(实现成本高的,你可能没这时间,价值低的,实现了也没太多意义);
怎么看待性能优化?
性能优化越来越流行,随着iOS开发越来越成熟和稳定,性能优化更是被提到了日程,网上也出现了各式各样的性能优化技术文。就像上面所说的那样,性能优化是折中选择,做性能优化有很多好处,可以得升用户体验,可以更充分地利用资源等,但在你打算做性能优化时,你不得不去考虑一些问题。
是否可以做性能优化?当你将要做的性能优化会一定程度上破坏程序架构或冲突时,你要怎么选择(当然是选择做性能优化咯,因为大部分的程序都没有架构设计这种东西😂);
是否需要做性能优化?需不需要做性能优化,个人的标准是:基础,关键的模块的性能优化是需要且必要的(什么是基础,关键的模块?举个例子,UILabel在iOS8以前的底图层是CALayer,在iOS8以后的底图层是_UILabelLayer,这是苹果对UILabel文本绘制的优化),但对于业务层,特别是展示层的性能优化,看模块是否为迭代,增量式开发(一般情况下,以迭代,增量的方式来完成和完善功能的模块都会有一个明确的战略目标,会趋向稳定),如果不是,建议不要花时间优化这个模块(除非你真的很有空,因为业务层的很多模块是产品一拍脑门想出来的,产品本身就没有明确的战略目标和方向,所以这个版本有,下次迭代可能就不要了)。
你的团队是否有能力做性能优化?即使可以做,需要做性能优化,最后还要看团队的实际情况,因为整个程序的优化由一个人来完成太困难了,优化所需的时间成本太高了,而且还可能会出现,你做好的优化被你的同事随便就改了(多半的理由是:“那样写太麻烦,这样写直接调用多方便,还能一改整个项目都改了”,是,一改整个项目都改了,一改整个项目都崩了)。
性能优化是折中选择,做不做性能优化也是折中选择,这些选择的标准也会随着你经验的增加,知识面的扩充,所在团队及角色的变化而有所不同。
怎么看待设计模式?
上周末有一个朋友很沮丧地跟我说:“我花了两个月看完了GOF23,但这些东西真的是然并卵,完全想不到有什么例子,更想不到要怎么用”。可能有很多人看了设计模式之后都有同样的感受,这些概念看上去好像挺有道理,但在实际开发中怎么应用?学习设计模式对自身有什么帮助?在讨论这些问题之前,我们先通过一些简单的例子来看看系统中的设计模式。
首先我们来看看触摸事件的整个过程(详细过程可以点这里),当触摸事件从硬件系统发送到当前Application之后,Application的主要操作分为两步,查找第一响应者,查找可以处理事件的响应者。
查找第一响应者,首先向Window对象发送hitTest消息,Window对象接收到消息后就开始进行自身的检测,通过pointInside方法来判断触摸点是否在本地坐标系上(忽略像userInteractionEnabled等判断),如果在,就向它的子视图对象发送hitTest消息,子视图对象再重复这一过程,直到最后一个符合条件的子视图对象,并返回这个子视图对象。
查找可以处理事件的响应者,通过上一步获取到第一响应者,向第一响应者发送事件处理的消息,把相应的事件传递过去,第一响应者开始检测自身是否可以处理事件,若不能处理事件,就向下一个响应者发送事件处理的消息,并把事件传递过去,若能处理事件,则处理事件并结束本次触摸事件,重复这一过程,直到有响者可以处理事件或放弃本次触摸事件。
当你去研究触摸事件的整个过程时,可能需要花上一些时间才能了解整个过程,才会发现整个过程分为两步,才大概了解到每一步的一些关键细节。当你了解了整个过程之后,可能又会产生一些疑问,为什么查找第一响应者和查找可以处理事件的响应者的过程是这样?整个过程看上去很繁琐,这样处理方式有什么好处?
OK!我们现在换一种方式,站在设计模式的角度来看触摸事件的整个过程(不会展开讲每一个模式的细节,只讲述正式定义,详细内容会在设计模式的篇章里讲述)。
整个过程的第一步是查找第一响应者,查找第一响应者利用了视图层次结构来完成,看到视图层次结构我们自然想到的是组合模式(Composite Pattern),大部分系统都是以组合模式来实现视图层次结构的,iOS也不例外。组合模式的正式定义:将对象组合成树形结构来表现“整体/部分”层次结构(例如,视图层次结构),Composite使得用户对单个对象和组合对象的使用具有一致性。组合模式使得Application对象不知道也不关心当前的UIWindow对象是单个对象还是组合对象(Window是否有子视图),这对于Application对象来说是透明的,以UIWindow对象为最高层组件的对象结构(视图层次结构)可以在程序运行时动态改变,但这些改变并不会响影Application的代码,因为Application对象对UIWindow对象的所有假定都基于UIWindow抽象的概念接口,而这些假定或者说Application对象对UIWindow对象的操作在编译时就已经固定下来。这样的设计使得Application对象与UIWindow对象既可以保持稳定的交互,又可以在程序运行时动态改变以UIWindow对象为最高层组件的对象结构,既保证了稳定性又提供了灵活性。所以,当Application对象向UIWindow对象发送hitTest消息时,UIWindow对象根据运行时的对象结构完成相应操作(递归结构)。
类结构 对象结构提供对象结构很重要,因为它展示了对象之间如何协作,而提供类结构同样很重要,对象结构中的每一个对象都是某一个类的实例,客户(Application对象)对服务器对象(UIWindow对象)作出的所有假定都是基于这个对象的类的抽象(类的接口)。
第二步是查找可以处理事件的响应者,查找是顺着响应链(响应链由一组响应者组成)来完成的,iOS中响应链是以职责链模式(Chain Of Responsibility Pattern)来实现。职责链模式的正式定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。第一步获取到的是职责链(响应链)中的第一个接收者(Handler,第一响应者),客户向第一个接收者发送事件处理的消息,消息沿着链转发,若当前接收者不能处理事件,就将消息传递给下一个接收者或者叫隐式接收者(Successor,即响应链中的nextResponder)。
沿职责链(响应链)传递消息在职责链模式中,既然没有一个明确的接收者,那么消息就不能保证一定会被处理。若有接收者可以处理事件,则本次消息传递结束,若没有接收者可以处理事件,则本次事件被放弃处理。与组合模式一样,职责链模式同样提供了稳定性和灵活性。Application对象与第一响应者(FirstResponder,即职责链中的Handler)的交互在编译时已经固定,而响应链的对象结构则在运行时动态地改变。
触摸事件的处理应用了组合模式和职责链模式,接下来我们再看一个应用职责链模式的例子,Objective-C的消息机制(假定读者知道消息机制的流程)。
Objective-C消息机制当我们向一个对象发送消息时,通过这个对象的isa指针来获取该对象所对应的类对象(或元类对象),再通过配对类对象(或元类对象)方法列表中的SEL来获取相应的IMP,若在当前类对象(或元类对象)的方法列表中可以找到与SEL配对的IMP,则返回相应的IMP并结束本次消息的查找,若不能在当前类对象(或元类对象)的方法列表中找到与SEL配对的IMP,则把查找消息传递给SuperClass,从SuperClass的方法列表中继续查找与SEL配对的IMP,重复这一过程,直到有类对象(或元类对象)处理消息或最终抛出doesNotRecognizeSelector异常。
职责链是根据类结构的继承关系在运行时动态决定的,或者说职责链是类对象和元类对象所构成的对象结构(Objective-C作为一门原型语言,类和元类本身也是对象),Handler就是当前对象所对应的类对象(或元类对象),Successor就是SuperClass,沿着这条职责链来查找消息。为什么说Objective-C是动态语言?Objective-C的动态性体现在很多方面,用链表来实现方法列表使得可以在运行时动态添加方法,用isa指针指向的类对象(或元类对象)使得可以在运行时改变指向(KVO,为什么KVO的实现是创建当前类的子类来实现监听而不是用hook的方式?因为直接hook会影响其它同类对象的操作)。用职责链模式实现消息查找路径使得可以在运行时动态地改变路径的对象结构,也为最终因找不到方法而进行消息转发提供了基础。
例子不再一一列举,留给有兴趣的读者自己去研究。无论是系统框架还是优秀的第三方库,或多或少都应用了设计模式来实现,虽然每个模式在应用时都会根据实际情况作出一定的调整或变种,但在结构上,所需要注要和考虑的细节上基本是一致的,当你对这些模式有所了解时,就可以帮助你更好地理解你所研究的框架或架构,就可以使你更快地知道哪些是要点,哪些是重点。这是对于你研究系统框架或优秀的第三方库时的帮助,除此以外,学习这些设计模式还为你的思考提供了另一个方向,提升了你的编程思想和程序的稳定性,可扩展性等。应用设计模式有很多优点,但同时也存在着一些缺点。一般情况下,你所做的基本抽象都是从问题域中来的,当你完成问题分析开始设计程序时,常常会在这些基本抽象或关键抽象上进行更高层的抽象,会创建新的类和对象来协作,这些额外的类和对象会增加程序设计的复杂度,还会降低效率等,而且当你的程序充满了设计模式时,你还将面临着另一个问题,是否过度设计?
除了设计模式,还有一些设计原则可以你帮助解决设计上的问题,但需要注意的是,设计原则只是指引,并非一定要照着来做,而且在实际开发当中也不太可能这样做(至少国内的IT公司好像大部分是这样子的)。例如单一功能原则,它规定一个类应该只有一个发生变化的原因,但这个原则会大大增加你的程序设计复杂度。细化对象的粒度是一个分类过程,应该通过多次迭代来完成,而不是一开始就花大量的时间去分析与设计(通常情况下你都没有时间去做设计和考虑),这跟做性能优化一样,避免对未来的发展做过多的预测,除非你所面对的模块有很明确的战略目标(扯蛋吧,在开发过程中不来改需求已经很庆幸了),或者你所在的团队有这些明确的规定。
学习完设计模式后要面临的最大问题是,怎么在实际开发中应用,相信很多人都有这个的疑问,为什么面对实际问题时会无从下手?其实不管你是否有学习过设计模式,在你的日常开发当中总会用到一个模式-策略模式(Strategy Pattern)。策略模式的正式定义:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。看到这个定义你想到了什么?举个例子,UICollectionView根据你所传递的UICollectionViewLayout进行布局,通过改变UICollectionViewLayout来改变布局,UICollectionViewLayout就是模式中的Strategy,UICollectionView就是模式中的Context。再看一个更常见的例子,代理回调,代理回调中的协议就是Strategy的接口,引入协议的类就是Strategy,而被代理的对象就是Context(例如,TableView还是那个TableView,但TableView的协议每次都有不同的实现,TableView根据所提供的协议实现来展示最终的效果)。
回到怎么应用这个问题,我个人的看法是,很多人在学设计模式时忽略了设计模式的基础是面向对象,GOF23对设计模式的定义是,对被用来在特定情景下解决一般设计问题的类和相互通信的对象的描述。如果你本身还不知道怎么用面向对象来分析和分解问题,还不清楚什么是对象模型,什么是类,什么是对象,怎么分类,你连基本的切入点和依据都没有,你怎么可能知道在实际开发中该如何应用设计模式,即使真的应用了,也只是套了个结构而已。
面向对象的问题,在此不展开讨论,因为这些知识或者说这些关于编程思想上的东西,并不是一两篇技术文就可以讲述清楚,就可以传达给你的,如果读者有兴趣,可以看一下《面向对象分析与设计》这本书,不管你目前是什么水平,或许你都可以在这本书里有所收获。如果读者想学设计模式,建议选Head First《设计模式》作为入门,GOF23作为拓展,《设计模式沉思录》作为应用后的思考,虽然我也很想支持国货,但真的不建议选《大话设计》作为入门(虽然销量很高),因为跟Head First《设计模式》相比,《大话设计》传递的是思考什么(相当于给你指了一条路,然后告诉你沿着这条路走就是了,这样就固化了你思想),而Head First《设计模式》传递的是如何思考(相当于告诉你什么是路,怎么区分路,根据你自己的想法来选择走什么路,这就给了你创造的空间),这在思维引导上差得真的不是一点半点。
个人感觉做程序设计要比做性能优化更看团队的情况,因为你需要考虑团队成员的水平,怎么协作,面对有限的开发时间怎么分配任务,怎么折中选择实现等,有时候会有些有心无力的感觉。
最后
如果你看到这里还没开喷,我也只能说声谢谢。看到这里你可能想说,文中的内容讲得太笼统了,这本来就不是专门针对某一块技术来写的帖。
最后想说的是,没有葵花宝典(即使有也不练啊😂),也不要想着有人出来教你一套独孤九剑。如果你想学习某一块技术,建议找相关的专题书来看,因为专题书所带给你的全面性和专业性,不是网上一两篇文章能给的。建议少用Objective-C的Runtime,多思考怎么从设计上解决当前遇到的问题。建议不要片面去追求底层技术的待刨根问底(我们要有刨根问底的精神,但不要片面追求),除非你真的很有兴趣和公司需要你这么发展,不然还是全面一点发展好,因为就像《面向对象分析与设计》这本书里面讲的,有很多程序员都是拿着一门面向对象的语言当作面向过程的语言来使用。