写点什么

Android 性能优化:看完这篇文章, 至少解决 APP 中 90 % 的内存异常问题

用户头像
Android架构
关注
发布于: 3 小时前

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。


目前虚拟机基本都是采用可达性分析算法,为什么不采用引用计数算法呢?下面就说说引用计数法是如果统计所有对象的引用计数的,再对比可达性分析算法是如何解决引用计数算法的不足。下面就来看下这 2 个算法:

[](

)引用计数算法


每个对象有一个引用计数器,当对象被引用一次则计数器加一,当对象引用一次失效一次则计数器减一,对于计数器为 0 的时候就意味着是垃圾了,可以被 GC 回收。


下面通过一段代码来实际看下


public class GCTest {


private Object instace = null;


public static void onGCtest() {


//step 1


GCTest gcTest1 = new GCTest();


//step 2


GCTest gcTest2 = new GCTest();


//step 3


gcTest1.instace = gcTest2;


//step 4


gcTest2.instace = gcTest1;


//step 5


gcTest1 = null;


//step 6


gcTest2 = null;


}


public static void main(String[] arg) {


onGCtest();


}


}


分析代码


//step 1 gcTest1 引用 + 1 = 1


//step 2 gcTest2 引用 + 1 = 1


//step 3 gcTest1 引用 + 1 = 2


//step 4 gcTest2 引用 + 1 = 2


//step 5 gcTest1 引用 - 1 = 1


//step 6 gcTest2 引用 - 1 = 1


很明显现在 2 个对象都不能用了都为 null 了,但是 GC 确不能回收它们,因为它们本身的引用计数不为 0 。不能满足被回收的条件,尽管调用 System.gc() 也还是不能得到回收, 这就造成了 内存泄漏 。当然,现在虚拟机基本上都不采用此方式。

[](

)可达性分析算法



从 GC Roots 作为起点开始搜索,那么整个连通图中额对象边都是活对象,对于 GC Roots 无法到达的对象便成了垃圾回收的对象,随时可能被 GC 回收。


可以作为 GC Roots 的对象


  • 虚拟机栈正在运行使用的引用

  • 静态属性 常量

  • JNI 引用的对象


GC 是需要 2 次扫描才回收对象,所以我们可以使用 finalize 去救活丢失的引用


@Override


protected void finalize() throws Throwable {


super.finalize();


instace = this;


}


到了这里,相信大家已经能够弄明白这 2 个算法的区别了吧?反正对于对象之间循环引用的情况,引用计数算法无法回收这 2 个对象,而可达性是从 GC Roots 开始搜索,所以能够正确的回收。

[](

)不同引用类型的回收状态

[](

)强引用


Object strongReference = new Object()


如果一个对象具有强引用,那垃圾回收器绝不会回收它,当内存空间不足, Java 虚拟机宁愿抛出 OOM 错误,使程序异常 Crash ,也不会靠随意回收具有强引用的对象来解决内存不足的问题.如果强引用对象不再使用时,需要弱化从而使 GC 能够回收,需要:


strongReference = null; //等 GC 来回收


还有一种情况,如果:


public void onStrongReference(){


Object strongReference = new Object()


}


在 onStrongReference() 内部有一个强引用,这个引用保存在 java 栈 中,而真正的引用内容 (Object)保存在 java 堆中。当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为 0 ,这个对象会被回收。


但是如果 mStrongReference 引用是全局时,就需要在不用这个对象时赋值为 null ,因为 强引用 不会被 GC 回收。

[](

)软引用 (SoftReference)


如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。


软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, java 虚拟机就会把这个软引用加入到与之关联的引用队列中。



注意: 软引用对象是在 jvm 内存不够的时候才会被回收,我们调用 System.gc() 方法只是起通知作用, JVM 什么时候扫描回收对象是 JVM 自己的状态决定的。就算扫描到了 str 这个对象也不会回收,只有内存不足才会回收。

[](

)弱引用 (WeakReference)


弱引用与软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。


弱引用可以和一个引用队列联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。



