Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
JinJieGu committed Nov 9, 2017
2 parents e96d29f + 2964c23 commit b29a08c
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 20 deletions.
75 changes: 74 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,75 @@
# WaveLineView
a beatiful wave line animation
## 一款内存友好的录音漂亮的波浪动画

# 效果图(实际效果更好)

![image](https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/%E6%95%88%E6%9E%9C.gif)

----------

# Usage
## Step1
```
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.Jay-Goo:WaveLineView:v1.0.2'
}
```
## Step2

```
<jaygoo.widget.wlv.WaveLineView
android:id="@+id/waveLineView"
android:layout_width="match_parent"
android:layout_height="120dp"
app:wlvBackgroundColor="@android:color/white"
app:wlvMoveSpeed="290"
/>
```
## Step3

```
waveLineView.startAnim();
waveLineView.stopAnim();
```

```
@Override
protected void onResume() {
super.onResume();
waveLineView.onResume();
}
@Override
protected void onPause() {
super.onPause();
waveLineView.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
waveLineView.release();
}
```

----------
# Attributes
attr | format | description
-------- | ---|---
backgroundColor|color|背景色
wlvLineColor|color|波浪线的颜色
wlvThickLineWidth|dimension|中间粗波浪曲线的宽度
wlvFineLineWidth|dimension|三条细波浪曲线的宽度
wlvMoveSpeed|float|波浪线移动的速度,默认值为290F,方向从左向右,你可以使用负数改变移动方向
wlvSamplingSize|integer|采样率,动画效果越大越精细,默认64
wlvSensibility|integer|灵敏度,范围[1,10],越大越灵敏,默认值为5

## [原理讲解传送门](https://github.com/Jay-Goo/WaveLineView/blob/master/blog.md)
32 changes: 13 additions & 19 deletions blog.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
![这里写图片描述](https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/logo.jpg)


看到咱们智课技术订阅号推送了好几遍精彩的技术分享了,写的都非常好,不过还没有移动端的文章,所以今天我这里也总结了一下我最近在做的一些东西,希望能抛砖引玉,吸引我们优秀的移动端小伙伴投稿。

# 前言
本文实战性较强,主要目的是通过一个自定义控件的开发,引出我对自定义控件性能优化的一些思考和实践,欢迎各位喜欢移动开发的小伙伴来拍砖~

本文由于篇幅有限,只讲解思路,并没有放出大量源代码,如果对本项目感兴趣,文末会放出Demo,可以自行去Github上fork和star。
Expand All @@ -26,7 +25,7 @@

整个绘制过程通过一个Choreographer定时器驱动调用更新,每16ms会刷新一次,通过树状结构存储的 ViewGroup,依次递归的调用到每个 View 的 onMeasure、onLayout、onDraw 方法,从而最后将每个 View 都绘制出来(为了保证绘制效率,并不是每个View的这些方法每个绘制周期都会调用,那些没有变化的不会被重绘)。

但是由于普通的 View 都处于主线程中,Android 除了绘制之外,在主线程中还需要处理用户的各种点击事件。很多情况,在主线程中还需要运行额外的用户处理逻辑、轮询消息事件等。 如果主线程过于繁忙,不能及时的处理和响应用户的输入,会让用户的体验急剧降低。如果更严重的情况,当主线程延迟时间达到5s的时候,还会触发 ANR(Application Not Responding)。 如果界面的绘制和动画比较复杂,计算量比较大的情况,就不再适合使用 View 这种方式来绘制了。
但是由于普通的 View 都处于主线程中,Android 除了绘制之外,在主线程中还需要处理用户的各种点击事件。很多情况,在主线程中还需要运行额外的用户处理逻辑、轮询消息事件等。 如果主线程过于繁忙,不能及时的处理和响应用户的输入,会让用户的体验急剧降低。如果更严重的情况,当主线程延迟时间达到5s的时候,还会触发 ANR(Application Not Responding)。 如果界面的绘制和动画比较复杂,计算量比较大的情况,就不再适合使用 View 这种方式来绘制了。

Android考虑到这种场景,提出了SurfaceView机制,它可以在非主线程进行图形绘制,释放了主线程的压力,所以我们可以把View的绘制放到SurfaceView中完成。如果对SurfaceView不太熟悉,可以自行百度,或参看demo中封装好的RenderView,这里由于篇幅限制,这里就不作详细介绍了。

Expand All @@ -37,13 +36,11 @@ Android考虑到这种场景,提出了SurfaceView机制,它可以在非主
# 动画实现
看了一下要实现的效果,感觉是由四条振幅不等的正弦曲线组成,这些曲线振幅中间比较高,两边比较低,应该有一个对称的衰减系数,然后这些曲线根据声音的大小上下波动,保持一个速率向右运动。

