Android App性能优化--内存篇

1 概述

本篇博客是Android App性能优化专题的第一篇文章,该专题会在渲染、计算、内存、电量方面进行讲解Android App的性能优化。

为了更加高效的优化App性能,Android Studio提供了一个Android Monitor工具,它位于Android Studio主窗口的下方,Android Monitor提供了实时记录和观察App以下信息的工具:

  • 系统或用户定义的Log信息
  • Memory,CPU和GPU使用率
  • Network流量(仅限硬件设备)

2 预备知识

2.1 Android App的内存结构

Random Access Memory(RAM)在任何软件开发环境中都是一个很宝贵的资源。这一点在物理内存通常很有限的移动操作系统上,显得尤为突出。系统会在RAM上为App进程分配固定的内存空间,然后该App进程就会运行在该内存空间上。该内存空间会被分成Stack内存空间和Heap内存空间,其中Stack内存空间里存放对象的引用,Heap内存空间里存放对象数据。

在Android的高级系统版本里面针对Heap内存空间有一个3级的Generational Heap Memory的模型,它包括Young Generation,Old Generation,Permanent Generation三个区域。最新分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间超过某个值的时候,会被移动到Old Generation,最后到Permanent Generation区域。整个结构如下图所示:



这3个区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到该区域的大小时,会触发GC的操作,以便腾出空间来存放其他新的对象,如下图所示:



最近刚分配的对象会放在Young Generation区域,这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的,如下图所示:

通常情况下,当GC线程运行时,其他线程会暂停工作(包括UI线程),直到GC完成,如下图所示:



虽然单个的GC操作并不会占用太多时间,但是频繁的GC操作有可能会影响到帧率,导致卡顿。

2.2 GC root and Dominator tree

Java中有以下几种GC root:

  • references on the stack
  • Java Native Interface (JNI) native objects and memory
  • static variables and functions
  • threads and objects that can be referenced
  • classes loaded by the bootstrap loader
  • finalizers and unfinalized objects
  • busy monitor objects

如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator tree。当优化内存时,可以通过释放一个dominator对象来释放其所有下级对象。 例如,在下图中,如果要删除对象B,那么也会释放其所主导的对象所使用的内存,即对象C,D,E和F,实际上,如果对象C,D, E和F被标记为删除,但对象B仍然指向它们,这可能是它们未被释放的原因。


3 Memory Monitor

Android Monitor提供了Memory Monitor工具,以便更轻松地实时监听App的性能和内存使用情况,通过该工具可以:

  • 显示空闲和已分配的Java内存随时间变化的图表。
  • 随着时间的推移显示垃圾回收(GC)事件。
  • 启动GC事件。
  • 快速测试UI线程卡顿是否与频繁GC事件有关。
    当GC线程运行时,其他线程都会暂停(包括UI线程),直到GC完成。频繁GC操作有可能会影响到帧率,导致卡顿,特别是性能比较差的手机上,尤为明显。
  • 快速测试app崩溃是否与内存不足(内存溢出或者内存泄漏)有关。

Memory Monitor的工作流程
为了分析和优化内存使用,典型的工作流程是运行app并执行以下操作:

  1. 使用Memory Monitor来分析是否由于不良GC事件模式导致的app性能问题。
  2. 如果在短时间内产生频繁的GC事件,就通过Dump Java Heap操作来查看当前内存快照,继而查找哪些类型的对象有可能发生了内存泄漏或者占用了太大内存。
  3. 最后通过Start allocation tracking操作来追踪对象分配内存时对应的方法调用。

在Memory Monitor中执行Dump Java Heap操作时,会创建一个Android-specific Heap/CPU Profiling (HPROF)文件,HPROF文件中保存了app该时刻内存中的GC root列表,文件创建完成后会自动在HPROF Viewer中打开, HPROF Viewer使用


图标标示GC root(深度为零)以及使用


图标标示Dominator tree。

4 常见内存性能问题模拟及优化

4.1 内存抖动现象模拟及优化

内存抖动是因为在短时间内大量的对象被创建又马上被释放导致的。因此下面的例子中我通过一个for循环来不断的创建和释放对象来模拟内存抖动的现象。
举个例子:

public class TestLeakActivity1 extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Button click = new Button(this);
        click.setOnClickListener(this);
        click.setText("模拟内存抖动");
        setContentView(click);
    }

    @Override
    public void onClick(View v) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    Bitmap result1;
                    result1 = BitmapFactory.decodeResource(getResources(), R.drawable.noah_silliman);
                }
            }
        }).start();
    }
}

