Android性能优化之内存优化1

作者 某人Valar
如需转载请保留原文链接
部分图片来自百度,如有侵权请联系删除

目录:

  • 常见的3大内存问题
  • Android开发中常用的内存分析工具
  • Android内存管理机制

前言(关于内存泄露问题的一些现状):

内存问题往往是隐蔽,在Android开发中使用的Java语言又自带了GC,导致大家在平时开发过程中对内存问题不够重视。

当我们真的碰到OutOfMemoryError异常时,报错位置也不一定是问题的根本原因。这里可能只是压死骆驼的最后一根稻草。

1. 常见的3大内存问题

1.1 内存抖动

内存抖动:在短时间内有大量的对象被创建或者被回收的现象。

内存抖动的一个直观表现就是内存占用情况呈锯齿状,如下图。其会导致GC频繁,从而导致界面卡顿。

GC过程中,根据GC收集器的不同,可能会分配一个或多个线程来进行GC,这时需要暂停掉其他工作线程,此时会影响UI的绘制,从而导致卡顿。

为什么GC时需要停止其他线程?

主要是防止其他线程在垃圾回收器检查期间访问了某个对象改变了这个对象的状态。 因为在垃圾回收最开始的阶段,会进行当前托管堆中所有对象进行遍历,进行标记,有引用的 标记为 1 没有引用的标记为 0,标记为0 的则是GC需要回收的。

1.2 内存泄露

内存泄露是指我们的某些对象没有被正确回收,导致可用内存逐渐减少。当可用内存越来越小,就容易触发内存溢出。

为什么某些对象没有被正确回收?

当较长声明周期的对象持有了较短生命周期的引用导致,就会较短生命周期对象无法被垃圾回收机制回收。

Android中常见的内存泄露:

1.2.1 单例引起的内存泄露

在安卓开发中,单例的生命周期是跟随整个app的生命周期的,如果在其中存在了某个Fragment、Activity的context,就会导致其无法释放。

public class Utils {
    private Context mContext;
    private static volatile Utils mUtils;
    
    private Utils(Context context){
        this.mContext = context;
    }
    
    public static Utils getInstance(Context context){
        if(mUtils == null){
            synchronized(Utils.class) {
                if(mUtils == null){
                    utils = new Utils(context);
                    //解决:可以使用aplicationContext作为其变量
                    //utils = new Utils(context.getApplicationContext());
                }
            }
        }
        return mUtils;
    }
}
复制代码
1.2.2 非静态内部类引起的内存泄露

大致可以分为2种情况:

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

此种情况的原因与上面的单例类似:

  • 非静态内部类默认会持有外部类的引用。
  • 使用该非静态内部类创建了一个静态的实例,该静态实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
public class MainActivity extend AppCompatActivity{
    //为避免频繁启动初始化,声明test为静态变量
    private static Test test;
    
    @Overide
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
    }
    
    private void initData(){
        if(test == null){
            test = new Test();
        }
    }
    
    //非静态内部类Test
    private class Test{
        
    }
}
复制代码

2. 由于内部类中的线程生命周期不可控导致的内存泄露

例如常见的handler、AsyncTask的使用。 原因:

  • 非静态匿名内部类对其外部类存在一个隐式引用。
  • 其外部类在销毁之前,如果该非静态内部类的异步任务还未完成,将会导致外部类的内存资源无法正常释放,造成了内存泄漏。

分析:当我们执行了 HandlerActivity的界面时,被延迟的消息会在被处理之前存在于主线程消息队列中5分钟,而这个消息中又包含了 Handler的引用,而我们创建的 Handler又是一个匿名内部类的实例,其持有外部 HandlerActivity的引用,这将导致了 HandlerActivity无法回收,进行导致 HandlerActivity持有的很多资源都无法回收, 如果5分钟之后延时任务完成,那么activity就会回收,所以说这种泄漏是临时的。

解决:

  • 使用静态内部类,静态的内部类不会持有外部类的引用。 如果想使用外部类的话,可以通过软引用或弱引用的方式保存外部类的引用。 静态类不持有外部类的对象,所以你的Activity可以随意被回收。由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference)。

  • 对于内部类线程生命周期不可控的问题,还可以通过程序逻辑来进行保护

    1. 在关闭Activity的时候停掉你的后台线程。线程停掉了,Activity自然会在合适的时候被回收。
    2. 如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
1.2.3 监听器

我们经常会通过Context.getSystemService()获取系统服务。这些服务通常在它们自己的进程中帮助我们处理一些后台任务或者与硬件的交互。当我们注册这些监听器后,其内部就持有了Context的引用,如果没有及时注销的话就可能导致内存泄露问题。

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});
复制代码
1.2.4 其他

如果你的代码使用到了C层的内存区域,那就需要自己去手动处理了,Java GC是不会帮你释放内存。

eg. 安卓3.0(API级别11)之前,Bitmap像素数据和Bitmap对象是分开存储的,像素数据是存储在native memory中,对象存储在Dalvik heap中。

这个Bitmap对象是由Java部分分配的,不用的时候系统就会自动回收了,但是那个对应的C可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用recycle()方法来释放C部分的内存。更多有关bitmap的内存问题之后的文章会单独说。

未关闭的文件(流)会引起内存泄露么?

