پیمایش یکنواخت RecyclerView در اندروید
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 4 دقیقه

پیمایش یکنواخت RecyclerView در اندروید

در این مقاله به بررسی عملکرد پیمایش در recyclerView در اندروید و بهبود آن می‌پردازیم تا تجربه‌ی کاربری بهتری را در استفاده از آن داشته باشیم.

مشکل

 recyclerView یکی از ابزارهای پر کاربرد در اندروید است، و عملکرد پیمایش یکنواخت با viewهای پیچیده می‌تواند یک چالش مهم باشد. بیشتر دستگاه‌ها با سرعت 60 فریم در ثانیه کار می‌کنند. این بدان معنی است که هر 16 میلی‌ثانیه یک فریم جدید وجود دارد. اگر اپلیکیشن شما بیشتر از 16 میلی‌ثانیه طول بکشد که بفهمد در فریم بعدی چه چیزی را نشان دهد، برنامه‌ی شما یک فریم را رد می‌کند.

اندازه‌گیری عملکرد پیمایش

یکی از جالب‌ترین راه‌ها برای دیدن اینکه برنامه شما در حال فریم کردن قاب است استفاده از ابزار profile Gpu Rendring است. این به شما تجسم خوبی در زمان اجرا می‌دهد، که به شما نشان می‌دهد که چه وقت فریم‌ها رد می‌شوند.

  • در دستگاه‌تان به بخش setting و تب developer option بروید
  • به قسمت Monitoring بروید و profile Gpu Rendring را انتخاب کنید
  • در پنجره‌‌ای که ظاهر می‌شود، on screen as bars را انتخاب کنید تا نمودارها را روی صفحه‌ی دستگاه را ببینید
  • اپلیکیشنی را که می‌خواهید نمودار آن را ببینید را باز کنید

پیش ذخیره کردن view در آداپتور

یکی از چیزهایی که ممکنه در حین پیمایش در RecyclerView اتفاق بی‌افتد، ساخت ViewHolder از طریق onCreateViewHolder است. هنگام پیمایش آداپتور شما تاوقتی که ViewHolder به اندازه‌ی کافی برای بازیافت داشته باشد، درخواست ساخت یکی جدید می‌دهد.

Inflate کردن view کند است. اگر layout شما پیچیده باشد ممکن است که بیشتر از 16 میلی‌ثانیه برای inflate کردن آن طول بکشد، و باعث رد کردن فریم شود. چه می‌شود اگر قبل از نمایش لیست به کاربر بتوانیم زودتر آن‌ها را inflate کنیم؟ما می‌توانیم این کار را با AsyncLayoutInflater انجام دهیم تا viewها در thread پس‌زمینه inflate شوند و هنگام آماده شدن پاسخ بدهند.

class SmoothListAdapter(val context: Context) : ListAdapter<ListItem, ListItemViewHolder>(MyDiffCallback()) {

    data class ListItem(val id: String, val text: String)

    class ListItemViewHolder(view: View) : ViewHolder(view) {
        fun populateFrom(listItem: ListItem) {
            //TODO: populate your view
        }
    }

    companion object {
        const val NUM_CACHED_VIEWS = 5
    }

    private val asyncLayoutInflater = AsyncLayoutInflater(context)
    private val cachedViews = Stack<View>()


