使用Android studio分析内存泄露
jopen
10年前
<p> Android使用java作为平台开发,帮助了我们解决了很多底层问题,比如内存管理,平台依赖等等。然而,我们也经常遇到OutOfMemoey问题,垃圾回收到底去哪了?</p> <p> 接下来是一个Handler Leak的例子,它一般会在编译器中被警告提示。</p> <h3> 所需要的工具</h3> <ul> <li> Android Studio 1.1 or higher </li> <li> Eclipse MemoryAnalyzer </li> </ul> <h3> 示例代码</h3> <pre class="brush:java; toolbar: true; auto-links: false;">public class MainActivity extends ActionBarActivity { TextView textView; public static final String TAG = NonStaticNestedClassLeakActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_non_static_nested_class_leak); textView = (TextView)findViewById(R.id.textview); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void textView.setText("Done"); } }, 800000L); } }</pre> <p> 这是一个非常基础的Activity.注意这个匿名的Runnable被送到了Handler中,而且延迟非常的长。现在我们运行这个Activity,反复旋转屏幕,然后导出内存并分析。</p> <h3> 导入 Memory 到Eclipse MemoryAnalyzer</h3> <h4> 使用Androidstudio导出 heap dump</h4> <div href="https://simg.open-open.com/show/5e219764842fae5fad2d3396242ca605.png"> <img src="https://simg.open-open.com/show/5e219764842fae5fad2d3396242ca605.png" alt="Android Studio dump Memory Analyze" height="170.6150341685649" width="700" /> <br /> <div> Android Studio dump Memory Analyze </div> </div> <ul> <li> 点击左下角的Android </li> <li> 选中你的程序的包名 </li> <li> 点击 initiates garbage collection on selected vm </li> <li> 点击 dump java heap for selected client </li> </ul> <h4> 打开EMA,进行分析</h4> <p> EMA是对java heap中变量分析的一个工具,它可以用于分析内存泄露。</p> <ul> <li> 点击OQL图标 </li> <li> 在窗口输入select * from instanceof android.app.Activity并按Ctrl + F5或者!按钮 </li> <li> 奇迹出现了,现在你发现泄露了许多的activity </li> <li> 这个真是相当的不容乐观,我们来分析一下为什么GC没有回收它 </li> </ul> <div href="https://simg.open-open.com/show/1df3a1a9204dfd917339116e7895d533.png"> <img src="https://simg.open-open.com/show/1df3a1a9204dfd917339116e7895d533.png" alt="EMA" height="395.97570235383444" width="700" /> <br /> <div> EMA </div> </div> <blockquote> <p> 在OQL(Object Query Language)窗口下输入的查询命令可以获得所有在内存中的Activities,这段查询代码是不是非常简单高效呢? </p> </blockquote> <p> 点击一个activity对象,右键选中Path to GC roots</p> <div href="https://simg.open-open.com/show/379d61be7f069ab239c13aa705d91e1a.png"> <img src="https://simg.open-open.com/show/379d61be7f069ab239c13aa705d91e1a.png" alt="GC root" height="303.73134328358213" width="700" /> <br /> <div> GC root </div> </div> <div href="https://simg.open-open.com/show/49b649d81ca855785d32e212a0377af8.png"> <img src="https://simg.open-open.com/show/49b649d81ca855785d32e212a0377af8.png" alt="Message in looper hold a reference to Activity" height="203.5143769968051" width="700" /> <br /> <div> Message in looper hold a reference to Activity </div> </div> <p> 在打开的新窗口中,你可以发现,你的Activity是被this$0所引用的,它实际上是匿名类对当前类的引用。this$0又被callback所引用,接着它又被Message中一串的next所引用,最后到主线程才结束。</p> <blockquote> <p> 任何情况下你在class中创建非静态内部类,内部类会(自动)拥有对当前类的一个强引用。 </p> </blockquote> <p> 一旦你把Runnable或者Message发送到Handler中,它就会被放入LooperThread的消息队列,并且被保持引用,直到Message被处理。发送postDelayed这样的消息,你输入延迟多少秒,它就会泄露至少多少秒。而发送没有延迟的消息的话,当队列中的消息过多时,也会照成一个临时的泄露。</p> <h4> 尝试使用static inner class来解决</h4> <p> 现在把Runnable变成静态的class</p> <div href="https://simg.open-open.com/show/36ada057f82d7dba527a75f85f238b74.png"> <img src="https://simg.open-open.com/show/36ada057f82d7dba527a75f85f238b74.png" alt="StaticClass" height="547.3548387096774" width="700" /> <br /> <div> StaticClass </div> </div> <p> 现在,摇一摇手机,导出内存</p> <div href="https://simg.open-open.com/show/f75dac8cf81bb3ce7df415cde80d39d6.png"> <img src="https://simg.open-open.com/show/f75dac8cf81bb3ce7df415cde80d39d6.png" alt="StaticClass_memory_analyze" width="698" height="451" /> <br /> <div> StaticClass_memory_analyze </div> </div> <p> 为什么又出现了泄露呢?我们看一看Activities的引用.</p> <div href="https://simg.open-open.com/show/58d66006d69e8d191ae1bd7b322a66fe.png"> <img src="https://simg.open-open.com/show/58d66006d69e8d191ae1bd7b322a66fe.png" alt="StaticClass_memory_analyze_explained" height="298.8031914893617" width="700" /> <br /> <div> StaticClass_memory_analyze_explained </div> </div> <p> 看到下面的mContext的引用了吗,它被mTextView引用,这样说明,使用静态内部类还远远不够,我们仍然需要修改。</p> <h4> 使用弱引用 + static Runnable</h4> <p> 现在我们把刚刚内存泄露的罪魁祸首 - TextView改成弱引用。</p> <div href="https://simg.open-open.com/show/01dce77f78e7d44c0d07a66a7fbed968.png"> <img src="https://simg.open-open.com/show/01dce77f78e7d44c0d07a66a7fbed968.png" alt="StaticClassWithWeakRef_code" height="606.140350877193" width="700" /> <br /> <div> StaticClassWithWeakRef_code </div> </div> <p> 再次注意我们对TextView保持的是<strong>弱引用</strong>,现在让它运行,摇晃手机</p> <blockquote> <p> 小心地操作WeakReferences,它们随时可以为空,在使用前要判断是否为空. </p> </blockquote> <div href="https://simg.open-open.com/show/85955e12e4d7b0525f6eaf5f3df78a9b.png"> <img src="https://simg.open-open.com/show/85955e12e4d7b0525f6eaf5f3df78a9b.png" alt="StaticClassWithWeakRef_memory_analyze" width="635" height="296" /> <br /> <div> StaticClassWithWeakRef_memory_analyze </div> </div> <p> 哇!现在只有一个Activity的实例了,这回终于解决了我们的问题。</p> <p> 所以,我们应该记住:</p> <ul> <li> 使用静态内部类 </li> <li> Handler/Runnable的依赖要使用弱引用。 </li> </ul> <p> 如果你把现在的代码与开始的代码相比,你会发现它们大不相同,开始的代码易懂简介,你甚至可以脑补出运行结果。</p> <p> 而现在的代码更加复杂,有很多的模板代码,当把postDelayed设置为一个短时间,比如50ms的情况下,写这么多代码就有点亏了。其实,还有一个更简单的方法。</p> <h3> onDestroy中手动控制声明周期</h3> <p> Handler可以使用removeCallbacksAndMessages(null),它将移除这个Handler所拥有的Runnable与Message。</p> <pre class="brush:java; toolbar: true; auto-links: false;">//Fixed by manually control lifecycle @Override protected void onDestroy() { super.onDestroy(); myHandler.removeCallbacksAndMessages(null); }</pre> <p> 现在运行,旋转手机,导出内存</p> <div href="https://simg.open-open.com/show/c1a73df808c13a6a27b0f5c3837e32dd.png"> <img src="https://simg.open-open.com/show/c1a73df808c13a6a27b0f5c3837e32dd.png" alt="removeCallbacks_memory_analyze" width="621" height="342" /> <br /> <div> removeCallbacks_memory_analyze </div> </div> <p> Good!只有一个实例。</p> <p> 这样写可以让你的代码更加简洁与可读。唯一要记住的就是就是要记得在生命周期onDestory的时候手动移除所有的消息。</p> <h3> 使用WeakHander</h3> <p> (这个是第三方库,我就不翻译了,大家去<a href="/misc/goto?guid=4959627884934702050" target="_blank">Github</a>上去学习吧)</p> <h3> 结论</h3> <p> 在Handler中使用postDelayed需要额外的注意,为了解决问题,我们有三种方法</p> <ul> <li> 使用静态内部Handler/Runnable + 弱引用 </li> <li> 在onDestory的时候,手动清除Message </li> <li> 使用Badoo开发的第三方的 <a href="/misc/goto?guid=4959627884934702050" target="_blank">WeakHandler</a> </li> </ul> <p> 这三种你可以任意选用,第二种看起来更加合理,但是需要额外的工作。第三种方法是我最喜欢的,当然你也要注意WeakHandler不能与外部的强引用共同使用。<br /> <br /> 来自:<a target="_blank" href="/misc/goto?guid=4958867210506314549">http://www.jianshu.com/p/c49f778e7acf</a> </p>