我们经常会看到各种网上资料说文件打开后,没有关闭会导致内存泄露,其实这种说法并不准确。

  1. 当我们这样FileInputStream("/hello.txt") 会获取一个file descriptor。
  2. 每个进程都会有可用的file descriptor 限制。 所以如果不释放file descriptor,会导致应用后续依赖file descriptor的行为(socket连接,读写文件等)无法进行。
  3. 当我们调用FileInputStream.close后,会释放掉这个file descriptor。

因此,当我们正常使用流的时候,是不会导致内存泄露的。不关闭流不是内存泄露问题,是资源泄露问题。

参考:www.reddit.com/r/learnjava…

droidyue.com/blog/2019/0…

1.3 内存溢出

内存溢出也就是我们常见的OOM(Out Of Memory)。就是当前占用的内存加上我们申请的内存超出了Java虚拟机所能分配的范围。内存泄露严重时,或者突然需要大量内存时,就可能引发该问题。

2 常用的内存分析工具

2.1 Memory Profiler

其是Andriod Studio自带的一个工具,使用实时图表的形式来展示内存的使用情况。

功能:

  • 方便直观的观察到内存使用情况、判断是否发生内存泄露、抖动
  • 堆转储、强制GC、跟踪内存分配

一般情况下其位于Android Studio的左下角,如这里没有可以点击 View-> Tool Windows-> Profiler

点击+可以选择要监听的应用。

具体的使用细节大家可以参考谷歌的官方文档:

使用 Memory Profiler 查看 Java 堆和内存分配

访问不了的可以查看这篇翻译:www.jianshu.com/p/20a2e7dad…

2.2 LeakCanary

由Square开源的一款轻量级第三方内存泄漏检测工具 github.com/square/leak… 主要原理:

  1. 监听 Activity 的生命周期
  2. 在 onDestroy 的时候,创建相应的 Reference 和 ReferenceQueue,并启动后台进程去检测
  3. 一段时间之后,从 ReferenceQueue 读取,若读取不到相应 activity 的 Reference,有可能发生泄露了,这个时候,再促发 gc,一段时间之后,再去读取,若在从 ReferenceQueue 还是读取不到相应 activity 的 Reference,可以断定是发生内存泄露了
  4. 发生内存泄露之后,Debug.dump,分析 hprof 文件,找到泄露路径。

2.3 Memery Analyzer Tool(MAT)

最开始是一个eclipse插件。现在也有独立版。下载地址:www.eclipse.org/mat/downloa… 我用的是windows64位1.9.1版本,zip包66.8M

  • 强大的Java Heap分析工具,可用于查找内存泄露及内存占用
  • 生成整体报告、分析问题等

3. Android的内存管理机制

看着一小节前,最好对Java的内存管理与GC有一定了解,可以看下Java的GC机制与内存机构

3.1 Android分配内存是弹性的

  • 分配值与最大值受具体设备影响。 一开始不会分配太多,不够时再分配,新分配的大小不是随机的,也是有限度的。 获取App分配的内存
    ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
    //最大分配内存
    int memory = activityManager.getMemoryClass();
    System.out.println("memory: "+memory);
    //最大分配内存获取方法2
    float maxMemory = (float) (Runtime.getRuntime().maxMemory() * 1.0/ (1024 * 1024));
    //当前分配的总内存
    float totalMemory = (float) (Runtime.getRuntime().totalMemory() * 1.0/ (1024 * 1024));
    //剩余内存
    float freeMemory = (float) (Runtime.getRuntime().freeMemory() * 1.0/ (1024 * 1024));
    System.out.println("maxMemory: "+maxMemory);
    System.out.println("totalMemory: "+totalMemory);
    System.out.println("freeMemory: "+freeMemory);

原文链接:https://blog.csdn.net/wolfking0608/article/details/83185943
复制代码

android:largeHeap="true"会请求虚拟机为App分配更大的内存空间,但不是系统有多少内存就可以申请多少,而是由dalvik.vm.heapsize限制,一般写在/system/build.prop文件中:

dalvik.vm.heapsize=128m  
dalvik.vm.heapgrowthlimit=64m  

<application
     .....
     android:label="XXXXXXXXXX"
     android:largeHeap="true">
    .......
</application>
复制代码

3.2 Low Memory Killer机制

进程优先级: 优先级越低的进程被杀死的概率越大。

  1. 前台进程:正常情况不会被杀死
  2. 可见进程:正常情况不会被杀死
  3. 服务进程:正常情况不会被杀死
  4. 后台进程:存放于一个LRU缓存列表中,先杀死处于列表尾部的进程
  5. 空进程:正常情况下,Android不会保存这些进程

回收收益:当Android系统开始杀死LRU缓存中的进程时,系统会判断每个进程杀死后带来的回收收益。因为Android系统会倾向于杀死一个能回收更多内存的进程,从而可以杀死更少的进程,来获取更多的内存。杀死的进程越少,对用户体验的影响也就越小。

3.3 Dalvik虚拟机与Art在内存回收上的区别:

  • Dalvik固定了一种垃圾回收算法

  • Art回收算法可根据具体场景选择:例如任务当前在前台,此时相应速度最重要,可选择简单的标记-清除算法。对于后台应用,可选择标记-整理算法。

4. 结语

下一篇主要会介绍Bitmap以及Bitmap中的内存问题。

参考:Eight Ways Your Android App Can Leak Memory github.com/francistao/…

猜你喜欢

转载自juejin.im/post/5df6f00a6fb9a016301d9272