专栏名称: Cocoa开发者社区
CocoaChina苹果开发中文社区官方微信,提供教程资源、app推广营销、招聘、外包及培训信息、各类沙龙交流活动以及更多开发者服务。
目录
相关文章推荐
51好读  ›  专栏  ›  Cocoa开发者社区

Android开发:性能优化——内存泄漏

Cocoa开发者社区  · 公众号  · ios  · 2017-07-17 11:18

正文


入门篇

简述


本篇是作为内存泄漏入门,主要说的是一些关于内存泄漏的概念,包括什么是内存泄漏,内存分配的几种策略,为什么会造成内存泄漏 及 如何避免内存泄漏等。


1
避免内存泄露的重要性


对于一个APP的评测,最直接的评分点就是用户体验,用户体验除界面设计外,就数APP是否运行流畅较为重要,当APP中出现越来越多内存泄漏时,卡顿特效就会随之而来。类比下电脑,cpu性能低下或内存不足时,程序运行效率就会降低,常见的现象就是运行卡顿。或许你会说现在的安卓手机配置多牛逼,8核的骁龙cpu,4G的运行内存,流畅的运行一个app足够啦,但实际情况是这样的吗?一个安卓APP是运行在一个dalvik虚拟机上的,系统分配给一个dalvik虚拟机的内存是固定的,如:16M,32M,64M(不同手机分配的内存不一样,可能现在的国产机分配的内存会更大,但绝对不会分配全部内存给一个安卓APP),分配给一个APP的运行内存只有几十M,想想是不是有点少了呢?所以在这有限的运行内存中,想让一个APP一直流畅的运行,解决内存泄漏是十分必要的。


2
java与c/c++的对比


作为一个使用java开发的程序员,我们知道,java比c/c++更“高级”,这里的“高级”不是说java比其他语言好(我不想引起圣战哈~),而是说java在内存申请与回收方面不需要人为管理,而c/c++则需要自己去分配内存和释放内存。下面对比下两者之间的差别:


1.申请内存:

java只要在代码中new一个Object,系统就会自己计算并分配好内存大小;而c/c++则相对麻烦,需要调用malloc(size_t size),手动计算并传入要分配的内存值。


2.释放内存:

java有回收机制,即GC,不需要调用(也可以通过代码调用),一段时间后便会自己去回收已经不需要的内存;而c/c++则需要手动调用free(void *ptr)来释放指针指向的内存空间。


所以说java比c/c++更“高级”,但是java的垃圾回收机制也没有那么智能,因为它在执行垃圾回收时需要根据一个标准去判断这块内存是否是垃圾,当这块垃圾不符合作为垃圾的标准时,GC就不会去回收它,这就产生了内存泄漏,下面开始进入正题。


  • 上述的标准是:某对象不再有任何的引用时才会进行回收。

  • 这里的内存指的是堆内存,堆中存放的就是引用指向的对象实体。


基本概念


1
什么是内存泄露


当一个对象已经不需要再使用,本该被回收时,而有另一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。简而言之,内存不在GC掌控之内了。


2
java中内存分配的几种策略


1)静态的


静态的存储区:内存在程序编译的时候就已经分配好,这块内存在整个程序的运行期间都一直存在。它主要存放静态数据、全局的static数据和一些常量。


2)栈式的


在执行函数(方法)时,函数中的一些内部变量的存储都可以放在栈中创建,函数执行结束时,这些存储单元就会自动被释放。


3)堆式的


也叫动态内存分配。java中需要调用new来申请分配一块内存,依赖GC机制回收。而c/c++则可以通过调用malloc来申请分配一块内存,并且需要自己负责释放。c/c++是可以自己掌控内存的,但要求程序员有很高的素养来解决内存的问题。而java这块对程序员而言并没有很好的方法去解决垃圾内存,需要在编程时就注意自己良好的编程习惯。


  • 堆管理很麻烦,频繁地new/remove会造成大量的内存碎片,这样就会慢慢导致程序效率低下。

  • 对于栈,采用先进后出,完全不会产生碎片,运行效率高且稳定。


下面通过一段代码,来说明一个类被创建时,往堆栈都存放了些什么:


public class Main {

    int a = 1; // a变量在堆中

    Person pa = new Person(); // pa变量在堆中,new Person()实例也在堆中


    public void hehe() {

        int b = 1; // b变量在栈中

        Person pb = new Person(); // pb变量在栈中,但new Person()实例在堆中

    }

}


成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)——因为它们属于类,类的实例是存放在堆中的。

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储在堆中。——因为它们属于方法当中的变量,生命周期会随着方法一直结束。


3
java中一些特殊类



  • 开发时,为了防止内存溢出,处理一些比较占用内存并且生命周期长的对象时,可以尽量使用软引用和弱引用。


实例


1
内存泄露例子


