系列文章旨在记录YouTube上谷歌发布的Android Performance Patterns系列视频,一共79个视频,每个视频也就几分钟。当然对于大部分安卓开发者来说,这些都是基础,可能你会说,看这些有什么用呢。但是魔鬼往往藏在细节之中,切忌眼高手低的问题。

本人英语水平有限,加之理解和经历尚浅,如果错误和不当之处欢迎指正。

1.关于线程那些事

众所周知,安卓系统中,主线程(UI线程)是十分重要的,很多工作如绘制、与用户交互事件处理等工作都会在主线程执行

由于主线程在大概16ms就会发出一次重绘UI的信号,为了保证用户体验的流畅和避免出现掉帧的情况,我们必须保证主线程没有大量的耗时操作。安卓系统为我们提供了很多将工作放到异步线程执行的方法,当然我们可以选择自己new Thread,但是自己管理线程总是比较麻烦,而且线程的创建和销毁也不是毫无成本的。安卓系统给我们提供了几个很好用的手段。

AsyncTask适用于一些较小的异步任务、简单的网络操作等。HandlerThread本质是一个带有handler的thread(好像是个废话),他可以把任务放在线程中执行,之后再利用handler进行UI的更新。ThreadPool线程池适合解决频繁创建线程的问题,IntentService作为一个service,可以有较长的生命周期,处理一些长期的任务。每一个都有它的适用场景,这里不再细细展开,大家可以查询相关资料进行进一步的学习。

需要注意的是,由于线程执行工作可以说基本与UI进行了分离,在页面被销毁或者旋转屏幕重建后,之前的对象可能就不复存在了,随之而来的内存泄露和对象不存在的问题也需要注意。内部类AsyncTask会持有外部activity的引用,在使用的时候最好使用静态内部类+弱引用,防止activity引用无法被系统回收。

2&3.理解UI线程&UI的更新

一张图片大概介绍了,一个线程需要的几个基本功能。当我们把安卓提供给我们的looper、handler、message等凑在一起之后呢。

一个很cool的handlerthread就诞生了,handler负责消息的排队执行,looper确保线程一直在运行,并在有新的消息到来时将其取出。  在app启动的时候,我们的ui线程就会被创建,并伴随app的整个生命周期,由于UI和异步线程的特殊性,可以将要更新的要求通过intent或者其他方式通知activity进行,确保UI的更新由持有他的activity进行,这样就不会出现因为activity销毁而导致内存泄露或者崩溃。

4&5.关于asyncTask&HandlerThread的使用

关于asyncTask任务的串行执行,在很早版本的安卓手机上,曾经有过并行执行的asyncTask,但是后来因为种种原因,asyncTask默认采用的还是串行执行的方式(当然也可以设置其并行执行任务,但是这种场景下,线程池可能是更好的选择)。

handlerThread适用于长时间的,无需与UI页面进行操作的任务执行。在初始化的时候需要记得指定一个合适的优先级,以免线程在竞争cpu资源的时候无法被调度。

6.徜徉线程池

想象如果只有一个线程进行任务,可能几十个个小任务就会占用一个线程上百ms的时间,导致其他任务无法被执行,但是线程池中多个线程一起处理任务的话会将效率大大提升。ThreadPool会帮你处理线程的创建、调度甚至是回收,开发者只需要关注于如何划分任务进行执行。需要注意的是关于最大线程数,CPU能同时处理的任务是有限的,当然通过这种方式获取的数值也不一定准确,比如双CPU机器,为了节省电能,系统将其中一个CPU休眠了,那么你获得的硬件进程数也是1。当然线程池也不是万能的,除非你一直有很多任务一直在执行,否则使用线程池有一些过于重量级。

7.IntentService

intentService像是service和handler的结合体,它是一种service,但是内部有handlerThread处理任务。作为service他就比我们使用普通的线程拥有更高的优先级,甚至在应用处于后台他也可以继续执行而不容易被系统杀死。但是注意,最好不要使用广播intent的方式回到主线程去更新UI,这样的代价过大,尽量使用handler或者runOnUiThread方法进行。

8.Loader&LoaderManager

由于线程和我们的activity生命周期的不一致性,比如activity销毁了,但是下载还未结束,这会导致activity的内存泄露以及下载完成后需要更新view不再存在产生错误,还会导致这期间资源的浪费,在activity重新被创建,整个过程又要重新来一遍。

loader和loadermanger是一套异步加载数据的标准机制,一个fragment或者activity只持有一个loaderManager,负责管理众多loader。loader则负责数据的加载。

