Android优化之内存优化倒计时篇
本文来自网易云社区
作者:聂雷震
本篇文章介绍的内容是如何在安卓手机上实现高效的倒计时效果,这个高效有两个标准: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("%02d:%02d:%02d:%03d", 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);二、没有及时释放类对象(占用内存)。那么优化的方法也相应的有两种:一、优化关键函数的复杂度;二、及时释放非必须的类对象。显然优化复杂度是不可能的,因为一开始说的问题就在内存上,而且update函数好像也没有可以优化的空间了吧。另一方面,如果要及时释放类的对象,就必须触发GC,不管是手动触发还是自动触发,GC操作的优先级都是高于UI刷新的,频繁GC必然会导致卡顿。于是脑洞大开,想着是不是有什么办法可以每次执行的时候都复用旧的对象,而不创建新的对象。
四、最初的尝试
看了一下String的源码,发现不创建新的对象是很难的,因为我们随随便便修改一下String的内容,java就会自动替我们创建一个新的对象,而不是修改原来的对象。(java虽然在使用上极大的方便了使用者,但是其精确控制的能力缺远不如C呀)为了解决问题,还是不怕麻烦使用了传说中的反射机制,这种看起来不是很安全的方法还是在一定程度上解决了重复创建对象的问题。
public final class String implements Serializable, Comparable, CharSequence { private final char[] value; private final int offset; private final int count;
}
String源码里主要的属性如上所示,其中value是保存String内容的char型数组,offset和count分别是String内容在数组中保存的起始位置和长度。由于倒计时功能显示内容的字数是固定的,所以我们通过反射的方法获取到value之后,只需要修改相应位置的字符即可。另一方面,我们没有通过setText的方法设置textView的属性,必须通过TextView的invalidate方法通知页面需要重新刷新View。
在自己的手机(Android 4.4.4)上跑了一下,感觉还不错,Mission Complete,正开心的时候测试的同学告诉我说,“倒计时有问题,时间不会变”,然后甩了一个Android 6.0给我。
单步分析问题,发现反射没有获取到value,再检查代码,发现没有问题,还是怀疑自己反射相关的代码写错了,于是打印出来所有通过反射获得的成员变量和方法,发现Android 6.0之后String类不在使用value+offset+count的方式存储字符串了。
五、当前的方法
在穷途末路的时候点开了TextView的源码,无意中发现了一个函数,顿时觉得不会再爱了,这个函数的注释如下:
/**
* 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)
查了一下文档,发现以char数组作为参数设置text的方法从api 1里面就加入了,可见Google公司最初考虑的还是蛮周全的。
最终实现的代码如下:
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', '\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,这样将会避免最低位始终不变的情况。
网易云产品免费体验馆,无套路试用,零成本体验云计算价值。
本文来自网易云社区,经作者聂雷震授权发布
相关文章:
【推荐】 基于Impala平台打造交互查询系统
【推荐】 在一台服务器上搭建相对高可用HiveServer实践
【推荐】 年轻设计师如何做好商业设计
Android优化之内存优化倒计时篇的更多相关文章
- [Android 性能优化系列]内存之基础篇--Android怎样管理内存
大家假设喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢 转载请标明出处(http://blog.csdn.net/kifile),再次感谢 原文地 ...
- [Android 性能优化系列]内存之提升篇--应用应该怎样管理内存
大家假设喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢 转载请标明出处(http://blog.csdn.net/kifile),再次感谢 原文地 ...
- 转 iOS和android游戏纹理优化和内存优化(cocos2d-x)
iOS和android游戏纹理优化和内存优化(cocos2d-x) (未完成) 1.2d游戏最占内存的无疑是图片资源. 2.cocos2d-x不同平台读取纹理的机制不同.ios下面使用CGImage, ...
- 微擎开启性能优化里面的性能优化memcache内存优化及数据库读写分离
http://www.mitusky.com/forum.php?mod=viewthread&tid=3135 [微擎 安装使用] 微擎开启性能优化里面的性能优化memcache内存优化及数 ...
- 关于android性能,内存优化
转:http://www.starming.com/index.php?action=plugin&v=wave&tpl=union&ac=viewgrouppost& ...
- KVM总结-KVM性能优化之内存优化
我们说完CPU方面的优化(http://blog.csdn.net/dylloveyou/article/details/71169463),接着继续第二块内容,也就是内存方面的优化.内存方面有以下四 ...
- Linux性能优化之内存优化(二)
前言 不知道大家看完前面一章关于CPU优化,是否受到相应的启发呢?如果遇到任何问题,可以留言和一起探讨这方面的问题.接下来我们介绍一些关于内存方面的知识.内存管理软件包括虚拟内存系统.地址转换.交换. ...
- 2017版:KVM 性能优化之内存优化
我们说完CPU方面的优化,接着我们继续第二块内容,也就是内存方面的优化.内存方面有以下四个方向去着手: EPT 技术 大页和透明大页 KSM 技术 内存限制 1. EPT技术 EPT也就是扩展页表,这 ...
- python性能优化、内存优化、内存泄露;与其他语音比较效率如何?
1.内存泄露:http://www.cnblogs.com/xybaby/p/7491656.html 2.内存优化:http://www.cnblogs.com/xybaby/p/7488216.h ...
随机推荐
- c++ new 与malloc有什么区别
前言 几个星期前去面试C++研发的实习岗位,面试官问了个问题: new与malloc有什么区别? 这是个老生常谈的问题.当时我回答new从自由存储区上分配内存,malloc从堆上分配内存:new/de ...
- Selenium Webdriver——Xpath轴定位(preceding)
1.preceding-sibling 选取当前节点之前的所有同级节点 text=出发之前的同级节点: 2.preceding 选取当前节点开始标签之前的所有节点 text=出发节点标签之前的所有i ...
- Selenium Webdriver——JS处理rich text(富文本框)
126邮件正文邮件的rich text 先让selenium切换到iframe中 driver.switchTo().frame(driver.findElement(By.className(&qu ...
- 「小程序JAVA实战」小程序的springboot后台拦截器(61)
转自:https://idig8.com/2018/09/24/xiaochengxujavashizhanxiaochengxudespringboothoutailanjieqi60/ 之前咱们把 ...
- 解决no-session延迟加载问题
解决no-session延迟加载问题 说明:dao层使用Hibernate从数据库查询数据信息返回给web层,在web层打印信息报no-Session错误, 产生原因 关联对象默认都是采用延迟加载 事 ...
- 向ArcGIS的ToolBarControl中添加任意的windows组建的方法[转]
向ArcGIS的ToolBarControl中添加任意的windows组建的方法[转] Link: http://www.cnblogs.com/mymhj/archive/2012/10/12/27 ...
- python传值&值引用
[python传值&值引用] 和其他语言不一样,传递参数的时候,python不允许程序员选择采用传值还是传引用.Python参数传递采用的肯定是“传对象引用”的方式.实际上,这种方式相当于传值 ...
- ios 单个ViewController屏幕旋转
如果需要旋转的ViewController 使用了UINavigationController,对UINavigationController进行如下扩展 @implementation UINavi ...
- mybatis整合spring的完整过程
1.1 整合思路 1.SqlSessionFactory对象应该放到spring容器中作为单例存在. 2.传统dao的开发方式中,应该从spring容器中获得sqlsession对象. 3.Mappe ...
- JetBrains全家桶激活地址
全家桶地址:https://www.jetbrains.com/products.html?fromMenu JetBrains 授权服务器(License Server URL):http://id ...