可见 weakReference 对象的生命周期基本由 GC 决定,一旦 GC 线程发现了弱引用就标记下来,第二次扫描到就直接回收了。


注意这里的 referenceQueuee 是装的被回收的对象。

[](

)虚引用 (PhantomReference)


@Test


public void onPhantomReference()throws InterruptedException{


String str = new String("123456");


ReferenceQueue queue = new ReferenceQueue();


// 创建虚引用,要求必须与一个引用队列关联


PhantomReference pr = new PhantomReference(str, queue);


System.out.println("PhantomReference:" + pr.get());


System.out.printf("ReferenceQueue:" + queue.poll());


}


虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。


虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue) 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

[](

)总结


| 引用类型 | 调用方式 | GC | 是否内存泄漏 |


| --- | --- | --- | --- |


| 强引用 | 直接调用 | 不回收 | 是 |


| 软引用 | .get() | 视内存情况回收 | 否 |


| 弱引用 | .get() | 回收 | 不可能 |


| 虚引用 | null | 任何时候都可能被回收,相当于没有引用一样 | 否 |


[](


)分析内存常用工具




工具很多,掌握原理方法,工具随意挑选使用。

[](

)top/procrank

[](

)meinfo

[](

)Procstats

[](

)DDMS

[](

)MAT

[](

)Finder - Activity

[](

)LeakCanary

[](

)LeakInspector


[](


)内存泄漏




产生的原因: 一个长生命周期的对象持有一个短生命周期对象的引用,通俗点讲就是该回收的对象,因为引用问题没有被回收,最终会产生 OOM。


下面我们来利用 Profile 来检查项目是否有内存泄漏

[](

)怎么利用 profile 来查看项目中是否有内存泄漏


  1. 在 AS 中项目以 profile 运行



  1. 在 MEMORY 界面中选择要分析的一段内存,右键 export



Allocations: 动态分配对象个数


Deallocation: 解除分配的对象个数


Total count: 对象的总数


Shalow Size: 对象本身占用的内存大小


Retained Size: GC 回收能收走的内存大小


  1. 转换 profile 文件格式


  • 将 export 导出的 dprof 文件转换为 Mat 的 dprof 文件

  • cd /d 进入到 Android sdk/platform-tools/hprof-conv.exe


//转换命令 hprof-conv -z src des


D:\Android\AndroidDeveloper-sdk\android-sdk-windows\platform-tools>hprof-conv -z D:\temp_\temp_6.hprof D:\temp_\memory6.hprof


  1. [下载 Mat 工具](


)


  1. 打开 MemoryAnalyzer.exe 点击左上角 File 菜单中的 Open Heap Dupm



  1. 查看内存泄漏中的 GC Roots 强引用


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2V


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


zLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xOTk1NjEyNy04MmEyNGMxNWZmOTQzZWI3LnBuZw?x-oss-process=image/format,png)


这里我们得知是一个 ilsLoginListener 引用了 LoginView,我们来看下代码最后怎么解决的。



代码中我们找到了 LoginView 这个类,发现是一个单例中的回调引起的内存泄漏,下面怎么解决勒,请看第七小点。


  1. 2 种解决单例中的内存泄漏

  2. 将引用置为 null


/**


  • 销毁监听


*/


public void unRemoveRegisterListener(){


mMessageController.unBindListener();


}


public void unBindListener(){


if (listener != null){


listener = null;


}


}


  1. 使用弱引用


//将监听器放入弱引用中


WeakReference<IBinderServiceListener> listenerWeakReference = new WeakReference<>(listener);


//从弱引用中取出回调


listenerWeakReference.get();


  1. 通过第七小点就能完美的解决单例中回调引起的内存泄漏。

[](

)Android 中常见的内存泄漏经典案例及解决方法


  1. 单例


示例 :


public class AppManager {


private static AppManager sInstance;


private CallBack mCallBack;


private Context mContext;


private AppManager(Context context) {


this.mContext = context;


}


public static AppManager getInstance(Context context) {


if (sInstance == null) {


sInstance = new AppManager(context);


}


return sInstance;


}


public void addCallBack(CallBack call){


mCallBack = call;


}


}


  1. 通过上面的单列,如果 context 传入的是 Activity , Service 的 this,那么就会导致内存泄漏。