LoaderManager.LoaderCallbacks:LoaderManager.LoaderCallbacks是LoaderManager中的内部接口,客户端与Loader的通信完全是事件机制,即客户端需要实现LoaderCallbacks中的各种回调方法,以响应Loader & LoaderManager触发的各种事件。客户端在调用LoaderManager的initLoader()或restartLoader()方法时,就需要客户端向这两个方法中传入一个LoaderCallbacks的实例。LoaderCallbacks有三个回调方法需要实现:onCreateLoader()、onLoadFinished()以及onLoaderReset()。

onCreateLoader:我们要在onCreateLoader()方法内返回一个Loader的实例对象。很多情况下,我们需要查询ContentProvider里面的内容,那么我们就需要在onCreateLoader中返回一个CursorLoader的实例,CursorLoader继承自Loader。当然,如果CursorLoader不能满足我们的需求,我们可以自己编写自己的Loader然后在此onCreateLoader方法中返回。

onLoadFinished:当onCreateLoader中创建的Loader完成数据加载的时候,我们会在onLoadFinished回调函数中得到加载的数据。在此方法中,客户端可以得到数据并加以使用,在这之前,如果客户端已经保存了一份老的数据,那么我们需要释放对老数据的引用。

onLoaderReset:当之前创建的Loader被销毁(且该Loader向客户端发送过数据)的时候,就会触发onLoaderReset()回调方法,此时表明我们之前获取的数据被重置且处于无效状态了,所以客户端不应该再使用这份“过期”的无效的老数据,应该释放对该无效数据的引用。

9.为线程指定优先级&10.GPU调试工具的使用

11.善用httpResponseCache

网络请求往往意味着高代价和高延时,对于一些非敏感数据,如需要重复使用配置文件,开关数据等,可以通过缓存到本地来避免频繁的从网络上获取。http协议头有设定缓存时间或者无缓存相关的字段。在我们自己实现缓存时,可以使用DiskLruCache(okhttp中使用的就是这种方式)进行缓存。当然对于不同的场景有不同的缓存策略,一些敏感的用户信息肯定是不允许缓存,一些图片或者UI显示资源在没有发生更新时甚至可以一直缓存在本地。

12&13.适时地进行网络请求

如用户已经很久没有打开过app、用户已经很久没有进入过页面进行网络请求,也可以使用逐渐放缓请求频率的策略如本次请求没有新数据,下次发起请求时间延长一段时间等。在wifi和充电情况下地请求可以稍微频繁一些等等等等。这些就看应用的场景和详细情况了,记住,在移动设备上,流量和电量都是稀缺资源,切忌频繁请求,会带来很不好的用户体验。

同时使用预取策略,一次性将合适量的数据进行下载,以避免频繁请求带来的资源损耗。

14.处理弱网情况

在一次请求发出到返回所用的时间是我们衡量真实网络情况的指标,如果应用监控到一个请求经历了几百毫秒才返回,那么我们就很有理由进行缓存,因为很可能用户处于一个较差的网络状态下。当一个请求快速返回的时候,我们可以进行一些较大的预取,这些请求策略多用于大量网络刷新的时候,比如很多新闻app的feed流,用户可能一直在刷新,这时候就需要我们有适时的预取和数据的分包处理。

15.选择合适大小的资源

对于图片的选择基本原则就是不要使用PNG,尽量使用JPEG这种对于网络传输和压缩友好的类型。其次就是根据显示需要来下载图片,比如缩略图可能一个很小分辨率的图片就可以实现。第二点比较有意思,我们都习惯了使用json或者xml来进行数据的传输,但是事实上json在解析时长、内存、甚至后续java的GC中都表现不佳,谷歌官方推荐我们使用flatBuffers来代替json,我也是第一次听说这种方式。简单来说,flatbuffer提供了一种序列化和反序列化的规则,它的缺点是易读性很差,而且需要生成类加入代码中,存在代码注入问题。但是他的解析速度、内存占用非常诱人,据说facebook在Android客户端的CS通信使用flatbuffers,取得了很不错的收益,我后续也会进行学习并出一个关于flatbuffers的文章(FlatBuffers源码解析_ruozhalqy的博客-CSDN博客),欢迎大家关注~flatbuffers官方主页

此外,对于压缩文件,我们应该注意将类似的对象放在一起,这样可以达到较好的压缩结果。

16.service性能优化

安卓开发者对于service应该非常熟悉了

service通常用于执行一系列后台工作,但是service的创建以及和我们ui进程的通信开销是很大的,其次,service执行在ui线程上,如果需要执行后台操作,需要注意另开线程执行(或者直接使用intentservice)。

视频中对于service的建议就是。。。能不用就不用,因为往往有很多更高效的方式去实现我们的需要。比如进行事件的监听和处理,broadcastReciver就是很好的选择。

需要注意,我们用到的两种service,一种是使用startService启动的,一种是bound service进行IPC通信,一种需要stopservice去停止,一种需要unbindservice调用,当我们混用的时候,注意停止service的调用,避免资源的无故损耗。