    init {
        //Create some views asynchronously and add them to our stack
        for (i in 0..NUM_CACHED_VIEWS) {
            asyncLayoutInflater.inflate(R.layout.list_item, null) { view, layoutRes, viewGroup ->
                cachedViews.push(view)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {
        //Use the cached views if possible, otherwise if we ran out of cached views inflate a new one
        val view = if (cachedViews.isEmpty()) {
            LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
        } else {
            cachedViews.pop().also { it.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) }
        }
        return ListItemViewHolder(view)
    }

    override fun onBindViewHolder(viewHolder: ListItemViewHolder, position: Int) =
        viewHolder.populateFrom(getItem(position))

    class MyDiffCallback : DiffUtil.ItemCallback<ListItem>() {
        override fun areItemsTheSame(firstItem: ListItem, secondItem: ListItem) =
                firstItem.id == secondItem.id

        override fun areContentsTheSame(firstItem: ListItem, secondItem: ListItem) =
                firstItem == secondItem
    }
}

توجه داشته باشید که در این مثال، هنگام ایجاد نمونه‌ی آداپتور viewها، inflate می‌شوند. این کار فقط در صورتی انجام می‌شود که زمان کافی برای inflate کردن viewها قبل از صدا زدن submitList در آداپتور داشته باشیم. در غیر این صورت اگر ما بلافاصله پس از ایجاد آداپتور submitList را صدا بزنیم، زمانی برای ذخیره کردن view وجود نخواهد داشت بنابراین این مثال کمک نخواهد کرد.

پخش کردن کار در چندین فریم

جای دیگری که ممکن است منجر به عملکرد کند شود، هنگام فراخوانی onBindViewHolder است. از آن جا که این متد معمولا درست قبل از وارد شدن به view فراخوانی می‌شود، باید سریع باشد. اما گاهی اوقات در یک view پیچیده ممکن است که طول بکشد. اگر صدا زدن onBindViewHolder بیشتر از 16 میلی‌ثانیه طول بکشد تا کامل شود، هنگام پیمایش شاهد آیتم‌هایی خواهیم بود که هنوز inflate نشده‌اند.

ما می‌توانیم با ایجاد یک زمان‌بند Ui این کار را بهبود ببخشیم. در viewHolder بخشی که کدهای اتصال view وجود دارد را به چند بخش تقسیم کنید، یکی برای تنظیم متن و یکی برای تنظیم تصاویر. ما می‌خواهیم مطمئن شویم که هر بخش در مدت زمان کوتاهی مانند کمتر از 8 میلی‌ثانیه کامل شود.

زمان‌بند هر کار را که ارسال می‌کنید به ترتیب انجام خواهد داد و مدت زمان لازم را پیگیری می‌کند. پس از رسیدن به حداکثر زمان اختصاص یافته در هر فریم، منتظر می‌ماند تا پردازش فریم بعدی ادامه پیدا کند. به این ترتیب Ui زمان دارد تا نفسی تازه کند و فریم بعدی را پردازش کند. در عمل من با استفاده از حداکثر زمان در هر فریم به زمان 4 میلی‌ثانیه رسیدم. که به مراتب کمتر از 16 میلی‌ثانیه موجود در هر فریم است. اما می‌توانید تنظیم کنید که چه چیزی برای شما کار می‌کند، که شبیه به زیر خواهد بود:

object UIJobScheduler {
    private const val MAX_JOB_TIME_MS: Float = 4f

    private var elapsed = 0L
    private val jobQueue = ArrayDeque<() -> Unit>()
    private val isOverMaxTime get() = elapsed > MAX_JOB_TIME_MS * 1_000_000
    private val handler = Handler()

    fun submitJob(job: () -> Unit) {
        jobQueue.add(job)
        if (jobQueue.size == 1) {
            handler.post { processJobs() }
        }
    }

    private fun processJobs() {
        while (!jobQueue.isEmpty() && !isOverMaxTime) {
            val start = System.nanoTime()
            jobQueue.poll().invoke()
            elapsed += System.nanoTime() - start
        }
        if (jobQueue.isEmpty()) {
            elapsed = 0
        } else if (isOverMaxTime) {
            onNextFrame {
                elapsed = 0
                processJobs()
            }
        }
    }

    private fun onNextFrame(callback: () -> Unit) =
            Choreographer.getInstance().postFrameCallback { callback() }
}

کارکرد آن ساده است:

class ListItemViewHolder(view: View) : ViewHolder(view) {
    fun populateFrom(listItem: ListItem) {
        UIJobScheduler.submitJob { setupText() }
        UIJobScheduler.submitJob { setupText2() }
        UIJobScheduler.submitJob { setupImage2() }
    }
}

نتیجه

onCreateViewHolder و onBindViewHolder دو ناحیه در آداپتور RcyclerView هستند که می‌تواند هنگام پیمایش کند باشند. می‌توانید عملکرد onCreateViewHolder با ذخیره کردن view بهبود ببخشیم، و می‌توانیم عملکرد onBindViewHolder را با پخش کردن کار در چندین فریم از طریق UIJobScheduler بهبود ببخشیم.

گزینه‌ای دیگر

فیسبوک برای بهبود عملکرد پیمایش، کتابخانه‌ی خود را با نام Litho ایجاد کرد. این کتابخانه از یک الگوی متفاوت استفاده کرده است و ممکن است نیاز به اصلاح کدهای موجود باشد، اما به نظر بسیار خوب عمل می‌کند.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@pouryasharifi78
پوریا شریفی
توسعه‌دهنده‌ی اندروید

ابتدا که با برنامه‌نویسی آشنا شدم به سمت php و طراحی وب رفتم، بعد از اون به توسعه‌ی اندروید علاقه‌مند شدم و تقریبا ۲ سال است که مشغول به برنامه‌نویسی اندروید هستم، همچنین عاشق یادگیری چیزهای جدید هستم.

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید