ConstraintLayout together with RecyclerVIew (ListAdapter) appears to use HUGE amounts of memory (up to 1GB) when loading a list with +6000 items

时光毁灭记忆、已成空白 提交于 2020-06-23 12:17:22

问题


I'm building a simple FileExplorer for my app, and using Coroutines I get the files in a given path, and while displaying them, there are spikes in memory usage. I show the profiler tool tabs at the bottom of the post. My best guess is that the adapter is creating a viewholder for every single item on the list and that is using all the memory of the app and the device itself.

Edit: by using RelativeLayout instead of ConstraintLayout, it decreased the memory usage by a factor of 3, and it takes a few seconds for the list to be displayed.

Quick summary of the content:

0 - function that gets the contents in the path

1 - OutOfMemoryException message on the Run console of AndroidStudio

2 - Garbage collector log

3 - Code snippet that the OOM error points to

4 - Where the above code snippet is called

5 - ViewHolder code

6 - Profiler screenshot showing overview of biggest spike (over 1GB)

7 - DialogFragment layout file where the RecyclerView is declared

8 - the Row

9 - Tabs from the profiler tool showing ConstraintLayout calls and onMeasure and related functions

10 - RecyclerView and ConstraintLayout versions

the function that actually gets the files

private fun getFilesOnPath(path : String, showHiddenFiles : Boolean = false, onlyFolders : Boolean = false) : List<File> {
    val file = File(path)

    var listOfFiles = listOf<File>()

    try {
        listOfFiles = file.listFiles()
             .filter { showHiddenFiles || !it.name.startsWith(".") }
             .filter { !onlyFolders || it.isDirectory }.toList()
    } catch (exception : IllegalStateException) {
        Timber.tag(LOG_TAG).e("${exception.message} \n ${exception.cause}")
    } finally {
         return listOfFiles
    }

}

"beforeMain" and "afterMain" are there for when I implement a ProgressBar and to show them at appropriate times

I notice that on folders with less files the UI doesn't lag until the list of files is loaded, but when clicking on this particularly large WhatsApp folder, the app will run out of memory with this appearing on the console (PS: This error has nothing to do with the list of items, it is loaded and filtered just fine):

at java.lang.Throwable.nativeFillInStackTrace(Native method)
        at java.lang.Throwable.fillInStackTrace(Throwable.java:775)
        at java.lang.Throwable.<init>(Throwable.java:258)
        at java.lang.Error.<init>(Error.java:70)
        at java.lang.VirtualMachineError.<init>(VirtualMachineError.java:53)
        at java.lang.OutOfMemoryError.<init>(OutOfMemoryError.java:58)
        at java.lang.reflect.Constructor.newInstance0(Native method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
        at android.view.LayoutInflater.createView(LayoutInflater.java:686)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:829)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:769)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:531)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:461)
        at goldengentleman.goldennotebook.adapters.FileExplorerAdapter$FilesViewHolder$Companion.from(FileExplorerAdapter.kt:153)
        at goldengentleman.goldennotebook.adapters.FileExplorerAdapter.onCreateViewHolder(FileExplorerAdapter.kt:52)
        at androidx.recyclerview.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:7201)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6332)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6216)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6212)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2314)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1631)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1591)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:668)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4215)
        at androidx.recyclerview.widget.RecyclerView.onMeasure(RecyclerView.java:3615)
        at android.view.View.measure(View.java:24967)
        at androidx.constraintlayout.widget.ConstraintLayout$Measurer.measure(ConstraintLayout.java:736)
        at androidx.constraintlayout.solver.widgets.analyzer.BasicMeasure.measure(BasicMeasure.java:399)
        at androidx.constraintlayout.solver.widgets.analyzer.BasicMeasure.measureChildren(BasicMeasure.java:105)
        at androidx.constraintlayout.solver.widgets.analyzer.BasicMeasure.solverMeasure(BasicMeasure.java:232)
        at androidx.constraintlayout.solver.widgets.ConstraintWidgetContainer.measure(ConstraintWidgetContainer.java:113)
        at androidx.constraintlayout.widget.ConstraintLayout.resolveSystem(ConstraintLayout.java:1480)
        at androidx.constraintlayout.widget.ConstraintLayout.onMeasure(ConstraintLayout.java:1555)
        at android.view.View.measure(View.java:24967)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7134)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
        at android.view.View.measure(View.java:24967)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7134)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
        at android.view.View.measure(View.java:24967)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7134)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
        at com.android.internal.policy.DecorView.onMeasure(DecorView.java:992)
        at android.view.View.measure(View.java:24967)
        at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3270)
        at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2001)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2300)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1861)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8478)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
        at android.view.Choreographer.doCallbacks(Choreographer.java:761)
        at android.view.Choreographer.doFrame(Choreographer.java:696)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7045)
        at java.lang.reflect.Method.invoke(Native method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:964)