17&18.移除没有使用的代码和资源

引用第三方库是开发中常进行的操作,当然,很多时候我们可能只需要一个功能,却不得不引用一整个库,这会使我们的代码不断膨胀。用户也需要下载更大的app。而且,安卓对于方法数有限制,一个app最多可以拥有65536个方法(可以使用multiDex进行拓展)。但是毫无疑问,代码膨胀总是一个不好的事情。

android提供了proguard工具帮助我们剔除没有使用的代码、类并可以进行代码的混淆。当然,如果有一些类只通过反射调用,我们需要将其“keep"住,以免被剔除。还有就是在module之间引用的时候,如果moudle被混淆,你在另一个moudle中就无法进行引用,此时就需要进行keep。

对于我们引用的第三方库,可能会有很多我们没有使用的功能,其相关资源也会占有大量空间。同样对于我们想要保留的资源,可以进行keep

同时gradle也有其局限性,一些文件夹并不在gradle的优化范围内,因为这些只有在运行时才会决定加载什么资源。这些就需要我们在开发的时候注意无用的资源管理。

19.缓存

缓存是现代计算机非常重要的组成部分,学过计算机组成原理的朋友一定有所了解CPU-缓存-内存-硬盘这样的多级存储设备,简单来说,缓存就是用于协调读取速度极快的CPU和速素的内存之间的矛盾,将常用的信息进行临时的存储,以免将时间花费在数据的读取上。当然这是计算机系统中的缓存系统,到了我们Android开发中,缓存的含义就可以非常广泛,可能是下载的图片进行缓存以节省网络资源,也可以将计算结果临时保存在内存里防止多次的运算。

视频中主要讲解了几种场景:1.能放在循环外的运算绝对不要放在循环内部。2.加载大型资源时,管理好显示部分和移除不需要的部分。3.预计算,不要用什么数据才算什么数据,充分利用空闲时间,尽量在用户需要数据的时候就能取到数据。

20.粗略计算

这里我们讲到了在完美结果但是资源消耗较大和可接受的资源消耗较少的结果中进行平衡。比如我们做了一个导航软件,用户从一个城市到另一个城市可能需要跨越几百公里,期间可能绝大部分都是高速公路,这是我们就不需要经常做网络请求来查询周边的情况,只需要保持GPS的精准即可。在出现了加油站、服务区等有价值的地点时再进行相关网络的请求和数据的分发即可。包括在沿途我们需要加载的资源,采用小体积低像素的图片即可,等到用户有兴趣详细查看的时候再进行大量资源的加载。

21.消除不必要的工作

我们常见的性能优化手段就是消除重绘,当我们确定一些视图最终展现出来的时候是被其他组件覆盖住时,我们就没有必要去进行绘制。关于重绘我会在工作中进行一版优化,希望大家关注后续的博文更新~

22.线程balabala 和前面内容比较重合

23.批处理

对于耗时长、工作量大的任务,我们往往关注比较多,处理的比较好。但是对于重复的小任务我们往往会忽略其消耗。比如频繁的网络请求,我们可以根据情况,将几个网络请求合并到一个进行处理。或者我绘制视图的时候,将多次的矩阵变换变成较大矩阵的一次变换。读取数据的时候尽量读取一批要使用的数据,而不是用一个查询一次。

24.更快、更小的序列化

简单来说,序列化就是将对象数据转换成可存储的文件数据以便下次进行读取。java提供的Serializable类是最常用的序列化方式,但是这种方式有占用内存较大、耗费时间长的问题,视频中称其为“bad choice”,Gson库也是一个不错的选择,更小的内存消耗很快的速度。但是由于其以json形式存储,为了实现其可读性,必然会引入大量冗余的信息。回到最后,我们还是推荐使用flatbuffers,其优点可以参照我进行的源码分析(FlatBuffers源码解析_ruozhalqy的博客-CSDN博客)

25.更小的序列化数据

序列化是开发中十分常见的操作,它将我们需要保存的对象或者数据永久存于设备上或者通过网络进行传输,并且可以通过反序列化获取到原本的对象。

我们习惯性按照某种规则去序列化对象,例如形成json文件,那么存在的问题就是存在很多重复的命名。同时,比如gzip压缩使用的是32k大小的窗口,如果我们的对象数据很多,在32k内没有产生较多的重复,那么压缩的效果也会大打折扣。解决方案首先从数据结构上进行改变,比如上面这个对象,完全可以采用数组方式进行序列化。同时可以将元素进行分离,以便于压缩算法更好的执行。之后我们就可以使用一些序列化工具进行更小更好的序列化了。

26.缓存UI数据

