首页 > 代码库 > Android内存管理分析

Android内存管理分析

大部分因为工作任务繁重,一般我们很少关心内存的事,只知道先把任务完成。只有真正到了发现UI卡顿 或者APP实在跑不下去了(一点一卡),才会考虑到内存优化。或者你所在的大公司比较关心手机运行流利程度,也需要对内存进行管理。

1.内存管理的基础知识

  1. 因为安卓的顶层也是 Java来实现的,作为客户顿的程序员应该懂得如何去管理内存。
  2. 又因为Java不像C语言可以执行free去主动释放内存,而是提供了一套Java的垃圾处理器。但是该处理器并不能时刻盯着内存,在内存不需要的时候直接清理(程序员比较方便,但是不够智能)。
  3. Java对象的内存引用分为如下几点:

    • 强引用:一般new一个对象就是强引用 一般强引用的对象 GC回收不了,一般存储在堆内存中。
    • 软引用:一般使用SoftReference来包含一个软引用对象,该对象在内存不足的情况下会被回收
    • 弱引用/虚引用:一般很容易被回收,其对象大部分也存储在栈内存中。
  4. 在内存中,一般对象的存储有如下特点:

    • 静态区:一般存储静态代码块,在程序开始运行的时候已经存在了
    • 内存堆:一般申请的内存空间,也就是用户new出来的,包括全局变量的引用,c语言malloc申请的内存空间,都存在堆内存中。
    • 内存栈:在方作用域中,每次方法执行完,内部的对象就会被销毁。那么这些对象的实体还是存在堆内存中,但是对象的引用则存放在栈内存中。

在开发的过程中,经常会出现代码的引用导致了内存泄漏,接下来我们模拟一个内存泄漏。

public class MainActivity extends AppCompatActivity {

    private TextView result_tv;
    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            //2.内部方法调用外部类的某个组件进行修改
            MainActivity.this.result_tv.setText("修改了UI");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    public void myClick(View v) {
        //1.模拟10秒后发送一个消息
        new Thread(){
            @Override
            public void run() {
                mHandler.sendEmptyMessageDelayed(0,10*1000);
            }
        }.start();
    }

    private void initView() {
        result_tv = (TextView) findViewById(R.id.result_tv);
    }
}

2.Android Studio的Monitor工具

上面的代码,当我点击按钮后,还没到达10秒我就关闭该界面了,此时MainActivity本该关闭然后被销毁,但是接收信息的方法里面却引用了MainActivity的全局变量,也就是MainActivity在本该销毁的时候没被销毁,导致了MainActivity内存泄漏。在退出界面后我马上导出内存分析文件,Monitor点击步骤如下:

技术分享

首先介绍Monitor的Memory模块:

技术分享

内存抖动 一词,是app所在进程的内存空间使用波动过大,高低起伏。使用安卓Studio的Memory工具就可以,观察到内存的波动情况,

简单的说一下图片的意思,波纹最高的地方,就是代表当前内存使用情况已经达到触发GC垃圾回收。GC工作时,当前的所有线程,包括UiThread,都将被短暂的暂停,等到GC工作完后才恢复正常。偶尔几次还是不能够造成界面卡顿,但是如果频繁的触发GC工作,必然会对界面造成卡顿内存抖动的原因 :频繁触发垃圾回收优化也简单:避免频繁的触发GC

上面模块的界面解析如下:

技术分享

从上面已经可以看出,虽然退出了MainActivity界面被退出了,但是MainActivity内部引用了一个Handler,而Handler内部又持有了MainActivity。Handler在Activity退出的10秒后还在子线程中执行任务。所以导致了内存泄漏。

3.Eclipse插件MAT工具

上面的代码实际上我们是已经知道内存泄漏在哪里的。而Monitor工具只是帮助我们分析下内存的情况如何。但至于如何知道内存泄漏 还要通过程序员的分析。

MAT全称是Memory Analyse Tool,其实就是一个内存分析工具,他分析的是hprof文件。所以我们可以将刚刚生成的hprof文件导出去给MAT分析。

1.导出Android studio的hprof文件。

技术分享

2.如果是第1次使用MAT 你可以先下载Eclipse和MAT插件:

下载地址:
Eclipse下载

MAT下载

安装的如下地址的插件:

技术分享

3.接下来一系列图解释如何导入文件:

技术分享

技术分享

技术分享

技术分享

5.分析总体情况:

技术分享

技术分享

6.分析对象的内存:

技术分享

搜索后如下图:

技术分享

