Android 请求更多内存 android内存占用过高_Android


Android系统发展到今天已经比较成熟,各种新技术,新框架也是层出不穷,极大的加快了一个App的开发效率。

尽管手机内存从一开始的一两百兆到今天动则4G,8G内存,但只要我们查看下自己的错误上报日志,OOM等内存问题依旧是最棘手的问题之一。内存优化一直都是Android开发不可避免的问题,也是Android应用开发从初级迈向高级的必经之路。

最近发现公司的Android app内存占用非常高,一启动就占用了70M,操作几个页面后内存直接飙到了120M,并且只要继续玩下去,内存曲线一直在上升。显然,代码存在严重的内存泄漏问题。

由于Android团队目前缺乏能够解决内存问题的开发人员,因此只能自己动手做内存优化相关的工作了,最后也给Android组员做了一次seminar。这里也对Android内存优化相关的知识做下总结。

本文包含以下三部分内容:

  1. Android的内存分配和垃圾回收机制;
  2. Android常见的内存泄漏及对应的解决方法;
  3. 实际案例解读;
  4. 总结。

从第一点开始,先了解下Android的内存分配和垃圾回收机制。

一、Android的内存分配和垃圾回收机制

JAVA是在JVM所虚拟出的内存环境中运行的,JVM的内存可分为三个区:堆(heap)、栈(stack)和方法区(method)。

  • 栈(stack):是简单的数据结构,只存放基本类型和对象的引用(不是对象)。
  • 堆(heap):堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机的垃圾回收器来管理。JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。上面new出来的task实例就存放在堆中。
  • 方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

下面,我们再来看看内存分配的一些基础知识。

  • 实例化:对象是类的一个实例,创建对象的过程也叫类的实例化。比如 task = new Task(); ,我们就创建了一个Task的实例task。
  • 引用:某些类的实例化需要其它的对象实例,比如TextView的实例化就需要Context对象,表示TextView持有对Context的引用,用有向图表示为TextView ← Context;

综合以上知识,App启动时的堆栈可以用下图表示:


Android 请求更多内存 android内存占用过高_Android_02


系统为每一个线程分配一个栈。在线程的运行过程当中,执行到一个新的方法调用,就在栈中增加一个内存单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。引用的对象保存在堆中。

当某方法运行结束时,该方法对应的frame将会从栈中删除,frame中所有局部变量和参数所占有的空间也随之释放。线程回到原方法继续执行,当所有的栈都清空的时候,程序也就随之运行结束。

而对于堆内存,堆存放着普通变量。在JAVA中堆内存不会随着方法的结束而清空,所以在方法中定义了局部变量,在方法结束后变量依然存活在堆中。

以上就是Java的简易的内存分配机制,接下来我们再来看看Java的垃圾回收机制。

我们将栈定义为root,遍历栈中所有的对象的引用,再遍历一遍堆中的对象。因为栈中的对象的引用执行完毕就删除,所以我们就可以通过栈中的对象的引用,查找到堆中没有被指向的对象,这些对象即为不可到达对象,对其进行垃圾回收。

什么时候会发生GC?假设Android系统给每个app分配的最大内存是512M,现在app已经占用了500M,那么下次申请内存时,如果大小超过12M,那么系统不会马上申请内存,而是先启动一次GC,回收那些不可达的对象,然后再判断剩余内存是否超过12M,如果超过,则分配内存,否则就会抛出OOM。

GC发生的时候,所有的线程都是会被暂停的,频繁的GC将会导致app卡顿。

二、Android常见的内存泄漏及对应的解决方案

通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。

  • MAT是一款强大的内存分析工具,功能繁多而复杂。
  • LeakCanary则是由Square开源的一款轻量级的第三方内存泄漏检测工具,当检测到程序中产生内存泄漏时,它将以最直观的方式告诉我们哪里产生了内存泄漏和导致谁泄漏了而不能被回收。

一般情况下LeakCanary就可以满足我们的要求了,接下来的例子也使用LeakCanary分析。

  • 单例引起的内存泄漏
public class Singleton {    
    private static Singleton sInstance;    
    private Context mContext;    
    private Singleton(Context context) {        
        this.mContext = context;
    }
    public static Singleton getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new Singleton(context);
        }
        return sInstance;
    }
}


上面,我们实现一个单例模式,接下来在activity中使用。


public class LeakActivity extends Activity {    
    private static Singleton singleton;    
    @Override
    private void onCreate(@Nullable Bundle savedinstanceState) {
        super.onCreate(savedinstanceState);
        setContentView(R.layout.act_main);
        if(singleton == null){
            singleton = Singleton.getInstance(this);
        }
    }}


由于activity的生命周期比App的生命周期短,而单例的生命周期与App的生命周期相同。activity生命周期结束时,由于单例持有activity的引用,将导致activity没法被回收,因此造成内存泄漏。