现代app基本都是基于网络拉取数据来显示UI,但是当新的数据没有到来时,也可能用户处于一种弱网情况下,展示白屏或者loading态过久是一个很不好的体验。我们要做的是可以缓存某次用户拉取的数据,或许这部分数据是过时数据或者无效数据,但是当新的数据没有返回时,展示这样的数据会让用户得到无缝的浏览体验。

28.有效管理内存

我们经常使用的hashmap,虽然从语言层面,这是非常好用的。由于冲突的处理成本较高,于是才用了空间换时间的方案,hashmap为了防止冲突的出现,会预先分配一个较大的空间用于map,但是对于稀疏的存储,没有用到的空间就会浪费掉,这显然对于内存不友好。但是android本身提供了适用于移动设备的ArrayMap,不太方便的地方是,key只能是int或者long类型。当然这个处理使得空间和效率都有较好的提升,之后我会根据阅读源码进行一个分析,感受一下其巧妙的设计(占个坑)。

当然,没有绝对的应该使用什么,hashmap用空间换去了时间,arrayMap则是对于较多对象,访问消耗时间更长一些。对于几百个元素的map,访问频繁删减较少,我们可以使用arraymap以减少内存的使用。但是当我们有map嵌套map的操作,我们最好还是使用hashmap。

30.自动封装的性能问题

大家对基本数据类型以及java中对其的封装都很熟悉,比如int与Integer,boolean与Boolean。后者为基本数据类型作为范型提供了良好的支持。但是需要注意的是,我们在使用Integer value=1这种语句时,java其实自动对我们的变量进行了封装。

当这种操作被用于循环中时,劣势就变得十分明显,当然这种写法基本不会有人采用,这个只是一个例子,说明封装基本数据类型对象会比直接使用基本数据类型产生更大的内存和时间开销。

在使用类似于hashmap这种范型容器时,我们的key如果是基本的数据类型,可以采用ArrayMap等以基本数据类型作为key的工具。同时他们在较小数据下内存消耗更少。

31.枚举类的性能问题

枚举类是开发中常用的类型,但是android官方并不建议使用这个类型,主要原因实在内存的损耗上,在视频中有一个例子。我们知道,在安卓应用开始运行时,系统会将我们的dex文件加载到内存中

上述简单的三个静态变量让打包出的dex文件增长了124bytes

如果使用一个简单的枚举类呢,这个体积增长就相当的夸张。

实际上,每一个枚举值由于创建散列和缓存等,都会有较大的开销。所以安卓团队建议压根就不要使用枚举类,否则对于大项目,久而久之都不会知道问题出在了哪里。那么对于枚举类提供的编译、运行时检查限制,我们可以使用@intdef @StringDef注解实现,而其本质上,还是int或者string类型。

32.关于内存回收

我们知道,在我们的应用进入后台的时候。系统为了保持流畅的前后台切换,理论上我们所有在堆内存的对象都会被保留在内存中。但是随着应用越开越多,当设备内存不足时,系统会选择杀掉一部分应用以释放内存。我们不清楚用户使用的习惯,但是我们肯定不希望我们的应用直接被杀死,以至于下次打开应用变得很慢。为此android为我们提供了一个回调函数,onTrimMemory(int level)。这个回调函数可以在四大组件中进行实现。它主要的作用是在应用进入后台后,在回收的各个阶段进行回调。我们可以实现其以释放一定的资源,从而避免应用被整个杀死,而是在下次快速热启,再进行一些资源加载。比如我们可以回收一些位图资源等。

Android系统会根据不同等级的内存使用情况,调用这个函数,并传入对应的等级:

  • TRIM_MEMORY_UI_HIDDEN 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源.
  • TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
  • TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
  • TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。

当应用程序是缓存的,则会收到以下几种类型的回调:

  • TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
  • TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
  • TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。

此外,在使用的时候尽量使用>=来进行判断,以免之后系统更新假如新的确定值情况。

33.注意view导致的内存泄露

通常来说,我们不会认为view是内存消耗的“大户”,这个倒没什么大问题,但是有时候,view引用的对象,可能是内存泄露的重点。view一般会持有其activity等的context,所以当view发生内存泄露,也会导致activity无法被回收,这就导致了很严重的内存泄露。开发时需要注意的问题:

1.尽量不要在view内部使用异步回调。首先,异步回调的执行时机并不确定,可能发生在view被销毁之后,也可能回调之前,activity应该被回收而没被回收,导致内存发生抢占等问题。

2.不要让view被静态对象持有,导致其生命周期不正常的长。

3.不要把view放到一些内存模式不确定的存储中,如WeakHashMap中以view作为value,由于WeakHashMap会把value作为强引用,又会产生一个难以察觉的引用阻碍GC。

34.定位服务与电池

