RecyclerViewMemoryLeak

所属分类:弱点检测代码
开发工具:kotlin
文件大小:0KB
下载次数:0
上传日期:2019-02-05 04:24:05
上 传 者sh-1993
说明:  尝试使用RecyclerView查看发生内存泄漏的情况,
(Experiment to see the condition where memory leak happens with RecyclerView,)

文件列表:
.idea/ (0, 2019-02-04)
.idea/codeStyles/ (0, 2019-02-04)
.idea/codeStyles/Project.xml (2033, 2019-02-04)
.idea/codeStyles/codeStyleConfig.xml (142, 2019-02-04)
.idea/compiler.xml (247, 2019-02-04)
.idea/dictionaries/ (0, 2019-02-04)
.idea/dictionaries/yuichi.xml (85, 2019-02-04)
.idea/gradle.xml (626, 2019-02-04)
.idea/misc.xml (2101, 2019-02-04)
.idea/runConfigurations.xml (564, 2019-02-04)
.idea/vcs.xml (180, 2019-02-04)
Resources/ (0, 2019-02-04)
Resources/Adapter.png (312287, 2019-02-04)
Resources/RxSubscriber.png (307792, 2019-02-04)
Resources/Structure-actual-for-view-holder.png (39042, 2019-02-04)
Resources/Structure-actual.png (40141, 2019-02-04)
Resources/Structure.png (35261, 2019-02-04)
Resources/structure-actual-for-adapter.png (37986, 2019-02-04)
Resources/structure-after-nullouting-adapter.png (38680, 2019-02-04)
Resources/structure-after-removing-adapter-reference.png (35564, 2019-02-04)
app/ (0, 2019-02-04)
app/build.gradle (1366, 2019-02-04)
app/proguard-rules.pro (751, 2019-02-04)
app/src/ (0, 2019-02-04)
app/src/androidTest/ (0, 2019-02-04)
app/src/androidTest/java/ (0, 2019-02-04)
app/src/androidTest/java/com/ (0, 2019-02-04)
app/src/androidTest/java/com/yfujiki/ (0, 2019-02-04)
app/src/androidTest/java/com/yfujiki/recyclerviewmemoryleak/ (0, 2019-02-04)
app/src/androidTest/java/com/yfujiki/recyclerviewmemoryleak/ExampleInstrumentedTest.kt (670, 2019-02-04)
app/src/main/ (0, 2019-02-04)
app/src/main/AndroidManifest.xml (792, 2019-02-04)
app/src/main/java/ (0, 2019-02-04)
app/src/main/java/com/ (0, 2019-02-04)
app/src/main/java/com/yfujiki/ (0, 2019-02-04)
app/src/main/java/com/yfujiki/recyclerviewmemoryleak/ (0, 2019-02-04)
... ...

