RecyclerView با اسکرول روان در اندروید — راهنمای مقدماتی

۱۲۰ بازدید
آخرین به‌روزرسانی: ۲۹ شهریور ۱۴۰۲
زمان مطالعه: ۴ دقیقه
RecyclerView با اسکرول روان در اندروید — راهنمای مقدماتی

RecyclerView یکی از پر تقاضاترین ویجت‌ها در اندروید است و داشتن عملکرد اسکرول روان در نماهای پیچیده گاهی اوقات با مشکل مواجه می‌شود. اغلب دستگاه‌ها با نرخ رفرش 60 فریم بر ثانیه کار می‌کنند. این بدان معنی است که حدوداً هر 16 میلی‌ثانیه باید یک فریم جدید وجود داشته باشد. اگر اپلیکیشن شما بیش از 16 ثانیه زمان صرف محاسبه فریم بعدی برای نمایش بکند، کاربر یک فریم را از دست می‌دهد.

کش کردن قبلی نماها در Adapter

یکی از تنگناهایی که در زمان اسکرول کردن RecyclerView ممکن است رخ بدهد، ایجاد ViewHolder از طریق ()onCreateViewHolder است. زمانی که اسکرول می‌کنید، آداپتر یک ViewHolder بنا به تقاضا ایجاد می‌کند تا این که تعداد کافی از آن‌ها برای بازیافت (Recycler) داشته باشد.

تولید نما (View) کُند است. اگر لی‌آوت XML پیچیده باشد، ممکن است بیش از 16 میلی‌ثانیه صرف تولید نما شود و از این رو یک فریم از دست برود. اما اگر نماها را از قبل و پیش از نمایش دادن آن‌ها به کاربر تولید بکنیم، چطور؟ این کار از طریق استفاده از withAsyncLayoutInflater برای تولید نماها روی نخ پس‌زمینه و دریافت یک callback در زمان آماده شدنشان میسر است.

در مثال زیر ما تنگناها را زمانی که Adapter ایجاد می‌شود، تولید می‌کنیم. در این وضعیت فرض می‌شود که تولید نما پیش از آن که داده‌ها به Adapter اضافه شوند، آغاز می‌شود. اگر داده‌ها پیش از ایجاد Adapter آماده نمایش باشند، در این صورت باید یک callback روی Adapter تنظیم کنیم تا زمانی که تولید همه نماها تکمیل شد به ما اطلاع دهد.

1class SmoothListAdapter(val context: Context) : ListAdapter<ListItem, ListItemViewHolder>(MyDiffCallback()) {
2
3    data class ListItem(val id: String, val text: String)
4
5    class ListItemViewHolder(view: View) : ViewHolder(view) {
6        fun populateFrom(listItem: ListItem) {
7            //TODO: populate your view
8        }
9    }
10
11    companion object {
12        const val NUM_CACHED_VIEWS = 5
13    }
14
15    private val asyncLayoutInflater = AsyncLayoutInflater(context)
16    private val cachedViews = Stack<View>()
17
18
19    init {
20        //Create some views asynchronously and add them to our stack
21        for (i in 0..NUM_CACHED_VIEWS) {
22            asyncLayoutInflater.inflate(R.layout.list_item, null) { view, layoutRes, viewGroup ->
23                cachedViews.push(view)
24            }
25        }
26    }
27
28    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {
29        //Use the cached views if possible, otherwise if we ran out of cached views inflate a new one
30        val view = if (cachedViews.isEmpty()) {
31            LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
32        } else {
33            cachedViews.pop().also { it.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) }
34        }
35        return ListItemViewHolder(view)
36    }
37
38    override fun onBindViewHolder(viewHolder: ListItemViewHolder, position: Int) =
39        viewHolder.populateFrom(getItem(position))
40
41    class MyDiffCallback : DiffUtil.ItemCallback<ListItem>() {
42        override fun areItemsTheSame(firstItem: ListItem, secondItem: ListItem) =
43                firstItem.id == secondItem.id
44
45        override fun areContentsTheSame(firstItem: ListItem, secondItem: ListItem) =
46                firstItem == secondItem
47    }
48}

بهبود عملکرد اتصال نما