定位服务在现代移动设备中不可或缺,但是受限于手机电量,频繁的位置请求可能会让电量消耗过快从而用户体验很差,我们往往需要在电量与功能上寻找平衡点。当然,一个固定数值的请求总是不太灵活。

可以参考网络传输中的移动窗口,当用户长时间位置没有太大变化,我们可以延长请求位置的间隔,相反,如果用户位置变化较大,我们可以适当缩小请求间隔。

同样,请求位置信息也有两种方式。

35.关于两次layout

layout是构建安卓页面的核心,但是如果不注意,layout将成为一个十分耗费性能的部分。

对于我们常用的relativelayout,安卓页面渲染的主要流程:计算大小与位置---通过不同view相互关系来进行边界的确定,也就是当一个view发生变化的时候,往往会带动其他view甚至于整个页面发生重新布局。当然这种情况在很多布局中都会出现,但是当我们使用了多层嵌套之后,这种重新布局的代价会高速增长。在开发中要注意以下问题

1.尽量使用层级少的布局

2.只在确定必要的时候进行重新布局

36\37.网络表现与批处理

对于移动设备来说,流量与电量是两个非常重要的资源。基本的网络请求大概有三种,一种是用户主动要求刷新数据,第二种是服务器希望用户更新的数据,第三种事一些实时的数据,包括位置信息、上传数据等等。其中后两者是我们需要极力减少的网络请求。

1.首先,不要进行轮询操作,这种操作或许在pc上有一定的使用场景,但是移动设备上不要做这种浪费电量带宽的事情。

2.将需要下载或者上传的数据进行批处理,即积攒一定的数据再进行网络的传输。

3.进行预下载预处理等工作,防止用户产生频繁的网络请求。

每一次网络请求过后,硬件都会在请求后保持一段时间的活跃以防其余的请求到来,所以当网络请求频繁的时候,硬件可能保持长时间的活跃。从而加快电量的消耗。

最简单的批处理策略就是,对于一个请求不要立刻进行处理,而是先放在一个队列中,等待适当策略进行处理。

38.任务调度

我们在日常使用中,很多的任务可能不需要及时的执行,那么选择一个合适的时机、比如连接了wifi或者充电时执行等。Android提供的JobScheduler是一个非常好的工具。

JobScheduler的使用和原理 - 简书

39-41.传感器相关,暂不更新

42.对象池

内存抖动的发生往往是因为在短时间内创建了大量的临时对象,频繁的创建以及销毁,导致内存中出现过多零碎的碎片。解决这个问题一个很好的方式是使用对象池。当我们使用完一个对象的时候,可以不立即释放内存,当下一个同类型的对象需要被创建时,我们可以直接使用之前保留的对象。

使用对象池可以有效防止内存抖动的发生,在程序运行过程中内存占用逐渐成为一个稳定状态,也可以减少gc时间的发生,节省帧时间。

1.尽量不要用一个对象再向对象池中分配一个,可以一次分配一组。

2.做好预分配

3.让使用频繁的对象进入对象池,注意对象池的管理和释放。

43.迭代器

首先我们要清除,每次使用迭代器,都会产生一定的内存占用。当然几个迭代器当然没有什么大问题。但是当我们在循环内部或者onDraw方法内部生成迭代器,可能导致较大的内存消耗。还有就是,迭代器在每个next调用之前,都会对容器的完整性进行检查。

实验对比了for loop、简写迭代器以及迭代器在arraylist和vector上的表现,显然速度上for loop是更快的。

44.LRU缓存

缓存已经是一个老生常谈的问题了,从pc机器发展初期,缓存就是一个很重要的手段。在移动端有线的资源上更是如此。LruCache是Android中非常好用的缓存方式,使用key value的形式存储,基本思想就是当前越常用的排位越靠前。

确保cache的容量不会太大,导致OOM,也不会太小,防止过多的缓存回收和重新加载。一般使用当前应用占用内存的1/8作为缓存大小。

整体使用方式像是一个hash表,当获取不到资源时,将文件重新放入缓存即可。LRUcache会处理资源的回收和释放。

45.使用lint检查

使用lint检查可以排查代码中的一些隐患。

46.阿尔法混合的隐藏代价

当我们绘制不透明元素时,每个像素只需要绘制一次,因为前面的总能覆盖后面的。但是alpha透明度会导致至少两次的绘制。当然alpha变化还有一些隐藏的代价。

首先,alpha会影响整个绘制指令的重排,对于不透明的绘制指令,显然我们只需要绘制最上层的元素,那么指令就可以进行归并和优化。而对于alpha指令却不是这样,因为涉及到颜色的混合,我们需要串行执行绘制指令已得到正确结果。

