11![ 这里写图片描述] ( https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/logo.jpg )
22
33
4- 看到咱们智课技术订阅号推送了好几遍精彩的技术分享了,写的都非常好,不过还没有移动端的文章,所以今天我这里也总结了一下我最近在做的一些东西,希望能抛砖引玉,吸引我们优秀的移动端小伙伴投稿。
5-
4+ # 前言
65本文实战性较强,主要目的是通过一个自定义控件的开发,引出我对自定义控件性能优化的一些思考和实践,欢迎各位喜欢移动开发的小伙伴来拍砖~
76
87本文由于篇幅有限,只讲解思路,并没有放出大量源代码,如果对本项目感兴趣,文末会放出Demo,可以自行去Github上fork和star。
2625
2726 整个绘制过程通过一个Choreographer定时器驱动调用更新,每16ms会刷新一次,通过树状结构存储的 ViewGroup,依次递归的调用到每个 View 的 onMeasure、onLayout、onDraw 方法,从而最后将每个 View 都绘制出来(为了保证绘制效率,并不是每个View的这些方法每个绘制周期都会调用,那些没有变化的不会被重绘)。
2827
29- 但是由于普通的 View 都处于主线程中,Android 除了绘制之外,在主线程中还需要处理用户的各种点击事件。很多情况,在主线程中还需要运行额外的用户处理逻辑、轮询消息事件等。 如果主线程过于繁忙,不能及时的处理和响应用户的输入,会让用户的体验急剧降低。如果更严重的情况,当主线程延迟时间达到5s的时候,还会触发 ANR(Application Not Responding)。 如果界面的绘制和动画比较复杂,计算量比较大的情况,就不再适合使用 View 这种方式来绘制了。
28+ 但是由于普通的 View 都处于主线程中,Android 除了绘制之外,在主线程中还需要处理用户的各种点击事件。很多情况,在主线程中还需要运行额外的用户处理逻辑、轮询消息事件等。 如果主线程过于繁忙,不能及时的处理和响应用户的输入,会让用户的体验急剧降低。如果更严重的情况,当主线程延迟时间达到5s的时候,还会触发 ANR(Application Not Responding)。 如果界面的绘制和动画比较复杂,计算量比较大的情况,就不再适合使用 View 这种方式来绘制了。
3029
3130Android考虑到这种场景,提出了SurfaceView机制,它可以在非主线程进行图形绘制,释放了主线程的压力,所以我们可以把View的绘制放到SurfaceView中完成。如果对SurfaceView不太熟悉,可以自行百度,或参看demo中封装好的RenderView,这里由于篇幅限制,这里就不作详细介绍了。
3231
@@ -37,13 +36,11 @@ Android考虑到这种场景,提出了SurfaceView机制,它可以在非主
3736# 动画实现
3837看了一下要实现的效果,感觉是由四条振幅不等的正弦曲线组成,这些曲线振幅中间比较高,两边比较低,应该有一个对称的衰减系数,然后这些曲线根据声音的大小上下波动,保持一个速率向右运动。
3938
40- 这里再取回还给高中老师的数学知识,下面是正弦曲线的公式:y=Asin(ωx+φ)+k
41-
42- A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离;ω 是角速度,换成频率是 2πf,能够控制波形的宽度;φ 是初始相位,能够决定正弦曲线的初始 x 轴位置;k 是偏距,能够控制在 y 轴上的偏移量
39+ 这里再取回还给高中老师的数学知识,下面是正弦曲线的公式:y=Asin(ωx+φ)+k,其中A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离;ω 是角速度,换成频率是 2πf,能够控制波形的宽度;φ 是初始相位,能够决定正弦曲线的初始 x 轴位置;k 是偏距,能够控制在 y 轴上的偏移量。
4340
4441那么我们只需根据时间改变φ,那么曲线就可以实现移动,通过一个对称衰减函数乘以A,就可实现曲线衰减变化,通过改变A的值可以实现上下波动。恩,完美,开干!
4542
46- 趁着给我们智课教育云的销售小伙伴们培训开会的功夫,我绘制调整了下大致的曲线函数, 这里我要推荐大家一个绘图网站 [ https://www.desmos.com/calculator ] ( https://www.desmos.com/calculator ) ,它可以帮你将函数转换成相应的图形,十分方便。
43+ 这里我要推荐大家一个绘图网站 [ https://www.desmos.com/calculator ] ( https://www.desmos.com/calculator ) ,它可以帮你将函数转换成相应的图形,十分方便。
4744函数很简单,正弦就行,主要是衰减函数的选取,这里要找一个对称的衰减函数,如图所示:
4845![ 衰减函数] ( https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/%E8%A1%B0%E5%87%8F%E5%87%BD%E6%95%B0%E5%9B%BE.png )
4946我们只要将每个点的x分别映射到衰减函数的一个对称区间,根据函数计算出相应的衰减系数,就可以实现振幅不同的波动曲线了。
@@ -52,10 +49,9 @@ A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离
5249接下来,我们只需要在 SurfaceView 中使用 Path,通过上面的公式计算出一个个的点,然后画直线连接起来就行啦!效果如图所示:
5350![ 曲线函数] ( https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/%E9%9D%99%E6%80%81%E6%95%88%E6%9E%9C%E5%9B%BE.png )
5451
55- 然后就是让它动起来了,前面也说了,可以根据时间改变曲线的相位值φ来实现移动,我在封装好的RenderView中实现了一个叫做onRender的方法,它主要是代替onDraw工作的,我们传入时间,然后让时间除以一个位移系数当φ,于是就让曲线实现了位移效果。然后再给一个volume变量 ,volume乘以一个初始振幅amplitude和根据横坐标算出的衰减系数,作为纵坐标,便实现了曲线形状和根据声音大小波动的效果。
52+ 然后就是让它动起来了,前面也说了,可以根据时间改变曲线的相位值φ来实现移动,我在封装好的RenderView中实现了一个叫做onRender的方法,它主要是代替onDraw工作的,我们传入时间millisPassed,定义位移系数offsetSpeed,那么相位值φ = π * millisPassed / offsetSpeed,每次渲染周期都将φ代入函数就可以让曲线实现位移效果了。最后再给一个volume变量 ,volume乘以一个初始振幅amplitude和根据横坐标算出的衰减系数,作为纵坐标,便实现了曲线形状和根据声音大小波动的效果。
5653
5754这里再总结下大致实现步骤:
58-
5955 - 计算出函数曲线和对称衰减函数
6056 - 根据函数计算出需要绘制的点,通过Path连线
6157 - 根据时间改变曲线的φ,实现曲线位移效果
@@ -72,7 +68,7 @@ A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离
7268## 降低绘制密度
7369Ok,让我们review下绘制过程,第二步的时候我们需要计算曲线的点,然后通过Path连线这些点,而现在的手机屏幕1960x1080已经成为了标配,如果我们把宽度的像素点叫做采样点,每次我们要把每个采样点的x代入函数求出y,然后调用lineTo连线,那么我们每16ms都需要做出大量的计算。
7470
75- 但是事实上人的肉眼是有一定容忍度的,特别是快速运动的动画,一些失真的地方,肉眼很难分辨,所以我们没必要把1080个点每个都算出来,经过试验发现我们只要在60个以上的采样点,效果还是十分的平滑,粗略计算,这样做可以将计算量减少到原来的1/16,于是可以释放大量的CPU时间。 (由于采样点的减少,图形会出现锯齿,我们可以通过Paint的抗锯齿属性优化)
71+ 但是事实上人的肉眼是有一定容忍度的,特别是快速运动的动画,一些失真的地方,肉眼很难分辨,所以我们没必要把1080个点每个都算出来,经过试验发现我们只要在60个以上的采样点,效果还是十分的平滑,粗略计算,这样做可以将计算量减少到原来的1/16,于是可以释放大量的CPU时间(由于采样点的减少,图形会出现锯齿,我们可以通过Paint的抗锯齿属性优化)。
7672
7773总结:通过动态调节自定义的绘制密度,在绘制密度与最终实现效果中找到一个平衡点(即不影响最后的视觉效果,同时还能最大限度的减少计算量),这个是最直接,也最简单的优化方法。
7874
@@ -85,12 +81,12 @@ Ok,让我们review下绘制过程,第二步的时候我们需要计算曲线
8581
8682学过计算机的都知道,CPU在计算加减乘是非常快的,但是除法是比较慢的,特别是浮点数除法,我们可以将这些浮点运算转换成整数除法,除数、被除数乘以一个统一的精确度,用到时再除以精确度,这个方法在大量浮点计算时是很有效的,但是注意处理整形溢出。另外还要避免一些乘方、开平方根等运算的重复计算。
8783
88- 就本例来讲,calcValue方法是为了计算每个点代表的衰减系数,但其实我们计算衰减函数的时候对于每次固定的x,我们算出的衰减系数都是一样,这就会产生大量重复的计算。
89- 我们可以把这些计算好的值直接放入表中,然后通过查表的方式,下次就不需要重复计算这些复杂运算了。关于存储,如果数量不是很多建议使用SparseArray,它可以避免自动装箱,节约不少时间。理论上是这样的,但其实由于本例的衰减函数不是很复杂,这种做法的优化空间并不是很大,而且由于前面已经降低了绘制密度,已经减少了大量的计算,统计了下,耗时节约了几ms左右,但这确实也是一个优化的好方向,特别是一些复杂的运算,还是很有意义的。
84+ 就本例来讲,calcValue方法是为了计算每个点代表的衰减系数,但其实我们计算衰减函数的时候对于每次固定的x,我们算出的衰减系数都是一样,这就会产生大量重复的计算。我们可以把这些计算好的值直接放入表中,然后通过查表的方式,下次就不需要重复计算这些复杂运算了。关于存储,如果数量不是很多建议使用SparseArray,它可以避免自动装箱,节约不少时间。理论上是这样的,但其实由于本例的衰减函数不是很复杂,这种做法的优化空间并不是很大,而且由于前面已经降低了绘制密度,已经减少了大量的计算,统计了下,耗时节约了几ms左右,但这确实也是一个优化的好方向,特别是一些复杂的运算,还是很有意义的。
9085
9186总结:尽量减少重复运算,对重复复杂的计算,可以适当使用空间换时间。尽量减少浮点数除法运算。
9287
9388经过前两步的优化,再看一下目前的CPU trace,发现已经降低了很多,动画也流畅了起来。
89+
9490![ 这里写图片描述] ( https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/%E4%BC%98%E5%8C%96%E5%90%8E%E6%95%88%E6%9E%9C%E5%9B%BE.png )
9591
9692## 内存泄漏
@@ -114,6 +110,7 @@ RenderThread持有了Activity的隐式context,导致Activity不能释放资源
114110 }
115111```
116112在 Java 中,非静态匿名内部类会持有其外部类的隐式引用,所以RenderThread所持有的context就是holder和它的Runable方法持有的引用,而Activity销毁时,因为Thread的持有强引用导致无法及时的释放掉内存,从而导致内存泄漏。
113+
117114解决方案就是,将RenderThread改为私有的静态内部类,这样它便不会持有其外部类的引用,另外可以对surfaceHolder使用弱引用,确保GC可以及时释放掉holder。
118115
119116```
@@ -131,20 +128,17 @@ RenderThread持有了Activity的隐式context,导致Activity不能释放资源
131128 1 . 减少对象的重复创建,例如Paint,Path,Rect等
132129 2 . 减少大量临时对象的创建,对于那些无法避免,每次又必须分配的,我们可以采用对象池模型的方式来分配对象。对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
133130 3 . 减少一些资源操作,例如getColor,这个方法中会创建多个 StringBuilder 的变量
131+
134132# 总结
135133通过一系列的优化,动画在中端手机上CPU稳定在2~ 3%左右,内存在2MB左右,在一些低端手机CPU占有率在控制在10%左右,内存在15MB左右(为什么内存这么高?我还没有研究),不过欣慰的是两者动画都十分流畅。
136134
137135本文介绍了从需求开始,如何一步步开发一个自定义控件,并通过降低绘制密度、减少重复实时计算量、避免和解决内存泄漏、如何优化内存等四方面对控件的性能进行了优化,希望能给大家平时开发工作带来一些启发和帮助,也希望大家可以提出更多更好的优化方案~
138136
139- 限于笔者的水平和经验有限,本文如果有纰漏和错误的地方,欢迎大家指出。如果大家有更多更好的建议,欢迎一起分享讨论,共同进步。同时我也是咱们智课技术订阅号的运维人员,如果大家有好的文章,欢迎踊跃投稿!
137+ 限于笔者的水平和经验有限,本文如果有纰漏和错误的地方,欢迎大家指出。如果大家有更多更好的建议,欢迎一起分享讨论,共同进步。
138+
140139# Github
141140[ https://github.com/Jay-Goo/WaveLineView ] ( https://github.com/Jay-Goo/WaveLineView )
142- # 关于作者
143-
144- - 部门:智课教育云事业部
145- - 项目:负责Android智课批改App的开发与维护
146- - 作者:谷进杰
147- - QQ:1015121748
141+ 欢迎各位Star,Mark
148142
149143
150144
0 commit comments