安卓性能优化之内存泄漏

时间:2021-12-08 06:04:18
ANR:Application Not Responding主线程做耗时操作超过5秒无响应就会导致ANR,BroadCastReceiver是10s,4.0之后网络IO不允许在主线程中, 

造成ANR的原因
应用程序的响应性是由ActivityManager和WindowManager系统服务监视的 
主线程做了耗时操作,(下载、IO流的读取、高耗时的计算)


Android的哪些操作是在主线程 
1、Activity的所有生命周期回调都是在主线程执行 
2、Service默认执行在主线程 
3、BroadcastReceiver的onReceive回调是执行在主线程 
4、没有使用子线程的looper的Handler的handleMessage,post(Runnable)在主线程执行
5、AsyncTask回调除了doInBackground,其他都是在主线程 

如何解决ANR
使用AsynctTask处理耗时IO操作 
使用Thread或HandlerThread提高优先级 
使用Handler来处理工作线程耗时任务 
在Activity的onCreate和onResume回调中尽量避免耗时的代码 

什么是OOM
当前占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存限制就会抛出OutOfMemory 
每个APP都有独立的内存空间,每个空间都有限制

易混淆的概念
内存溢出/内存抖动/内存泄漏
内存溢出:OOM 
内存抖动:短时间内频繁的GC操作。由于短时间内创建了大量的对象,又被很快的释放,刚刚创建的对象很快又被回收就会造成短时间内频繁的GC,很消耗性能。 
内存泄漏:在Java或安卓中,一个对象已经不再使用,但是他的引用仍然被持有,导致GC无法正常回收掉这个不再使用的对象,这就是内存泄漏,当累积多了之后导致没有可以继续分配的内存,就产生了内存溢出。 

如何解决OOM 
1、有关Bitmap优化,图片显示 :
1加载合适尺寸的图片,加载缩略图的时候不要去加载大图; 
2在ListView滑动的时候不要进行网络请求,停止的时候去加载网络图片;  
3及时释放内存 recycle  
4图片压缩 inSampleSize 采样率压缩  
5 inBitmap属性:图片复用属性,inBitmap只能在3.0之后使用。使用inBitmap有三个条件:* bitmap一定是可变的,即inmutable设置一定为true* 4.4以下系统,要保证inBitmap和即将得到decode的Bitmap尺寸规格一致* 4.4以上,inBitmap的尺寸大于要decode得到的Bitmap尺寸即可。满足条件之后,在加载图片的时,系统对图片进行decode的时候会检查内存中是否有可复用的Bitmap,避免频繁的从SD卡上加载图片从而影响系统性能。  
6 捕获异常:在实例化Bitmap对象的时候,为避免OOM要进行异常的捕获,捕获的是OutOfMemoryError,是错误不是一个Exception 

2、其他  
1 listview:convertView/lru  
2 避免在onDraw方法里面执行对象的创建,onDraw调用比较频繁,这样会造成内存抖动现象,积累到一定程度进而导致内存溢出  
3 谨慎使用多进程 


LRU原理
LruCache内部实现,有一个LinkedHashMap来保存对象,当缓存满了的时候,调用trimToSize来清除旧的缓存。 
private final LinkedHashMap<K, V> map;
LRUCache算法内部是通过一个泛型类,并用LinkedHashMap来实现的,提供了put和get方法,添加和得到缓存,其中最重要的一个方法是trimToSize方法,它会删除最近最少使用的内存。

Dalvik VM(DVM)和Java VM(JVM)的区别:
1、Dalvik基于寄存器,JVM基于栈。JVM字节码中,局部变量会被放入局部变量表中,继而被压入堆栈供操作码进行运算,当然JVM也可以只使用堆栈而不显式地将局部变量存入变量表中。Dalvik字节码中,局部变量会被赋给65536个可用的寄存器中的任何一个,Dalvik指令直接操作这些寄存器,而不是访问堆栈中的元素。 
2、字节码的区别
JVM字节码由.class文件组成,每个文件一个class。JVM在运行的时候为每一个类装载字节码。相反的,Dalvik程序只包含一个.dex文件,这个文件包含了程序中所有的类。Java编译器创建了JVM字节码之后,Dalvik的dex编译器删除.class文件,重新把它们编译成Dalvik字节码,然后把它们写进一个.dex文件中。这个过程包括翻译、重构、解释程序的基本元素(常量池、类定义、数据段)。常量池描述了所有的常量,包括引用、方法名、数值常量等。类定义包括了访问标志、类名等基本信息。数据段中包含各种被VM执行的函数代码以及类和函数的相关信息(例如DVM所需要的寄存器数量、局部变量表、操作数堆栈大小),还有实例变量。 
 
 
3、Dalvik 和 Java 运行环境的区别
  Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
     Dalvik虚拟机在android2.2之后使用JIT (Just-In-Time)技术,与传统JVM的JIT并不完全相同
  Dalvik虚拟机有自己的 bytecode,并非使用 Java bytecode。

