美文网首页
Android内存优化

Android内存优化

作者: 月影路西法 | 来源:发表于2020-02-07 14:34 被阅读0次

    GC算法

    当内存不足时,系统就会触发GC,GC采用垃圾标记算法为跟搜索算法

    跟搜索算法.png

    从图中可以看书obj4是科大的对象,标识他正在被引用,一次不会标记为可回收对象。obj5,obj6,obj7都是不可达对象,obj5跟obj6虽然互相引用,但是因为他们到gcRoots是不可达的,所以他们也被标记为gc回收对象

    内存泄漏的原因

    1.由开发者自己编码造成的内存泄漏
    2.第三方框架在成泄漏
    3.由Android系统或者第三方room造成内存泄漏

    以上三种情况2,3是不可控的,我们只能控制1。

    内存泄漏场景

    1.非静态内部类导致内存泄露
    在java中,内部类虽然和外部类都写在同一个java文件中,但是编译完成后,还是会生成各自的class文件,之所以内部类对象可以访问外部类对象中的成员(包括成员变量和成员方法),是因为内部类对象持有指向外部类对象的引用

    public class Outer {  
        int outerField = 0;  
          
        public class Inner{  
            void InnerMethod(){  
                int i = outerField;  
            }  
        }
    

    反编译Outer&Inner.class文件

    
    class Outer$Inner{  
        final Outer this$0;
          
        public Outer$Inner(Outer outer){  
            this.this$0 = outer;  
            super();  
        }
        
        void InnerMethod(){  
            int i = this.this$0.outerField;  
        }
    
    

    我们会发现:

    • 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象(this)的引用;
    • 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
    • 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。
      这也就解释了内部类为什么能访问外部类的成员了。

    看到这里我们能想到,由于非静态内部类(包括匿名内部类)默认就会持有外部类的引用,那么当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。

    解决办法定义一个静态的类

    2.Handle中的内存泄漏
    一般的人都会这么写handle

    public class MainActivity extends AppCompatActivity {
     
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 1) {
                    // TODO
                }
            }
        };
     
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            
            notifySomething();
    
    

    也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄露呢,显然不是这样的!

    熟悉Handler消息机制的童鞋都知道,mHandler会作为成员变量保存在发送的消息msg中,代码如下:

    
    public static Message obtain(Handler h) {
        Message m = obtain();
        m.target = h;
        return m;
    
    

    即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。

    解决方法可以调用handler.removeCallbacksAndMessages(null)。来移除消息队列,从而解决,但是这样的话,队列中的消息就不会被处理了

    上面在介绍GC的垃圾回收机制时有说到,强引用会造成GC回收不了,而弱引用不会,那我们在解决这种要使用内部类,但又要规避内存泄露时,一般都会采用静态内部类+弱引用的方式:

    private static class MyHandler extends Handler {
     
        private WeakReference<MainActivity> mRef;
     
        public MyHandler(MainActivity activity) {
            mRef = new WeakReference<>(activity);
        }
     
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mRef.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        }
    }
    

    3.未正确的使用Context

    再有用到Context的地方,尽量使用Application的Context来进行使用,这样可以避免

    4.静态VIew

    使用静态的view可以避免每次启动Activity都去渲染view,但是静态view会持有Activity的引用,导致Activity无法回收,所以在OnDestory方法中,将view制成NULL

    5.监听器没有关闭

    很多的系统服务,如TelephoneyMannager,SensorManager等,都需要register和unregister监听,我们需要确保在核实的时候,及时unregister这些监听器,自己手动添加的Listener,要记得在核实的时候及时移除Listerner

    6.资源未关闭

    如使用Cursor,File等,他们往往都使用了缓冲,会造成内存泄漏。因此,在资源对象不使用的时候,一定要确保他们都close了,并且引用置为null.

    7集合未清理

    在实际开发过程中难免会有把对象添加到集合容器(比如 ArrayList)中的需求,如果在一个对象使用结束之后未将该对象从该容器中移除掉,就会造成该对象不能被正确回收,从而造成内存泄漏,解决办法当然就是在使用完之后将该对象从容器中移除。

    8BitMap对象

    临时创建的某个相对比较大的BitMap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的Bitmap,这样能够更快释放原始Bitmap所占用的空间。避免静态变量持有较大的BitMap对象或者其他大的数据对象,如果已经持有,要尽快质控该静态变量。

    9WebView

    不同的Android版本的webview都有差异,加上厂商定制的rom,webview存在着很大的兼容性问题。只要使用一次webview,内存就不会被释放
    通常的解决方法是将webView单独开一个进程,然后使用Aidl与主进程进行通信。然后根据业务要求,销毁webview进程

    内存的检测

    1.adb shell && Memory Usage
    以通过命令 adb shell dumpsys meminfo [package name] 来将指定 package name 的内存信息打印出来,这种模式可以非常直观地看到 Activity 未释放导致的内存泄漏:

    或者也可以通过 Android studio 的 Memory Usage 功能进行查看,最后的结果是一样的:

    2.利用finalise方法
    上面介绍finalise方法时有说过:当GC准备回收一个Java Object(所有Java对象都是Object的子类)的时候,GC会调用这个Object的finalize方法。也就意味着如果某个对象比如activity泄漏的话,那么在退出这个activity时,GC不会调用这个activity的finalize方法,我们可以利用这一点,重写activity的finalise方法,在里面打印一段日志,如下所示:

    我们balabala打开很多activity,再返回到主页,通过IDE手动触发几次GC操作,如果这时某个activity没有打印finalise方法里的日志时,说明这个activity就发生内存泄漏了。怎么样?是不是很简单粗暴!

    需要注意一点的是,由于重写了finalize方法的对象要第二次GC才能会真正被GC回收,这也是一种泄漏,所以检查完内存泄漏后,别忘了删除重写的finalise方法。

    3.Allocation Tracker
    Android studio 还自带一个 Allocation Tracker 工具,功能和 DDMS 中的基本差不多,这个工具可以监控一段时间之内的内存分配:

    在内存图中点击途中标红的部分,启动追踪,再次点击就是停止追踪,随后自动生成一个 .alloc 文件,这个文件就记录了这次追踪到的所有数据,然后会在右上角打开一个数据面板:

    4.Android Memory Monitor
    Memory Monitor 是 Android Studio 自带的一个监控内存使用状态的工具,入口如下所示:

    // TODO 这里需要图片

    在 Android Monitor 点开之后 logcat 的右侧就是 Monitor 工具,其中可以检测内存、CPU、网络等内容,我们这里只用到了 Memory Monitor 功能,点击红色箭头所指的区域,就会 dump 此时此刻的 Memory 信息,并且生成一个 .hprof 文件,dump 完成之后会自动打开这个文件的显示界面,如果没有打开,可以通过点击最左侧的 Capture 界面或者 Tool Window 里面的 Capture 进入 dump 的 .hprof 文件列表:

    首先左上角的下拉框,可以选择 App Heap、Image Heap 和 Zygote Heap,对应的就是上篇博客讲到的 Allocation Space,Image Space 和 Zygote Space,我们这里选择 Allocation Space,然后第二个选择 PackageTreeView 这一项,展开之后就能看见一个树形结构了,然后继续展开我们应用包名的对应对象,就可以很清晰的看到有多少个 Activity 对象了,然后可以点击展开右侧的 Analyzer Tasks 项,勾选上需要检测的任务,然后系统就会给你分析出结果:

    从分析的结果可以看到泄漏的 Activity 有两个,非常直观,然后点开其中一个,观察下面的 ReferenceTree 选项:

    可以看到 Thread 对象持有了 SecondActivity 对象的引用,也就是 GC Root 持有了该 Activity 的引用,导致这个 Activity 无法回收,问题的根源我们就发现了,接下来去处理它就好了。
    5.MAT
    MAT(Memory Analyzer Tools)是一个 Eclipse 插件,它是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗,MAT 插件的下载地址:Eclipse Memory Analyzer Open Source Project,上面通过 Android studio 生成的 .hprof 文件因为格式稍有不同,所以需要经过一个简单的转换,然后就可以通过 MAT 去打开了:   通过 MAT 去打开转换之后的这个文件:

    用的最多的就是 Histogram 功能,点击 Actions 下的 Histogram 项就可以得到 Histogram 结果:

    我们可以在左上角写入一个正则表达式,然后就可以对所有的 Class Name 进行筛选了,很方便,顶栏展示的信息 “Objects” 代表该类名对象的数量,剩下的 “Shallow Heap” 和 “Retained Heap” 则和 Android Memory Monitor 类似。咱们接着点击 SecondActivity,然后右键:

    在弹出来的菜单中选择 List objects->with incoming references 将该类的实例全部列出来:

    通过这个列表我们可以看到 SecondActivity@0x12faa900 这个对象被一个 this$00x12c65140 的匿名内部类对象持有,然后展开这一项,发现这个对象是一个 handler 对象:

    快速定位找到这个对象没有被释放的原因,可以右键 Path to GC Roots->exclude all phantom/weak/soft etc. references 来显示出这个对象到 GC Root 的引用链,因为强引用才会导致对象无法释放,所以这里我们要排除其他三种引用:

    这么处理之后的结果就很明显了:

    一个非常明显的强引用持有链,GC Root 我们前面的博客中说到包含了线程,所以这里的 Thread 对象 GC Root 持有了 SecondActivity 的引用,导致该 Activity 无法被释放。

    MAT 还有一个功能就是能够对比两个 .hprof 文件,将两个文件都添加到 Compare Basket 里面:    添加进去之后点击右上角的 ! 按钮,然后就会生成两个文件的对比:

    同样适用正则表达式将需要的类筛选出来:

    结果也很明显,退出 Activity 之后该 Activity 对象未被回收,仍然在内存中,或者可以调整对比选项让对比结果更加明显:

    也可以对比两个对象集合,方法与此类似,都是将两个 Dump 结果中的对象集合添加到 Compare Basket 中去对比,找出差异后用 Histogram 查询的方法找出 GC Root,定位到具体的某个对象上。

    6.LeakCanary
    LeakCanary可能是上面几个工具中最好用的了
    使用后如下图


    image.png

    可以检测到某个Activity有内存泄漏,然后LeakCanary就会给出提示

    相关文章

      网友评论

          本文标题:Android内存优化

          本文链接:https://www.haomeiwen.com/subject/nsnlxhtx.html