垃圾回收及内存调试工具的介绍

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卡顿

来来来,动画专业的我给你们介绍下不同帧率对视觉的直观感受



  帧率(FPS),全称Frames Per Second,我们制作3D动画一般30FPS足矣,电视剧25FPS,电影24FPS(所以以前有本杂志叫24格)。当然这些都是因为制作成本关系才保持在如此的帧率下,人眼最高能分辨的帧率大致在60FPS左右,这种顺滑如丝的视觉感受显然是人人都想要追求的,咱们程序员也一直为了这几帧的优化而愁眉苦脸。

要想达到每秒60帧,这意味着每一帧你只有16ms=1000/60的时间来处理所有的任务。并且Android也确实会每16ms自动刷新界面,如果没刷新,跳过了几帧,大多数可能是性能优化不够。


再科普下一个钟摆动画的制作



  这段小动画里包含了动画原理中的“慢入“和”慢出”,按照12FPS的帧率将1-9这几个画面依次显示,并循环往复播放,即会出现一个可爱的钟摆动画,如果我们在每两张的中间插入一张“中间张”,并且以24FPS播放,即会出现一个更流畅的动画效果。

咳咳,扯多了。


内存诊断工具##

1.Memory Monitor

一张图显示了前3个工具



  此界面就是Memory Monitor,在常用的logcat边上,很多人早就接触到了这个工具,可以很方便地发现运行过程中的性能问题,比如内存抖动,比如CPU占用过多,比如网络链接频繁耗费流量等等。
  
  点击Initiate GC按钮后,即可手动启动垃圾回收操作。

2.Allocation Tracker

点击Start Allocation Tracking按钮后,经过一段想要记录的时间后,再次点击,即可生成一份alloc结尾的文件,此处我查看了自己的应用这个时间段产生的各种类的实例,点击Jump to Source,会跳转到对象所产生的类,而点击右侧圆形按钮,则会出现圆盘状图显示各个类的层级关系与所占大小。


3.Heap Viewer

Heap Viewer工具给我们提供了内存快照的功能,在手动GC之前进行快照,手动GC之后进行快照,如果发现该被回收的对象并没有被回收,那就是发生了内存泄漏,需要进行debug。
  


4.LeakCanary

这个第三方工具,真是强大,只要稍加配置即可在手机中实时提示出现的内存泄漏现象。
  github地址:leakcanary
  6.0以上的虚拟机需要使用github中的最新版本。


  
  出现了内存泄漏就会在通知栏上显示这些
  我们的MainActivity中的消息队列引用了此活动,导致活动不能被回收,于是内存泄漏了。


实际操作####

使用上文提到的工具,针对一个存在内存泄漏的工程下载地址,进行修改操作。
  
  开始分析:
  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.运行模拟器,效果展示如下:
  
  


  
  看上去好像没什么问题,现在先来测试一下STARTACTIVITYB,看看点击之后会出现什么情况。
  
  通知栏里提示MainActivity已经发生了泄漏,我们点击进去查看详情会发现由于static类型的sTextView引用了mContext导致了MainActivity发生了内存泄漏,这就是LeakCanary工具的方便之处,直接把发生内存泄漏的地方明确的告诉你了,既然已经发现了一个内存泄漏的地方,我们先把它修复了再继续优化。
  
  回到代码,怎么修改呢?既然static有问题,那就直接把static去掉就好了,把sTextView变成非静态的,同时把sTextView改成mTextView,看上去一切好像都是那么简单,借助LeakCanary一切都变简单了,这就是工具带给我们的便利,但是这里面还存在一个不易被发现的内存泄漏行为:

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{
        WeakReference<MainActivity> mWeakReference;

        public MyRunnable(MainActivity activity) {
            mWeakReference = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void run() {
            System.out.println(POST);
        }
    }

public static class MyHandler extends Handler{
        WeakReference<MainActivity> mWeakReference;

        public MyHandler (MainActivity activity){
            mWeakReference = new WeakReference<MainActivity>(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();

把下面之前定义的删除即可,同时记得把变量名称修改,大功告成,所有的问题都解决了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容