当前位置: 首页 > 科技观察

Android性能优化:从卡顿和ANR彻底理解内存泄漏原理和优化

时间:2023-03-13 12:18:40 科技观察

前言JAVA程序,因为有垃圾回收机制,应该不会有内存泄漏。我们已经知道,如果一个对象从根节点可达,即从根节点到该对象存在引用链,那么该对象就不会被GC回收。如果这个对象不再被使用,它就没有用了。如果我们仍然持有它的引用,就会导致内存泄漏。例如,一个长时间在后台运行的线程持有对Activity的引用。这时Activity执行了onDestroy方法,那么这个Activity就是一个从根节点可达的无用对象,这个Activity对象就是一个泄漏对象,分配给这个对象的内存不会被回收。如果我们的java运行时间长了,这种内存泄漏不断的发生,到最后就没有可用的内存了。当然,在java中,内存泄漏和C/C++是不一样的。如果java程序完全结束,它的所有对象都是不可达的,系统可以对它们进行垃圾回收。它的内存泄漏仅限于自身,不会影响整个系统。C/C++的内存泄漏更甚。它的内存泄漏是在系统级别。即使C/C++程序退出,其泄漏的内存也无法被系统回收,除非重启机器,否则永远无法使用。我们的文章开始总结Android内存泄漏;一、Android内存泄漏介绍1、什么是内存泄漏?无法释放,导致系统内存浪费,导致程序运行速度变慢甚至系统崩溃等严重后果。内存泄漏缺陷具有隐蔽性和累积性,比其他内存非法访问错误更难检测。因为内存泄漏的原因是内存块没有释放,所以属于遗漏缺陷而不是故障缺陷。此外,内存泄漏通常不会直接产生可观察到的错误症状,而是逐渐累积,降低系统的整体性能,极端情况下可能导致系统崩溃;Android应用程序中的内存泄漏对其他应用程序几乎没有影响。为了让Android应用程序安全、快速地运行,每个Android应用程序都会使用一个专用的Dalvik虚拟机实例来运行,该实例由Zygote服务进程孵化,也就是说每个应用程序都运行在自己的进程中。Android为不同类型的进程分配不同的内存使用限制。如果程序在运行过程中发生内存泄漏,应用进程使用的内存超过了这个限制,就会被系统认为是内存泄漏而被杀死,这使得只有自己的进程被杀死,而不会影响其他进程(如果system_process等系统进程出现问题,会导致系统重启)。2、内存泄漏的危害用户对单次内存泄漏没有感知,但当泄漏累积到内存被消耗时,就会造成卡顿甚至死机;频繁的gc回收导致应用卡死ANR:当内存不足时GC会主动回收无用的内存。但是,内存回收也需要时间。内存回收和gc回收垃圾资源的高频交替执行,会造成内存抖动。大量的数据会污染内存堆。马上就会有很多GC。由于这种额外的内存压力,计算量也会突然增加,从而导致卡顿。任何线程的任何操作都需要暂停。等待GC操作完成,然后其他操作才能继续运行。因此,垃圾回收运行的次数越少,对性能的影响就越小;3、内存泄漏的原因①内存空间在使用后没有回收,会导致内存泄漏。虽然Java有垃圾回收机制,但是Java中还是有很多代码逻辑会导致内存泄漏。垃圾回收器会回收大部分的内存空间,但是有些内存空间仍然保留着引用,但是逻辑上不再被再次使用的对象,此时垃圾回收器是无能为力的,无法回收它们,例如:忘记释放分配的内存;应用程序不需要该对象,但不释放该对象的引用;垃圾收集器不能回收这个对象;持有对象生命周期过长,无法回收;②Android(Java)平台的内存泄漏是指在无用的对象资源和GCRoots之间维护一条可访问的路径,导致系统无法回收;③那么从栈中弹出的对象就不会被当作垃圾回收处理,即使程序不再使用栈中的这些对象,它们也不会被回收,因为栈中仍然保存着这个对象的引用,俗称作为过期引用,这个内存泄漏非常隐蔽;2.Detect内存泄漏检测工具①MemoryMonitor位于AndroidMonitor中,该工具可以:方便的显示内存使用情况和GC状态快速定位freeze是否与GC有关快速定位Crash是否与内存占用高有关关于快速定位潜在内存leaks(内存占用一直在增长)但没有准确定位问题②AllocationTracker这个工具的用途:可以定位代码中分配的对象类型、大小、时间、线程、栈等信息可以定位内存抖动问题配合HeapViewer定位内存泄漏问题(可以找出泄漏对象创建的位置等)使用方法:MemoryMonitor中有一个StartAllocationTracking按钮可以开始跟踪。点击停止追踪后,会显示统计结果。③HeapViewer该工具用于:显示内存快照信息,收集每次GC后的信息,发现内存泄漏。使用方法:内存监视器中有一个DumpJavaHeap按钮,点击它,在统计报告的左上角选择包分类。通过MemoryMonitor的initiateGC(executeGC)按钮,可以检测内存泄漏等情况。④LeakCanarydependencies{debugImplementation'com.squareup.leakcanary:leakcanary-android:1.6.3'releaseImplementation'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'//可选,ifyouusesupportlibraryfragments:debugImplementation'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'}直接在Application中使用,然后运行APP会自动检测,检测到会在另一个APP上通知,显示详情publicclassExampleApplicationextendsApplication{@OverridepublicvoidonCreate(){super.onCreate();if(LeakCanary.isInAnalyzerProcess(this)){//ThisprocessisdedicatedtoLeakCanaryforheapanalysis.//Youshouldnotinityyourappinthisprocess.return;}LeakCanary.install(this);//Normalappinitcode...}}3.常见内存泄漏场景详解1.单例Android开发中经常使用单例模式,但如果使用不当,会导致内存泄漏。由于单例的静态特性,它的生命周期与应用程序的生命周期一样长。如果一个对象不再有用,但单例仍然持有它的引用,那么在应用程序的整个生命周期中都不能正常使用它。回收,导致内存泄漏。publicclassAppSettings{privatestaticvolatileAppSettingssingleton;privateContextmContext;privateAppSettings(Contextcontext){this.mContext=context;}publicstaticAppSettingsgetInstance(Contextcontext){if(singleton==null){synchronized(AppSettings.class){if(singleton==null){singleton=newAppSettings(context);}}}returnssingleton;}}对于像上面代码这样的单例,如果我们调用getInstance(Contextcontext)方法时传入的context参数是Activity、Service等的context,就会造成内存泄露。以Activity为例,当我们启动一个Activity,调用getInstance(Contextcontext)方法获取AppSettings的单实例时,传入Activity.this作为context,这样AppSettings类的单实例sInstance持有Activity的引用,当我们退出Activity时,Activity是无用的,但是由于sIntance作为静态单例(存在于应用程序的整个生命周期中)会继续持有Activity的引用,导致Activity对象无法回收释放,造成内存泄漏。为了避免此类单例导致内存泄漏,我们可以将context参数改为全局上下文:privateAppSettings(Contextcontext){this.mContext=context.getApplicationContext();}2.静态变量导致内存泄漏静态变量存放在方法区,它的生命周期从类加载开始,到整个进程结束。一旦一个静态变量被初始化,它持有的引用直到进程结束才会被释放。比如下面这种情况,为了避免在Activity中重复创建info,将sInfo作为静态变量使用:}classInfotext{priv;publicInfo(Contextcontext){this.mContext=context;}}}Info是Activity的静态成员,持有Activity的引用,但是作为静态变量,sInfo的生命周期肯定比Activity长。所以当Activity退出时,sInfo仍然引用Activity,Activity无法被回收,从而导致内存泄漏。在Android开发中,静态持有可能经常会因为生命周期不一致而导致内存泄露,所以我们在创建静态持有变量时需要考虑各个成员之间的引用关系,尽量少用静态持有变量,避免内存泄露。当然,我们也可以在适当的时候将静态值重置为null,使其不再持有引用,这样也可以避免内存泄漏。3.非静态内部类导致内存泄漏。默认情况下,非静态内部类(包括匿名内部类)将保存对外部类的引用。当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。非静态内部类导致的内存泄漏是Android开发中使用Handler的典型场景。很多开发者使用Handler这样写:publicclassMainActivity2extendsAppCompatActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);start();}privatevoidstart(){Messagemessage=Message.obtain();message.what=1;mHandler.sendMessage(message);}privateHandlermHandler=newHandler(){@OverridepublicvoidhandleMessage(Messagemsg){super.handleMessage(msg);if(msg.what==1){//doNothing}}};}可能有人会说mHandler不将Activity的引用作为静态变量持有,其生命周期不得长于Activity。它不一定会导致内存泄漏。显然不是这样!熟悉Handler消息机制的人都知道,mHandler会作为成员变量存储在发送消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即,mHandler持有Activity的Reference,那么我们可以理解为msg间接持有Activity的引用。msg发送后,先放入消息队列MessageQueue,然后等待Looper的轮询处理(MessageQueue和Looper都是关联线程的,MessageQueue是Looper引用的成员变量,Looper存放在ThreadLocal中)。那么当Activity退出时,msg可能还存在于MessageQueue中未处理或正在处理,这将导致Activity不被回收,导致Activity内存泄漏。通常在Android开发中,如果既要使用内部类又要避免内存泄漏,一般会使用静态内部类+弱引用。MyHandlermHandler;publicstaticclassMyHandlerextendsHandler{privateWeakReferencemActivityWeakReference;publicMyHandler(Activityactivity){mActivityWeakReference=newWeakReference<>(activity);}@OverridepublicvoidhandleMessage(Messagemsg){super.handleMessage(msg);}}mHandler通过弱引用的方法ActivityGC在进行垃圾回收时,遇到Activity会回收并释放占用的内存单元。这样就不会发生内存泄漏。上面的做法确实避免了Activity造成的内存泄漏。发送的msg不再持有Activity的引用,但是msg可能还存在于MessageQueue中,所以最好在Activity销毁时删除mHandler。回调和发送的消息被删除。@OverrideprotectedvoidonDestroy(){super.onDestroy();mHandler.removeCallbacksAndMessages(null);}另一种非静态内部类导致内存泄漏的情况是使用Thread或AsyncTask。为了避免内存泄漏,还是需要像上面的Handler一样使用静态内部类+弱应用的方法(代码就不一一列举了,参考上面Hanlder的正确写法)。4.取消注册失败或者回调导致内存泄露比如我们在一个Activity中注册了一个广播,如果Activity销毁后没有取消注册,那么这个广播会一直存在于系统中,持有这个Activity只是像上面提到的非静态内部类引用,导致内存泄漏。所以注册广播后,必须在Activity销毁后取消注册。在注册观察规则模式时,如果不及时取消,也会造成内存泄漏。比如使用Retrofit+RxJava注册网络请求的观察者回调,也持有外部引用作为匿名内部类,所以需要记得在不使用或者销毁的时候取消注册。5、Timer和TimerTask导致内存泄漏Timer和TimerTask在Android中通常用来做一些定时或循环任务,比如无限轮播的ViewPager:privatevoidstopTimer(){if(mTimer!=null){mTimer.cancel();mTimer.purge();mTimer=null;}if(mTimerTask!=null){mTimerTask.cancel();mTimerTask=null;}}@OverrideprotectedvoidonDestroy(){super.onDestroy();stopTimer();}当我们的Activity被销毁了,有可能Timer还在等待执行TimerTask,它持有的Activity的引用无法回收。因此,当我们的Activity被销毁时,Timer和TimerTask必须立即取消,以避免内存泄漏。6.容易理解集合中的对象没有清理干净导致内存泄漏。如果一个对象被放置在一个集合中,比如ArrayList、HashMap等,这个集合就会持有该对象的一个??引用。当我们不再需要这个对象时,我们不会将它从集合中移除,所以只要集合还在使用中(并且这个对象没用),这个对象就会导致内存泄漏。而如果集合是静态引用的,集合中那些无用的对象就会造成内存泄漏。因此,在使用集合时,要及时从集合中移除不用的对象或清除集合,以免内存泄漏。7.资源没有关闭或释放,导致内存泄漏。在使用IO、File流或者Sqlite、Cursor等资源时,一定要及时关闭。这些资源通常在执行读写操作时使用缓冲区。如果不及时关闭,这些缓冲对象将一直被占用而得不到释放,从而导致内存泄漏。因此,我们在不需要使用的时候及时关闭它们,让缓冲区及时释放,避免内存泄漏。8.属性动画导致内存泄漏动画也是一个耗时的工作。比如属性动画(ObjectAnimator)是在Activity中启动的,但是在销毁的时候并没有调用cancel方法。虽然我们看不到动画,但是动画还是会继续播放,动画指的是它所在的控件,而它所在的控件指的是Activity,导致Activity不能正常释放。所以在Activity销毁的时候也需要取消属性动画,避免内存泄露。9、WebView导致内存泄漏关于WebView的内存泄漏,由于WebView在加载一个网页后会长期占用内存无法释放,所以我们需要在Activity销毁后调用它的destroy()方法销毁它释放内存。另外,在查看WebView内存泄漏的资料时,看到了这种情况:Webview下的Callback持有Activity引用,导致Webview内存释放失败。即使调用Webview.destory()等方法也无法解决问题(Android5.1之后)。最终的解决方案是:在销毁WebView之前,需要先将WebView从父容器中移除,然后再销毁WebView。总结对于生命周期比Activity长的对象(单例),为了避免直接引用Activity的上下文,可以考虑使用ApplicationContext,在静态变量不用的时候及时清空;Handler持有的引用最好使用弱引用。释放的时候记得清除Message,取消Handler对象的Runnable;非静态内部类和非静态匿名内部类将自动持有对外部类的引用。为避免内存泄漏,可以考虑将内部类声明为static;广播接收在controller、EventBus等使用过程中,注册/注销要成对使用,但所有注册都要注销;Cursor、File、Bitmap等不再使用的资源对象应正确关闭;集合中的东西如果有join,就应该有相应的deletion。属性动画及时取消,注意webview内存泄露问题