7.分析包下所有类的消耗内存情况:

技术分享

4.内存泄漏分析与注意点

  1. 大部分情况下,当UI执行某个操作后卡顿,我们就可以想到内存泄漏了。
  2. 分析Monitor中的内存抖动,创建hprof报告。
  3. 如果在android studio可以分析,只直接根据代码分析,如果不能,则需要导入到专业一点的MAT工具中。

当然上面的代码只是分析对象的操作。在安卓开发中,还有还要尽量避免以下情况:

1.静态变量引起的内存泄漏

下面我创建了一个工具类,该类引用了Context,代码如下:

public class BaseUtil {

    private static BaseUtil sInstance;
    private final Context mContext;

    private BaseUtil(Context c) {
        mContext=c;
    }

    public static BaseUtil getInstance(Context c){
        if (sInstance==null){
            sInstance=new BaseUtil(c);
        }
        return sInstance;
    }

    //other methods ...
}

接着在Activity中,创建该工具类,代码如下:

public class TestActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        BaseUtil baseUtil = BaseUtil.getInstance(this);
    }
}

如果我们第一启动该界面,那么工具类内部的静态变量直接引用了TestActivity,假如该类在启动一个新界面后关闭当前的界面,此刻TestActivity本应该被销毁,但是由于有静态变量的引用导致了该销毁的对象没被销毁,造成了内存的泄漏。
解决的方案很简单,只要传进来的上下文不要直接引用Activity的Context就可以了,改成Application的Context即可。

2.非静态内部类引起内存泄露

public class TestActivity extends AppCompatActivity {

    private int a=0;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new Thread(){
            @Override
            public void run() {
                while (true){
                    TestActivity.this.a+=1;
                    SystemClock.sleep(2000);
                }
            }
        }.start();
    }

}

上面的代码中我创建了一个子线程,并在内部实现一个死循环。假如我退出当前的Activity,因为线程内部的Rannable对象引用了Activity的全局变量a,导致了Activity无法本该被销毁而无法被销毁。这就引起了内存泄漏。

解决的代码如下,下面的代码其实就是将非静态内部对象变成静态类,该类为了持有Activity,将Activity变成弱引用对象,当我点击退出Activity的时候,因为Runnable内部是一个弱引用对象,所以直接Activity被销毁了:

public class TestActivity extends AppCompatActivity {

    private int a=0;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new Thread(new MyRunnable(this)).start();
    }

    public static class MyRunnable implements Runnable{

        private WeakReference<TestActivity> mTestActivityReference;

        public MyRunnable(TestActivity testActivity) {
            mTestActivityReference=new WeakReference<TestActivity>(testActivity);
        }

        @Override
        public void run() {
            while (true){
                TestActivity activity = mTestActivityReference.get();
                if (activity!=null){
                    activity.a+=1;
                }
                SystemClock.sleep(2000);
            }
        }
    }

}

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

SensorManager manager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor lightSensor = manager.getDefaultSensor(Sensor.TYPE_LIGHT);
SensorEventListener sensorEventListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {

    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }
};
manager.registerListener(sensorEventListener,lightSensor,SensorManager.SENSOR_DELAY_NORMAL);

实际上,监听类似于光线传感器的过程中是十分耗费性能的。所以当我们退出传感器所需要监听的界面的时候 需要实时注销。

manager.unregisterListener(sensorEventListener);

又比如,我们偶尔使用内容观察者需要实现注册:

ContentResolver cr = getContentResolver();
Uri uri= Uri.parse("content://sms/");
ContentObserver contentObserver = new ContentObserver(new Handler()) {
    @Override
    public void onChange(boolean selfChange, Uri uri) {
        super.onChange(selfChange, uri);
    }
};
cr.registerContentObserver(uri, true, contentObserver);

当不需要实现监听的时候,应该直接注销:

cr.unregisterContentObserver(contentObserver);

上面的例子不胜枚举,还记得动态注册屏幕锁屏的广播事件吗,当不需要的时候也要注销的。

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

ContentResolver cr = getContentResolver();
Uri uri= Uri.parse("content://sms/");
Cursor cursor = cr.query(uri, null, null, null, null);
//do you code ...
cursor.close();

除了游标对象,还有常见的io流,或者你玩过音乐播放器,视频播放器,记得不用的时候要关闭资源,清空内部的缓冲数据。

5.内存泄漏&内存溢出

内存泄露(Memory Leak):

  • 进程中某些对象已经没有使用价值了,但是他们却还可以直接或者间接地被引用到GC Root导致无法回收。