单例模式导致对象无法释放从而造成内存泄露


/**

 * @创建者 CSDN_LQR

 * @描述 一个简单的单例

 */

public class CommonUtil {

    private static CommonUtil mInstance;

    private Context mContext;

    public CommonUtil(Context context) {

        mContext = context;

    }

    public static CommonUtil getmInstance(Context context) {

        if (mInstance == null) {

            synchronized (CommonUtil.class) {

                if (mInstance == null) {

                    mInstance = new CommonUtil(context);

                }

            }

        }

        return mInstance;

    }

    ...

}


这种单例工具类在开发中是很常见的,它本身并没有什么问题。但如果使用不善,那问题就来了:


/**

 * @创建者 CSDN_LQR

 * @描述 内存泄漏

 */

public class MemoryLeakActivity extends AppCompatActivity {


    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_memory_leak);

        CommonUtil.getmInstance(this);

    }

}


在MemoryLeakActivity中获取CommonUtil对象时,把自己作为参数传给了CommonUtil,这会有什么问题呢?因为CommonUtil对象使用了static修饰,是静态变量,在整个APP的运行期内,GC不会回收CommonUtil实例,并且它持有了传入的Activity,当Activity调用onDestroy()销毁时(例如屏幕旋转时,Activity会重建),发现自己还被其他变量引用了,所以该Activity也不会被回收销毁。


2
Memory Monitor的简单使用


Android Studio提供了一套Monitors工具,可以实时查看APP的内存分配、CPU占用及网络等情况,本篇主要针对内存分配,所以使用Memory Monitor来验证上面的说法。


1)找到Memory Monitor

图中的几个说明很详细,请细看。


MemoryMonitors


2)运行APP,证明内存泄漏

先打开APP,看到目前分配的内存为3.43MB。


程序默认占用内存


接着打开MemoryLeakActivity界面(从这里开始),查看到APP目前分配的内存为3.51MB。


打开界面后查看内存占用


我旋转下屏幕,可以看到APP目前分配的内存增加到了3.60MB。(可以认为每创建一个简单的Activity就会占用大约0.1MB内存)


旋转屏幕后,查看内存占用


点击Initiate GC(启动GC),再点击Dump Java Heap(获取当前内存快照)。


启动GC并获取当前内存快照


在Capture区找到刚刚获取的内存快照,找到MemoryLeakActivity,可以发现内存中有2个实例。


其实上一步中点击Initiate去启动GC,只是证明竖屏时创建的MemoryLeakActivity已经没办法被GC回收,也就是MemoryLeakActivity[0]不在GC的掌握之内,即内存泄漏了。


内存快照


分别点击MemoryLeakActivity实例0和1,可以看到坚屏MemoryLeakActivity[0]还被CommonUtil引用,而横屏MemoryLeakActivity[1]没有被CommonUtil引用。


坚屏MemoryLeakActivity


横屏MemoryLeakActivity


3
为什么会内存泄漏


如果不在onCreate()中获取CommonUtil对象的话,在改变屏幕方向后,竖屏的MemoryLeakActivity在调用onDestroy()时,会被GC回收。而这里出现了内存泄漏,就是因为在代码中获取CommonUtil对象搞的鬼。详情如下图所示:


屏幕旋转


4
解决方案


既然CommonUtil实例是静态的,存在于整个APP生命周期中,而ApplicationContext在整个APP的生命周期中也一直存在,那就给它传ApplicationContext对象即可。代码修改如下:


public class MemoryLeakActivity extends AppCompatActivity {

    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_memory_leak);

        CommonUtil.getmInstance(getApplicationContext());

    }

}


之后重覆上述步骤,可以看到,内存中只有一个MemoryLeakActivity实例了。


没有内存泄漏了


工具分析篇


简述


在上一篇《性能优化——内存泄漏(1)入门篇》中,介绍了内存泄漏的基本概念,并举了一个Demo,结合简单的代码分析,猜测出Demo中存在内存泄漏,并用Android Studio自带的Memory Monitor证明了我们的猜测,但开发中,业务逻辑可能比较复杂,对象引用繁多,难道都要这样去做代码分析吗?肯定不行,程序员的精力有限,且“很懒”(追求效率),我们需要工具来帮助我们进行分析。下面就来看看都有什么神器吧。


工具分析


1
System Information


System Information是Android Studio自带的分析工具,可以通过它来判断APP整体是否存在内存泄漏。


一个不存在内存泄漏的APP,在其退出并执行过GC后,APP中所有的View和Activity都会被销毁,所以,我们可以根据退出后内存中View和Activity的数量来判断这个APP是否存在内存泄漏。操作如下:


以上一篇中的Demo为例:

运行APP-->打开一个Activity-->屏幕旋转-->点返回键(两次)直到到桌面-->执行GC-->System Information-->Memory Usage