其次,我们除了对view进行alpha动画,也时常会对viewgroup进行alpha变更。这里就会出现一个问题,如果单独对每个元素进行渲染,重叠部分的alpha渲染会发生叠加,如右图所示,字和图片的颜色显然更淡一些。对于这个问题,Android已经有了解决方案:绘制一帧未做alpha变换的数据到内存中,然后把这一帧的数据做alpha处理之后,渲染到屏幕(这部分数据会在会知道屏幕上之后会被抛弃)。这个确实可以解决显示上的问题。但是这却导致了整个viewgroup的重绘,明明页面并没有发生变化。

针对上述问题,可以进行下面两种方案解决

1.使用缓存数据,用空间换时间。

或者api>16使用

在进行alpha渲染之前和之后进行上述调用。以通知渲染器重新使用数据。
2.上述因为重叠部分的UI出现了异常的问题,如果你的确信不会出现UI重叠,或者为了性能可以接受因此产生的UI问题,可以重写View的hasOverlappingRendering方法,结果返回false, 它会带来渲染性能的提升。

47.避免在onDraw中分配对象

1.对象分配是有消耗的操作,虽然很小,但是对于一个高频调用的函数,其消耗会被放大。

2.过多的对象分配会导致较多的内存碎片,从而增加gc的消耗。

3.大部分2d绘图底层代码均为c++实现,过多的临时变量创建和销毁也会多次调用c++中的析构函数。从而影响整体的性能。

4.适当使用静态对象可以更好的增加onDraw的性能。

48.严苛模式工具

为了防止主线程被耗时任务占用从而导致anr,我们可以在开发过程中使用严苛模式进行调试。

我们可以使用严苛模式,对需要在严苛模式下进行的线程规定相对应的警告错事,如输出log等。

49.自定义view的性能

虽然Android提供了几十种标准view,但是开发中我们总会想实现一些标准view无法提供的能力,那么就产生了自定义view。但是自定义view性能维护全靠我们来进行。常见的问题

1.绘制函数调用过于频繁,导致视图可能没有改变就重新绘制

2.绘制了一些多余的区域,这些区域可能最终是不可见的

3.在绘制函数内部执行了一些耗时操作

view的绘制一定是通过重绘方法进行,但是要在真正需要调用的时候才进行调用

通过矩形区域来限制,哪些元素被覆盖、或者没有发生变化,这部分是不需要进行重新绘制的。

利用好裁剪区域,裁剪区域会将剔除不需要的绘制的区域,在cliprect之后的draw方法都会生效,知道下一个cliprect被调用。

当然,在一些功能强大gpu上,有可能cpu在计算裁剪时候的花销比gpu取消重绘的花销还要大。这就要求我们在绘制函数保持高效运行。

1.不要绘制不可见(在屏幕外部的元素)的元素

2.不要使用硬件加速不支持的绘制方法,因为在绘制方面,gpu方法往往都会比cpu执行要高效的多。

3.前面说到的,不要在onDraw方法内进行对象的分配。

50.延时批处理

51.更小的图像格式

png、jpeg等图片格式确实在网络传输条件下节省了带宽。但是当我们把图片存入磁盘或者读取到内存时,仍会有问题出现。首先,当图片读入内存时,虚拟机会为图片分配一块连续的内存。

如果这个区域太大,可能图片即使被回收了,其他对象也不会进行内存调整。这可能导致应用可使用的连续内存越来越少,从而导致gc发生。

不管是什么格式的图片,在加载到内存时,都是一种可渲染的格式。因此为了网络传输而进行的压缩操作在这一步是无效的。

而且在Android中,bitmap会默认采用32位数据进行渲染,也就是一个像素数据占32位内存。即使没有alpha通道的图片也会采用这种方式。而且在渲染之前,这部分数据会作为纹理传递到gpu,也就是一个位图同时使用了内存和显存资源。安卓提供了牺牲视觉效果以换取空间的方式进行渲染。需要我们在实际应用中进行取舍。如ARGB_4444将每个像素占用内存越缩小一般等

一些简单的图标,也可以使用单独的alpha通道+颜色值进行混合渲染。

52.更小的png

png采用的无损压缩算法,可以很好的保留图片的质量。但是也容易造成体积的膨胀。如果对于品质要求不高或者无需alpha通道,我们可以使用jpeg有损压缩。也可以单独拆分出alpha通道,在使用的时候进行合并。或者使用谷歌开发的webp格式,它的体积比同质量的jpeg要小百分之四十,但是解码时间却长数倍(来源百度百科),实际应用中还需要进行适当的取舍。

当然,文件压缩都是在传输或者存储的时候有效,当他们被加载到内存中时,依然会被解压成可供渲染的格式。

53.预先缩小bitmap

在实际图片的大小大于需要显示的大小时,如果还是将原图进行显示,就会产生性能损耗。在我们使用bitmap时,可以优先读取图片宽高等配置,根据实际显示图片的大小,对图片进行预压缩和缩放。得到最好的性能和效果的平衡。