And there are the obvious Garbage Collector logs on the Console:

I/nnotebook.debu: Clamp target GC heap from 216MB to 192MB
    Alloc concurrent copying GC freed 0(0B) AllocSpace objects, 0(0B) LOS objects, 0% free, 192MB/192MB, paused 145us total 826.488ms

The problem is probably the ridiculosly large list with actually 6000 items on it, but the error makes it seem like the problem is on the Adapter, this is where the console points to:

companion object {
     fun from(parent : ViewGroup) : FilesViewHolder = FilesViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.row_file_explorer_file, parent, false))
}

and where it's calling the above function:

override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder = when (viewType) {
        MODE_FOLDERS -> FoldersViewHolder.from(parent)
        MODE_FILES -> FilesViewHolder.from(parent)
        else -> FilesViewHolder.from(parent)
    }

Edit: Here is the ViewHolder class (PS: Don't try to understand the onClick and onClickListeners, it's just logic for multiselection):


    class FilesViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {

        private val fileName : TextView = itemView.findViewById(R.id.file_name_textView)
        private val fileIcon : ImageView = itemView.findViewById(R.id.file_icon_imageView)
        private val fileFormat : TextView = itemView.findViewById(R.id.file_format_textView)
        private val fileSize : TextView = itemView.findViewById(R.id.file_size_textView)
        private val fileTimeCreated : TextView = itemView.findViewById(R.id.time_created_textView)
        private val root : ConstraintLayout = itemView.findViewById(R.id.root)

        fun bind(item : FileModel, context : Context, adapter : FileExplorerAdapter) {

            if (item.fileType == FileType.FOLDER) {
                fileName.text = item.name
                fileIcon.setImageDrawable(context.getDrawable(R.drawable.folder_icon))
                fileSize.visibility = View.INVISIBLE
                fileTimeCreated.visibility = View.INVISIBLE
                fileFormat.visibility = View.INVISIBLE
                root.setBackgroundResource(R.drawable.border_square)
            } else {
                fileSize.visibility = View.VISIBLE
                fileTimeCreated.visibility = View.VISIBLE
                fileFormat.visibility = View.VISIBLE
                fileName.text = item.name
                fileFormat.text = item.extension
                fileSize.text = item.sizeInMB
                fileTimeCreated.text = Time.convertUnixToDateTime(item.timeCreated)
                val ext = item.extension
                fileIcon.apply {
                    setImageDrawable(context.getDrawable(
                        if (item.fileType == FileType.FOLDER) R.drawable.folder_icon
                        else if (ext == "pdf") R.drawable.pdf_box
                        else if (ext == "doc" || ext == "docx") R.drawable.file_word
                        else if (ext == "mp3" || ext == "3gp") R.drawable.music
                        else if (ext == "mp4" || ext == "webm") R.drawable.video
                        else if (ext == "jpg" || ext == "png") R.drawable.image
                        else R.drawable.file))
                }
                if (item in adapter.selectedFiles) {
                    root.setBackgroundResource(R.drawable.border_square_selected)
                } else {
                    root.setBackgroundResource(R.drawable.border_square)
                }
            }

            root.setOnClickListener {
               // Does multi-selection stuff like changing the rows background
            }

            root.setOnLongClickListener {
                // does the same as the above onClickListener
                true
            }

        }

        companion object {
            fun from(parent : ViewGroup) : FilesViewHolder = FilesViewHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.row_file_explorer_file, parent, false))
        }
    }

