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


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

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

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

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

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

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
    

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

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

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

«جدول زمان‌بندی کار» (Job scheduler) هر کار را به صورت پشت سر هم تحویل می‌دهد و میزان زمانی که طول می‌کشد را ثبت می‌کند. زمانی که به بیشینه زمان اختصاص یافته برای هر فریم نزدیک می‌شویم، این جدول صبر می‌کند تا فریم بعدی شروع شود و سپس به ادامه پردازش کارها می‌پردازد. بدین ترتیب 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  setupImage1() 
        UIJobScheduler.submitJob  setupImage2() 
    

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

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

سخن پایانی

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

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

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

  • مجموعه آموزش‌های برنامه‌نویسی اندروید
  • گنجینه برنامه نویسی اندروید (Android)
  • مجموعه آموزش‌های برنامه‌‌نویسی
  • ساخت کتابخانه اندروید و انتشار آن — به زبان ساده
  • نوتیفیکیشن اندروید — اصول مقدماتی

==

به عنوان حامی، استارتاپ، محصول و خدمات خود را در انتهای مطالب مرتبط مجله فرادرس معرفی کنید.

telegram
twitter
polson55

میثم لطفی

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری همه علاقه‌مندی‌های خود در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و تولید محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

نوشته RecyclerView با اسکرول روان در اندروید — راهنمای مقدماتی اولین بار در مجله فرادرس. پدیدار شد.