ConstraintLayout вместе с RecyclerVIew (ListAdapter), похоже, использует ОГРОМНЫЕ объемы памяти (до 1 ГБ) при загрузке списка с +6000 элементами - PullRequest
1 голос
/ 18 июня 2020

Я создаю простой FileExplorer для своего приложения, и с помощью Coroutines я получаю файлы по заданному пути, и при их отображении наблюдаются всплески использования памяти. Я показываю вкладки инструментов профилировщика внизу сообщения. Мое лучшее предположение состоит в том, что адаптер создает средство просмотра для каждого отдельного элемента в списке и использует всю память приложения и самого устройства.

Изменить: с помощью RelativeLayout вместо ConstraintLayout он уменьшился использование памяти в 3 раза, и для отображения списка требуется несколько секунд.

Краткое описание содержимого:

0 - функция, которая получает содержимое в пути

1 - сообщение OutOfMemoryException на консоли запуска AndroidStudio

2 - журнал сборщика мусора

3 - фрагмент кода, на который указывает ошибка OOM

4 - Где приведенный выше фрагмент кода называется

5 - Код ViewHolder

6 - Снимок экрана профилировщика, показывающий обзор самого большого всплеска (более 1 ГБ)

7 - Файл макета DialogFragment, в котором Объявлен RecyclerView

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 и отображать их в нужное время

Я заметил, что в папках с меньшим количеством файлов пользовательский интерфейс не задерживается, пока не будет загружен список файлов, но при нажатии на эту особенно большую папку 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)
    }

Изменить: вот класс ViewHolder (PS: Не трогайте y, чтобы понять onClick и onClickListeners, это просто logi c для множественного выбора):


    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 во время его самого большого всплеска:

Ridiculously large spike in memory usage

Это js, где recyclerView объявлен в файле макета диалогового фрагмента:

<?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, например:

AppHeap

Biggest class in ShallowSize and TotalCount

и многие другие также в основном показывают то же самое, много разных вызовов onMeasure и связанных функций пользовательского интерфейса.

Версии RecyclerView и ConstraintLayout в app / buildgradle

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

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

Ответы [ 2 ]

2 голосов
/ 18 июня 2020

В getFilesOnPath много распределений. listFiles, каждый filter и toList создают новые коллекции. Для предотвращения этого используйте Sequence.

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
    }
}
0 голосов
/ 19 июня 2020

Очевидно "ошибка" все время была в библиотеке ConstraintLayout. Версия, которая у меня была:

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

По-видимому, была проблема с утечкой памяти, которую решила версия 2.0.0-beta7. К сожалению, моя AndroidStudio не показала мне, что вообще было обновление. Огромное спасибо @YuriyMysochenko за то, что это заметил, и людям, которые пытались мне помочь!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...