通过上述操作,会生成一个txt文本,其中就记录着View和Activity的数量。如下图所示,它们的值都为0(主要看Activities),说明本APP不存在内存泄漏。相反的,只要值不为0,那么APP是存在内存泄漏的。



这种方式只能判断整个APP是否存在内存泄漏,但无法知道哪里有内存泄漏,也无法得知是哪些对象引用造成的内存泄漏。


2
Analyzer Tasks


Analyzer Tasks 是Android Studio自带的分析工具,可以帮助我们快速定位内存泄漏,而且使用上 very easy~


打开一个内存快照(.hprof文件),Android Studio的右侧会出现Analyzer Tasks。



打开Analyzer Tasks后,可以发现有2个选项和一个按钮,说明如下图所示,我们需要的就是第一个"Detect Leaked Activities"。



执行任务后,下方的Analysis Results中会得到泄漏的Activity,而且直接定位到了MemoryLeakActivity[0],要上篇中得到的结果一样。


3
MemoryAnalyzer(MAT)


这工具,真心觉得有点复杂,但功能比Android Studio自带的Analyzer Tasks要强大的多。


MemoryAnalyzer是基于eclipse开发的,是一个单独的软件,它也是对.hprof文件进行分析,但必须是标准的.hprof文件,可以从Android Studio中导出。



运行MAT,通过File-->Open Dump分别打开刚刚导出的test1.hprof和test2.hprof,选择Leak Suspects Report-->Finish。



分析的方式有两种,一种是单文件分析,一种是多文件分析,我们先进行单文件分析。


1)单文件分析


切换到test2.hprof,打开Histogram。



点击Histogram后,会出来一个Histogram标签,会列出APP中所有类的实例个数,感觉跟Android Studio的.hprof查看器差不多,MAT中的Objects相当于AS中查看器的Total Count。



通过包名筛选,找出了我们自己写的代码,Android Studio的话可以通过切换视图来定位。可以发现MemoryLeakActivity的实例有2个,说明它可能存在泄漏,再联系之前在获取快照之前执行过GC,而这个Activity还没有销毁,说明这个Activity应该是泄漏了。好,到这里单文件分析就结束了。



2)多文件分析


分别将test1.hprof和test2.hprof的histogram添加到Compare Basket,详细操作如下图所示。



在Compare Basket中会有刚刚添加的两个.hprof,点击红色感叹号进行对比。



出来的Compared Tables列表跟Histogram很像,不过Objects和Shallow Heap标签都是成双成对。



一样,我们只关心我们自己的代码,所以通过Regex进行筛选。通过数据分析,可以知道,在旋转屏幕前后,MemoryLeakActivity的实例由1增加到了2,又因为test2.hprof是在执行了GC后获取的,所以可以判定MemoryLeakActivity在旋转屏幕后,内存泄漏了。



3)引用跟踪


通过单文件分析或多文件分析,我们知道MemoryLeakActivity发生了内存泄漏(被别的实例引用导致无法被GC回收),所以回到test2.hporf的Histogram,筛选出MemoryLeakActivity,右击List objects-->with incoming references。



出来的结果有2大块,即对内存中的2个MemoryLeakActivity实例分别被引用的结果进行了分类,可以看到每个MemoryLeakActivity都被好多个对象引用了。



结合上篇中提到的java中几个特殊类,我们知道SoftReference、WeakReference和PhantomReference在GC一般不会造成内存泄漏,所以这些我们可以不管,也说是说,我们可以对它们进行排除。



对两个Activity分别排除软、弱、虚引用后,得到的结果分别如下:



简单分析下就知道了,第一个MemoryLeakActivity实例是泄漏的,引用它的对象就是CommonUtil。


总结


1
工具方面


个人还是比较推荐使用Android Studio集成的System Information和Analyzer Tasks,主要是使用上方便快捷,还很简单,如果你是高手,并且需要有更强大的功能来帮助你检查APP中的内存泄漏的话,建议使用MAT,在网上多找些比较详细的文章看看,本文对MAT的介绍只是冰山一角。


2
操作方面


工具仅仅只是帮助我们快速定位APP中的内存泄漏,并不能直接告诉我们哪里有内存泄漏,实际开发中,需要我们去猜测,去思考,要有清晰的思维,想方设法的构思内存泄漏的判定依据。这需要我们自己去摸索,积累一定的经验。最后,在获取内存快照(Dump Java Heap)之前,建议多点几次Initiate GC,等内存稳定成一条线时再获取内存快照。


代码分析篇


简述


在上一篇《性能优化——内存泄漏(2)工具分析篇》中,介绍了如何使用工具帮助我们检查APP中是否存在内存泄漏、及如何定位到内存泄漏,但项目并不能完全依赖工具来检查,毕竟定位内存泄漏比较麻烦,还不如在开发时就考虑到内存泄漏问题,尽可能减少内存泄漏,后续优化才不会那么痛苦。下面就来看看开发中,哪些代码可能造成内存泄漏,及避免内存泄漏的对应解决方案。


