【0190】Android 性能优化之内存泄露

1.基本概念

1.1 内存泄露

【什么是内存泄露】内存不在GC掌控之内了。

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。

相比C/C++ 自己去分配内存和释放内存--手动管理。

【GC内存回收】某对象不再有任何的引用的时候才会进行回收。

1.2 内存分配的几种策略

【内存分配的几种策略】

1.静态的
静态的存储区:内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在。
它主要存放静态数据、全局的static数据和一些常量。

2.栈式的
在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉。
栈内存包括分配的运算速度很快,因为内置在处理器的里面的。当然容量有限。
3.堆式的
也叫做动态内存分配。有时候可以用malloc或者new来申请分配一个内存。在C/C++可能需要自己负责释放(java里面直接依赖GC机制)。
在C/C++这里是可以自己掌控内存的,需要有很高的素养来解决内存的问题。java在这一块貌似程序员没有很好的方法自己去解决垃圾内存,需要的是编程的时候就要注意自己良好的编程习惯。

【区别】堆是不连续的内存区域,堆空间比较灵活也特别大。
               栈式一块连续的内存区域,大小是有操作系统觉决定的。

1.成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体)---因为他们属于类,类对象最终还是要被new出来的
2.局部变量的基本数据类型和引用存储于栈当中,引用的对象实体存储在堆中。---因为他们属于方法当中的变量,生命周期会随着方法一起结束

1 public class Main{
2     int a = 1;  //a 和 1 都在堆中
3     Student s = new Student(); // s 和 Student都在堆中
4     public void XXX(){
5         int b = 1;//b 和 1 都在栈(方法栈)里面
6         Student s2 = new Student();//s2 在栈(方法栈)里面,new Student()在堆中
7     }
8 }

基本数据类型是放在栈中还是放在堆中,这取决于基本类型声明的位置。

