Android内存优化全解析

本文已同步发表到我的技术微信公众号,扫一扫文章底部的二维码或在微信搜索 “程序员驿站”即可关注,每天都会更新优质技术文章。

Android的内存优化,一直是个让开发者头痛的问题,这篇文章是”Android电量优化全解析“后关于Android性能的又一篇原创文章,希望对大家有所帮助。今天我讲述的内容按照下面的结构进行。

Android电量优化全解析

内存与垃圾回收器

不是所有指令都执行得又快又好,下面介绍内存及它如何影响系统运行。

普遍认为,多数程序语言接近硬件或高性能,如C、C++和Fortran,通常程序员会自己管理内存,高手工程师对内存的分配,会慎重处理,并在未来结束使用时再次分配,一旦确认何时及怎样分配内存,内存管理的品质就依赖于工程师的技能跟效率。实际情况是工程师们,不都会去追踪那零碎的内存碎片。程序开发是个混乱又疯狂的过程,内存通常都没办法完全被释放,这些被囚禁的内存叫内存泄露

内存泄露占用了大量资源,这些资源其实可以更好地使用,为减少泄露引起的混乱、负担、甚至资金损失,便有了内存管理语言

这些语言在运行时跟踪内存分配,以便当程序不再需要时释放系统内存,完全不用工程师亲自操作,这些内存回收艺术或科学,在内存管理环节下叫垃圾清理。这个设计概念在1959年,当初为了解决lisp语言问题,由John McCarthy发明的。

垃圾清理的基本概念有:

  • 第一,找到未来无法存取的数据,例如所有不受指令操控的内存。
  • 第二,回收被利用过的资源。

原理简单,但是两百万行编码,跟4gigs的分配,在实际操作时却非常困难。如果在程序中有20000个对象分配,垃圾清理会让人困惑,哪一个是没用的?或者,何时启动垃圾清理释放内存?这些问题其实很复杂。好在50年来,我们找到了解决问题的方法,就是Android Runtime中的垃圾清理。比McCarthy最初的方法更高级,速度快且是非侵入性的。经由分配类型,及系统如何有效地组织分配以利GC的运行,并作为新的配置。所有影响android runtime的内存堆都被分割到空间中,根据这些特点,哪些数据适合放到什么空间,取决于哪个Android版本。

最重要的一点是,每个空间都有预设的大小,在分配目标时要跟踪综合大小,且空间不断地扩大,系统需要执行垃圾清理,以确保内存分配的正常运行,值得一提的是使用不同的Android runtime,GC的运行方式就会不同。例如在Dalvik中很多GC是停止事件,意思是很多指令的运行直到操作完成才会停止。

当这些GCs所用时间超过一般值,或者一大堆一起执行会耗费庞大的帧象时间,这是很麻烦的事情。


Android工程师花费大量时间降低干扰,确保这些程序以最快的速度运行,话虽如此,在指令中影响程序执行的问题仍然存在,首先程序在任意帧内执行GCs所用的时间越多,消除少于16毫秒的呈像障碍,所必需的时间就会变少,如果有许多GCs或一大串指令一个接一个地操作,帧象时间很可能会超过16毫秒的呈像障碍,这会导致隐形的碰撞或闪躲。其次,指令流程可能造成GCs强制执行的次数增多,或者,执行时间超过正常值。例如,在一个长期运行的循环最内侧分配囤积对象,很多数据就会污染内存堆,马上就会有许多GCs启动,由于这一额外的内存压力,虽然内存环境管理良好,计算比其他语言复杂,内存泄露仍会产生,这些漏洞在GCs启动时,通过无法被释放的数据污染内存堆,严重降低可用空间的总量,并以常规方式强制GC的执行。就是这样,如果要减少任意帧内启动GC的次数,需要着重优化程序的内存使用量,从指令的角度看,或许很难追踪这些问题的起因,但是,多亏Android SDK拥有一组不错的工具。

Memory Monitor工具

我们来介绍一个叫作Memory Monitor的工具,Memory Monitor用于测试程序在一段时间后占用了多少内存,下面来操作一下。点击打开,然后会在Android Studio右下边的视窗里,开启一个制表键,一旦发现在运行的程序,就会马上开始记录内存使用量,正如这里所示,在Memory Monitor视窗的左上端,可以切换当前连接的装置,右边这里可以选择要监测的程序。几乎占用全部视窗的叠层图,表示还有多少内存可用。深蓝色的区域,表示当前正在使用中的内存总量,浅蓝色或者浅灰色区域,表示空闲内存或者叫作未分配内存。图表会在内存使用量变化时不断更新,随着时间推移,它也会不断显示可用内存量。随着时间推移,它也会不断显示可用内存量,总之,如果程序都没有在运行,图表就完全是平坦的。


