本文介绍如何在手机上实现高效的倒计时效果。这个效率有两个标准: 1、 刷新率足够高,让用户觉得这个倒计时确实是倒计时,而不是幻灯片; 2、不能占用过多的内存资源和CPU资源,给用户一种“我的手机真的挂了,倒计时效果完全激活”的错觉。本文内容如有明显错误,请及时指出。如果您有更好的想法,我希望与您分享。 (段落首行不能缩进,布局看起来很不舒服)
一、实现的功能
倒计时是倒计时。举个简单的例子,每30ms刷新一次剩余时间的显示
图1.倒计时控件显示效果
二、优化效果
鱼和熊掌不能兼得。控件刷新频率的增加必然会导致资源消耗的增加。由于本文的定位是内存优化,所以我们将控件的刷新频率作为一个常数易语言内存优化命令如何使用,对比优化前后倒计时对内存资源消耗的影响。
图2.优化前倒计时效果的内存占用
图3.优化后倒计时效果的内存占用
有图有真相。在30ms刷新间隔的情况下,优化前内存消耗每分钟会增加大约2.52MB。优化后对内存消耗几乎没有影响。
三、问题思路
先贴代码,因为代码比较简单,所以全部贴出来。全文如下:
public class MainActivity extends Activity {
private static final int MSG = 0;
private final static int INTERVAL = 30;
private TextView textView;
private long timeUp = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
textView = new TextView(this);
textView.setTextSize(60);
setContentView(textView);
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
timeUp = System.currentTimeMillis() + 3600 * 1000;
update();
super.onResume();
}
private void update() {
long time = timeUp - System.currentTimeMillis();
if (time < 0) {
return;
}
String str = String.format("d:d:d:d", time / 60 / 60000 % 60, time / 60000 % 60, time / 1000 % 60, time % 1000);
textView.setText(str);
handler.sendEmptyMessageDelayed(MSG, INTERVAL);
}
private Handler handler = new Handler() {
@Override
public void handleMessage(android.os.Message msg) {
if (msg.what == MSG) {
update();
}
}
};
由于倒计时函数需要不断刷新倒计时数字,资源量大的原因主要有两个:一、耗时函数重复执行(占用CPU); 二、 类对象未及时释放(使用内部内存)。那么对应的优化方法有两种:一、优化关键函数的复杂度; 二、 及时释放非必要的类对象。显然无法优化复杂度,因为开头提到的问题是在内存上,函数似乎没有优化的余地。另一方面,如果要及时释放类的对象,则必须触发GC。无论是手动触发还是自动触发,GC操作的优先级都高于UI刷新。频繁的GC难免会导致死机。所以我的大脑是开放的,想知道是否有任何方法可以在每次执行时重复使用旧对象而不是创建一个新对象。
四、第一次尝试
我查看了源码,发现不创建新对象很难,因为如果我们随意修改内容,Java会自动为我们创建一个新对象,而不是修改原来的对象。 (虽然java对用户使用非常方便,但其准确控制的能力远不及C)为了解决问题,我不怕麻烦使用传说中的反射机制,这似乎不是一个很安全的方法在一定程度上解决了重复创建对象的问题。
public final class String implements Serializable, Comparable, CharSequence {
private final char[] value;
private final int offset;
private final int count;
}
源码中的主要属性如上图,其中value是存放内容的char类型数组,count分别是存放内容的起始位置和长度。由于倒计时功能显示的字数是固定的,我们只需要通过反射得到数值后修改对应位置的字符即可。另一方面,我们没有传递设置属性的方法,必须传递该方法来通知页面需要刷新View。
我在手机上跑了(4.4.4),感觉还不错。开心的时候测试的同学跟我说:“有问题随着倒计时。它不会改变”,然后向我倾倒了一个6.0。
问题单步分析,发现反射没取到值,再查代码发现没有问题,或者怀疑反射相关的代码写错了,于是全部打印出来通过反射得到的成员变量和方法,发现6.0后,该类不再使用value++count来存储字符串。
五、当前方法
在路的尽头点开源码的时候易语言内存优化命令如何使用,无意中发现了一个函数,突然觉得再也不会爱了。该函数的注释如下:
/**
* Sets the TextView to display the specified slice of the specified
* char array. You must promise that you will not change the contents
* of the array except for right before another call to setText(),
* since the TextView has no way to know that the text
* has changed and that it needs to invalidate and re-layout.
*/
public final void setText(char[] text, int start, int len)
查了文档,发现api 1增加了以char数组为参数设置text的方法,可见公司一开始考虑的还是挺周到的。
最终实现代码如下:
public class MainActivity extends Activity {
private static final int MSG = 0;
private final static int INTERVAL = 30;
private TextView textView;
private char[] value = new char[] { '0', '0', ':', '0', '0', ':', '0', '0',
':', '0', '0', '0', '' };
private int offset = 0;
private int count = 12;
private long timeUp = 0;
private Handler handler = new Handler() {
@Override
public void handleMessage(android.os.Message msg) {
if (msg.what == MSG) {
update();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
textView = new TextView(this);
textView.setTextSize(60);
setContentView(textView);
textView.setText(value, offset, count);
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
timeUp = System.currentTimeMillis() + 3600 * 1000;
update();
super.onResume();
}
private void update() {
long time = timeUp - System.currentTimeMillis();
if (time < 0) {
return;
}
value[0] = (char) ('0' + time / 60 / 60000 % 60 / 10);
value[1] = (char) ('0' + time / 60 / 60000 % 60 % 10);
value[3] = (char) ('0' + time / 60000 % 60 / 10);
value[4] = (char) ('0' + time / 60000 % 60 % 10);
value[6] = (char) ('0' + time / 1000 % 60 / 10);
value[7] = (char) ('0' + time / 1000 % 60 % 10);
value[9] = (char) ('0' + time % 1000 / 100);
value[10] = (char) ('0' + time % 1000 % 100 / 10);
value[11] = (char) ('0' + time % 1000 % 10);
textView.invalidate();
handler.sendEmptyMessageDelayed(MSG, INTERVAL);
}
}
六、补充
在开发过程中,很多同事问我为什么一定要把刷新间隔设置为30ms。这个原因主要是和人眼的视觉残留有关(傻傻的一点点,也算几个)。大家都知道很多电影的帧率是24fps(虽然很多电影为了更好的视觉效果把帧率提高到60fps,但动画始终是动画,不是高清电影),刷新间隔大概是1000/24= 41.6ms。另一方面,由于目前很多倒计时功能只显示两位毫秒,即10ms的级别,如果刷新频率设置为40ms,很有可能最后一位会出现在一个循环中0,4,8,2,6,0。 ,4,8,2,6,0… 这在视觉上显然是不合理的。基于以上两个原因,我认为将刷新间隔设置为30最为合适。如果要将毫秒显示为三位数字,可以尝试将刷新间隔设置为27ms或33ms,这样可以避免这种情况其中最低位始终相同。
网易云产品,无常规试用,零成本体验云计算的价值。
本文来自网易从业者社区,作者聂雷珍授权发布
文章来源:https://sq.163yun.com/blog/article/198922551012814848
感谢您的来访,获取更多精彩文章请收藏本站。