一:在方法中声明的变量,即使变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因。 
在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。 
(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中 
(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆内存中的

二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。 
同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量 
(1)当声明的是基本类型的变量其变量名及其值放在堆内存中的 
(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中.

总结: 

1.3 引用

我们所讨论内存泄露,主要讨论堆内存,他存放的就是引用指向的对象实体。

有时候确实会有一种情况:当需要的时候可以访问,当不需要的时候可以被回收也可以被暂时保存以备重复使用。

比如:ListView或者GridView、REcyclerView加载大量数据或者图片的时候,
图片非常占用内存,一定要管理好内存,不然很容易内存溢出。
滑出去的图片就回收,节省内存。看ListView的源码----回收对象,还会重用ConvertView。
如果用户反复滑动或者下面还有同样的图片,就会造成多次重复IO(很耗时),
那么需要缓存---平衡好内存大小和IO,算法和一些特殊的java类。
算法:lrucache(最近最少使用先回收)
特殊的java类:利于回收,StrongReference,SoftReference,WeakReference,PhatomReference

【StrongReference】强引用:
回收时机:从不回收 使用:对象的一般保存 生命周期:JVM停止的时候才会终止
【SoftReference】软引用
回收时机:当内存不足的时候;使用:SoftReference<String>结合ReferenceQueue构造有效期短;生命周期:内存不足时终止
【WeakReference】弱引用
回收时机:在垃圾回收的时候;使用:同软引用; 生命周期:GC后终止
【PhatomReference】 虚引用
回收时机:在垃圾回收的时候;使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动; 生命周期:GC后终止

开发时,为了防止内存溢出,处理一些比较占用内存大并且生命周期长的对象的时候,可以尽量使用软引用和弱引用
软引用比LRU算法更加任性,回收量是比较大的,你无法控制回收哪些对象。

比如使用场景:默认头像、默认图标。
ListView或者GridView、RecyclerView要使用内存缓存+外部缓存(SD卡

1.4  获取和显示Java堆的快照

【文章链接】https://www.jianshu.com/p/7d958959cf33

要想查看Java堆的快照,必须要经过下面两步:

    1. 在内存监视工具里显示一个正在运行的程序
    2. 点击Java堆转储按钮
      当转储成功是这个内存监视器显示的图标会改变。Android studio创建的HPROF文件名格式为package_yyyy.mm.dd_hh.mm.ss.hprof,使用包名和转储的时间来命名,例如com.android.calc_2015.11.17_14.58.48.hprof。

为什么要查看Java堆

Java堆展示如下信息:

  • 按类展示实例对象的内存使用情况;
  • 每次垃圾回收事件的样本数据,不管是系统触发还是你手动触发的垃圾回收事件;
  • 帮助分析哪些对象类型也许会导致内存泄漏。

但是,HPROF文件只是展示了某一时刻Java堆的使用情况,如果你需要了解某段时间内Java堆使用情况的, 你需要通过分析不同时间点生成的HPROF文件来找出其中的变化。HPROF分析工具可以自动分析出以下两种类型的问题:

  • 所有已经被销毁,但是不能回收的Activity的实例;
  • 重复定义的字符串。

理解HPROF文件查看工具的显示

HPROF文件查看工具界面如下:


HPROF文件查看工具

这个工具显示了如下信息:

名称描述
Class name 类名
Total Count 该类的实例总数
Heap Count 所选择的堆中该类的实例的数量
Sizeof 单个实例所占空间大小(如果每个实例所占空间大小不一样则显示0)
Shallow Size 堆里所有实例大小总和(Heap Count * Sizeof)
Retained Size 该类所有实例所支配的内存大小
Instance 具体的实例
Reference Tree 所选实例的引用,以及指向该引用的引用。
Depth GC根节点到所选实例的最短路径的深度
Shallow Size 所选实例的大小
Dominating Size 所选实例所支配的内存大小

如果你点击了Analyzer Tasks就会展示HPROF分析工具,界面如下图右边板块:


HPROF文件分析工具

用HPROF分析工具,可以检测到泄漏的activities、分析出重复定义的字符串。

查看一个已经保存好的HPROF文件

堆转储后,Android studio会自动保存HPROF文件,以便你再次查看。用HPROF查看工具查看HPROF文件的步骤如下:

    1. 在主窗口中点击Captures按钮,或者选择 View > Tools Windows > Captures,打开Captures窗口;
    2. 打开堆快照文件夹
    3. 双击你想要查看的HPROF文件,打开HPROF文件查看工具界面
    4. 选择你想查看的堆
      • App heap - 当前app使用的堆
      • Image heap - 当前app在硬盘上的内存映射
      • Zygote heap - zygote 复制时继承来的库、运行时类和常量的数据集。zygote空间设备启动时创建,从不分配这里的空间。
    5. 选择你想查看的视图选项:
      • Class List View
      • Package Tree View 

1.4 内存泄露的实例

【实例】使用单例模式时,将Context作为参数。

【单例模式源码】Context作为成员变量。

 1 public class CommonUtils {
 2     
 3     private static CommonUtils instance;
 4     
 5     private Context mContext;
 6 
 7     public CommonUtils(Context mContext) {
 8         this.mContext = mContext;
 9     }
10     
11     public static CommonUtils getInstance(Context context){
12         if (instance == null){
13             instance = new CommonUtils(context)
14         }
15         
16         return instance;
17     }
18 }

【MainActivity.java】使用时将MainAcitivity作为了Context传入。会引入内存泄露。

 1 public class MainActivity extends AppCompatActivity {
 2 
 3     @Override
 4     protected void onCreate(Bundle savedInstanceState) {
 5         super.onCreate(savedInstanceState);
 6         setContentView(R.layout.activity_main);
 7 
 8         CommUtil commUtil = CommUtil.getInstance(this);
 9 
10     }
11 }

【原因】

原因是:当手机进行横竖屏切换的时候或者Activity过多内存使用紧张的时候,系统销毁Activity,但在这个时候该Activity被CommUtil工具类所持有,导致Activity无法被系统回收从而导致内存泄露

【问题解决】

【方法1】使用全局Application上下文

【Context和AppContext区别】https://blog.csdn.net/ecjtuhq/article/details/53999582

1 public class PerformsActivity{
2       @override
3       protected void onCreate(Bundle saveInstanceState){
4            super.onCreate(savedInstanceState);
5            setContentView(R.layout.activity_performs);
6            CommUtils commUtils = CommUtils.getInstance(getApplicationContext());
7       }
8 }

【方法2】使用静态变量的弱引用

 1     private static WeakReference<CommUtils> WeakReferenceInstance;
 2     private Context context;
 3     public CommUtils(Context context) { 
 4          this.context = context;  
 5      }  
 6     public static CommUtils getInstance(Context context){ 
 7        if (WeakReferenceInstance == null || WeakReferenceInstance.get() == null) {
 8            WeakReferenceInstance = new WeakReference<CommUtils>(new CommUtils(context));
 9         }  
10       return WeakReferenceInstance.get();
11     }
12 }

【方法3】

 1 public class CommUtil {
 2     private static CommUtil instance;
 3     private Context context;
 4     private CommUtil(Context context){
 5         this.context = context;
 6     }
 7 
 8     public static CommUtil getInstance(Context context){
 9         if(instance == null){
10 //            instance = new CommUtil(context);
11             instance = new CommUtil(context.getApplicationContext());
12         }
13         return instance;
14     }
15 
16 }

1.6 要点说明

首先说明几个要点:

【要点1】

旋转3次:会在内存里面开辟三个MainActivity
实际上3次以上都只会有2个MainActivity。当GC回收的时候会将除了第0个和最后这一个留着其他的都会被回收

在多次的横竖屏切换之后,产生了多个实例,但是只有两个是被引用的实例,其他的都是待回收的垃圾。

【要点2】出现了多个实例。必然说明了是存在内存泄露。

【要点3】引用树只是提供了引用的参考,需要通过源码进行查看修改。

 【规则】

查找引用了该对象的外部对象有哪些,
然后一个一个去猜,查找可能内存泄露的嫌疑犯,依据:看(读代码和猜)他们的生命周期是否一致(可以通过快照对比),如果生命周期一致了肯定不是元凶。

 如:A类引用了B类,猜测是否A类造成了B类的内存泄露,可以将A类置空,然后gc,再查看B类的对象是否还存在,如果不存在,说明不是A类导致的内存泄露,因为生命周期一致。如果存在,则说明生命周期不一致,是A类造成的内存泄露。

下面介绍使用MAT分析,做好心理准备,也只能是提供线索

2. MAT工具的使用

2.1 MAT导入要分析的文件

MAT工具也只是能够提供内存泄露的线索,主要还是需要判断的经验。经验!经验!经验!

as中没有集成MAT,根据不同系统环境进行下载,下载地址:http://www.eclipse.org/mat/downloads.php

 as中直接生成的文件是无法使用MAT分析的,需要Export保存。

此处以没有旋转屏幕前的状态的文件2018.07.20_14.40.hprof为例,。

同样打开出现内存泄露的2018.07.20_14.44.hprof文件是一样的,看到生成的两个MainAcitivity引用。

展开之后:

2.2 去除判断软引用、弱引用、虚引用

【去除判断软引用、弱引用、虚引用】

没有去除引用展开之后的列表如下,作对对比。

去除引用之后的列表,比较清楚的看到了被引用的可能的造成泄露的类

 2.3 对比文件的资源判断

同理,将文件2也一起加入到CompareBasket中。

对比之后的列表如下:如果前后的两个快照对比具有数量级(1变为了10、100、1000、1000...)的改变,就是应该引起怀疑的对象了。

 3.内存泄露实例2

 【自定义View】

 1 public class CustomView extends View{
 2     public CustomView(Context context){
 3         super(context);
 4         init();
 5     }
 6 
 7     public interface CustomListener {
 8         public void CustomListenerCallback();
 9     }
10 
11     private void init(){
12         ListenerCollector collector = new ListenerCollector();
13         collector.setsListener(this, customListener);
14     }
15 
16     private CustomListener customListener = new CustomListener() {
17         @Override
18         public void CustomListenerCallback() {
19             Logger.d("调用有效");
20         }
21     };
22 }

 【监听集合类】

1 public class ListenerCollector {
2     static private WeakHashMap<View, CustomView.CustomListener> sListener = new WeakHashMap<>();
3     public void setsListener(View view, CustomView.CustomListener listener){ sListener.put(view,listener);}4 }

 【调用】

 1 public class MainActivity extends AppCompatActivity {
 2 
 3     @Override
 4     protected void onCreate(Bundle savedInstanceState) {
 5         super.onCreate(savedInstanceState);
 6 //        setContentView(R.layout.activity_main);
 7 
 8         CustomView view = new CustomView(this);
 9         setContentView(view);
10 
11     }
12 }

【内存变化情况】程序运行在真机,横竖屏切换,在下图中看到非常明显的内存泄露。

 

在多次手动gc后,等到内存图平稳后,截取内存快照图如下:Instance非常明显看到了内存泄露。

使用Mat分析工具打开,同样看到具有15个实例对象!

 

进行屏幕切换前后的资源对比:201807211016-1.hprof:屏幕多次切换之后的快照。 201807211017-0.hprof:屏幕没有切换的快照。

参考本文 4.2  确定内存泄露的对象

内存泄露的原因:在MainAcitivity切换屏幕时候会生成一个新的CustomView,在CustomView中会生成CustomListener;

在init设置监听的 时候,将customView和CustomListener都最后都存在了ListenerCollector对象的静态WeakHashMap集合中。

静态对象时是不会释放的,因此切换屏幕之后的customView和CustomListener仍然存在于WeakHashMap集合中,而customView仍然持有上一个MainAcitivity的对象,

因此导致了MainAcitivity对象的内存泄露。

长的旧的customView持有了短生命周期的MainAcitivity的对象,导致内存泄露。

 1 public class CustomView extends View{
 2 
 3     private Context mContext;
 4 
 5     //持有了Context对象
 6     public CustomView(Context context){
 7         super(context);
 8         this.mContext = context;
 9         init();
10     }
11 
12     //初始化的时候将View放入到HashMap中保存
13     private void init(){
14         ListenerCollector collector = new ListenerCollector();
15 //        Logger.d("setsListener Before:"+collector.getsListener().toString());
16 //        Toast.makeText(mContext, "setsListener Before:"+collector.getsListener().toString(), Toast.LENGTH_SHORT).show();
17         collector.printWeakHashMap();
18 
19         collector.setsListener(CustomView.this, customListener);
20         collector.printWeakHashMap();
21 //        Logger.d("setsListener After:"+collector.getsListener().toString());
22 
23     }
24 
25     /**
26      * 为View增加监听功能
27      */
28     private CustomListener customListener = new CustomListener() {
29         @Override
30         public void CustomListenerCallback() {
31             Logger.d("调用有效");
32         }
33     };
34 
35     public interface CustomListener {
36         public void CustomListenerCallback();
37     }
38 }

在每次MainAcitivity重建的时候,旧的CustomView的对象仍然持有上一个MainAcitivity的对象引用。

查看weakHashMap的内存存储数据:在多次横竖屏切换之后内部的数据,持有了大量的MainAcitivity对象。

【解决办法】导致的大量的旧的对象存在的原因:就是static修饰的WeakHashMap存储了大量的旧对象,因此在MainAcitivity合适的时机清除这些对象即可。

 此处的问题可能有人会说将static移除即可,这样虽然解决了内存泄露的问题,但是需求使用ListenerCollector来维护一个WeakHashMap进行view和Listener的成对的存储,与需求违背了。

 1 public class ListenerCollector {
 2     static private WeakHashMap<View, CustomView.CustomListener> sListener =
 3             new WeakHashMap<>();
 4 
 5     public void setsListener(View view, CustomView.CustomListener listener) {
 6         sListener.put(view, listener);
 7     }
 8 
 9     public static void clearListeners() {
10         //移除所有监听。
11         sListener.clear();
12     }
13 
14 }

【调用的时机】

 1 public class MainActivity extends AppCompatActivity {
 2 
 3     @Override
 4     protected void onCreate(Bundle savedInstanceState) {
 5         super.onCreate(savedInstanceState);
 6 
 7         CustomView myView = new CustomView(this);
 8         setContentView(myView);
 9 
10     }
11 
12     @Override
13     protected void onStop() {
14         super.onStop();
15         ListenerCollector.clearListeners();
16     }
17 }

看在在gc之前虽然具有垃圾待回收,但是只有一个引用。在gc之后,所有的垃圾都被回收。只剩下一个引用。

4.总结

4.1 确定是否存在内存泄露

确定项目中某个动作的触发是否会引起内存泄露有两种方法:

1)Android Monitors的内存分析
最直观的看内存增长情况,知道该动作是否发生内存泄露。
动作发生之前:GC完后内存1.4M; 动作发生之后:GC完后内存1.6M

2)使用MAT内存分析工具
MAT分析heap的总内存占用大小来初步判断是否存在泄露
Heap视图中有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。
在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,
一般情况下,这个值的大小决定了是否会有内存泄漏。
我们反复执行某一个操作并同时执行GC排除可以回收掉的内存,注意观察data object的Total Size值,
正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。
反之如果代码中存在没有释放对象引用的情况,随着操作次数的增多Total Size的值会越来越大。
那么这里就已经初步判断这个操作导致了内存泄露的情况。

看到下图中的TotalSize具有明显的0.8M内存的大小的变化。

 

4.2  确定内存泄露的对象

MAT对比操作前后的hprof来定位内存泄露是泄露了什么数据对象。
(这样做可以排除一些对象,不用后面去查看所有被引用的对象是否是嫌疑)
快速定位到操作前后所持有的对象哪些是增加了(GC后还是比之前多出来的对象就可能是泄露对象嫌疑犯)
技巧:Histogram中还可以对对象进行Group,比如选择Group By Package更方便查看自己Package中的对象信息。

【说明】

【包名的选择依据】:导致的内存泄露的此动作的类文件所在的包名。这样做排除了系统的类,只关注自己写的类,没有必要系统的类。

【比较的依据】【1】object的个数的增加【2】HeapSize的数量级的增加。

 4.3 定位内存泄露的原因  

MAT分析hprof来定位内存泄露的原因所在。(哪个对象持有了上面怀疑出来的发生泄露的对象)

1)Dump出内存泄露“当时”的内存镜像hprof,分析怀疑泄露的类;
2)把上面2得出的这些嫌疑对象一个一个排查个遍。步骤:
(1)进入Histogram,过滤出某一个嫌疑对象类