And this is the Profiler during its biggest spike:

This js where the recyclerView is declared in the dialogfragment layout file:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:app = "http://schemas.android.com/apk/res-auto"
    xmlns:tools = "http://schemas.android.com/tools"
    android:layout_width = "match_parent"
    android:layout_height = "match_parent"
    android:background = "@drawable/dialog_fullscreen_background">

    <com.google.android.material.appbar.AppBarLayout
        android:id = "@+id/appBarLayout"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:elevation = "12dp"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layout_constraintStart_toStartOf = "parent"
        app:layout_constraintTop_toTopOf = "parent">

        <androidx.appcompat.widget.Toolbar
            android:id = "@+id/toolbar"
            android:layout_width = "match_parent"
            android:layout_height = "?attr/actionBarSize"
            android:background = "?attr/toolbar_bottom_nav_color"
            android:paddingStart = "6dp"
            android:paddingEnd = "16dp"
            android:elevation = "@dimen/toolbar_nav_elevation"
            app:subtitleTextColor = "?attr/secondary_text_color"
            app:titleTextColor = "?attr/primary_text_color"
            app:contentInsetStartWithNavigation = "0dp"
            app:navigationIcon = "@drawable/close_x"
            tools:title = "@string/internal_storage" />
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id = "@+id/recyclerView"
        android:layout_width = "match_parent"
        android:layout_height = "0dp"
        app:layoutManager = "androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toTopOf = "@+id/_constraintLayout2"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layout_constraintStart_toStartOf = "parent"
        app:layout_constraintTop_toBottomOf = "@+id/appBarLayout"
        tools:listitem = "@layout/row_file_explorer" />

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id = "@+id/_constraintLayout2"
        android:layout_width = "match_parent"
        android:layout_height = "64dp"
        android:background = "?attr/toolbar_bottom_nav_color"
        android:elevation = "@dimen/toolbar_nav_elevation"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layout_constraintHorizontal_bias = "0.0"
        app:layout_constraintStart_toStartOf = "parent">

        <Button
            android:id = "@+id/save_button"
            style = "@style/dialogButtonStyle"
            android:layout_width = "wrap_content"
            android:layout_height = "wrap_content"
            android:layout_marginEnd = "8dp"
            android:text = "@string/save"
            app:layout_constraintBottom_toBottomOf = "parent"
            app:layout_constraintEnd_toEndOf = "parent"
            app:layout_constraintTop_toTopOf = "parent" />

        <Button
            android:id = "@+id/cancel_button"
            style = "@style/dialogButtonStyle"
            android:layout_width = "wrap_content"
            android:layout_height = "wrap_content"
            android:layout_marginEnd = "8dp"
            android:text = "@string/cancel"
            app:layout_constraintBottom_toBottomOf = "parent"
            app:layout_constraintEnd_toStartOf = "@+id/save_button"
            app:layout_constraintTop_toTopOf = "parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