上面代码很简单,当点击模拟内存抖动按钮时,通过Memory Monitor工具可以看到出现了非常明显的内存抖动情况,如下图所示:


当内存抖动的峰值快达到Young Generation区域的容量时就会触发GC操作,因此为了触发GC操作,就在代码中加载来一张非常大图片(5184*3456),对应的GC log如下:

08-22 10:53:51.579 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 39% free, 17MB/29MB, paused 492us total 52.970ms
08-22 10:53:51.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 370us total 36.902ms
08-22 10:53:52.329 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 365us total 36.754ms
08-22 10:53:52.664 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.072ms
08-22 10:53:52.952 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 8.791ms for cause Alloc
08-22 10:53:52.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.178ms
08-22 10:53:53.396 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 9.036ms for cause Alloc
08-22 10:53:53.444 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 493us total 43.976ms
08-22 10:53:53.809 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 11.791ms for cause Alloc
08-22 10:53:53.853 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 373us total 38.598ms
08-22 10:53:54.181 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 311us total 32.794ms
08-22 10:53:54.617 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 18(736B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 481us total 46.280ms

通过上面的log中的时间点证明了发生了频繁的GC, 由于导致GC的原因是Alloc(可以参考调查 RAM 使用情况来理解GC Log),因此可能会在不久的将来会发生OOM异常;当我点击两次按钮时,确实引发OOM异常;频繁GC操作有可能会影响到帧率,导致卡顿。

Allocation Tracker功能(可以参考Allocation Tracker)对于识别和优化内存抖动是非常有效的,接下来就通过这个功能来定位上面发生内存抖动的位置:


点击右下角红色框中的按钮,即开始执行Allocation Tracker,等一段时间,再点击一下右下角红色框中的按钮就会停止Allocation Tracker,此时下面的波形图中矩形区域就是Allocation Tracker执行的周期,并且会生成和打开一个alloc格式的文件,通过上图可知分配内存最多的是Thread 22线程,打开Thread 22线程的调用stack,定位到TestLeakActivity1的34行就是分配内存的位置,接下去的问题修复也就显得相对简单了,尽量避免在for循环里面分配对象,尝试把对象的创建移到循环体之外,对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,注意在对象池没用时需要手动释放对象池中的对象。

4.2 内存泄漏现象模拟及优化

内存泄漏是指不再用到的对象由于被错误引用而无法被GC回收,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然会导致每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。

举个例子:

public class TestLeakActivity2 extends AppCompatActivity implements View.OnClickListener {

    private Button testLeakBtn = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_leak2);
        testLeakBtn = (Button) findViewById(R.id.button_test_leak);
        testLeakBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(this, TestLeakActivity3.class);
        startActivity(intent);
    }
}

public class TestLeakActivity3 extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ImageView imageView = new ImageView(this);
        imageView.setImageResource(R.drawable.noah_silliman);
        setContentView(imageView);
        handler.sendEmptyMessageDelayed(0, 60000);
    }

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
}

上面的代码很简单,运行App,多次并且快速执行操作(从TestLeakActivity2跳转到TestLeakActivity3,然后再回到TestLeakActivity2),接着利用Memory Monitor工具的Dump Java Heap功能(可以参考HPROF Viewer and Analyzer)列举此时Heap中各种类型对象的多少和大小:


点击右上角的箭头,可以解析出当前泄漏的activity,然后选中instance窗口中的第一个泄漏的实例,下面的reference tree窗口就会立即显示该实例对应的reference tree,可以看出:
1> TestLeakActivity3$1@316570880是TestLeakActivity3@315071952的Dominator tree。
2> TestLeakActivity3$1@316570880的类型是Message中target的类型,即Handler类型。
3> 由于TestLeakActivity3$1@316570880通过this$0引用TestLeakActivity3@315071952,因此TestLeakActivity3$1是TestLeakActivity3的内部类。
4> target和handler是同一个实例(TestLeakActivity3$1@316570880),并且handler是TestLeakActivity3@315071952的一个属性。
由上面的4条信息可以得出只要TestLeakActivity3中handler的生命周期在TestLeakActivity3生命周期之内,就可以避免TestLeakActivity3实例的泄漏,接下去的问题修复也就显得相对简单了,就不在赘叙了。

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

推荐阅读更多精彩内容