(2)然后分析持有此类对象引用的外部对象(在该类上面点击右键List Objects--->with incoming references)

(3)再过滤掉一些弱引用、软引用、虚引用,因为它们迟早可以被GC干掉不属于内存泄露

(在类上面点击右键Merge Shortest Paths to GC Roots--->exclude all phantom/weak/soft etc.references)

【注意】只要是通过滤掉一些弱引用、软引用、虚引用之后剩下的全部都是内存泄露。

4.4 逐个分析每个对象的GC路径是否正常

此时就要进入代码分析此时这个对象的引用持有是否合理,这就要考经验和体力了!
(比如上课的例子中:旋转屏幕后MainActivity有两个,肯定MainActivity发生泄露了,
那谁导致他泄露的呢?原来是我们的CommonUtils类持有了旋转之前的那个MainActivity他,
那是否合理?结合逻辑判断当然不合理,由此找到内存泄露根源是CommonUtils类持有了该MainActivity实例造成的。
怎么解决?罪魁祸首找到了,怎么解决应该不难了,不同情况解决办法不一样,要靠你的智慧了。)

 

 5.查看一个APP中的内存泄露的状态

判断一个应用里面内存泄露避免得很好,怎么看?

【判断的依据】当app退出的时候,这个进程里面所有的对象应该就都被回收了,尤其是很容易被泄露的(View,Activity)是否还内存当中。
可以让app退出以后,查看系统该进程里面的所有的View、Activity对象是否为0.