光从性能角度看,这是相当理想的状态,但随着程序分配跟内存释放,图表的分配总量也在跟着变化。如果要装的程序急需大量内存,内存分配也急剧增加,显示在空格里,不然的话,装置内存不足会导致死机。所以对于内存分配,不管什么时候都要特别小心,当垃圾清理开启时就要特别留意内存量,在这个范例中垃圾清理运作良好。另外,如图所示这里也可能有问题,这里有个程序占用了大量内存,然后又一下子释放了刚被占用的内存。生成这些又细又窄的锋线,不断重复,这就是程序在花大量时间运行垃圾清理,运行垃圾清理所用的时间越多,其他可用时间就越少,像播放和发送录音。我们来看下实际情况。
momory monitor已经在监测Sunshine情况了,点击一个日期,看下具体内容,点击返回键,重复这个动作,内存就会持续被占用,如这里所显示的。如果想要新的数据,只要改变几次坐标就行了,看下所得的天气预报,不错,星期三天气明朗。内存被慢慢的占用,最终,内存会被全部占用,这种情况如果持续下去,垃圾清理就会启动,释放大块的内存,这里可以看到变化。要记得,因为Android内存管理系统是固有的,所以垃圾清理不会释放所有的内存。我们的利器,可以强制执行单项的垃圾清理,在Memory Monitor的左上方有个garbage truck工具,单击一下,就会开启单项的垃圾清理,注意图表右边的变化。现在可以多点击几次,再继续点击,所有可被释放的内存都会被释放,装置会恢复到初始状态。接下来我们将了解内存泄露和heap viewer工具。

内存泄露

Android的Java语言有个最大的优点,是托管内存环境,对象在创建或消除时不用特别小心。这点尽管不错,但也有些潜在的问题不易被发现。划分到Android运行时的内存堆,是根据声明类型和利于垃圾清理操作的角度来分配的,每一区域都有其预设的内存空间。

当一个程序所需的总存储空间接近上限,垃圾清理就会启动,删除掉没用的数据,一般情况下不用特别注意垃圾清理的执行。

但是大量的清理动作不断地重复,很快地消耗掉帧像周期,花费在垃圾清理上的时间越多,播放或发送录音等事情的时间就越少。

工程师们制造的内存泄露,是垃圾清理运行的常见因素,内存泄露是不能被继续使用的空间,但是垃圾收集器却无法辨别出来,结果他们就一直存在于堆中,占用有效空间,永远无法被删除,随着内存不断泄露,堆中的可用空间就不断变小,这意味着为了执行常用的程序,垃圾清理需要启动的次数越来越多

搜索跟修复泄露是个很棘手的问题,有些泄露很容易就会产生,例如对没有使用的对象的循环引用。不过有些也很复杂,例如,在类别载入器安装未完成就强制执行,不管怎样,一个程序想要运行得又快又好,就需留意可能存在的内存泄露。你的代码将允许在各种各样的设备上,又互相结合,不是所有的数据都占用同样的内存,不过,还在有一个简单的工具,可以查看Android SDK中潜在的漏洞。

Heap Viewer工具

Heap Viewer是个很简单的工具,利用它可以查看内存状态,以及空间占用率的情况。通过Heap Viewer可知程序在特定时间内的内存使用量,跟原来一样,先在装置上打开Android Studio里的sunshine,在执行start Heap Viewer前,先打开Android Device Monitor。 我们看到,每次垃圾清理后,Heap都会更新,点击Cause GC,发现所有的数据都更新了,更新后的表格显示,在Heap上哪些数据是可用的,选中其中任一行数据,就可以看到详细数据,点击class object,屏幕上马上出现大量更新的数据,矩形图列出这一数据内存分配的数量,跟确切的容量。我们这里讨论的是class object,heap viewer可以有效地分析程序在堆中所分配的数据类型,以及数量和大小。这里列出在堆中各别类型程序的总容量,例如,这两个在堆里超过1400的数据组,用掉约1200个千字节,而这个只有27的数据组,却占用了约2个兆字节。heap viewer能够准确地,辨别出程序分配的类型和数量,以及各自在堆中的容量。比方说,这个27的数据组占用了近2兆的字节,可这4个2000的数据组,目前占用了228个千字节。在搜索内存漏洞时,这是个相当不错的工具。

使用Memory Monitor观察内存泄露