以 Activity 为例,当 Activity 调用 getInstance 传入 this ,那么 sInstance 就会持有 Activity 的引用,当 Activity 需要关闭的时候需要 回收的时候,发现 sInstance 还持有 没有用的 Activity 引用,导致 Activity 无法被 GC 回收,就会造成内存泄漏


  1. addCallBack(CallBack call) 这样写看起来是没有毛病的。但是当这样调用在看一下勒。


//在 Activity 中实现单例的回调


AppManager.getInstance(getAppcationContext()).addCallBack(new CallBack(){


@Override


public void onStart(){


}


});


这里的 new CallBack() 匿名内部类 默认持有外部的引用,造成 CallBack 释放不了,那么怎么解决了,请看下面解决方法


解决方法:


  1. getInstance(Context context) context 都传入 Appcation 级别的 Context,或者实在是需要传入 Activity 的引用就用 WeakReference 这种形式。

  2. 匿名内部类建议大家单独写一个文件或者


public void addCallBack(CallBack call){


WeakReference<CallBack> mCallBack= new WeakReference<CallBack>(call);


}


  1. Handler


示例:


//在 Activity 中实现 Handler


class MyHandler extends Handler{


private Activity m;


public MyHandler(Activity activity){


m=activity;


}


// class.....


}


这里的 MyHandler 持有 activity 的引用,当 Activity 销毁的时候,导致 GC 不会回收造成 内存泄漏。


解决方法:


1.使用静态内部类 + 弱引用


2.在 Activity onDestoty() 中处理 removeCallbacksAndMessages()


@Override


protected void onDestroy() {


super.onDestroy();


if(null != handler){


handler.removeCallbacksAndMessages(null);


handler = null;


}


}


  1. 静态变量


示例:


public class MainActivity extends AppCompatActivity {


private static Police sPolice;


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


if (sPolice != null) {


sPolice = new Police(this);


}


}


}


class Police {


public Police(Activity activity) {


}


}


这里 Police 持有 activity 的引用,会造成 activity 得不到释放,导致内存泄漏。


解决方法:


//1. sPolice 在 onDestory()中 sPolice = null;


//2. 在 Police 构造函数中 将强引用 to 弱引用;


  1. 非静态内部类


参考 第二点 Handler 的处理方式


  1. 匿名内部类


示例:


public class MainActivity extends AppCompatActivity {


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


new Thread(){


@Override


public void run() {


super.run();


}


};


}


}


很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程ThreadAsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。


解决方法:


//静态内部类 + 弱引用


//单独写一个文件 + onDestory = null;


  1. 未取消注册或回调


示例:


public class MainActivity extends AppCompatActivity {


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


registerReceiver(mReceiver, new IntentFilter());


}


private BroadcastReceiver mReceiver = new BroadcastReceiver() {


@Override


public void onReceive(Context context, Intent intent) {


// TODO ------


}


};


}


在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit + RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。


解决方法:


//Activity 中实现 onDestory()反注册广播得到释放


@Override


protected void onDestroy() {


super.onDestroy();


this.unregisterReceiver(mReceiver);


}


  1. 定时任务


示例:


public class MainActivity extends AppCompatActivity {


/*模拟计数/


private int mCount = 1;


private Timer mTimer;


private TimerTask mTimerTask;


@Override


protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);


setContentView(R.layout.activity_main);


init();


mTimer.schedule(mTimerTask, 1000, 1000);


}


private void init() {


mTimer = new Timer();


mTimerTask = new TimerTask() {


@Override


public void run() {


MainActivity.this.runOnUiThread(new Runnable() {


@Override


public void run() {


addCount();


}


});


}


};


}


private void addCount() {


mCount += 1;


}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android性能优化:看完这篇文章,至少解决 APP 中 90 % 的内存异常问题