【具体的操作】在应用存在的界面按下back键,等待程序完全退出。退出之后在AndroidMonitor中,多次点击gc,等待内存平稳。
工具:使用AndroidStudio--AndroidMonitor--System Information--Memory Usage查看Objects里面的views和Activity的数量是否为0.

 

【说明】下图的参数为Views:339,Acitivities:23;判断为Acitivities出现了内存泄露。

注意:此处不能判断view出现了内存泄露,因为Acitivity本身就持有了view;

如果:Activities为0,view大于0,则可以说明view出现了内存泄露。

在修改内存泄露之后的状态:变为正常的0

6.内存泄露常见的实例

【内存泄露】(Memory Leak):
进程中某些对象已经没有使用价值了,但是他们却还可以直接或者间接地被引用到GC Root导致无法回收。
当内存泄露过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出OOM.
【内存溢出(OOM)】 
当应用占用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出。比如:加载大图片。

6.1 静态变量引起的内存泄露

【静态变量】静态变量的生命周期:在类加载时候创建的,在类卸载的时候结束。

当调用getInstance时,如果传入的context是Activity的context。只要这个单例没有被释放,那么这个Activity也不会被释放一直到进程退出才会释放。

 1 public class CommUtil {
 2         private static CommUtil instance;
 3         private Context context;
 4         private CommUtil(Context context){
 5         this.context = context;
 6         }
 7 
 8         public static CommUtil getInstance(Context mcontext){
 9         if(instance == null){
10             instance = new CommUtil(mcontext);
11         }
12         return instance;
13       }

 【注意】特别是在Acitivity跳转的时候,禁用静态变量进行传值,因为静态变量会将数据全部存在内存中。

 6.2 静态内部类引起内存泄露(包括匿名内部类)

典型错误实例:

 1   public void loadData() {//隐士持有MainActivity实例。MainActivity.this.a
 2         new Thread(new Runnable() {
 3             @Override
 4             public void run() {
 5                 while (true) {
 6                     try {
 7                         //int b=a;
 8                         Thread.sleep(1000);
 9                     } catch (InterruptedException e) {
10                         e.printStackTrace();
11                     }
12                 }
13             }
14         }).start();
15     }

 

【错误原因】匿名内部类默认隐式持有外部类的实例。上述的newThread属于匿名内部类,默认支持有了外部类的实例(MainAcitivity)。

非静态内部类在方法之外是无效的。

假设上述加载数据的方法loadData(),内部的线程执行的时间是5分钟。默认持有了外部类MainAcitivity的引用。

但是Mainacitivity随时都有会被回收,如果5分钟之内没有加载结束数据,MainAcitivity被退出,则线程仍然存在,内部类继续持有外部类的对象,导致了MainAcitivity的内存泄露。两者的生命周期不一致。

静态是属于类的,不是属于对象的。静态没有外部引用。

【解决方案】将非静态内部类修改为静态内部类。(静态内部类不会隐式持有外部类)

【方法1】直接书写类继承Thread

 1 public static class Load extends Thread{
 2         @Override
 3         public void run() {
 4             super.run();
 5             try {
 6                 Thread.sleep(1000);
 7             } catch (InterruptedException e) {
 8                 e.printStackTrace();
 9             }
10         }
11     }

 

【方法2】方法改为静态方法,改为静态内部类。

 1 public static void loadData() {
 2         new Thread(new Runnable() {
 3             @Override
 4             public void run() {
 5                 while (true) {
 6                     try {
 7                         Thread.sleep(1000);
 8                     } catch (InterruptedException e) {
 9                         e.printStackTrace();
10                     }
11                 }
12             }
13         }).start();
14     }

 

【典型错误实例2】错误原因与上面的一致。

 1 /加上static,里面的匿名内部类就变成了静态匿名内部类
 2     public  void loadData(){//隐士持有MainActivity实例。MainActivity.this.a
 3        new Timer().schedule(new TimerTask() {
 4            @Override
 5            public void run() {
 6                while(true){
 7                    try {
 8                        //int b=a;
 9                        Thread.sleep(1000);
10                    } catch (InterruptedException e) {
11                        e.printStackTrace();
12                    }
13                }
14            }
15        }, 20000);//这个线程延迟5分钟执行

 

【说明】有种方式是在Acitivity的onDestory中将Timer.cancel。虽然可以达到目的,但是强烈不提倡这样做。如果MainAcitivity在中途挂掉,可能不会执行MainAcitivity的onDestory()生命周期方法。

还是要照上面的方法,将匿名内部类变成静态匿名内部类。

6.3 Handler 内存泄露

【错误的实例】同样是因为匿名内部类。

 1 public class MainActivity extends AppCompatActivity {
 4     //错误的示范:
 5     //mHandler是匿名内部类的实例,会引用外部对象MainActivity.this。如果Handler在Activity退出的时候,它可能还活着,这时候就会一直持有Activity。
 6     private Handler mHandler = new Handler(){
 7         @Override
 8         public void handleMessage(Message msg) {
 9             super.handleMessage(msg);
10             switch (msg.what){
11                 case 0:
12                     //加载数据
13                     break;
14 
15             }
16         }
17     };

 

【解决措施】

【方法1】使用静态内部类;如果需要使用MainAcitivity,则使用弱引用。

【注】static类只有在new的时候才会产生对象。

 1 //解决方案:
 2     private static class MyHandler extends Handler{
 3 //        private MainActivity mainActivity;//直接持有了一个外部类的强引用,会内存泄露
 4         private WeakReference<MainActivity> mainActivity;//设置软引用保存,当内存一发生GC的时候就会回收。
 5 
 6         public MyHandler(MainActivity mainActivity) {
 7             this.mainActivity = new WeakReference<MainActivity>(mainActivity);
 8         }
 9 
10         @Override
11         public void handleMessage(Message msg) {
12             super.handleMessage(msg);
13             MainActivity main =  mainActivity.get();
14             if(main==null||main.isFinishing()){
15                 return;
16             }
17             switch (msg.what){
18                 case 0:
19                     //加载数据
20 //                    MainActivity.this.a;//有时候确实会有这样的需求:需要引用外部类的资源。怎么办?
21                     int b = main.a;
22                     break;
23 
24             }
25         }
26     };
 1 @Override
 2     protected void onCreate(Bundle savedInstanceState) {
 3         super.onCreate(savedInstanceState)
 4         setContentView(myView);
 5         Load load = new Load();
 6         load.start();
 7 
 8         //使用方法1
 9         mHandler.postDelayed(new Runnable() {
10             @Override
11             public void run() {
12 
13             }
14         },2000);
15 
16         //使用方法2
17         Message obtain = Message.obtain();
18         obtain.arg1 = 1;
19         obtain.arg2 = 2;
20         obtain.obj = "obj";
21         mHandler.sendMessage(obtain);
22     }

【问1】当使用软引用或者弱引用的时候,MainActivity难道很容易或者可以被GC回收吗?从而导致Handler变为空指针异常。
GC回收的机制是什么?当MainActivity不被任何的对象引用的时候才会被回收。

【答】

【1】如果MainAcitivity都被回收了,Handler运行的依赖体都不存在,Handler没有存在的必要了。

【2】其他对象仍然引用MainAcitivity。虽然Handler里面用的是软引用/弱引用,但是并不意味着不存在其他的对象引用该MainActivity。此时的Handler是不会被回收的。

【问2】MainAcitivity是否会被回收?

【情况2】假设MainAcitivity在APP中没有对象在引用Handler,MainAcitivity正常退出/或者非正常退出,但此时是弱引用,故MainAcitivity对象是被gc回收的。

【问3】为什么使用弱引用而不是软引用?

【答】软引用的回收时机是在内存不够使用的时候才会gc,则MainAcitivity的回收时机则无法估量,必然会造成内存泄露。

【方法2】

正是因为被延时处理的 message 持有 Handler 的引用,Handler 持有对 Activity 的引用,形成了message – handler – activity 这样一条引用链,导致 Activity 的泄露。因此我们可以尝试在当前界面结束时将消息队列中未被处理的消息清除,从源头上解除了这条引用链,从而使 Activity 能被及时的回收。
1 @Override
2     protected void onDestroy() {
3         super.onDestroy();
4         mHandler.removeCallbacksAndMessages(null);
5     }

 

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

【实例1】

【说明】add相关的监听需要remove,否则持有对象,导致内存泄露。

【原理】也是将监听放到集合中;

 1  mTv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver
 2                 .OnWindowFocusChangeListener() {
 3             @Override
 4             public void onWindowFocusChanged(boolean hasFocus) {
 5                 if (hasFocus) {
 6                     mTv.setTextColor(Color.BLACK);
 7                     mTv.setTextColor(Color.CYAN);
 8                     mTv.setBackgroundColor(Color.RED);
 9                     mTv.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG);
10                     String width = String.valueOf(mTv.getWidth());
11                     String height = String.valueOf(mTv.getHeight());
12 
13 
14                     Logger.i("onWindowFocusChanged:" + width + ":" + height);
15                     Logger.i("onWindowFocusChanged");
16 
17                     mTv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
18                 }
19 
20 
21             }
22         });

 

 【实例2】

 1  SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
 2         Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
 3         Logger.i(sensor.getName());
 4         //不需要用的时候记得移除监听
 5         SensorEventListener listener = new SensorEventListener() {
 6             @Override
 7             public void onSensorChanged(SensorEvent event) {
 8 
 9             }
10 
11             @Override
12             public void onAccuracyChanged(Sensor sensor, int accuracy) {
13 
14             }
15         };
16         sensorManager.registerListener(listener,sensor,10);
17         sensorManager.unregisterListener(listener);

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

比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定义属性attribute
attr.recycle()回收。当不需要使用的时候,要记得及时释放资源。否则就会内存泄露。

 6.6 无限循环动画

没有在onDestroy中停止动画,否则Activity就会变成泄露对象。比如:轮播图效果。

=====================================================================================================

7.常见的工具使用

7.1 Allaction Tracking

【使用DDMS中的AllactionTracking】

追踪内存分配信息。可以很直观地看到某个操作的内存是如何进行一步一步地分配的。

【使用AS中的AllactionTracking】将debug程序运行在设备端,①手动gc-->②点击StartAllactionTracking-->③执行动作,可以同时查看到内存分配的情况-->④再次点击StartAllactionTracking,停止,生成报告。

生成报告,选择GroupbyAllocator

 

【图像形式的显示】在图像外双击变为最初始的状态;在颜色块双击进入/返回到对应的包内/文件内。

【用处】如果查看到自己的写的类,分配的次数过多,可以作为【1】内存的泄露依据【2】内存抖动的依据【3】写的代码块内的变量分配次数太多,有待改善。

 7.2 LeakCanary

【说明】安装在设备端的内存泄露检测工具,如果出现内存泄露,则在设备端提示错误信息。原理内部也是通过dump内存信息生成文件然后分析,如果时间比较长,可能会等待几分钟才会出现结果。

Square公司
可以直接在手机端查看内存泄露的工具
实现原理:本质上还是用命令控制生成hprof文件分析检查内存泄露。
然后发送通知。

1 Application
2     install()
3 LeakCanary
4     androidWatcher()
5 RefWatcher
6     new AndroidWatcherExecutor() --->dumpHeap()/analyze()(--->runAnalysis()关键的方法)--->Hprof文件分析
7     new AndroidHeapDumper()
8     new ServiceHeapDumpListener

 

【官网地址】https://github.com/square/leakcanary

 【添加依赖】对release版本的app没有任何的影响

In your build.gradle:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
  // Optional, if you use support library fragments:
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
}