内存溢出(OOM):

  • 当内存泄露过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出OOM.
  • 当应用占用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出。比如:加载大图片。

6.Dalvik&ART

Android5.0 之前使用Dalvik虚拟机,之后使用ART虚拟机,下面是一些比较:

  • Dalvik在运行时将字节码转换为机器码,ART在安装的时候就转换为机器码,这样安装好的应用会占用更大的空间,但是运行时少了转换的时间,所以运行更快

  • ART提供了更好的垃圾回收表现,将垃圾回收时,程序的暂停次数由两次(分析、清理)减少到一次;程序暂停时,并行的进行垃圾回收处理;回收新近分配的、生命期短的对象,垃圾回收器花费的时间更少

7.Allocation Tracker&Memory Usage

Allocation Tracker实际上就是对对象申请内存的一个列表展示,方便我们去观察对象是如何创建的,其在内存中申请了多少位的内存空间等等。

1.打开Monitors管理器。点击如下按钮,该按钮可以帮助我们在某个操作时段内记录有多少个对象申请了内存空间,该按钮既是开始记录也是结束记录的按钮:

技术分享

2.当记录完成后,系统会根据该时段内申请的内存空间与对象进行一个记录,选择Group by Allocator,点击某个item右键jump to source可以查看申请内存的代码:

技术分享

3.上面的操作是我在MainActivity点击3次按钮所记录的内存情况。这3次操作中创建1个不变的字符串和三个新申请的字符串,另外因为BaseUtil是单例的所以点了几次无所谓。代码如下:

public void myClick(View v) {
    String s=new String("aaaa");
    BaseUtil.getInstance(this);
}

我们可以将该工具作为申请内存观察工具。这里再提一下,还有一个工具可以帮助我们大体观察下内存是否被占用的:

技术分享

技术分享

该工具最主要可以帮助我们粗略计算出当前界面是否出现内存泄漏,举个例子,比如应用启动的时候,界面展示,接着点击退出应用,如果使用该工具发现Activities不为0,说明该界面出现了内存泄漏。或者界面启动了,但是View的个数异常增多了,也要考虑下是否出现内存泄漏。

8.Lint检测工具

Lint工具主要是用来优化整个应用的,在Android Studio2.3中,我们可以这样调出Lint工具:

技术分享

技术分享

下面的面板列出了当前项目出现的问题:

技术分享

嫌疑1:告诉我们字符串可以使用res/string代替
嫌疑2:Handler必须变成静态不然可能出现内存泄漏
嫌疑3:声明的变量必须加上final修饰符
嫌疑4:点击中的v参数没被使用过。

注意;上面的嫌疑不用启动应用就能检测了,并且不只是检查内存泄漏,他包括了资源是否没引用就打包,单词拼写是否支持驼峰式等等,根据具体检测而定,而系统给出的建议也不一定就需要修改,可以让程序员根据自己的需要来修改

9.LeakCanary

我们既可以在 Android Studio中使用内存泄漏检测工具,也可以在应用中使用检测工具,以下推荐一款square公司提供的内存的检测工具LeakCanary.它可以方便在应用出现内存泄漏的时候给予用户提示。它的GITHUB地址,我们来看下怎么用吧:

1.在app的gradle文件中添加依赖:

dependencies {
    ...
    debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.5.1‘
    releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5.1‘
    testCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5.1‘
}

2.在你的Application下添加如下代码:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

3.在Activity中,模拟内存泄漏(点击按钮后10秒内退出界面):

public class MainActivity extends AppCompatActivity {

    private TextView result_tv;
    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            //2.内部方法调用外部类的某个组件进行修改
            MainActivity.this.result_tv.setText("修改了UI");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    public void myClick(View v) {
        //1.模拟10秒后发送一个消息
        new Thread(){
            @Override
            public void run() {
                mHandler.sendEmptyMessageDelayed(0,10*1000);
            }
        }.start();

    }

    private void initView() {
        result_tv = (TextView) findViewById(R.id.result_tv);
    }
}

4.实际上等你部署好应用/出现内存泄漏之后,发现手机里出现了一个新的 Leak英勇帮助你检测内存泄漏,但是只对那些在应用内部配置好需要检测的参数的应用才有效。当然你肯定要允许给他设置特定的权限

技术分享

5.当内存泄漏后,会出现提示:

技术分享

技术分享

上面截图说明 当界面退出的时候,子线程还持有Activity的全局变量Handler对象,导致MainActivity无法被销毁而造成的内存泄漏。