讨论下内存泄露的问题,内存泄露的行踪,常常神出鬼没,常慢慢不动声色的出现,有时要几天或几个星期后,才会被发现。实际上,可能到程序莫名其妙地操作缓慢时,才会发现内存不足的问题。只要用对工具,耐心分析,解决内存泄露不是难事。首先用Memory Monitor,观察漏洞是怎样生成的,在下一个影片中,再利用Heap Viewer做初步确认。举例说明漏洞的生成,以及SDK工具,如何侦测这样微小的漏洞,先把手机旋转几下,然后打开Memory Monitor,这样做的目的是要说明,一个简单的动作就会产生漏洞。像这样不断改变手机方向,就会有漏洞产生,听起来很奇怪,但是借由这一动作,可知漏洞是怎么缓慢且隐秘地产生的。首先,漏洞慢慢吞噬程序内的可用内存,直到GC的启动,再来,值得注意的是由于程序上有漏洞,导致GC无法回收全部垃圾。结果大约30秒后,就会启动第二次GC,当漏洞吞噬所有的可用内存时,Android调整并分配给程序更高的内存上限。这样做的同时,如果漏洞没有修复,内存会不断地被吞噬,结果导致系统无法再配置,手机也就没办法再用了,最后死机。稍等下,第三次的GC就会启动,第四次跟前两次类似,现在这组指令在持续运行,系统分配更多的内存量,可以用同样的方法操作Heap Viewer。

使用Heap Viewer观察内存泄露

通过Heap Viewer,可知第一次GC仅释放了1.39兆内存,这种结果显示,因为漏洞的存在,垃圾清理无法回收全部垃圾。Heap viewer显示第二次GC后,系统必须经由配置更多的内存,来调整内存量。堆从第一次GC的20兆,增加到32兆,此次Java堆释放了12.9兆,这是,系统不断地为程序配置更多的内存。以上动作如果一再重复,系统终会无法配置内存,程序也就挂了。切记,内存漏洞非常缓慢又不易被发现,需要时间,跟适当的环境来确认,有时,这样的数据,也表示内存的正当存取。比如,处理图片跟照片的程序,表面看似内存在泄露,实际上它针对核心功能的存储器,不停地进行数据评估。因此,要明白内存泄露如何显示在SD上,也要清楚,内存泄露如何显示在拥有SDK的工具上,如Memory Monitor和Heap Viewer。但是,各位可能不知道他们源于何地,以下这些方法可以防止漏洞的出现。利用编码查看程序的寿命,清理不用的文件,接下来,辨别漏洞产生的原因。

追踪内存泄露的代码

ListenerCollector.java

import android.view.View;

import java.util.WeakHashMap;

public class ListenerCollector {
    static private MyView.MyListener   sListener;

    public void setsListener(View view, MyView.MyListener listener){ 
        sListener =  listener;
    }
}

MyView.java

import android.content.Context;
import android.view.View;


public class MyView extends View{
    public  MyView(Context context){
        super(context);
        init();
    }

    public interface MyListener{
        public void myListenerCallback();
    }

    private void init(){
        ListenerCollector collector = new ListenerCollector();
        collector.setsListener(this,myListener);
    }

    public MyListener myListener = new MyListener() {
        @Override
        public void myListenerCallback() {
            System.out.print("有被调用");
        }
    };

}

注意到自定义控件init方法中如下代码:

private void init() {
	ListenerCollector collector = new ListenerCollector();
	collector.setListener(this, mListener);
}

存储一个Activity中所有视图监听器,这个想法看似无害,但如果你忘了清理它们,你可能会不经意地造成一个缓慢的泄漏。相关代码:

collector.setListener(this, mListener);

当Activity被销毁和创建时,这一问题被复杂化。在示例中,由于设备的方向变化使一个新的Activity创建,相关联的监听被创建,但是当Activity被销毁时,该监听永远不会被释放。这意味着,监听无法被GC回收,这里导致了内存泄露。当设备旋转并调用当前Activity的onStop方法时,一定要清理所有视图的监听。

ListenerCollector可以做如下优化:

import android.view.View;

import java.util.WeakHashMap;

public class ListenerCollector {
    static private WeakHashMap<View,MyView.MyListener> sListener = new WeakHashMap<>();

    public void setsListener(View view, MyView.MyListener listener){ 
        sListener.put(view,listener);
    }

    public static void clearListeners(){
        //移除所有监听。
        sListener.clear();
    };
}

使用Allocation Tracker观察内存泄露

另外,分配追踪器,可以辨别额外的内存膨胀,这是由于内存的历史浏览记录不断扩充产生的。选择一组仍在堆中的数据或者程序,这组数据堆中,在这个操作里,堆中数据叫作onCreate。这样一来,手机每旋转一次就有新的动作,类似的数据组,基本上就会在堆中膨胀。所以,如果在漏洞存在时旋转手机,垃圾清理无法清除这些数据,就会在堆中产生大量的垃圾。借由分配追踪器,可以弄清这一问题。

什么是内存抖动?