【初始化】Application 类中添加初始化代码

 1 public class CustomApplication extends Application {
 2 
 3   @Override public void onCreate() {
 4     super.onCreate();
 5     if (LeakCanary.isInAnalyzerProcess(this)) {
 6       // This process is dedicated to LeakCanary for heap analysis.
 7       // You should not init your app in this process.
 8       return;
 9     }
10     LeakCanary.install(this);
11     // Normal app init code...
12   }
13 }

出现内存泄露会弹出通知栏消息:会一直随着app的生命周期一直在后台运行。

 7.3 Lint分析工具

Android Studio很方便 很好用。

具有下面的功能:
检测资源文件是否有没有用到的资源。
检测常见内存泄露
安全问题SDK版本安全问题
是否有费的代码没有用到
代码的规范---甚至驼峰命名法也会检测
自动生成的罗列出来
没用的导包
可能的bug

 

【分析依赖关系】

 

【代码检查】

.

内存泄露的检查

 【无用资源的检查】在打包的时候进行删除

【提供的安全建议】

【编程style的检查】

【自动生成代码的提示】

【import问题】

【语言规范】

【坑能存在的bug】

【没有引用的变量】

【拼写问题】

7.4 Heap Viewer工具

Heap Viewer能做什么?

  • 实时查看App分配的内存大小和空闲内存大小
  • 发现Memory Leaks