解决方法:由于单例的生命周期与app的生命周期相同,因此构造单例时可以将context转化为application的context。


public class Singleton {    
    ...   
    private Singleton(Context context) {        
        this.mContext = context.getApplicationContext();
    }
    ...
}


  • 非静态内部类创建静态实例造成的内存泄漏


Android 请求更多内存 android内存占用过高_Android_03


实际开发中我们常常会使用到非静态内部类,有时为了能够在不同activity间共享数据,于是又声明了一个static变量,虽然这样避免了资源的重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例会一直持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。

解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

  • Handler内存泄漏


Android 请求更多内存 android内存占用过高_Android_04


在上面的代码中,Message在消息队列中延时了10分钟,然后才处理该消息。而这个消息引用了Handler对象,Handler对象又隐性地持有了Activity的对象,当发生GC时因为 message – handler – acitivity 的引用链导致Activity无法被回收,所以发生了内存泄露的问题。

解决方法一:使用弱引用


Android 请求更多内存 android内存占用过高_内存泄漏_05


在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

解决方法二:及时清除消息


Android 请求更多内存 android内存占用过高_内存泄漏_06


正是因为被延时处理的 message 持有 Handler 的引用,Handler 持有对 Activity 的引用,形成了message – handler – activity 这样一条引用链,导致 Activity 的泄露。因此我们可以尝试在当前界面结束时将消息队列中未被处理的消息清除,从源头上解除了这条引用链,从而使 Activity 能被及时的回收。

  • 资源未关闭造成的内存泄漏

对于使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。

  1. 比如在Activity中register了一个BroadcastReceiver,但在Activity结束后没有unregister该BroadcastReceiver。
  2. 资源性对象比如Cursor、Stream、MediaRecorder、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
  3. 对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
  • 集合容器造成的内存泄漏


Android 请求更多内存 android内存占用过高_android person类_07


我们通常把一些对象的引用加入到了集合容器(比如ArrayList,HashMap)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

  • WebView造成的内存泄漏

关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

另外在查阅WebView内存泄露相关资料时看到这种情况:

Webview下面的 Callback持有 Activity引用,造成 Webview内存无法释放,即使是调用了 Webview.destory()等方法都无法解决问题(Android5.1之后)。

最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView


Android 请求更多内存 android内存占用过高_Android 请求更多内存_08


另外,在XML中使用webview也会导致内存泄漏,所以webview应该在需要的地方动态new出来,这样才是万无一失的。

//mWebView=new WebView(this);
mWebView=new WebView(getApplicationContext());
LinearLayout linearLayout = findViewById( http:// R.id.xxx );
linearLayout.addView(mWebView);

三、实际案例分析

会讲是我们公司的一个新产品,目前还未对外做过推广。如一开始所说,apk有23M左右,启动后占用内存70M左右,简单操作几步,内存飙升到120M,并且随着用户使用,内存一直往上升。说实话,看到这样一个产品,心里拔凉拔凉的。

下面是打开LeakCanary之后,初步运行app的泄漏情况。


Android 请求更多内存 android内存占用过高_android person类_09


结果与预期一致,代码存在非常严重的内存泄漏。打开最上面RecordActivity查看详情:


Android 请求更多内存 android内存占用过高_内存泄漏_10


原来是RecordActivity的非静态内部类View持有了activity的引用,而presenter又持有了view的引用,presenter实现了(implement)网络请求的回调接口,但在activity退出时没有移除对应的task,导致activity没法释放。


Android 请求更多内存 android内存占用过高_内存泄漏_11


那答案应该是在activity的onDestroy()方法中执行mRecordPresenter的相应方法最终跑到cancelAllNetworkReq()就可以了。

重新运行一下,还是一样的泄漏栈。这就奇怪了,明明已经执行了移除网络请求的函数,怎么还泄漏了。继续跟踪下面的函数:


Android 请求更多内存 android内存占用过高_android person类_12


最终定位到OkProvider类中。


Android 请求更多内存 android内存占用过高_内存泄漏_13


到这里就可以理解了。函数只是cancel了task,但并没有将task从Map中remove掉,所以task依旧在内存中,从而导致activity不能被回收。

这是一个导致所有activity都泄漏的bug,加一句代码后,最终解决了框架性的泄漏问题。


Android 请求更多内存 android内存占用过高_android person类_14


四、总结

上面写了很多,我们可以总结为,目前内存问题依旧是Android开发比较重要的一个话题。我们可以通过各种内存泄漏检测组件,LeakCanary实时发现内存泄漏,MAT查看内存占用,Memory Monitor跟踪整个App的内存变化情况,Heap Viewer查看当前内存快照,Allocation Tracker追踪内存对象的来源,以及利用错误统计如bugly上报错误等多个方面对App性能进行监控和优化。每个App功能,逻辑,架构不一样,造成的内存问题也不尽相同,掌握好工具的使用,形成良好的编码习惯,才能对症下药。