And the row:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:app = "http://schemas.android.com/apk/res-auto"
    xmlns:tools = "http://schemas.android.com/tools"
    android:id = "@+id/root"
    android:layout_width = "match_parent"
    android:layout_height = "75dp"
    android:background = "@drawable/border_square"
    android:foreground = "@drawable/custom_ripple_no_border">

    <TextView
        android:id = "@+id/file_name_textView"
        android:layout_width = "wrap_content"
        android:layout_height = "wrap_content"
        android:layout_marginStart = "16dp"
        android:ellipsize = "start"
        android:singleLine = "true"
        android:textColor = "?attr/primary_text_color"
        android:textSize = "20sp"
        app:layout_constraintBottom_toBottomOf = "@+id/file_icon_imageView"
        app:layout_constraintStart_toEndOf = "@+id/file_icon_imageView"
        app:layout_constraintTop_toTopOf = "@+id/file_icon_imageView"
        tools:text = "File file file" />

    <ImageView
        android:id = "@+id/file_icon_imageView"
        android:layout_width = "50dp"
        android:layout_height = "50dp"
        android:layout_marginStart = "16dp"
        android:layout_marginTop = "16dp"
        android:layout_marginBottom = "16dp"
        android:tint = "?attr/primary_text_color"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintStart_toStartOf = "parent"
        app:layout_constraintTop_toTopOf = "parent"
        app:srcCompat = "@drawable/folder_icon"
        tools:ignore = "ContentDescription" />

    <TextView
        android:id = "@+id/file_format_textView"
        android:layout_width = "wrap_content"
        android:layout_height = "wrap_content"
        android:layout_marginEnd = "8dp"
        android:layout_marginBottom = "8dp"
        android:textColor = "?attr/secondary_text_color"
        android:textSize = "12sp"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintEnd_toEndOf = "parent"
        tools:text = "Image File" />

    <TextView
        android:id = "@+id/file_size_textView"
        android:layout_width = "wrap_content"
        android:layout_height = "wrap_content"
        android:layout_marginTop = "8dp"
        android:layout_marginEnd = "8dp"
        android:textColor = "?attr/secondary_text_color"
        android:textSize = "12sp"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layout_constraintTop_toTopOf = "parent"
        tools:text = "100KB" />

    <TextView
        android:id = "@+id/time_created_textView"
        android:layout_width = "wrap_content"
        android:layout_height = "wrap_content"
        android:layout_marginTop = "4dp"
        android:textColor = "?attr/secondary_text_color"
        android:textSize = "12sp"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintStart_toStartOf = "@+id/file_name_textView"
        app:layout_constraintTop_toBottomOf = "@+id/file_name_textView"
        tools:text = "25/08/2000" />

</androidx.constraintlayout.widget.ConstraintLayout

Now, for the profiler tool tabs. It shows A LOT of stuff that appears to be related to ConstraintLayout, such as:

and many others also basically shows the same, many many calls to onMeasure and related ui functions.

RecyclerView and ConstraintLayout versions on app/buildgradle

// ConstraintLayout
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'

// RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03'

回答1:


There is a lot of allocations in getFilesOnPath. listFiles, each filter and toList create new collections. Use Sequence to prevent this.

private fun getFilesOnPath(path : String, showHiddenFiles : Boolean = false, onlyFolders : Boolean = false) : List<File> {
    var listOfFiles = listOf<File>()

    try {
        listOfFiles = File(path)
            .walk() // Creates a FileTreeWalk, which is a Sequence
            .maxDepth(1) // files in "path" directory only
            .drop(1) // drop "path" directory 
            .filter { showHiddenFiles || !it.name.startsWith(".") }
            .filter { !onlyFolders || it.isDirectory }
            .toList()
    } catch (exception : IllegalStateException) {
        Timber.tag(LOG_TAG).e("${exception.message} \n ${exception.cause}")
    } finally {
         return listOfFiles
    }
}



回答2:


Apparently the "bug" was in the ConstraintLayout lib the whole time. The version I had:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'

Apparently had a memory leak problem of sorts that the 2.0.0-beta7 version solved. Unfortunatelly my AndroidStudio didn't show me that there even was an update at all. Huge thanks to @YuriyMysochenko for spotting it, and for the people that tried to help me!



来源:https://stackoverflow.com/questions/62441342/constraintlayout-together-with-recyclerview-listadapter-appears-to-use-huge-am

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!