1.内存泄露的BUG猛增。近期在App的mokey测试中发现了一些内存泄漏问题。在前天的测试中,发帖者瞬间收到了4条这样的bug指令,瞬间脑子里纠结到了极点。真的有几万只羊驼朝我跑来。登录页面有内存泄漏??!!楼主的代码这么完美无懈可击,怎么可能会有这么多漏洞?插曲什么是Activityleak:Activity在Android中代表一个页面,有一个生命周期,生命周期结束后,Activity对象应该在以后合适的时候被VM回收。泄漏是指Activity生命周期结束后,VM发现Activity一直被持有,并没有回收这些无用内存。根据以往的经验,大部分的Activity泄漏都是由于Handler内部类长时间挂在线程中造成的。我们的应用程序的这一部分已经被考虑和处理过。泄漏在哪里?2、WebView导致内存泄漏。众所周知,我持怀疑态度,为了证明自己的清白,我一一点了进去。一共有三种不同的参考链。对于后续的解释,这里起一个名字:①AuthDialog引用链②BrowserFrame引用链③IClipboradDataPaste引用链看来这次的情况有点不一样啊!由于Monkey测试机型比较少,这里的bug全部来自三星GT-I9300@android+4.3手机。楼主为了快速解决问题,请教了其他同事和StackOverflow,发现有很多人提到的CookieSyncManager、WebView、WebViewClassic这三个类,它们会造成内存泄漏!初步结论如下:1.CookieSyncManager是一个全局静态Singleton,操作系统内部使用App的Activity作为Context来构造它的实例。我们应该在App启动的时候先帮助系统创建这个单例,使用applicationContext使其不引用Activity。2、使用WebView页面(Activity)时,当页面生命周期结束退出(onDestory)时,需要主动调用WebView.onPause()和WebView.destory()让系统释放WebView-相关资源。4、WebView内存泄漏众所周知,建议另起进程独占运行WebView。没有9998,没有9999,我们要100%!WebView用完后杀掉进程,即使泄漏。根据以上结论,我们都认为这是WebView造成的。但!我们应用的主进程LoginActivity根本没有使用WebView!!!3、第三方jar包使用WebView。这有什么好?根据上面的AuthDialog参考链,宿主将目标锁定在了某个SDK上:一段时间后,对于混淆后的代码,找到下面一段。SDK确实创建了一个WebView实例,使用客户端程序的Activity对象作为WebViewContext,如下所示:c和j都是SDK中继承自WebView的子类,k是登录界面的入参Activity。在创建c对象后,upshape被分配给j。网上已经有很多例子,直接以Activity为参数构建WebView,极有可能造成Activity泄露。不过我也看到代码中调用了WebView的destroy()方法来释放资源。但是似乎不能保证dismiss()会被执行。这里的问题比较麻烦。SDK是我们的第三方包。我们不能让第三方包不使用WebView,或者让第三方包在另一个进程中运行WebView!所以,在AppEvasion上做,暂时很难实现。于是找来SDK童鞋一起分析。最终,大家有了初步的共识。在Android4.3以下的旧版本中,使用Activity对象创建WebView确实可能会导致内存泄漏。很高兴有SDK童鞋的大力支持,一起分析,问题到这里有了初步的进展。4.心里还是没解,看WebView的源码才明白根本原因。不过楼主心里还有一个很严重的疑惑没有解开(疑惑是什么?)。于是就把Android4.3的源码拿来翻了一遍,希望能在这里找到病根,做个小记录,画了一张Java层WebView结构的不严谨的类图:源码来源:http://安卓参考。com/4.3_r2.1/(请复制上面的链接在浏览器中打开)大致情况是这样的:在WebView的结构中,有一个提供静态方法的工厂类WebViewFactory。Android4.3(JellyBean)版本通过WebViewFactory工厂类创建一个全局单例对象WebViewClassic$Factory,然后使用这个Factory创建一套完整的实现代码(XXXClassic):WebViewClassic、CookieManagserClassic、WebViewDatabaseClassic。WebViewClassic是WebView各种API的真正实现。WebViewClassic创建并维护一个WebViewCore对象。WebViewCore创建了一个子线程“WebViewCoreThread”,它是一个全局的单例Thread,一旦启动就不会停止!WebViewCore将在该子线程中创建、维护和调用BrowserFrame方法。BrowserFrame本身是属于“WebViewCoreThread”线程的Handler子类。BrowserFrame会被native(c++)层调用,然后这些调用会切换到“WebViewCoreThread”线程去执行,比如刷新进度或者处理屏幕旋转事件等。BrowserFrame也会调用CookieSyncManager.createInstance(),这是系统框架中唯一的地方!看到这里,宿主发现了上面说的,有可能提前为系统调用CookieSyncManager.createInstance(contenxt.getApplicationContext())是没有效果的,因为系统本来就是这么设计的。手机厂商修改这个的可能性不大。什么是CookieSyncManager?同样,它也创建了一个子线程,线程名为“CookieSyncManager”,全局单例不会停止!该线程将每5分钟将cookie缓存在内存中PersistsyncFromRamToFlash()。这里我们比较关心的是Activity为什么会泄露,所以关键是看哪些类对象持有Activity(Context)的引用:WebViewClassic、WebViewCore、BrowserFrame。这个结构里面有很多静态单例和子线程,想想就恶心。并且三个关键类都持有Activity引用。但是我们发现,其实WebViewClassic和WebViewCore这两个对象和WebView对象的生命周期是一样的。当Activity被销毁时,WebView也被销毁,另外两个对象也随着WebView被销毁而被销毁。烟雾散去。..两个孤独的子线程还在运行,还有一个全局静态的钉钉子对象。但!BrowserFrame本身就是一个Handler。如果是因为native层的调用,给“WebViewCoreThread”挂了消息,那么可以建立一个引用链:Thread->MessageQueue->Message->Handler(BrowserFrame)->Activity,楼主的疑惑是什么?五。***的疑惑我们来看看AuthDialog的引用链。如果换成MAT就更清楚了:楼主发现这里的CookieSyncManager线程居然直接引用了Message对象!这到底是什么?一般HandlerThread持有一个MessageQueue对象,MessageQueue持有Message队列。JavaLocal:局部变量。例如,输入参数,或仍在线程堆栈中的方法的本地创建对象。本机堆栈。本机代码中的输入或输出参数,例如用户定义的JNI代码或JVM内部代码。许多方法都有本机部分,作为方法参数处理的对象成为垃圾收集根。例如,用于文件、网络、I/O或反射操作的参数。这说明CookieSyncManager线程中有一个Message局部变量,由于线程还没有结束,局部变量还没有释放。Message.obj成员引用AuthDialog$3对象。这是一个内部类。楼主发现内部类混淆后的命名规则是:一出现就命名。AuthDialog中有很多内部类:如上图,MAT中引用链中的AuthDialog$3这里指的是OnDismissListener这个匿名内部类!那我们看看Dialog.setOnDismissListener中做了什么:纳尼!OnDismissListener实际上是分配给Message.obj成员的!于是,我们脑海中生成的一个引用链是这样的:Thread(main)->MessageQueue->Message->obj(OnDismissListener)->AuthDialog->Activity是错误的,我们可以发现与引用链无关CookieSyncManager子线程!再对比一下:子线程CookieSyncManager是从主线程拿到消息的!!不好了!!什么情况???此消息在某处被错误引用?子线程通过JNI获取native中Java层的对象?好吧,发帖人承认自己学了一个晚上,一点进步都没有。..6.原来是它!——对话注:以下分析来自Github上的一篇文章:《一个内存泄漏引发的血案》https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-25/%E4%B8%80%E4%B8%AA%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88-Square.md感兴趣的童鞋,请??复制链接到浏览器打开??,详细阅读!这里简单解释一下,作者的结论是:在AndroidLollipop之前使用AlertDialog可能会导致内存泄露!作者发现局部变量的生命周期在DalvikVM和ART/JVM之间是不一样的。在DVM中,如果线程死循环或阻塞,线程栈帧中的局部变量如果不设置为null则不会被回收。下面的代码用阻塞队列来说明问题:在子线程中调用loop()无限循环,不断从阻塞队列中取出一个MyMessage对象并将该对象引用赋值给局部变量message,过一会循环,虚拟机应该结束while花括号中局部变量的生命周期,并释放堆内存中对应的MyMessage对象。但是,DVM不会这样做!!在VM中,每个栈帧都是局部变量的集合,垃圾收集器是保守的:只要有活引用,就不会收集。每次循环后,局部变量不再可访问,但局部变量仍然持有对Message的引用。当局部变量不可访问时,解释器/JIT理论上应该设置对局部变量的引用。然而,他们并没有这样做,引用还活着,不会被置为null,这样就不会被回收!!这种场景不就是AndroidHandler消息机制的处理方式吗?!Looper不停地从阻塞队列MessageQueue中取出下一条消息Message,并将引用赋值给局部变量msg。一旦一个循环结束,msg没有被设置为null,相应的Message对象也没有被回收,所以就泄露了。但是Message有自己的回收机制,被任何线程共享。从上面的源码我们可以看出,每条Message经过Handler处理后都会recycle(),清空所有成员变量,放入回收池中。那么,被CookieSyncManager子线程的Looper循环了一次的Message对象,也和其他的一样被回收了,放到了回收池中。这时候刚好遇到了Dialog!!这家伙只是通过obtainMessage()(CookieSyncManager线程的局部变量引用)从回收池中拿到了Message,而Message.obj变量就是OnDismissListener。拿到之后,Dialog居然拥有了!!作为会员,我很喜欢它!由于Dialog拥有mDismissMessage对象,所以不会让它挂在消息队列中,每次使用时只是复制一份。Message.obtain(mDismissMessage),所以这个Message永远不会回到回收池,直到Dialog被销毁,mDismissMessage变量也被设置为null。但是这个Message还是占用了堆内存,被一个“空闲”的子线程局部变量msg引用!!于是就有了这个引用链:Thread(CookieSyncManager)->Message->AuthDialog$3(OnDismissListener)->AuthDialog->Activity7.总结一些注意事项对于Android4.3及以下版本,或者使用DVM时Android版本使用WebView,需要注意确保调用了destroy()。考虑是否使用applicationContext()构建WebView实例调用Dialog设置在使用OnShowListener、OnDismissListener、OnCancelListener时,注意内部类是否泄露了Activity对象,尽量不要自己持有Message对象。