10.UI优化的原理

UI卡顿是如何来的

  1. 在主线程中直接访问网络(不过该操作在4.4版本后被禁止了)/ 进行密集的IO操作 / 执行sql操作。
  2. 在一个方法或者代码块中不断的申请空间并执行GC清理。造成了短时间内GC执行大量的操作。GC的处理会暂停主线程的操作。
  3. 总的来说就是主线程/UI线程执行了耗时的操作。

渲染刷新的机制

1.首先你要知道,安卓的理想刷新频率是60Hz,也就是1秒内UI绘制的帧数理想是60次。
2.根据上面的计算,1秒=1000ms, 那么每一帧的刷新时间是 10000ms/60=16.6ms。我们称,安卓理想的每一帧刷新时间是16.6ms.

技术分享

3.上面的图中正常刷新每一帧的逻辑必须是16毫秒内,但是如果有一帧刷新的事件超过了16毫秒,就会出现丢帧的现象(也就是24毫秒的那一帧无法显示出来就超过时间了,直接绘制下一帧)。
4.这里有一个名词叫60fps,他指的是一秒内绘制的帧数。在60fps内,系统会得到不断发送的VSYNC(垂直刷新)信号去进行渲染,并且正常地绘制。

5.了解所谓的VSYNC垂直刷新,首先要了解2个概念:
1)Refresh Rate:屏幕在一秒时间内刷新屏幕的次数—-有硬件的参数决定,比如60HZ.
2)Frame Rate :GPU在一秒内绘制操作的帧数,比如:60fps。