这里再取回还给高中老师的数学知识,下面是正弦曲线的公式:y=Asin(ωx+φ)+k

A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离;ω 是角速度,换成频率是 2πf,能够控制波形的宽度;φ 是初始相位,能够决定正弦曲线的初始 x 轴位置;k 是偏距,能够控制在 y 轴上的偏移量
这里再取回还给高中老师的数学知识,下面是正弦曲线的公式:y=Asin(ωx+φ)+k,其中A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离;ω 是角速度,换成频率是 2πf,能够控制波形的宽度;φ 是初始相位,能够决定正弦曲线的初始 x 轴位置;k 是偏距,能够控制在 y 轴上的偏移量。

那么我们只需根据时间改变φ,那么曲线就可以实现移动,通过一个对称衰减函数乘以A,就可实现曲线衰减变化,通过改变A的值可以实现上下波动。恩,完美,开干!

趁着给我们智课教育云的销售小伙伴们培训开会的功夫,我绘制调整了下大致的曲线函数,这里我要推荐大家一个绘图网站 [https://www.desmos.com/calculator](https://www.desmos.com/calculator),它可以帮你将函数转换成相应的图形,十分方便。
这里我要推荐大家一个绘图网站 [https://www.desmos.com/calculator](https://www.desmos.com/calculator) ,它可以帮你将函数转换成相应的图形,十分方便。
函数很简单,正弦就行,主要是衰减函数的选取,这里要找一个对称的衰减函数,如图所示:
![衰减函数](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)
我们只要将每个点的x分别映射到衰减函数的一个对称区间,根据函数计算出相应的衰减系数,就可以实现振幅不同的波动曲线了。
Expand All @@ -52,10 +49,9 @@ A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离
接下来,我们只需要在 SurfaceView 中使用 Path,通过上面的公式计算出一个个的点,然后画直线连接起来就行啦!效果如图所示:
![曲线函数](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)

然后就是让它动起来了,前面也说了,可以根据时间改变曲线的相位值φ来实现移动,我在封装好的RenderView中实现了一个叫做onRender的方法,它主要是代替onDraw工作的,我们传入时间,然后让时间除以一个位移系数当φ,于是就让曲线实现了位移效果。然后再给一个volume变量,volume乘以一个初始振幅amplitude和根据横坐标算出的衰减系数,作为纵坐标,便实现了曲线形状和根据声音大小波动的效果。
然后就是让它动起来了,前面也说了,可以根据时间改变曲线的相位值φ来实现移动,我在封装好的RenderView中实现了一个叫做onRender的方法,它主要是代替onDraw工作的,我们传入时间millisPassed,定义位移系数offsetSpeed,那么相位值φ = π * millisPassed / offsetSpeed,每次渲染周期都将φ代入函数就可以让曲线实现位移效果了。最后再给一个volume变量,volume乘以一个初始振幅amplitude和根据横坐标算出的衰减系数,作为纵坐标,便实现了曲线形状和根据声音大小波动的效果。

这里再总结下大致实现步骤:

- 计算出函数曲线和对称衰减函数
- 根据函数计算出需要绘制的点,通过Path连线
- 根据时间改变曲线的φ,实现曲线位移效果
Expand All @@ -72,7 +68,7 @@ A 代表的是振幅,对应的波峰和波谷的高度,即 y 轴上的距离
## 降低绘制密度
Ok,让我们review下绘制过程,第二步的时候我们需要计算曲线的点,然后通过Path连线这些点,而现在的手机屏幕1960x1080已经成为了标配,如果我们把宽度的像素点叫做采样点,每次我们要把每个采样点的x代入函数求出y,然后调用lineTo连线,那么我们每16ms都需要做出大量的计算。

但是事实上人的肉眼是有一定容忍度的,特别是快速运动的动画,一些失真的地方,肉眼很难分辨,所以我们没必要把1080个点每个都算出来,经过试验发现我们只要在60个以上的采样点,效果还是十分的平滑,粗略计算,这样做可以将计算量减少到原来的1/16,于是可以释放大量的CPU时间(由于采样点的减少,图形会出现锯齿,我们可以通过Paint的抗锯齿属性优化)
但是事实上人的肉眼是有一定容忍度的,特别是快速运动的动画,一些失真的地方,肉眼很难分辨,所以我们没必要把1080个点每个都算出来,经过试验发现我们只要在60个以上的采样点,效果还是十分的平滑,粗略计算,这样做可以将计算量减少到原来的1/16,于是可以释放大量的CPU时间(由于采样点的减少,图形会出现锯齿,我们可以通过Paint的抗锯齿属性优化)

总结:通过动态调节自定义的绘制密度,在绘制密度与最终实现效果中找到一个平衡点(即不影响最后的视觉效果,同时还能最大限度的减少计算量),这个是最直接,也最简单的优化方法。

Expand All @@ -85,12 +81,12 @@ Ok,让我们review下绘制过程,第二步的时候我们需要计算曲线

学过计算机的都知道,CPU在计算加减乘是非常快的,但是除法是比较慢的,特别是浮点数除法,我们可以将这些浮点运算转换成整数除法,除数、被除数乘以一个统一的精确度,用到时再除以精确度,这个方法在大量浮点计算时是很有效的,但是注意处理整形溢出。另外还要避免一些乘方、开平方根等运算的重复计算。

就本例来讲,calcValue方法是为了计算每个点代表的衰减系数,但其实我们计算衰减函数的时候对于每次固定的x,我们算出的衰减系数都是一样,这就会产生大量重复的计算。
我们可以把这些计算好的值直接放入表中,然后通过查表的方式,下次就不需要重复计算这些复杂运算了。关于存储,如果数量不是很多建议使用SparseArray,它可以避免自动装箱,节约不少时间。理论上是这样的,但其实由于本例的衰减函数不是很复杂,这种做法的优化空间并不是很大,而且由于前面已经降低了绘制密度,已经减少了大量的计算,统计了下,耗时节约了几ms左右,但这确实也是一个优化的好方向,特别是一些复杂的运算,还是很有意义的。
就本例来讲,calcValue方法是为了计算每个点代表的衰减系数,但其实我们计算衰减函数的时候对于每次固定的x,我们算出的衰减系数都是一样,这就会产生大量重复的计算。我们可以把这些计算好的值直接放入表中,然后通过查表的方式,下次就不需要重复计算这些复杂运算了。关于存储,如果数量不是很多建议使用SparseArray,它可以避免自动装箱,节约不少时间。理论上是这样的,但其实由于本例的衰减函数不是很复杂,这种做法的优化空间并不是很大,而且由于前面已经降低了绘制密度,已经减少了大量的计算,统计了下,耗时节约了几ms左右,但这确实也是一个优化的好方向,特别是一些复杂的运算,还是很有意义的。

总结:尽量减少重复运算,对重复复杂的计算,可以适当使用空间换时间。尽量减少浮点数除法运算。

经过前两步的优化,再看一下目前的CPU trace,发现已经降低了很多,动画也流畅了起来。

![这里写图片描述](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)

## 内存泄漏
Expand All @@ -114,6 +110,7 @@ RenderThread持有了Activity的隐式context,导致Activity不能释放资源
}
```
在 Java 中,非静态匿名内部类会持有其外部类的隐式引用,所以RenderThread所持有的context就是holder和它的Runable方法持有的引用,而Activity销毁时,因为Thread的持有强引用导致无法及时的释放掉内存,从而导致内存泄漏。

解决方案就是,将RenderThread改为私有的静态内部类,这样它便不会持有其外部类的引用,另外可以对surfaceHolder使用弱引用,确保GC可以及时释放掉holder。

```
Expand All @@ -131,20 +128,17 @@ RenderThread持有了Activity的隐式context,导致Activity不能释放资源
1. 减少对象的重复创建,例如Paint,Path,Rect等
2. 减少大量临时对象的创建,对于那些无法避免,每次又必须分配的,我们可以采用对象池模型的方式来分配对象。对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
3. 减少一些资源操作,例如getColor,这个方法中会创建多个 StringBuilder 的变量

# 总结
通过一系列的优化,动画在中端手机上CPU稳定在2~3%左右,内存在2MB左右,在一些低端手机CPU占有率在控制在10%左右,内存在15MB左右(为什么内存这么高?我还没有研究),不过欣慰的是两者动画都十分流畅。

本文介绍了从需求开始,如何一步步开发一个自定义控件,并通过降低绘制密度、减少重复实时计算量、避免和解决内存泄漏、如何优化内存等四方面对控件的性能进行了优化,希望能给大家平时开发工作带来一些启发和帮助,也希望大家可以提出更多更好的优化方案~

限于笔者的水平和经验有限,本文如果有纰漏和错误的地方,欢迎大家指出。如果大家有更多更好的建议,欢迎一起分享讨论,共同进步。同时我也是咱们智课技术订阅号的运维人员,如果大家有好的文章,欢迎踊跃投稿!
限于笔者的水平和经验有限,本文如果有纰漏和错误的地方,欢迎大家指出。如果大家有更多更好的建议,欢迎一起分享讨论,共同进步。

# Github
[https://github.com/Jay-Goo/WaveLineView](https://github.com/Jay-Goo/WaveLineView)
# 关于作者

- 部门:智课教育云事业部
- 项目:负责Android智课批改App的开发与维护
- 作者:谷进杰
- QQ:1015121748
欢迎各位Star,Mark



Expand Down

0 comments on commit b29a08c

Please sign in to comment.