![platform](https://img.shields.io/badge/platform-Android-blue.svg) ![language](https://img.shields.io/badge/language-Kotlin1.3-green.svg) ![twitter](https://img.shields.io/badge/twitter-@yfujiki-blue.svg) ## Preface This repository is mostly meant for novis to mid level Android programmers, who haven't really digged into [LeakCanary](https://github.com/square/leakcanary) yet. I myself used it for the first time recently after delving into Android development for a year. And I am pleasantly surprised how powerful this tool is. This is definitely a must-include tool in every project. At the same time, I was surprised how Android maintains references under the hood for `RecyclerViews`. With naive expectation that `RecyclerView` itself should avoid circular references, you can easily fall into a trap of memory leaks. (And that's exactly the kind of reason that Square guys implemented [LeakCanary](https://github.com/square/leakcanary) and everybody should use it) ## How to use LeakCanary It's pretty simple to use LeakCanary. As instructed in the [README section](https://github.com/square/leakcanary#getting-started), you just need to **1. describe dependency in gradle** and **2. write a few lines in your `Application` subclass**. And then LeakCanary will alert you of the memory leak in your __debug build__. However, as straight-forward as it sounds, there was one pitfall I got into. If you are like me and prefers to press _Debug_ button instead of _Run_ button on Android Studio, **LeakCanary doesn't run while you are debugging**. You have to stop the debugging, and start the installed debug build from the launcher. I have summarized this flow into a video, if this helps : [![How to use LeakCanary](http://img.youtube.com/vi/RiYGSjguI9k/0.jpg)](http://www.youtube.com/watch?v=RiYGSjguI9k "How to use LeakCanary ((after you have finished implementation)") ## Two cases you can easily make memory leak around RecyclerView I have put two cases of memory leaks I encountered into this sample program. All codes are written in Kotlin. ### Case 1: `RecyclerView.adapter` outlives `Activity`. #### Memory Leak: This sample program follows very standard structure as follows. ![Structure.png](./Resources/Structure.png) `Fragment` shows `RecyclerView` and it's `adapter` provides custom `Viewholder`s. One thing that deviates from standard(!?) structure is that the `Fragment` keeps reference to the `adapter`. This reference is meant to reuse `adapter` even after `Activity` is refreshed due to rotation etc. We are showing `RecyclerView` on top of the `Fragment`, so I think it is a sensible option to match the lifetime of `RecyclerView`'s `adapter` to the one of the `Fragment`. This structure looks memory leak safe because there is no circular references. However, the expectation is false and LeakCanary detects that. The object reference path provided by [LeakCanary](https://github.com/square/leakcanary) looks like this. ![Adapter.png](https://qiita-image-store.s3.amazonaws.com/0/108030/343ca8a3-ed73-f31e-d21b-a419d9100872.png) To my surprise, this diagram tells me that `RecyclerView.mAdapter` holds an indirect reference to `MainActivity` through `RecyclerView.mContext`. This is not a reference we made ourselves. This is a "hidden" reference, if we may call it. So, the actual structure with this "hidden" reference (indicated by the dashed lines) is like the next diagram. ![structure-actual-for-adapter.png](Resources/structure-actual-for-adapter.png) You can see there is a beautiful circular reference from `MainFragment` => `MainRecyclerViewAdapter` => `RecyclerView` => `MainActivity` => `MainFragment` and so on... Rotation happens, and `MainActivity` gets recreated, but since `MainFragment` still lives after rotation and keeps indirect reference to the old `MainActivity`, the old `MainActivity` will never reclaimed by GC and leaks. As a side note, the `RecyclerView` is always recreated after rotation and reference from `MainFragment` to the old `RecyclerView` through Android-Kotlin extension never stays after rotation (indicated by the red cross in the diagram). That's how Android works. #### Solution 1 A simple solution is to shorten the lifetime of `adapter` to match with the one of the `Activity`. ```kotlin class MainActivityFragment : Fragment() { // Discard permanent reference to the adapter - val adapter = MainRecyclerViewAdapter() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) return inflater.inflate(R.layout.fragment_activity_main, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Recreate adapter instance every time after rotation - recyclerView.adapter = adapter + recyclerView.adapter = MainRecyclerViewAdapter() recyclerView.layoutManager = LinearLayoutManager(activity!!) } } ``` Every time when rotation happens, you will ditch `adapter` that holds an indirect reference to the old `Activity`, so that the `Activity` will not have a zombie reference. In terms of structure, we don't have the circular reference we had before, because there is no link from `Fragment` to `adapter` now. ![structure-after-removing-adapter.png](Resources/structure-after-removing-adapter-reference.png) The cons of this approach is that you cannot save the temporary state in the `adapter`, because the `adapter` is initialized at every rotation. We have to save the temporary state somewhere else, and let the `adapter` to fetch the state after every initialization. #### Solution 2 Another simple solution is to call `recyclerView.adapter = null` from `onDestroyView`. ```kotlin class MainActivityFragment : Fragment() { // Discard permanent reference to the adapter val adapter = MainRecyclerViewAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) return inflater.inflate(R.layout.fragment_activity_main, container, false) } + override fun onDestroyView() { + super.onDestroyView() + // Note that this recyclerView is an old one + // and different instance from the one in onViewCreated. + recyclerView.adapter = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Note that this recyclerView is a new one // and different instance from the one in onDestroyView. recyclerView.adapter = adapter } } ``` Actually, I was surprised that this approach works. Even if you null out the reference from `RecyclerView` to `adapter`, as long as the `adapter` has a reference to `RecyclerView`, you still have circular reference. The only way I can comprehend this is that Android nulls out the reference from `adapter` to the `RecyclerView` as well when you null out the reverse relationship, thereby eliminating the circular reference. ![structure-after-nullouting-adapter.png](Resources/structure-after-nullouting-adapter.png) ### Summary of case 1 Even though I think solution 1 is by-the-book approach, note that it has a con that you can not let `adapter` to hold temporary status. If you need `adapter` to maintain temporary status, then probably better to pick solution 2. Another interesting point I want to note is that this type of memory leak does not occur with `ViewPager`. The way the `ViewPager` set "hidden" references should be a bit different from how `RecyclerView` does it. ## Case 2 : When you created Rx `disposable` in `RecyclerView.ViewHolder`, but you didn't/couldn't dispose it ### Memory Leak: This case actually belongs to Rx domain rather than `RecyclerView` domain, and it is a trivial case in that it happens when you failed to dispose `Disposable` properly. But I found it not very trivial as of __when__ we should dispose `Disposable`, and that perspective is specific to the `RecyclerView`. ```kotlin class MainRecyclerViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { val disposable = CompositeDisposable() init { disposable += AppState.stateSubject.subscribe { itemView.textView.text = "Status : $it" someMethod() } } fun someMethod() { println("Doing nothing...") } } ``` A custom `ViewHolder` instance is subscribing to static Rx `subject`. Apparently in this case the subscription block leaks because we are not disposing the `disposable`. However, in addition to that, `ViewHolder` leaks asw well because the subscription block references `ViewHolder`. And to my surprise, the `ViewHolder` has a "hidden" indirect reference to `Activity`, and the `Activity` leaks too. (It surprised me quite a bit in this case especially because we are placing `RecyclerView` on a `Fragment`.) The object reference path provided by LeakCanary would look like this. ![RxSubscriber.png](Resources/RxSubscriber.png) You can see that there is a reference from `MainRecyclerViewHolder` to `MainActivity`. ![Structure-actual-for-view-holder.png](Resources/Structure-actual-for-view-holder.png) ### Solution 1 We can dispose `disposable` in `ViewHolder.finalize()`. One thing you have to note in this case is that you should reference `ViewHolder` instance as a __weak__ reference. Otherwise, the subscription block's reference keeps the `ViewHolder` instance alive, and `ViewHolder.finalize()` is never called. As a result, `disposable.dispose()` is never called as well. ```kotlin val disposable = CompositeDisposable() init { + val weakItemView = WeakReference(itemView) + val weakSelf = WeakReference(this) + disposable += AppState.stateSubject.subscribe { - itemView.textView.text = "Status : $it" - someMethod() + weakItemView.get()?.textView?.text = "Status : $it" + weakSelf.get()?.someMethod() } } fun someMethod() { println("Doing something...") } + + protected fun finalize() { + if (!disposable.isDisposed()) { + disposable.dispose() + } + println("MainRecyclerViewHolder reclaimed") + } ``` This solution is probably an anti-pattern. Resorting to implementation in `finalize()` is always frowned upon, because you never know when it will be called. After `ViewHolder` instance was detached from the window after rotation, `finalize()` will not be called until the next GC. I haven't observed this, but it is theoretically possible that the detached `ViewHolder`s keeps receiving Rx events even though they are considered "dead" from application's perspective. But you can save this as a last resort. The "dead" instances will be claimed eventually for sure. ### Solution 2 We can create `Disposable` instance in `Activity` instance and trickle it down to `ViewHolder`s for their use. This way, we can match the lifetime of `Disposable` and `Activity. MainActivity: ```kotlin class MainActivity : AppCompatActivity() { + val disposable = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } + override fun onDestroy() { + super.onDestroy() + + if (!disposable.isDisposed) { + disposable.dispose() + } + } ``` MainActivityFragment: ```kotlin class MainActivityFragment : Fragment() { - val adapter = MainRecyclerViewAdapter() + private lateinit var adapter: MainRecyclerViewAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + adapter = MainRecyclerViewAdapter((activity as MainActivity).disposable) retainInstance = true } ... ``` MainRecyclerViewAdapter: ```kotlin -class MainRecyclerViewAdapter: RecyclerView.Adapter() { +class MainRecyclerViewAdapter(val activityDisposable: CompositeDisposable) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecyclerViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.view_holder_main, parent, false) - return MainRecyclerViewHolder(itemView) + return MainRecyclerViewHolder(itemView, activityDisposable) } ... ``` MainRecyclerViewHolder: ```kotlin -class MainRecyclerViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { +class MainRecyclerViewHolder(itemView: View, val activityDisposable: CompositeDisposable): RecyclerView.ViewHolder(itemView) { val disposable = CompositeDisposable() init { disposable += AppState.stateSubject.subscribe { itemView.textView.text = "Status : $it" someMethod() } + activityDisposable.add(disposable) } } ``` This solution is perfect __in situation where the lifetime of `ViewHolder` and `Activity` should match.__ However, if there is a possibility that __`Viewholder` instance outlives `Activity` instance, this solution breaks__. After `Activity` is dead, there will be `ViewHolder`s who cannot receive Rx events. Having said that, I don't come up with any situation that would cause this situation. (i.e., `ViewHolder` to outlive `Activity`) On the other hand, if a __`ViewHolder` instance is detached from the window while an `Activity` instance is alive, detached `ViewHolder` could keep receiving Rx events__. This scenario is more realistic. If you have multiple `Fragment`s in the `Activity` and if you are switching the `Fragment`s with `ViewPager`, a `RecyclerView` and its `ViewHolder`s on one `Fragment` could be detached from the window when another `Fragment` is displayed. In this case, __you can trickle down `Disposable`s from the `Fragment` instead of the `Activity`, because you want to match the lifetime of the `ViewHolder` to that of the `Fragment`__. ### Summary of case 2 I think the solution 2 can handle all of the cases. You will need to choose whether to match the `ViewHolder`'s lifetime to `Activity` or `Fragment`, but that's it. I don't come up with a situation where `ViewHolder` instance can outlive parent `Activity`/`Fragment` instance, but if you have any idea, I would be pleased to receive a comment. My lament is that our life would be so much easier if `ViewHolder` itself had a life cycle callback like `onDestroy`... ## Summary - Memory leak can happen when `RecyclerView.Adapter` outlives `Activity`. But there is a solution for that! - Memory leak can happen when you use Rx in `RecyclerView.ViewHolder`. But there is a solution for that! I totally didn't have that in my mental model, but according to LeakCanary's reference path, __`RecyclerView.Adapter`/`RecyclerView.Viewholder` has indirect reference to parent `Activity`. (Even when you placed `RecyclerView` on top of a `Fragment`)__. ![Actual Structure](Resources/Structure-actual.png) Bye bye memory leaks. Long live LeakCanary!!

近期下载者

相关文件


收藏者