6.图形处理器(GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑/移动设备上图像运算工作的微处理器。GPU刷新的时候,会帮助我们将UI组件等计算成纹理Texture和三维图形Polygons

7.VSYNC的刷新原理如下:

技术分享

8.如何定位View本身的卡顿。

- 可以使用Allocation Tracking来定位大致的情况
- 可以使用TraceView来确定详细的问题所在。

Allocation Tracking定位

这里首先提供一个项目的地址.点击首页的第2个按钮,进入下面的界面,对按钮进行检测:

技术分享

技术分享

我们可以通过顶部的按钮观察自己应用的哪个类使用的内存高点:

技术分享

上面的记录文件我们可以看出自己应用中某个自己写的类占用了当时20%的内存空间。接下来可以在右上角记录该类所在的包进行分析:

技术分享

1.点击左上角的图标出现饼图
2.上面我们已经找到某个类占用内存控件很大,导致了UI卡顿。记录包名,在右上角的输入框中输入,此刻饼图会产生对应的变化。
3.右边有个按钮可以设置图片的展示方式。这里默认是饼图
4.饼图向外扩展分别是从包名一层一层往里进去。就就是点击倒数第2层,就能找到对应的类,而最里面的那层,在这里是com。
5.找到Activity的外一层你会发现,char[]和String的内存占用很多。由此退出可以存在内存泄漏。

点击上上图的Activity类,右键Jump to source。可以看到下面的代码申请了大部分内存空间并且用完销毁,导致GC内存抖动,代码如下:

/**
 * 排序后打印二维数组,一行行打印
 */
public void imPrettySureSortingIsFree() {
    int dimension = 300;
    int[][] lotsOfInts = new int[dimension][dimension];
    Random randomGenerator = new Random();
    for(int i = 0; i < lotsOfInts.length; i++) {
        for (int j = 0; j < lotsOfInts[i].length; j++) {
            lotsOfInts[i][j] = randomGenerator.nextInt();
        }
    }

    for(int i = 0; i < lotsOfInts.length; i++) {
        String rowAsStr = "";
        //排序
        int[] sorted = getSorted(lotsOfInts[i]);
        //拼接打印
        for (int j = 0; j < lotsOfInts[i].length; j++) {
            rowAsStr += sorted[j];
            if(j < (lotsOfInts[i].length - 1)){
                rowAsStr += ", ";
            }
        }
        Log.i("ricky", "Row " + i + ": " + rowAsStr);
    }
}

public int[] getSorted(int[] input){
    int[] clone = input.clone();
    Arrays.sort(clone);
    return clone;
}

TraceView定位

TraceView主要用来分析哪些方法在主线程执行的耗时操作情况,话不多说,接下来看看如何操作该工具。

技术分享

你可以点击启动Trace监控,然后运行一个你的觉得耗时的操作,比如刚刚我们的那个导致gif卡顿的代码:

技术分享

上面的面板主要介绍如下:
1.代表右边是main 线程执行任务的密集度
2.堆栈任务的进程执行情况
3.代表当前时间轴指在哪个点上
4.总的检测时间轴,也就是我们刚点的开始和结束时间段。
5.双击时间轴可以进行缩小时间轴。
6.按住拖动到某个位置,可以将中间的那部分时间轴进行方大。
7.时间轴中间的每2个点之间代表一个方法执行的时间段。

除了时间轴的,还有下面的方法面板,默认是按着你记录的时间段内的方法从上到下的调用:

技术分享

你也可以搜索对应的方法,然后会展开方法的详细,这里介绍内部的结构:

技术分享

Parent表示调用该方法的外部方法
Children表示该方法内部包含的其他方法。

技术分享

Incl Cpu Time
某函数占用的CPU时间,包含内部调用其它函数的CPU时间
Excl Cpu Time
某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间
Incl Real Time
某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间
Excl Real Time
某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间
Call+Recur Calls/Total
某函数被调用次数以及递归调用占总调用次数的百分比

11.UI渲染机制深入

运行的原理

渲染功能是应用程序最普遍的功能,开发任何应用程序都是这样,一方面,设计师要求为用户展现可用性最高的超然体验,另一方面,那些华丽的图片和动画,并不是在所有的设备上都能刘畅地运行。

Android系统的渲染管线分为两个关键组件:CPU和GPU,它们共同工作,在屏幕上绘制图片,每个组件都有自身定义的特定流程。我们必须遵守这些特定的操作规则才能达到效果。

技术分享

CPU本身其实也具有显示的功能,但是CPU要做的事情特别多,也为了让显示更加专业化,又添加了一个专门用来显示图像的处理器GPU。该图像处理器可以实现栅格化图像显示。举个例子:

技术分享

我们要知道,一个UI对象转换为一系列多边形和纹理的过程肯定相当耗时,从CPU上传处理数据到GPU同样也很耗时。所以很明显,我们需要尽量减少对象转换的次数,以及上传数据的次数,幸亏,OpenGL ES API允许数据上传到GPU后可以对数据进行保存,当我们下次绘制一个按钮时,只需要在GPU存储器里引用它,然后告诉OpenGL如何绘制就可以了,一条经验之谈:渲染性能的优化就是尽可能地上传数据到GPU,然后尽可能长地在不修改的情况下保存数据,因为每次上传资源到GPU时,我们都会浪费宝贵的处理时间.

Android系统在降低、重新利用GPU资源方面做了很多工作,这方面完全不用担心.比如:任何我们的主题所提供的资源,例如Bitmaps、Drawables等都是一起打包到统一的纹理当中,然后使用网格工具上传到GPU,例如Nine Patches等,这样每次我需要绘制这些资源时,我们就不用做任何转换,他们已经存储在GPU中了,大大加快了这些视图类型的显示。

现在Android系统已经解决了大多数性能问题,除非我们还有更高要求,我们基本不会发现与GPU相关的问题,然后还有一个GPU性能问题瓶颈,这个问题困扰着每个程序员,这就是过度绘制。

过度绘制

实际上,当一个UI界面经过多重渲染后,最先渲染的背景颜色值会被后面渲染的背景覆盖了,导致绘制浪费,为了明白绘制浪费是否严重,我们将绘制分为4个等级,红色代表严重过度绘制,一般最理想的就是1级绘制(灰色/紫色):

技术分享

接下来提供如下代码供用户练习优化项目地址,导入项目,打开过度绘制的开关:

技术分享

一般选第二项选中,如果有红绿色盲,可以选最后一项。点击之后,界面如下,根据上面的提到的渲染等级来看,genymotion的界面做的还是挺不错的:

技术分享

接下来启动我们的项目:

技术分享

首先你会发现该应用的标题都出现了过度绘制。为什么会导致这样呢?因为在绘制的时候,首先该应用的主题已经为整个界面添加了一层背景。我们可以将该背景去掉,在onCreate()方法中添加:

getWindow().setBackgroundDrawable(null);

效果如下,你会发现标题的背景从2级变成1级:

技术分享

实际上你会发现代码中activity的背景和fragment背景都都一样是白色,去掉fragment背景之后,2级绿色的背景颜色没了,编程白色:

技术分享

将item布局中,图片右边模块的容器里面所有背景去掉,界面如下,虽然点击的时候因为波纹效果导致的过度绘制,但这因为效果需要浪费的性能是可以的:

技术分享

未完。。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android内存管理分析