我们解决了哪些讨厌的泄露,现在遇到了更大的问题,内存抖动。要知道,堆内存都有一定的大小,能容纳的数据是有限制的,当Java堆的大小太大时,垃圾收集会启动停止堆中不再应用的对象,来释放内存。现在,内存抖动这个术语可用于描述在极短时间内分配给对象的过程。例如,当你在循环语句中配置一系列临时对象,或者在绘图功能中配置大量对象时,这相当于内循环,当屏幕需要重新绘制或出现动画时,你需要一帧帧使用这些功能,不过它会迅速增加你的堆的压力。这两种情况下,我们都制定了解决方案,可在短时间内创造大量的对象。根据创造的对象的量,或者每个对象的大小,你可能很快就消耗掉所有剩余内存,导致垃圾收集强行开启。随着它们的开启运行,会消耗更多宝贵的帧时间,所以,高性能的应用很有必要,你需要鉴别并从内循环里,取消会被重复执行的代码配置。为了更好的寻找到这些代码配置,Android Studio为此特别打造了一个方便的工具.

使用Allocation Tracker

现在看一下你的应用内存分配图,这能有效的获悉大部分数据到底用在哪里,以及正在分配哪种类型的数据,这能帮你找到现有的不必要分配的数据。可惜Heap Viewer不能显示你的数据具体分配在代码的何处,为此,我们需要一个叫做分配追踪器的工具。和以前一样,我们打开Android Studio Device Monitor,在前台载入Sunshine,打开DDMS视图点击start allocation tracking按钮,然后使用应用,隔一段时间在点击stop allocation tracking按钮。停止之后在DDMS出现了一个列表,这个列表显示了你在使用应用期间,所有的分配情况,这里的每一行都代表不同的分配,allocation order这一栏会提示你,分配进行的具体时间,分配类别这一栏显示了分配数据的类型,以及大小,还有其他信息来告诉你哪个线程具体决定了这一分配。最后,分配站这一栏告诉你代码的哪一个功能实际分配了内存。比如,我们选择整型,测试的值决定了这个整型的分配,如果你点击一个分配,你可以看见完整的调用堆栈。这个表格包含大量信息!

通过Trace View找出内存抖动

本次练习,我们来运行内存抖动活动。下面点这个按钮,对数组来点有意思的事情,你会发现跳着舞的海盗会暂停,但最后都会接着跳舞。这就是讨厌的卡顿,让我们解决它吧。通过跟踪显示来剖析这个活动,打开trace view的面板,注意短时间内发生的频繁的垃圾收集活动,可能会伤害到应用的性能。记住,我们还可以采集这个内存监控器图像,这个截屏展示了内存抖动是怎样通过Memory Monitor清晰显示的。

什么导致了内存抖动?

我们已经使用SDK工具采集足够多的数据,能知道内存抖动情况出现的时间,现在来揪出导致这种情况的代码吧。Trace View给我们提供了一个方法,让我们仔细看一下在主线程里,选择方式时的数据配置文件,当你选择主线程方式时,你会发现反复出现的Java字符串赋值操作,比如这个。再看调用堆栈,我们会更加确定数据队列副本被运用于扩大字符串缓冲。来看MemoryChurnActivity的源代码,正如OnClickListener所显示,我们称此功能为imPrettySureSortingIsFree,让我们来看这个代码。此处的方法叫作imPrettySureSortingIsFree,这个代码产生了新的字符串,通过字符串连接每次都有一个单元值,看一下我说的这个代码的指导提示,但是,出现连接的地方比较特别。这个初看起来似乎没什么问题,为什么这个代码会导致内存抖动? 频繁使用垃圾清理会造成两种后果,一是,每个单元值的连结都会生成新的字符数组,这是因为,在循环之内骤然接到重复指令组合而成,二是,通过定位追踪器,确认字符数组的膨胀,更新一下数据,在下一节中,向大家介绍所得的结果。

修改代码减少内存抖动

我们可以在我们的代码进行小的调整,以防止内存抖动。让我们来看看对比图,而不是在一个时间串联一个单元格值打造每一行,让我们使用一个StringBuilder实例,并用一个字符串构造每一行,需要注意的是StringBuilder中的实例化的循环外。因此它的内存分配一次,然后,我们只是作为一个缓冲,在每次循环我们先清除它,然后我们追加,整数的一个字符串来表示对于循环迭代的行。更多细节见导师的笔记到这个代码段,运行memory_churn_optimized,确认我们减少的GC在短期时间窗中发生的量,您也可以使用allocation tracker验证。现在对于我们来说,即使修改了代码,海盗动画仍然会出现卡顿的现象,这意味着该处理放到后台处理可能更加合适。

工具的特色

1)Memory Monitor:获得内存的动态视图
2)Heap Viewer:显示堆内存中存储了什么
3)Allocation Tracke: 具体是哪些代码使用了内存

关注我的技术公众号"程序员驿站",每天都有优质技术文章推送,微信扫一扫下方二维码即可关注:

posted @ 2019-03-04 09:53  伯兮  阅读(570)  评论(0编辑  收藏  举报