篇章目标介绍
之前看到网易云,酷我音乐都发布过用于播放器页面粒子动效的效果,之前写的第一篇文章已经重点介绍了粒子动效实现的核心问题和完成效果的主要代码介绍;计划在第二篇文章针对粒子动效的资源占用进行优化和完善UI展示效果。本文是第二篇文章,负责性能优化的方案说明,需要解决第一阶段完成的DEMO占用CPU高达108%的问题,期望降低至50%以内
性能测试方法
可以基于cmd命令窗口使用top命令测试。首先进入adb shell模式
adb shell
然后使用top命令测试,-d后的数字表示测试周期,grep后表示过滤用的包名,以下命令可以实现周期5s查看指定报名的CPU占用
top -d 5 | grep com.guo.loading+
测试情况如下
可以看到改善前CPU占用为105%~108%区间
性能优化方案
要想降低CPU消耗需要从降低运算复杂度和降低运算频率两个方向展开,在优化过程中一共提炼出如下图所示的6个优化点。
因为整个动效当中最为耗费资源的两处分别是绘制可以旋转的圆形专辑图,和绘制大量的粒子,经过测算两处分别占据CPU开销的30%和70%。圆形专辑图是需要基于裁剪生成的圆形图片,设置Matrix旋转来达到旋转的效果,生产圆形图片这一步骤是可以避免重复处理的。方案当中需要绘制的是约300个左右的粒子,由于动效过程中需要从粒子集移除超出显示边界的粒子,同时不断的补充新的粒子,所以粒子集是有大量的增加和删除节点的操作,这种操作模式下高效的数据结构就是链表,因此排除了使用数组的考虑。粒子部分的优化点基本就是控制粒子群大小,控制粒子群刷新频率。
性能优化代码
1.粒子寿命算法优化:原方案中寿命是通过每次进行是否超出边界判断,移动速度是否降为0,透明度是否降为0这3个条件判断,其中超出边界中使用了多个Math.cos,Math.sin和Math.sqrt多个高精度的运算。因为粒子运行方向确定,速度可以预估,可以思路是设置一个变量age表示寿命,在构建粒子时事先计算好寿命值,每次刷新粒子age减小。
//计算age值
int moveAge = (int)Math.sqrt(Math.pow(mOutSideStarRadius, 2)*1.0f - (Math.pow(x - width/2, 2) + Math.pow(y - height/2, 2))*1.0f) / velocity + 1;
int alphaAge = alpha / 30 + 1;
age = Math.min(moveAge, alphaAge);
2.控制粒子数量:在保证显示效果的前提下,适当降低粒子密度,将原来平均间隔10个像素放置一组粒子组改为间隔15个像素放置
//平均间隔15个像素的圆环分配一个粒子组
PARTICLE_GROUP_SIZE = (int)(mAlbumCircleRadius * 2 *Math.PI / 15);
3.避免重复建圆形Bitmap和提取主题色:在初始实现效果当中未对已获取有效的圆形Bitmap进行判断,导致了重复的计算
//避免重复生产圆形图片和提取主题色
if(null == roundBitmap){
//将ImageView的原图裁剪成圆形图片
Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
roundBitmap = RoundBitmapUtil.createRoundBitmap(bitmap, mInsideImageRadius);
//提取主题色
int colorTheme = BitmapColorUtil.extractColor(bitmap);
mCirclePaint.setColor(colorTheme);
mParticlePaint.setColor(colorTheme);
mBubblePaint.setColor(colorTheme);
}
4.降低SQRT计算频率:由于粒子速度是x向和y向组合产生的位移,那么在分配了粒子运动速度后,需要转化为x向和y向的位置,当中涉及一定运算量的系数,为了避免重复计算,该系统设置为一个内部变量,在创建粒子时生成结果,后续直接使用.
sqrt = (float) (1.0f / Math.sqrt(1 + Math.pow(slope, 2)));
5.取消抗锯齿:由于抗锯齿在绘制中也存在一定的消耗,本次绘制的都是圆形,且没有渐变色,因此决定取消抗锯齿,产生了较为明显的效果
mParticlePaint.setAntiAlias(false);
6.降低粒子补充的频率:由于刷新过程中也需要不断的补充新的粒子,因此降低补充的频率也尤为重要,前提是需要保证显示的效果。优化过程中尝试将原来的33ms刷新一次调整为100ms刷新一次
//100ms后更新粒子位置和气泡位置
postDelayed(new Runnable() {
@Override
public void run() {
changeParticleGroup();
changeBubbleGroup();
//补充粒子
initParticleGroup(PARTICLE_GROUP_SIZE);
//补充气泡
initBubbleGroup(PARTICLE_GROUP_SIZE / 60);
}
}, 100);
上述即是优化要点部分的主要代码,现在再共享下优化之后的整体的代码
初始化粒子群大小的测量代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
//计算流星最外轨道半径
mOutSideStarRadius = Math.min(width, height) / 2 * 9 / 10;
//计算中心原图的半径
mInsideImageRadius = mOutSideStarRadius * 2 / 3;
//计算适合的粒子群个数
mAlbumCircleRadius = (mInsideImageRadius + 20) < mOutSideStarRadius ? (mInsideImageRadius + 20) : (mInsideImageRadius + (int)mCirclePaint.getStrokeWidth()/2);
//平均间隔15个像素的圆环分配一个粒子组
PARTICLE_GROUP_SIZE = (int)(mAlbumCircleRadius * 2 *Math.PI / 15);
//初始化粒子群
initParticleGroup(PARTICLE_GROUP_SIZE);
//初始化气泡群
initBubbleGroup(PARTICLE_GROUP_SIZE / 100);
}
绘制代码
@Override
protected void onDraw(Canvas canvas) {
//设置画布透明
canvas.drawARGB(0,0,0,0);
//绘制中间的圆形图片
Drawable drawable = getDrawable();
if(null == drawable){
return;
}
//避免重复生产圆形图片和提取主题色
if(null == roundBitmap){
//将ImageView的原图裁剪成圆形图片
Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
roundBitmap = RoundBitmapUtil.createRoundBitmap(bitmap, mInsideImageRadius);
//提取主题色
int colorTheme = BitmapColorUtil.extractColor(bitmap);
mCirclePaint.setColor(colorTheme);
mParticlePaint.setColor(colorTheme);
mBubblePaint.setColor(colorTheme);
}
//通过Matrix设置圆形Bitmap旋转
mMatrix.reset();
mMatrix.setRotate(mRotateAngle);
//获取旋转后的Bitmap
Bitmap rotateBitmap = Bitmap.createBitmap(roundBitmap, 0, 0, 2*mInsideImageRadius, 2*mInsideImageRadius, mMatrix, false);
//在画布上绘制旋转后的Bitmap,注意基于Matrix旋转后的Bitmap与原图的大小并不相等,故计算中心位置时应以转换后的Bitmap进行计算
canvas.drawBitmap(rotateBitmap, width / 2 - rotateBitmap.getWidth()/2 , height / 2 - rotateBitmap.getHeight()/2, null);
//绘制专辑图外围的圆环
canvas.drawCircle(width/2, height/2, mAlbumCircleRadius, mCirclePaint);
//绘制粒子群
drawParticleGroup(canvas);
//绘制气泡群
drawBubbleGroup(canvas);
//100ms后更新粒子位置和气泡位置
postDelayed(new Runnable() {
@Override
public void run() {
changeParticleGroup();
changeBubbleGroup();
//补充粒子
initParticleGroup(PARTICLE_GROUP_SIZE);
//补充气泡
initBubbleGroup(PARTICLE_GROUP_SIZE / 60);
}
}, 100);
}
粒子实体代码
/**
* 粒子信息由半径,x坐标,y坐标,alpha透明度,velocity移动速度构成
* 半径范围控制在1~2以内
* alpha呈现减少趋势
*/
private class Particle implements CircleFactory<Particle>{
private int r;
private int x;
private int y;
private int alpha;
private int velocity;
//斜率
private float slope;
//方向,x向增加为1,x向减小为-1
private int direction;
//粒子寿命,velocity降为0或者超出粒子云外径视为寿命终结
private int age;
private float sqrt;
@Override
public Particle setX(int x) {
this.x = x;
return this;
}
@Override
public Particle setY(int y) {
this.y = y;
return this;
}
public int getR() {
return r;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getAlpha() {
return alpha;
}
public float getSlope() {
return slope;
}
public int getDirection() {
return direction;
}
public int getVelocity() {
return velocity;
}
public int getAge() {
return age;
}
public void setR(int r) {
this.r = r;
}
public void setAlpha(int alpha) {
this.alpha = alpha;
}
public void setVelocity(int velocity) {
this.velocity = velocity;
}
public void setSlope(float slope) {
this.slope = slope;
}
public void setDirection(int direction) {
this.direction = direction;
}
public void setAge(int age) {
this.age = age;
}
public void setSqrt(float sqrt) {
this.sqrt = sqrt;
}
public float getSqrt() {
return sqrt;
}
@Override
public Particle build() {
r = mRandomInt.nextInt(2) + 1;
alpha = 255;
velocity = mRandomInt.nextInt(3) + 3;
slope = 1.0f*(y - height/2) / (x - width/2);
direction = (x >= width/2 ? 1 : -1);
//计算age值
int moveAge = (int)Math.sqrt(Math.pow(mOutSideStarRadius, 2)*1.0f - (Math.pow(x - width/2, 2) + Math.pow(y - height/2, 2))*1.0f) / velocity + 1;
int alphaAge = alpha / 30 + 1;
age = Math.min(moveAge, alphaAge);
sqrt = (float) (1.0f / Math.sqrt(1 + Math.pow(slope, 2)));
return this;
}
/**
* 改变该粒子的x,y坐标
*/
@Override
public void change() {
x += direction*velocity *sqrt;
y += direction*velocity*slope *sqrt;
alpha -=30;
ageOver();
}
/**
* 粒子寿命终结时移除
*/
public void ageEnd(){
boolean exceedingBoarder = (Math.pow(x - width/2, 2) + Math.pow(y - height/2, 2) - Math.pow(mOutSideStarRadius, 2) >= 0.1f);
boolean moveState = (velocity > 0);
boolean visible = (alpha > 0);
if(exceedingBoarder | !moveState | !visible){
remove();
}
}
public void ageOver(){
age--;
boolean moveState = (velocity > 0);
if(age <= 0 | !moveState){
remove();
}
}
@Override
public void remove(){
mParticleList.remove(this);
}
}
性能优化效果
优化过程的CPU测量数据如下
优化之后通过top命令测试的数据如下图所示
优化之后的DEMO效果如以下视频展示,仍然保证了良好的显示效果。
心得
高级动效要服务于工业应用,则必须要考虑其资源占用情况,以确保其运行的体验。本次优化将CPU占用率从108%降到了30%,达成了设定的目标,并没有损失使用体验。后续计划将本次设计的组件做成开源库共享和推广