设置insampleSize是一个很快速的操作,通过在读取像素的时候,通过间隔读取的方式来压缩图片。当然我们有时候可能需要的不是2的次幂倍的缩小,就需要inDensity对图片进行滤波处理,当然滤波操作是一个很耗费时间的操作,所以我们可以先对像素进行压缩,再通过滤波得到我们希望的尺寸。

54.重用bitmap

大量的bitmap往往会导致内存的抖动,因为bitmap会占用内存中一块较大的连续内存,在bitmap回收后,需要进行gc才能将这样的大碎片清理掉。所以在我们日常使用中,如果需要使用大量的bitmap,可以使用对象池的方式来解决内存抖动的问题。使用inBitmap属性可以将新的像素数据加载到现有的bitmap中。需要注意的是:

1.在api19之前,新的bitmap必须和旧的保持一致大小,19之后则需要小于等于目前的bitmap

2.不要改变编码格式

55-57.一些工具的使用

58-59. 渲染性能

手机硬件会在每16ms尝试进行一次刷新,以求达到60帧的绘制帧数,如果绘制操作没有在16ms内准备完成,就会出现丢帧的现象。

出现这种现象的原因很多,常见的是:绘制了太多不需要绘制的内容,运行了大量的动画。当不同的视图有重叠时,很显然重叠部分只需要保留最上方的绘制,例如我们的页面有一个背景,同时页面上还有一个卡片,那么显然,卡片下方是不需要有背景的,我们可以利用clipRect限定背景的绘制区域。从而减少这个卡片上的重新绘制。当然如果重新绘制区域不大可能不需要进行这种处理,因为现代手机的GPU性能较高,可能重绘带来的花销并不比计算裁剪的cpu花销高,所以需要我们在开发中进行适当取舍。

60.VSYNC

了解vsync之前先了解两个概念:刷新率(xxhz),表示硬件支持的刷新频率。帧率(xxfps),表示gpu每秒钟刷新重绘的次数。当刷新率和帧率不一致的时候,就会出现问题。

例如,屏幕正在绘制上一帧的图像,绘制到一半时。GPU缓冲区发生了变化,导致两张帧出现在同一个画面中。

所以解决这个问题就需要双缓冲的介入。在gpu绘制时,数据写入到backbuffer中,写入完毕,backbuffer会进入帧缓冲供屏幕绘制。周而复始。这个过程就需要vsync协助。

理想状态下,gpu绘制的帧数大于屏幕刷新频率,屏幕每次绘制完成,总会等到一个新的gpu绘制内容。但是如果屏幕刷新慢于gpu绘制,屏幕会在多个帧内保持同一个画面,也就出现了卡顿。

61.绘制相关

在我们打开gpu监控后,我们可以看到三种颜色的绘制bar

1.蓝色部分:它表示了java将绘制元素转化为gpu可以使用的格式,并将其转化为一个绘制列表的时间。如果这部分过高,可能是大量view不可用或者onDraw中有过多的操作。

2.红色部分:这部分表示Android用于渲染绘制列表所用的时间,安卓使用opengES api来进行绘制,这部分绘制需要将数据传输到gpu再进行绘制。一般来说,越复杂的view需要越复杂的opengl指令进行绘制。这也是红色部分升高的主要原因。集中的红色部分徒增可能是产生了大量重新绘制的视图。如视图旋转等。

3.橙色部分:这部分是处理时间,就是cpu通知gpu已经完成了一帧的渲染,这部分会产生阻塞,cpu会等待gpu返回的数据。这个栏目如果过长,说明gpu有很多事件需要处理。

62.60帧&16ms

63-64.Android UI与GPU

Android使用OPENGL ES api将绘制信息在cpu转换成gpu可识别格式,之后传递到gpu,由gpu进行光栅化绘制到屏幕上。实际上,从cpu传递数据到gpu是一个很耗时的操作,opengl允许数据在显存中保存,这样可以在后续用到时不再进行数据的传递。但是实际使用中,数据可能变化的极其复杂,如复杂的页面、各式各样的文字、甚至动画每一帧都在进行改变。

视图绘制的时候,安卓会生成一个内部对象,displaylist,其中包含gpu可用资源的集合、进行绘制的opengl命令等。当我们后续需要这个按钮进行一个变化的时候,只需要修改当前list,再进行一次绘制即可,但是当按钮和之前不一样的时候,我们就需要重绘操作。需要注意的是,有时候我们修改了一个视图,可能连带父视图以及其他子视图发生变化。如一个按钮的大小发生变化,会触发meaure过程,同时会触发重新layout,并影响到其他的组件。每一个环节都是需要耗时的,所以大规模的视图无效化可能导致帧丢失。比如一大组元素的不可见与一大组元素的出现,需要在开发中尤其注意。

