内存优化小结
内存优化
内存主要有三大问题:内存溢出、内存泄露、内存抖动;内存问题轻则会导致应用卡顿,重则导致应用崩溃
内存溢出
内存溢出是指当前应用程序内存不够用了,系统会抛出OOM异常;
-
系统为每一个app分配的最大内存空间大小是固定的,java层和native层最大内存限制不一样,这个大小不同版本不同厂商限制会有所不同,系统源码中可以修改
-
发生的场景:创建线程数量过多、静态变量保存过多的数据没有及时释放、一些大图资源没有及时释放内存等等
优化方法:
-
在线程使用方面,尽量使用线程池对线程进行复用,避免频繁创建
-
数据尽量不要保存到静态变量中,需要持久化的数据保存到磁盘中
-
像图片这种比较占内存的不展示时应该及时回收内存
-
注册监听低内存事件,及时释放不需要的内存对象;onLowMemory、onTrimMemory(会传递int值表示不同情况下内存情况)、registerComponentCallbacks()
内存泄露
内存泄露的本质是长生命周期对象强引用短生命周期对象,导致不再使用的对象不能及时被回收;当内存泄露发生时,会导致内存占用越来越高,GC频繁甚至内存溢出的发生;
常见的内存泄露场景有:
-
静态变量:静态变量是存放在方法区,一般情况下引用的对象是不会自动回收的;被静态变量引用的对象会长期存在内存中,除非手动释放;所以使用静态变量一定要记得手动释放内存;比如尽量不要静态变量去保存数据集合,或者去静态引用View;View因为会默认持有Activity;被静态变量引用后会导致Activity的内存无法被回收,从而造成内存泄露
-
非静态内部类:非静态内部类会隐式持有外部类,如果是Activity,则可能会被其他长生命周期对象间接持有
-
单例模式:由于单例对象都是保存在静态变量里的,所以如果单例对象持有其他对象也会造成内存泄露,比较常见的是传入Activity,这个时候应该用弱引用去持有Activity或者使用ApplicationContext代替
-
广播:这里是指动态注册的广播,当广播接收器注册时,会被保存到LoadedApk中,LoadedApk又被Application和ActivityThread这种长生命周期对象持有,如果我们定义的广播接收器不是静态内部类,就会默认隐式持有当前Activity,导致Activity退出时发生内存泄露;
-
引用链:Activity->BraodcastReceiver->LoadedApk->Application & ActivityThread
-
对于本地广播来说,本地广播它是单例模式,被静态变量引用,所以也会导致内存泄露
-
所以平时动态注册广播时要将广播接收器声明为静态内部类或者独立出来单个类,退出时取消注册
-
除了广播,像一些其他需要注册监听事件的,都需要在必要时刻取消注册
-
-
动画:如果Activity退出时,动画还没结束,就可能导致Activity内存泄露;这是因为动画开始后,会通过AnimationHandler注册垂直同步信号监听,当前动画对象实现了监听接口,它会被保存到AnimationHandler中,而AnimationHandler实例是保存在静态的ThreadLocal变量中的,ThreadLocal会间接被当前线程持有,一般更新UI是在主线程上,也就是说它的生命周期是非常长的,如果传入的动画监听事件是非静态内部类,就会隐式的持有Activity,并且退出时没有取消动画,则会导致内存泄露;
-
引用链:Activity-> AnimatorUpdateListener-> ValueAnimator -> AnimatorHandler -> ThreadLocal -> Thread(UI)
-
解决办法就是动画监听事件改成静态内部类或者独立出一个类文件,并且退出时取消动画
-
ObjectAnimation传入的View是弱引用,但是会发现如果不取消动画,Activity短时间内不会被回收掉,这是因为在更新动画时,会获取View实例,这个时候View会被作为GCRoot的局部变量所引用,GC发生时可能刚好正在刷新动画,而动画刷新的频率是很快的,根据垂直同步信号来的,大概16.66毫秒就会来一次
-
ThreadLocal在设计之初为了避免内存泄漏,在保存到ThreadLocalMap时构建的Entry是弱引用key的,但是value却是强引用的,也就是会出现key为null,value不为null的数据,当每次get/set操作时会去清掉key为null的数据;但是如果数据初始化之后没有调用过get/set方法则数据会一直存在线程中不能释放,尤其是在线程池复用线程情况下,可能导致后面使用线程时ThreadLoad数据还是旧的;不再使用时最好主动调用remove方法移除
-
-
Handler:Handler发送消息时,Message会持有Handler,Message又被MessageQueue持有,MessageQueue又被Looper持有,主线程的Looper是保存在静态变量中,其他线程的Looper是保存到ThreadLocal中,都是被长生命周期持有;如果Handler创建时是非静态内部类,就会隐式持有外部的Activity,导致Activity间接被Looper持有,当退出时消息还没处理外,这种引用关系就一直存在,导致内存泄露;
-
引用链:Activity->Handler->Message->MessageQueue->Looper->Static/()ThreadLocal->Thread)
-
解决办法是将Handler声明为静态内部类或者独立出来,并且在退出时将未处理的消息全部移除
-
ObjectAnimator不会造成内存泄露,因为它传入的View对象是弱引用,当刷新动画时检测到View不再了,就会主动取消动画
-
-
WebView内存泄露问题:WebView在onAttatchedToWIndow方法中会通过AWContext注册一个组件回调函数(registerComponentCallbacks),这个回调函数会被保存到Application中,这个回调函数是个非静态内部类,隐式持有了AWContext,而AWContext有持有了WebView的Context也就是Activity;也就是Application间接会持有Activity;
-
引用链:Activity->WebView->AWContext->ComponentCallbacks->Application
-
在onDetachedFromWindow方法中,会先判断有没有destroy,如果已经destroy了,则不会去取消注册监听;所以如果退出时我们只调用WebView的destroy方法,在接下来onDetachedFromWindow方法中就不会取消注册,从而造成内存泄露;
-
如果说我们不去调用destroy方法,则可能因为某些没有取消其他注册监听而造成其他内存泄露问题
-
解决办法是在调用destroy方法之前,主动将它从父View中remove掉,提前触发onDetachedFromWindow方法;或者让WebView运行在一个独立进程中,退出时结束该进程
-
在新版本(Android5.0)中,会在destroy方法中主动调用onDetachedFromWindow方法去取消注册
-
内存抖动
内存抖动是指:短时间内频繁创建对象、导致GC频繁发生,从而引起卡顿
容易出现内存抖动的场景有:
-
在自定义View的onDraw方法中创建对象
-
在for循环中创建对象
-
在动画回调方法中创建对象
-
onTouchEvent
解决办法:
-
将临时创建的对象改成全局的,只创建一次
-
如果创建的对象不能公用一个,则使用享元模式,建立回收池,控制创建对象的量
内存监控
内存问题主要以检测内存泄漏为主,主要是通过借助第三方工具来做,比如早期的MAT、Android Studio自带的Profiler、LeakCanary,还有JVM提供的监测工具JVMTI等等;
-
MAT和Profiler主要是通过手动GC后,获取它的堆栈信息,查看对象的引用链,比如打开一个Activity,退出后手动GC,观察堆区是否还存在该对象,如果存在多个实例则可能内存泄漏了,并通过引用链定位产生内存泄漏的地方
-
LeakCanary实现原理,则是在Application中注册Activity生命周期监听,当Activity销毁时,将当前Activity实例保存到一个弱引用中并传入一个队列(ReferenceQueque),当该实例将要被回收时会进入该队列,所以当检测到该队列中有该Activity实例时,说明没有内存泄漏可以正常被GC回收,如果没检测到进入该队列,则主动触发一次GC,GC过后还是没有被回收,则表示可能存在内存泄漏,接着获取堆栈信息获取到该实例的引用链,最后弹出内存泄漏提示