在加载包含 +6000 个项目的列表时,ConstraintLayout 和 RecyclerVIew (ListAdapter) 似乎使用了大量内存(最多 1GB)

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

我正在为我的应用程序构建一个简单的 FileExplorer,并使用协程获取给定路径中的文件,在显示它们时,内存使用量出现峰值。我在 post 底部显示分析器工具选项卡。 我最好的猜测是适配器正在为列表中的每个项目创建一个查看器,并且正在使用应用程序和设备本身的所有内存。

编辑:通过使用 RelativeLayout 而不是 ConstraintLayout,内存使用量减少了 3 倍,列表显示需要几秒钟。

Quick内容总结:

0 - 获取路径中内容的函数

1 - AndroidStudio运行 控制台上的 OutOfMemoryException 消息

2 - 垃圾收集器日志

3 - OOM 错误指向的代码片段

4 - 上面的代码片段被调用的地方

5 - ViewHolder 代码

6 - Profiler 屏幕截图显示了最大峰值(超过 1GB)的概览

7 - 声明 RecyclerView 的 DialogFragment 布局文件

8 - 行

9 - 分析器工具中的选项卡显示了 ConstraintLayout 调用和 onMeasure 及相关函数

10 - RecyclerView 和 ConstraintLayout 版本

真正获取文件的函数

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" 和 "afterMain" 用于当我实现 ProgressBar 并在适当的时间显示它们时

我注意到在文件较少的文件夹上 UI 不会滞后,直到加载文件列表,但是当单击这个特别大的 WhatsApp 文件夹时,应用程序将 运行 退出出现在控制台上的内存(PS:这个错误与项目列表无关,它被加载和过滤得很好):

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)

控制台上有明显的垃圾收集器日志:

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

问题可能出在实际上有 6000 个项目的大得可笑的列表,但错误使问题看起来像是在适配器上,这是控制台指向的地方:

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

以及调用上述函数的位置:

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)
    }

编辑: 这里是ViewHolderclass(PS:onClick和onClickListeners不要去理解,只是多选的逻辑):


    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))
        }
    }

这是 Profiler 在其最大峰值期间:

在dialogfragment布局文件中声明recyclerView的这个js:

<?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>

行:

<?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

现在,对于探查器工具选项卡。 它显示了很多似乎与 ConstraintLayout 相关的东西,例如:

和许多其他人也基本上显示相同,对 onMeasure 和相关 ui 函数的许多调用。

app/buildgradle

上的 RecyclerView 和 ConstraintLayout 版本
// ConstraintLayout
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'

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

显然 "bug" 一直在 ConstraintLayout 库中。我的版本:

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

显然 2.0.0-beta7 版本解决了某种内存泄漏问题。不幸的是,我的 AndroidStudio 根本没有告诉我有更新。 非常感谢@YuriyMysochenko 发现它,并感谢那些试图帮助我的人!