代码分析


1
静态变量引起的内存泄露


1)错误示例


这个可以拿之前的Demo来说明,Demo代码如下:


// 单例工具类

public class CommonUtil {

    private static CommonUtil mInstance;

    private Context mContext;

    public CommonUtil(Context context) {

        mContext = context;

    }

    public static CommonUtil getInstance(Context context) {

        if (mInstance == null) {

            synchronized (CommonUtil.class) {

                if (mInstance == null) {

                    mInstance = new CommonUtil(context);

                }

            }

        }

        return mInstance;

    }

    ...

}


// Activity中使用单例工具

public class MemoryLeakActivity extends AppCompatActivity {

    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_memory_leak);

        CommonUtil.getInstance(this);

    }


当调用getInstance()时,如果传入的context是Activity,那么只要这个单例没有被释放,则这个Activity也不会被释放,直到进程退出后才会释放。


2)解决方案


不要传入Activity,可以使用getApplicationContext()来代替。


2
非静态内部类引起内存泄露(包括匿名内部类)


在Java中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用。


1)错误示例


public class MemoryLeakActivity extends AppCompatActivity {


    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_memory_leak);

        loadData();

    }


    public void loadData() {

        new Thread() {

            @Override

            public void run() {

                try {

                    Thread.sleep(10000);

                    System.out.println("模拟同步网络数据完毕");

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }.start();

    }

}


上面的代码中,使用Thread匿名内部类开辟线程同步网络数据,而这个内部类会隐式持有外部类的引用,当退出界面后,该内部类任务还在进行,导致该界面无法被GC回收,于是就会产生内存泄漏。



APP退出后,执行GC,获取内存快照。可以看到MemoryLeakActivity的Total Count为1,说明存在内存泄漏。


2)解决方案


静态的内部类不会持有外部类的引用。


(1) 使用静态方法


public static void loadData() {

    new Thread() {

        @Override

        public void run() {

            try {

                Thread.sleep(10000);

                System.out.println("模拟同步网络数据完毕");

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }.start();

}


(2) 使用静态内部类


public void loadData() {

    new MyThread().start();

}


static class MyThread extends Thread {

    @Override

    public void run() {

        try {

            Thread.sleep(10000);

            System.out.println("模拟同步网络数据完毕");

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}


3)拓展


但项目开发中,可能会存在一定要使用内部类去持有外部类的情况,比如数据同步完成后,需要修改界面上的文本信息,而静态内部类无法直接持有外部类的引用,这又该怎么解决呢?其实可以通过内部类的构造函数将外部类传入,并使用弱引用保存(GC执行后释放),并在内部类中做好判空即可。


static class MyThread extends Thread {

    Reference mReference;


    public MyThread(Context context) {

        mReference = new WeakReference<>(context);

    }


    @Override

    public void run() {

        try {

            Thread.sleep(10000);

            MemoryLeakActivity context = (MemoryLeakActivity) mReference.get();

            if (context != null) {

                context.mTv.setText("模拟同步网络数据完毕");

            }

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}



APP退出后,执行GC,获取内存快照。可以看到MemoryLeakActivity的Total Count为0,说明没有内存泄漏。


3
不需要用的监听未移除会发生内存泄露


1)错误示例


public class MemoryLeakActivity extends AppCompatActivity implements SensorEventListener {


    private SensorManager mSm;


    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_memory_leak);


        mSm = (SensorManager) getSystemService(SENSOR_SERVICE);

        Sensor sensor = mSm.getDefaultSensor(Sensor.TYPE_ALL);

        mSm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);

    }

}


本例中使用getSystemService()获取传感器服务,并设置了监听,但没有在onDestroy()方法中将监听移除,此类监听没有及时清除的话,必定造成内存泄漏。



2)解决方案


只需在onDestroy()中移除监听即可。


@Override

protected void onDestroy() {

    super.onDestroy();

    mSm.unregisterListener(this);

}



4
资源未关闭引起的内存泄露情况


1)错误示例


比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性。

当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。


2)解决方案


这里以Cursor、IO流和自定义属性为例。


(1)Cursor或IO流


try {

    ...

    使用cursor/io读取数据操作

    ...

} catch (Exception e) {

    e.printStackTrace();

} finally {

    if (cursor/io != null) {

        cursor/io.close();

        cursor/io = null;

    }

}


(2)自定义属性


TypedArray a = theme.obtainStyledAttributes(attrs,

                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);

TypedArray appearance = null;

int ap = a.getResourceId(

        com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);

a.recycle();// 不执行回收会造成内存泄漏


作者:CSDN_LQR

链接:http://www.jianshu.com/p/5572eef42ba5

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。