65.

Android Performance Patterns 系列视频学习记录(持续更新中)相关推荐

  1. Android开发人员不得不收集的代码(持续更新中)(http://www.jianshu.com/p/72494773aace,原链接)

    Android开发人员不得不收集的代码(持续更新中) Blankj 关注 2016.07.31 04:22* 字数 370 阅读 102644评论 479喜欢 3033赞赏 14 utilcode D ...

  2. 人生最好的php,mysql,linux,redis,docker等相关技术经典面试题,新手收藏学习,持续更新中。。。

    php面试题 1.写出你能想到的所有HTTP返回状态值,并说明用途(比如:返回404表示找不到页面) # 200:服务器请求成功 # 301:永久重定向,旧网页已被新网页永久替代 # 302:表示临时 ...

  3. 阿里开发10年大牛:Android开发人员不得不收集的代码(持续更新中)

    前言 1.软件吃掉世界,而机器学习正吃掉软件 在数据爆炸的时代,如何创建「智能系统」成为焦点.这些应用程序内所体现的智能技术,并非是将实用指令添加到代码中,而是可以让软件自己去识别真实世界中发生的事件 ...

  4. typescript-----javascript的超集,typescript学习笔记持续更新中......

    Typescript,冲! Typescript 不是一门全新的语言,Typescript是 JavaScript 的超集,它对 JavaScript进行了一些规范和补充.使代码更加严谨. 一个特别好 ...

  5. [源码、文档、分享] iOS/iPhone学习系列、代码教程----~~~持续更新中~~~

    转自:http://www.devdiv.com/iOS_iPhone-iOS_iPhone%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97%E3%80%81%E4%BB%A3 ...

  6. Work Like Alibaba系列分享回顾整理(含演讲幻灯片、视频):持续更新中

    摘要: 2017年8月26日,在阿里巴巴西溪园区访客中心举行了第一期work like alibaba线下沙龙活动,会场近200人参与现场分享与学习. 2017年8月26日,在阿里巴巴西溪园区访客中心 ...

  7. iOS/iPhone学习系列、代码教程----~~~持续更新中~~~

    http://www.devdiv.com/forum.php?mod=viewthread&tid=48165   part 1--入门: 1. xcode 版本下载 以及 iphone s ...

  8. R语言绘图、数据处理学习记录持续更新

    目录 20220411--基础知识学习 20220412--读写操作和基本函数 20220415--循环语句学习 20220418--数据框的操作 20220419--可视化练习 20230107-- ...

  9. Python学习记录——持续更新

    python获取当前日期 time.strftime('%Y-%m-%d') python通过命令行传参在py文件中如何读取 通过sys模块中的 sys.argv可以访问到所有的命令行参数,返回值是包 ...

最新文章

  1. [Android1.5]DigitalClock自定义日期输出格式
  2. 虚拟机安装中文输入法
  3. 让linux服务器支持安全http协议(https)
  4. Python常见面试题:TCP 协议中的三次握手与四次挥手相关概念详解
  5. kafka自动提交offset失败:Auto offset commit failed
  6. 假如给Go语言加上注解,程序会变怎样?
  7. 微软开源 TensorFlow-DirectML,为 WSL2 提供 GPU 支持
  8. PCB设计流程图 思路清晰远比卖力苦干重要
  9. 什么是 NAS? 为什么要用 NAS?有什么好玩的功能?
  10. ARM与DSP的区别
  11. MySQL——插入语句
  12. 2021-07-01 Leetcode题解:545,915,1647,722
  13. 汇编作业:人均GDP
  14. 二手15年13寸MacBook Pro性价比有多高?网友表示值得买
  15. wps office 2005:不得不用的14绝技
  16. 阿里云服务器高主频内存型hfr7磁盘I/O性能表
  17. 75条笑死人的知乎神回复,用60行代码就爬完了
  18. 锐捷三层交换机配置DHCP
  19. 深度优先搜索和深度优先搜索的区别
  20. WSL 安装22.04 出现something went wrong错误

热门文章

  1. jsp 如何 返回数据库数据供前端访问 /简单的jsp接口如何编写 /jsp如何链接数据库
  2. plt.scatter legend
  3. 使用http-middle-ware中间件进行多个接口请求转发
  4. 沪深A股指数列表数据API接口(JSON标准格式,Get请求方式)
  5. 小功能⭐️解决Unity无法对一个物体上的所有材质球进行更改
  6. 低调做人,快乐一生!
  7. 虚拟机WIN98操作系统下安装trw显示不正常的处理方法
  8. 【百日冲大厂】第十篇,牛客网选择题+编程题井字棋+密码强度等级
  9. 中兴2022海外岗位招聘经验
  10. 2017年电赛综合测评题