<commit_before><commit_msg>Create blog.md<commit_after>![这里写图片描述](http://img.blog.csdn.net/20170730214723276?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29vZ2xlX2FjbWVy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)


看到咱们智课技术订阅号推送了好几遍精彩的技术分享了，写的都非常好，不过还没有移动端的文章，所以今天我这里也总结了一下我最近在做的一些东西，希望能抛砖引玉，吸引我们移动端的小伙伴投稿。本文实战性较强，欢迎各位喜欢移动开发的小伙伴来拍砖~

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


----------


# 动画效果
这是最近正在开发功能里的一个录音控件，我们的UI设计说效果做成英语流利说的效果，于是仿照它做了一个，相似度还是很高的：

![这里写图片描述](https://github.com/Jay-Goo/WaveLineView/blob/master/pictures/%E6%95%88%E6%9E%9C.gif)

----------


# 知识储备
 众所周知，一般自绘动画我们都是在View中实现的，一般会重写onMeasure（测量）、onLayout（布局）、onDraw（绘制）三个方法。
 
 Android系统为了简化线程开发，将这三个过程都放在主线程中执行，以保证绘制系统的线程安全。

 整个绘制过程通过一个Choreographer定时器驱动调用更新，每16ms会刷新一次，通过树状结构存储的 ViewGroup，依次递归的调用到每个 View 的 onMeasure、onLayout、onDraw 方法，从而最后将每个 View 都绘制出来（为了保证绘制效率，并不是每个View的这些方法每个绘制周期都会调用，那些没有变化的不会被重绘）。
 
  但是由于普通的 View 都处于主线程中，Android 除了绘制之外，在主线程中还需要处理用户的各种点击事件。很多情况，在主线程中还需要运行额外的用户处理逻辑、轮询消息事件等。 如果主线程过于繁忙，不能及时的处理和响应用户的输入，会让用户的体验急剧降低。如果更严重的情况，当主线程延迟时间达到5s的时候，还会触发 ANR（Application Not Responding）。 如果界面的绘制和动画比较复杂，计算量比较大的情况，就不再适合使用 View 这种方式来绘制了。

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


----------


# 动画实现
看了一下要实现的效果，感觉是由四条振幅不等的正弦曲线组成，这些曲线振幅中间比较高，两边比较低，应该有一个对称的衰减系数，然后这些曲线根据声音的大小上下波动，保持一个速率向右运动。

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

A 代表的是振幅，对应的波峰和波谷的高度，即 y 轴上的距离；ω 是角速度，换成频率是 2πf，能够控制波形的宽度；φ 是初始相位，能够决定正弦曲线的初始 x 轴位置；k 是偏距，能够控制在 y 轴上的偏移量

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

趁着给我们智课教育云的销售小伙伴们开会的功夫，我绘制了下这个大致的曲线，这里推荐一个绘图网站 [https://www.desmos.com/calculator](https://www.desmos.com/calculator)，他可以帮你将函数转换成相应的图形，十分方便。这里衰减函数和相应图形如图所示：

我们只要将每个点的x分别映射到衰减函数的一个对称区间，根据函数计算出相应的衰减系数，就可以实现振幅不同的波动曲线了。
最后通过一些调整，我们可以大致可以得出和目标效果图相似的曲线：

接下来，我们只需要在 SurfaceView 中使用 Path，通过上面的公式计算出一个个的点，然后画直线连接起来就行啦！效果如图所示：


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

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

 - 计算出函数曲线和对称衰减函数
 - 根据函数计算出需要绘制的点，通过Path连线
 - 根据时间改变曲线的φ，实现曲线位移效果
 - 根据volume和衰减函数改变振幅，实现曲线上下波动

# 动画优化
你以为效果实现就完事那就大错特错了！当我把动画运行到一个性能较差的低端机时，看到CPU的占有率达到30%多，有时候还会一卡一卡的时候，惊呆了……和想象的不一样啊！到底是哪里出了问题呢？
## 降低绘制密度
Ok，让我们review下绘制过程，第二步的时候我们需要计算曲线的点，然后通过Path连线这些点，而现在的手机屏幕1960x1080已经成为了标配，我们把宽度的像素点叫做采样点，那么如果我们把每个采样点的x代入函数求出y，那么我们每16ms都需要做出大量的计算，我们可以通过Android Studio的Monitor工具的CPU method trace查看到底是哪些方法占用了我的CPU：


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

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



## 减少重复实时计算量
虽然现在的设备的CPU已经足够强大，但是由于每16ms中，系统要做大量工作，为了保证动画流畅稳定，我们还是要尽量的减少一些重复的计算。

最常用的方法就是使用查表法，利用空间换时间（注意把握空间和时间的关系，莫要一味追求时间而牺牲大量内存空间，那么就得不偿失了）。

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

我们计算衰减函数的时候其实对于每次计算的固定的x，我们算出的衰减系数都是一样，我们可以把这些计算好的值直接放入表中，然后通过查表的方式，下次就不需要重复计算这些复杂运算了。关于存储，如果数量不是很多建议使用SparseArray，它可以避免自动装箱，节约不少时间。但是就本例而言，其实这种做法的优化空间不是很大，由于已经降低了绘制密度，并且衰减函数只有一个平方和除法比较费时，所以并没有耗时太多，统计了下，大约相差几ms左右，但这确实也是一个优化的好方向。

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

## 内存泄漏
CPU占有率降下去了，动画也流畅了，不过还有问题需要特别注意，那就是内存泄漏。

Android 在内存分配和释放方面，采用了 JAVA 的垃圾回收 GC 模式。 当分配的内存不再使用的时候，系统会定时帮我们自动清理。这给我们应用开发带来了极大的便利，我们从此不再需要过多的关注内存的分配与回收，也因此减少很多内存使用的风险。但是由于一些不正确的操作，当一个对象已经不需要再使用了，本该被回收时，而另外一个正在使用的对象持有它的引用从而导致它不能被回收，这就导致本该被回收的对象不能被回收而停留在堆内存中，内存泄漏就产生了，内存泄漏时导致程序OOM的原因之一，而OOM就意味着Crash。

常见的内存泄漏检测方法是通过手动GC以及监听Java heap的情况，通过查看Reference Tree的层级确定是否内存泄漏，通过MAT工具，分析具体泄漏原因，但是，不得不说，这个方法确实很复杂，如果不是很熟练，很难发现隐藏的一些内存泄漏，这里推荐使用LeakCanary，通过代码接入的方式，监听内存泄漏，它会以插件的形式伴随程序一起，如果发生内存泄漏，LeakCanary会给出泄漏的层级，十分清晰。

就本例，我通过多次创建销毁Activity，检测程序是否发生了内存泄漏。
结果LeakCanary提示程序发生了内存泄漏，如图所示
RenderThread持有了Activity的隐式context，导致Activity不能释放资源，
追踪到代码，我们发现这样一段代码：

```
 public RenderThread(SurfaceHolder holder) {
            super("RenderThread");
			surfaceHolder = holder 
 }
```
在 Java 中，非静态匿名内部类会持有其外部类的隐式引用，所以RenderThread所持有的context就是holder和它的Runable方法持有的引用，而Activity销毁时，因为Thread的持有强引用导致无法及时的释放掉内存，从而导致内存泄漏。
解决方案就是，将RenderThread改为私有的静态内部类，这样它便不会持有其外部类的引用，另外可以对surfaceHolder使用弱引用，确保GC可以及时释放掉holder。

```
            surfaceHolder = new WeakReference<>(holder);

```
获取surfaceHolder时可以使用（注意判空）`surfaceHolder.get()`
## 优化内存
尽量减少内存的分配次数，因为每次GC都是会耗一定时间的，如果GC的频率过高也会引起动画有卡顿感，而且减少内存的分配次数还可以有效的避免产生内存抖动问题。
这里其实已经做得不错，主要是总结下一些常见的方案：

 1. 减少对象的重复创建，例如Paint，Path，Rect等
 2. 减少大量临时对象的创建，对于那些无法避免，每次又必须分配的，我们可以采用对象池模型的方式来分配对象。对象池来解决频繁创建与销毁的问题，但是这里需要注意结束使用之后，需要手动释放对象池中的对象。
 3. 减少一些资源操作，例如getColor，这个方法中会创建多个 StringBuilder 的变量
# 总结
通过一系列的优化，动画在一些低端手机CPU占有率在控制在10%左右，内存在15MB左右，在中端手机上CPU稳定在2~3%左右，内存在2MB左右。动画都十分流畅。

限于笔者的水平和经验有限，如果有很多纰漏和错误的地方，欢迎大家指出。如果大家有更多更好的建议，欢迎一起分享讨论，共同进步。同时我也是咱们智课技术订阅号的运维人员，如果大家有好的文章，欢迎踊跃投稿！
# Github
[https://github.com/Jay-Goo/WaveLineView](https://github.com/Jay-Goo/WaveLineView)
# 关于作者
部门：智课教育云事业部
项目：负责Android智课批改App的开发与维护
作者：谷进杰
QQ：1015121748

{'fraction_non_alphanumeric': 0.09431425278524778, 'fraction_numerical': 0.017479830964271994, 'mean_word_length': 14.451038575667656, 'pattern_counts': {'":': 0, '<': 4, '<?xml version=': 0, '>': 4, 'https://': 5, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3410302', 'n_tokens_mistral': 4462, 'n_tokens_neox': 5031, 'n_words': 126}