Heap Viewer使用条件

  • 5.0以上的系统,包括5.0
  • 开发者选项可用

Heap Viewer启动

可以直接在Android studio工具栏中直接点击小机器人启动: 

还可以在Android studio的菜单栏中Tools也可以: 

如果你不用Android studio,可以在SDK下的tools下的monitor程序打开: 

Heap Viewer面板

 

按上图的标记顺序按下,我们就能看到内存的具体数据,右边面板中数值会在每次GC时发生改变,包括App自动触发或者你来手动触发。

ok,现在来解释下面板中的名词

总览

列名

意义

Heap Size

堆栈分配给App的内存大小

Allocated

已分配使用的内存大小

Free

空闲的内存大小

%Used

Allocated/Heap Size,使用率

Objects

对象数量

详情

 

类型

意义

free

空闲的对象

data object

数据对象,类类型对象,最主要的观察对象

class object

类类型的引用对象

1-byte array(byte[],boolean[])

一个字节的数组对象

2-byte array(short[],char[])

两个字节的数组对象

4-byte array(long[],double[])

4个字节的数组对象

non-Java object

非Java对象

下面是每一个对象都有的列名含义:

列名

意义

Count

数量

Total Size

总共占用的内存大小

Smallest

将对象占用内存的大小从小往大排,排在第一个的对象占用内存大小