4、其他
1、Dalvik主要是完成对象生命周期管理,堆栈管理,线程管理,安全和异常管理,以及垃圾回收等等重要功能。 
2、Dalvik负责进程隔离和线程管理,每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,其代码在虚拟机的解释下得以执行。 
3、不同于Java虚拟机运行java字节码,Dalvik虚拟机运行的是其专有的文件格式Dex。   
4、dex文件格式可以减少整体文件尺寸,提高I/O操作的类查找速度。 
5、odex是为了在运行过程中进一步提高性能,对dex文件的进一步优化。 


内存:
寄存器:速度最快的存储场所,因为寄存器位于处理器内部,在程序中无法控制。
栈:存放基本类型的数据和对象的引用,对象本身不放在栈中,放在堆中。 
堆:存放new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器GC来管理。 
静态存储区:在固定的位置存放应用程序运行时一直存在的数据,Java在内存中专门划分了一个静态存储区来管理一些特殊的数据变量,如静态的数据变量。 
常量池:JVM虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集合,包括直接常量(基本类型,String)和其他类型、字段和方法的符号引用。 


内存泄漏相关知识:

1、Java内存的分配策略
(1) 静态存储区,也叫方法区,主要存放静态变量和全局变量等,在程序编译的时候已经分配好了,在整个程序运行期间都存在。
(2) 栈区 在方法执行的时候,方法中的局部变量会在栈上创建内存空间,在方法执行结束后这些局部变量的内存空间会被自动释放。栈区内置于
   处理器中,执行效率很高,但是栈区的内存空间容量有限。
(3) 堆区  又称为动态内存分配,存放的是通过new创建的对象,这部分内存在不使用的时候由Java的垃圾回收器负责回收。


2、Java是如何管理内存的
Java的内存管理其实就是对象的分配和释放,通过关键字new创建对象,在堆中申请内存空间,释放是通过垃圾回收器回收。
内存的分配是由开发人员控制的,内存的释放是由gc自动完成的。减轻了开发人员的工作,加重了Java虚拟机的工作(运行效率慢的原因之一)。

gc为了正确的释放内存,必须监控对象的每一个状态,包括对象的申请,引用,被引用,赋值等等,

可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边。



3、Java中的内存泄漏
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时的释放,从而造成内存空间的浪费称为内存泄漏。
内存泄漏最终会导致内存溢出。


4、Android中的内存泄漏
(1) 单例
单例模式很常见,使用不当容易造成内存泄漏;单例的静态特性使他的生命周期和APP应用的生命周期是一样的。如果一个对象不再需要使用了,而
这个单例对象仍然持有该对象的引用,那么这个对象将不能被正常回收,就会造成内存泄漏。

正确的写法是:传入的context是Application的Context,避免造成传入Activity的Context,持有Activity的引用而影响Activity的正常回收。
private Context context;  private SPUtils(Context context) {
    this.context = context.getApplicationContext(); }

public static SPUtils getInstance(Context context) {
    if (mSpUtils == null) {
        synchronized (SPUtils.class) {
            if (mSpUtils == null) {
                mSpUtils = new SPUtils(context);  return mSpUtils;  }
        }
    }
    return mSpUtils; }

(2) 匿名内部类
Java中非静态内部类会默认持有外部类的引用,如果在非静态内部类里面创建了一个静态的实例,就会使内部类的生命周期和APP的生命周期相同,
当对象不再使用的时候它仍然持有外部类(例如Activity)的引用,导致外部类的资源无法正常回收。

正确的是把内部类写成静态内部类

(3)Handler
handler造成的内存溢出比较常见,在处理网络请求或者更新ui的时候会用Handler进行一个回调,

下例的mHandler对象是MainActivity的非静态内部类的实例,它会默认持有外部类MainActivity的引用。
Handler内部是一个Looper消息循环机制,当MainActivity退出的时候,如果handler的消息队列中还有未处理的消息,而消息中的Message还持有mHandler的实例引用,mHandler还持有MainActivity的引用,就会造成Activity的资源无法正常回收。
所以这种写handler的方式很难保证Handler和Activity的生命周期是一样的。


public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Handler mHandler = new Handler() {
 @Override  public void handleMessage(Message msg) { super.handleMessage(msg);  //.....  } };


正确使用Handler代码如下
//正确写法 private MyHandler mHandler = new MyHandler(this);  private static class MyHandler extends Handler {
    private WeakReference<Context> reference;   public MyHandler(Context context) {
        reference = new WeakReference<>(context);  }

    @Override  public void handleMessage(Message msg) {
        MainActivity mainActivity = (MainActivity) reference.get();  if (mainActivity != null) {
            //mainActivity doing something  }
    }
}



(4) 避免使用static变量
如果把成员变量声明为static,那么类的生命周期就和整个APP生命周期是一样的;如果APP的进程是常驻内存的,那么即使APP切换到后台,那么static的变量的内存也是不会释放的,


(5) 资源未关闭造成内存泄漏
广播接受者,contentobserver,文档,游标,stream,socket,这些在Activity销毁的时候在onDestroy方法中进行关闭。

(6) AsyncTask造成内存泄漏
AsyncTask也是由于非静态内部类持有外部类Activity的引用所导致的,导致Activity无法正常回收。要在onDestroy方法中调用AsyncTask的cancel方法。

(7) 大图片bitmap的处理,ListView的优化等。