Android应用性能优化
内存的优化
垃圾回收及内存调试工具的介绍
概要:
Android的Generational Heap Memory模型和几个内存调试工具:Memory Monitor、Allocation Tracker、Heap Viewer
Android性能优化典范:http://android.jobbole.com/80611/ 此链接是根据谷歌官方推出的翻译总结版本,实在是强大。
Android的垃圾回收机制
java拥有一个方便的GC机制,让开发人员从繁重的对象分配回收工作中解放出来,专心于代码的高级实现。
Android相对原始JVM的GC机制进行了大幅优化,其内置了一个三级Generation的内存模型,最近分配的对象会存放在Young Generation(年轻代)区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation(老年代),最后到Permanent Generation(永久代)区域。
每一个级别的内存区域都有固定的大小,此后不断有新的对象被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。比如在Young Generation区中:
- 大多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
内存泄漏
内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。
GC导致的性能问题
在GC操作中,所有的线程都会被暂停,而GC的处理时间随着Generation的老化而加长。
比如大量内存泄露导致Permanent Generation被占满,从而在此处进行了频繁的GC操作,并且此处的GC操作是相当费时的,显然会导致程序的其它命令无法顺利执行,最典型的表现为UI卡顿。
再比如在for循环中瞬间新建了大量对象,常常会导致Memory Churn(内存抖动),瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。
内存抖动
为什么感受到了UI卡顿
来来来,动画专业的我给你们介绍下不同帧率对视觉的直观感受
要想达到每秒60帧,这意味着每一帧你只有16ms=1000/60的时间来处理所有的任务。并且Android也确实会每16ms自动刷新界面,如果没刷新,跳过了几帧,大多数可能是性能优化不够。
再科普下一个钟摆动画的制作
咳咳,扯多了。
内存诊断工具
1.Memory Monitor
一张图显示了前3个工具
2.Allocation Tracker
点击Start Allocation Tracking按钮后,经过一段想要记录的时间后,再次点击,即可生成一份alloc结尾的文件,此处我查看了自己的应用这个时间段产生的各种类的实例,点击Jump to Source,会跳转到对象所产生的类,而点击右侧圆形按钮,则会出现圆盘状图显示各个类的层级关系与所占大小。
3.Heap Viewer
Heap Viewer工具给我们提供了内存快照的功能,在手动GC之前进行快照,手动GC之后进行快照,如果发现该被回收的对象并没有被回收,那就是发生了内存泄漏,需要进行debug。
4.LeakCanary
这个第三方工具,真是强大,只要稍加配置即可在手机中实时提示出现的内存泄漏现象。 github地址: 6.0以上的虚拟机需要使用github中的最新版本。
实际操作
使用上文提到的工具,针对一个存在内存泄漏的工程,进行修改操作。 开始分析: 1.首先打开AndroidManifest.xml文件,如图:
可以看到程序的主入口点是MainActivity,按住ctrl+鼠标左键直接点进去查看MainActivity的内容:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static TextView sTextView; private Button mStartBButton; private Button mStartAllocationButton; Handler mHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { return false; } }); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sTextView = (TextView) findViewById(R.id.tv_text); sTextView.setText("Hello World!"); mStartBButton = (Button) findViewById(R.id.btn_start_b); mStartBButton.setOnClickListener(this); mStartAllocationButton = (Button) findViewById(R.id.btn_allocation); mStartAllocationButton.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_start_b: startB(); break; case R.id.btn_allocation: startAllocationLargeNumbersOfObjects(); break; } } private void startB() { finish(); startActivity(new Intent(this, ActivityB.class)); mHandler.postDelayed(new Runnable() { @Override public void run() { System.out.println("post delayed may leak"); } }, 5000); Toast.makeText(this, "请注意查看通知栏LeakMemory", Toast.LENGTH_SHORT).show(); } private void startAllocationLargeNumbersOfObjects() { Toast.makeText(this, "请注意查看MemoryMonitor 以及AllocationTracker", Toast.LENGTH_SHORT).show(); for (int i = 0; i < 10000; i++) { Rect rect = new Rect(0, 0, 100, 100); System.out.println("-------: " + rect.width()); } }复制代码
通过查看onClick的代码知道了程序的运行逻辑,startB()是开启一个新的Activity,startAllocationLargeNumbersOfObjects()是进行大量对象的创建,现在可以先运行一下程序查看运行效果,再配合工具进行分析了。 2.运行模拟器,效果展示如下:
mHandler.postDelayed(new Runnable() { @Override public void run() { System.out.println("post delayed may leak"); } }, 5000);复制代码
这里开启了一个延时的线程,5000ms似乎没有问题,LeakCanary也没有提示,把5000换成20000呢?事实证明会有如下的提示:
看提示似乎是由于匿名接口Runnable持有了对当前Activity的引用,那我们需要对Runnable和Handler同时进行修改,这时候我们要使用WeakReference来对代码进行修改,即实现弱引用,保证可以引用的对象可以被及时垃圾回收。 直接在MainActivity中加入如下的代码:
public static final String POST="post delayed may leak";public static class MyRunnable implements Runnable{ WeakReferencemWeakReference; public MyRunnable(MainActivity activity) { mWeakReference = new WeakReference (activity); } @Override public void run() { System.out.println(POST); } }public static class MyHandler extends Handler{ WeakReference mWeakReference; public MyHandler (MainActivity activity){ mWeakReference = new WeakReference (activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }复制代码
在MainActivity成员变量中添加:
public static final String WATCH="请注意查看通知栏LeakMemory";private MyRunnable mRunnable;private MyHandler mHandler;复制代码
修改StartB方法中删除原来的延时线程操作,加入如下代码:
mHandler = new MyHandler(MainActivity.this);mMyRunnable=new MyRunnable(MainActivity.this);mHandler.postDelayed(mMyRunnable,20000);Toast.makeText(this, WATCH, Toast.LENGTH_SHORT).show();复制代码
重新运行,这次没有任何内存泄漏的提示了,看来由匿名内部类和Handler引起的内存泄漏问题解决了,我们接下来继续分析看看这个项目还存在什么问题。 首先打开Monitor,再连续点击StartAllocation,会出现出现内存抖动现象,这个需要我们来处理一下。
使用Allocation Tracking工具进行分析抖动的位置,在内存抖动开始时点击按钮,在抖动结束后再点击一下结束探测。
把Activity展开后会发现进行很多Rect和StringBuilder对象的创建,看来这就是问题所在。
private void startAllocationLargeNumbersOfObjects() { Toast.makeText(this, "请注意查看MemoryMonitor 以及AllocationTracker", Toast.LENGTH_SHORT).show(); for (int i = 0; i < 10000; i++) { Rect rect = new Rect(0, 0, 100, 100); System.out.println("-------: " + rect.width()); }复制代码
在for循环中一直在创建对象及字符串的拼接,改进方案是把Rect对象的创建放到成员变量中在onCreate中进行初始化,为了避免在logcat输出时产生大量的String对象,改进方案是在onCreate中把String对象创建好,这样就不会重复创建了,还要把里面的字符串提取出来,放到strings.xml中,有的要设置为static final类型的字符串资源,还有一点就是Toast的弹出过于频繁,可以对其弹出速度进行限制,不过这里就不做处理了,这个地方的问题基本上解决了,修改代码如下:
成员变量
public static final String WATCHAGAIN="请注意查看MemoryMonitor 以及AllocationTracker"; public Rect mRect; public StringBuilder mStringBuilder;复制代码
OnCreate
mRect = new Rect(0, 0, 100, 100); mStringBuilder = new StringBuilder("-------: " + mRect.width()); 复制代码
startAllocationLargeNumbersOfObjects
private void startAllocationLargeNumbersOfObjects() { Toast.makeText(this, WATCHAGAIN, Toast.LENGTH_SHORT).show(); for (int i = 0; i < 10000; i++) { System.out.println(mStringBuilder); } }复制代码
下面来寻找这个项目中最后的问题,由于在MainActivity的布局文件中使用了自定义的View,所以最后看看自定义View有没有什么问题:
MyView.java
智能的Android Studio已经发现了问题,不要在onDraw中创建对象,看来和上面的问题差不多嘛,修改如下:
成员变量
private RectF rect = new RectF(0, 0, 100, 100); private Paint paint = new Paint();复制代码
把下面之前定义的删除即可,同时记得把变量名称修改,大功告成,所有的问题都解决了。