Largest

将对象占用内存的大小从小往大排,排在最后一个的对象占用的内存大小

Median

将对象占用内存的大小从小往大排,拍在中间的对象占用的内存大小

Average

平均值

当我们点击某一行时,可以看到如下的柱状图:

横坐标是对象的内存大小,这些值随着不同对象是不同的,纵坐标是在某个内存大小上的对象的数量

Heap Viewer的使用

我们说Heap Viewer适合发现内存泄漏的问题,那你知道何为内存泄漏么?

内存泄漏

英文名:Memory Leaks 
标准解释:无用的单纯,但是还是没GC ROOT引用的内存 
通俗解释:该死不死的内存

检测

那么如何检测呢?Heap Viewer中的数值会自动在每次发生GC时会自动更新,那么我们是等着他自己GC么?既然我们是来看内存泄漏,那么我们在需要检测内存泄漏的用例执行过后,手动GC下,然后观察data object一栏的total size(也可以观察Heap Size/Allocated内存的情况),看看内存是不是会回到一个稳定值,多次操作后,只要内存是稳定在某个值,那么说明没有内存溢出的,如果发现内存在每次GC后,都在增长,不管是慢增长还是快速增长,都说明有内存泄漏的可能性。

实例

先来看3个图: 
1.刚打开首页,手动GC一下: 
 

2.首页到详情页10遍,最后回到首页,手动GC一下,直到数值不再变化: 

3.首页到详情页10遍,最后回到首页,手动GC一下: 

从data object一栏看到该类型的数值会在不断增长,可能发生了内存泄漏,而我们也可以从上面三个图的标红部分来看,Allocated分别增加了2.418M和1.084M,而且你继续这么操作下去,内存依然是增长的趋势

补充

Heap Viewer不光可以用来检测是否有内存泄漏,对于内存抖动,我们也可以用该工具检测,因为内存抖动的时候,会频繁发生GC,这个时候我们只需要开启Heap Viewer,观察数据的变化,如果发生内存抖动,会观察到数据在段时间内频繁更新

【参考文章】

【常见的内存泄漏原因及解决方法】https://www.jianshu.com/p/90caf813682d

posted @ 2018-07-19 10:35  OzTaking  阅读(509)  评论(0)    收藏  举报