مورد دیگری که منجر به عملکرد کُند می‌شود، فراخوانی onBindViewHolder است. از آنجا که این متد عموماً برای هر ردیف درست پیش از وارد شدن به نما فراخوانی می‌شود، باید سریع باشد. اما گاهی اوقات در نماهای پیچیده ممکن است نیاز به محاسبات زیادی باشد، حتی فراخوانی setText()‎ روی چند Textiew ممکن است برای تکمیل شدن به چندین میلی‌ثانیه زمان نیاز داشته باشد.

این وضعیت را با ایجاد یک جدول زمان‌بندی کارهای UI می‌توانیم بهبود بخشیم. در ViewHolder کد اتصال نما را به چند تابع تقسیم می‌کنیم که یکی برای تنظیم متن و یکی برای تنظیم تصویر است. می‌خواهیم مطمئن شویم که هر گام در مقدار زمان بسیار کمی اجرا می‌شود و کمتر از 8 میلی‌ثانیه زمان می‌گیرد.

«جدول زمان‌بندی کار» (Job Scheduler) هر کار را به صورت پشت سر هم تحویل می‌دهد و میزان زمانی که طول می‌کشد را ثبت می‌کند. زمانی که به بیشینه زمان اختصاص یافته برای هر فریم نزدیک می‌شویم، این جدول صبر می‌کند تا فریم بعدی شروع شود و سپس به ادامه پردازش کارها می‌پردازد. بدین ترتیب UI فضایی برای نفس کشیدن پیدا می‌کند و فریم بعدی را رندر می‌کند. در عمل بهترین نتایج در زمان استفاده از بیشینه زمان 4 میلی‌ثانیه برای هر فریم به دست می‌آید که بسیار کمتر از 16 میلی‌ثانیه زمان موجود برای هر فریم است. اما می‌توانید این عدد را روی مقادیر مختلف تنظیم کنید تا نتایج را دقیق‌تر بررسی کنید.

1object UIJobScheduler {
2    private const val MAX_JOB_TIME_MS: Float = 4f
3
4    private var elapsed = 0L
5    private val jobQueue = ArrayDeque<() -> Unit>()
6    private val isOverMaxTime get() = elapsed > MAX_JOB_TIME_MS * 1_000_000
7    private val handler = Handler()
8
9    fun submitJob(job: () -> Unit) {
10        jobQueue.add(job)
11        if (jobQueue.size == 1) {
12            handler.post { processJobs() }
13        }
14    }
15
16    private fun processJobs() {
17        while (!jobQueue.isEmpty() && !isOverMaxTime) {
18            val start = System.nanoTime()
19            jobQueue.poll().invoke()
20            elapsed += System.nanoTime() - start
21        }
22        if (jobQueue.isEmpty()) {
23            elapsed = 0
24        } else if (isOverMaxTime) {
25            onNextFrame {
26                elapsed = 0
27                processJobs()
28            }
29        }
30    }
31
32    private fun onNextFrame(callback: () -> Unit) =
33            Choreographer.getInstance().postFrameCallback { callback() }
34}

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

1class ListItemViewHolder(view: View) : ViewHolder(view) {
2    fun populateFrom(listItem: ListItem) {
3        UIJobScheduler.submitJob { setupText() }
4        UIJobScheduler.submitJob { setupImage1() }
5        UIJobScheduler.submitJob { setupImage2() }
6    }
7}

مشکلات احتمالی

کلاس UIJobScheduler برای ادامه اتصال نما ممکن است به یک یا دو فریم اضافی نیاز داشته باشد که معنی آن این است نما می‌تواند داده‌های قدیمی را پیش از به‌روزرسانی نمایش دهد. ما در زمان تست مشکلی با این مسئله نداشتیم، اما به هر حال احتمال بروز مشکل وجود دارد.

سخن پایانی

()onCreateViewHolder و ()onBindViewHolder دو متدی هستند که آداپتر RecyclerView ممکن است کند شود و موجب تأخیر در اسکرول شود. عملکرد onCreateViewHolder را می‌توان با کش کردن قبلی نماها بهبود بخشید. همچنین عملکرد onBindViewHolder را می‌توان با پخش کارها روی فریم‌های مختلف از طریق سینگلتون UIJobScheduler بهبود داد.

فیسبوک یک کتابخانه اختصاصی به نام Litho (+) برای بهبود عملکرد اسکرول خود توسعه داده است. این کتابخانه از پارادایم متفاوتی استفاده می‌کند که نیازمند ریفکتور کردن کد موجود اندروید است، اما عملکرد آن بسیار خوب به نظرمی رسد.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
polson55
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *